97 lines
5.8 KiB
TypeScript
97 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
async function source(relativePath: string): Promise<string> {
|
|
return readFile(new URL(relativePath, import.meta.url), 'utf8');
|
|
}
|
|
|
|
test('migration creates a default-off encrypted outbox separate from employee replies', async () => {
|
|
const sql = await source('../migrations/020_leader_task_summary_notifications.sql');
|
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS leader_task_summary_subscriptions/);
|
|
assert.match(sql, /enabled boolean NOT NULL DEFAULT false/);
|
|
assert.match(sql, /scope text NOT NULL DEFAULT 'organization'/);
|
|
assert.match(sql, /CHECK \(include_manual OR include_agentbus\)/);
|
|
assert.match(sql, /recipient_address_ciphertext text NOT NULL/);
|
|
assert.match(sql, /conversation_id_ciphertext text NOT NULL/);
|
|
assert.match(sql, /payload_ciphertext text NOT NULL/);
|
|
assert.match(sql, /target_verified_at timestamptz/);
|
|
assert.match(sql, /UNIQUE \(organization_id, leader_user_id\)/);
|
|
assert.match(sql, /UNIQUE \(subscription_id, subscription_revision, task_id, milestone\)/);
|
|
assert.doesNotMatch(sql, /INSERT\s+INTO/iu, 'schema migration must not backfill or send historical tasks');
|
|
assert.doesNotMatch(sql, /REFERENCES\s+agentbus_deliveries/iu);
|
|
assert.doesNotMatch(sql, /recipient_address\s+text/iu);
|
|
assert.doesNotMatch(sql, /conversation_id\s+text/iu);
|
|
assert.doesNotMatch(sql, /payload\s+jsonb/iu);
|
|
});
|
|
|
|
test('projection is organization-scoped, future-only, role-safe and source-selective', async () => {
|
|
const service = await source('../src/leader-notification-service.ts');
|
|
assert.match(service, /event\.topic = 'task\.updated'/);
|
|
assert.match(service, /event\.created_at >= subscription\.starts_at/);
|
|
assert.match(service, /task\.assigned_user_id <> subscription\.leader_user_id/);
|
|
assert.match(service, /assignee\.role <> 'admin'/);
|
|
assert.match(service, /task\.source IN \('manual', 'agentbus'\)/);
|
|
assert.match(service, /subscription\.include_manual/);
|
|
assert.match(service, /subscription\.include_agentbus/);
|
|
assert.match(service, /event\.payload ->> 'archived'.*IS DISTINCT FROM 'true'/s);
|
|
assert.match(service, /event\.payload ->> 'restored'.*IS DISTINCT FROM 'true'/s);
|
|
assert.match(service, /subscription\.target_verified_at IS NOT NULL/);
|
|
assert.match(service, /pg_try_advisory_xact_lock/);
|
|
assert.match(service, /encryptText\(this\.config, payloadText\)/);
|
|
assert.match(service, /FOR UPDATE OF delivery SKIP LOCKED/);
|
|
assert.match(service, /sha256Text\(payloadText\).*payload_fingerprint/s);
|
|
assert.match(service, /sha256Text\(recipientAddress\).*recipient_address_fingerprint/s);
|
|
assert.match(service, /sha256Text\(conversationId\).*conversation_id_fingerprint/s);
|
|
assert.match(service, /delivery_status = 'cancelled'.*订阅设置已变化/s);
|
|
assert.match(service, /leader_summary_agentbus_disabled/);
|
|
assert.doesNotMatch(service, /reply_to/);
|
|
});
|
|
|
|
test('HTTP configuration is administrator-only and has no live test-send endpoint', async () => {
|
|
const server = await source('../src/server.ts');
|
|
const routeStart = server.indexOf("app.get('/api/settings/leader-summary-subscriptions'");
|
|
const routeEnd = server.indexOf("app.post('/api/auth/logout'", routeStart);
|
|
const routes = server.slice(routeStart, routeEnd);
|
|
assert.ok(routeStart > 0 && routeEnd > routeStart);
|
|
assert.match(routes, /requireAdminSession/);
|
|
assert.match(routes, /requireAdminMutationSession/);
|
|
assert.match(routes, /expectedRevision: body\.expected_revision/);
|
|
assert.doesNotMatch(server, /leader-summary-subscriptions.*test-send|leader-summary-subscriptions.*test\/send/s);
|
|
});
|
|
|
|
test('AgentBus sends summaries as reserved low-priority proactive events without plaintext frame logging', async () => {
|
|
const agentbus = await source('../src/agentbus.ts');
|
|
const ignoreStart = agentbus.indexOf('function inboundFrameIgnoreReason');
|
|
const ignoreEnd = agentbus.indexOf('export function parseAgentBusFrame', ignoreStart);
|
|
assert.match(agentbus.slice(ignoreStart, ignoreEnd), /'task\.summary'/);
|
|
assert.match(agentbus.slice(ignoreStart, ignoreEnd), /reserved_event/);
|
|
assert.match(agentbus, /id: `leader-summary-\$\{delivery\.id\}`/);
|
|
assert.match(agentbus, /to: delivery\.recipient_address/);
|
|
assert.match(agentbus, /conversation_id: delivery\.conversation_id/);
|
|
const frameStart = agentbus.indexOf('export function createLeaderTaskSummaryFrame');
|
|
const frameEnd = agentbus.indexOf('export function taskResultStatus', frameStart);
|
|
assert.doesNotMatch(agentbus.slice(frameStart, frameEnd), /reply_to/);
|
|
const flushStart = agentbus.indexOf('private async flushOutboundDeliveries');
|
|
const sendEnd = agentbus.indexOf('private async sendDurableDelivery', flushStart);
|
|
const leaderDelivery = agentbus.slice(flushStart, sendEnd);
|
|
assert.match(leaderDelivery, /await this\.flushDurableDeliveries\(\)/);
|
|
assert.match(leaderDelivery, /await this\.flushLeaderDeliveries\(\)/);
|
|
assert.match(leaderDelivery, /recipient_fingerprint/);
|
|
assert.match(leaderDelivery, /conversation_fingerprint/);
|
|
assert.doesNotMatch(leaderDelivery, /this\.logFrame/);
|
|
});
|
|
|
|
test('administrator UI explains safety boundaries and never asks the API to backfill', async () => {
|
|
const html = await source('../../LianSyn-platform/index.html');
|
|
const app = await source('../../LianSyn-platform/app.js');
|
|
assert.match(html, /组长任务摘要抄送/);
|
|
assert.match(html, /默认关闭/);
|
|
assert.match(html, /只处理保存设置后的新结果,不补发历史任务/);
|
|
assert.match(html, /已经到达微信的消息无法撤回/);
|
|
assert.match(app, /目标已核对/);
|
|
assert.match(app, /expected_revision/);
|
|
assert.match(app, /消息正文不含原始指令、客户\/游客资料或技术错误/);
|
|
assert.doesNotMatch(app, /leader-summary-subscriptions[^'"\n]*backfill/);
|
|
});
|