fix: auto-enable leader summary routing
This commit is contained in:
1 parent
8fb066f345
commit
0b3aa5c42d
14 files changed
+618
-623
No files matched your search
@@ -453,6 +453,6 @@ test('account authorization editor uses a scroll-safe open layout without overri
|
||||
assert.match(openLayoutSource, /overflow:\s*visible/);
|
||||
assert.doesNotMatch(styles, /\.account-panel\s*\{\s*grid-template-rows:/);
|
||||
|
||||
assert.match(index, /styles\.css\?v=20260907-leader-summary-1/);
|
||||
assert.match(index, /app\.js\?v=20260907-leader-summary-1/);
|
||||
assert.match(index, /styles\.css\?v=20260907-leader-summary-2/);
|
||||
assert.match(index, /app\.js\?v=20260907-leader-summary-2/);
|
||||
});
|
||||
@@ -175,6 +175,78 @@ test('AgentBus configuration stays disabled until connection fields are supplied
|
||||
assert.equal(config.AGENTBUS_LOG_PAYLOADS, false);
|
||||
});
|
||||
|
||||
test('team-lead listener automatically observes its inbound AgentBus route', async (t) => {
|
||||
const socket = new FakeSocket();
|
||||
const observed: Array<Record<string, unknown>> = [];
|
||||
let ingested = 0;
|
||||
const tasks: AgentBusTaskGateway = {
|
||||
events: new EventEmitter(),
|
||||
async ingestMessage() {
|
||||
ingested += 1;
|
||||
return { task: makeTask('completed'), attached: false, created: true };
|
||||
},
|
||||
async getTask() {
|
||||
return makeTask('completed');
|
||||
}
|
||||
};
|
||||
const leaderNotifications: LeaderNotificationGateway = {
|
||||
async observeLeaderRoute(input) {
|
||||
observed.push(input as unknown as Record<string, unknown>);
|
||||
},
|
||||
async claimDeliveries() {
|
||||
return [];
|
||||
},
|
||||
async markDeliveryDelivered() {},
|
||||
async markDeliveryFailed() {},
|
||||
async releaseDeliveries() {}
|
||||
};
|
||||
const listener = new AgentBusListener({
|
||||
config: testConfig(),
|
||||
tasks,
|
||||
leaderNotifications,
|
||||
organizationId: 'org-1',
|
||||
scheduleParseQueue: async () => {},
|
||||
socketFactory: () => socket as unknown as AgentBusSocket,
|
||||
channel: {
|
||||
id: 'channel-leader-route',
|
||||
displayName: '组长微信',
|
||||
wsUrl: 'wss://mesh.nianxx.cn/ws',
|
||||
wsToken: 'leader-route-token',
|
||||
botAddress: 'bot:leader-route:listener',
|
||||
ownerUserId: 'leader-route-user',
|
||||
ownerRole: 'team_lead'
|
||||
}
|
||||
});
|
||||
t.after(() => listener.stop());
|
||||
|
||||
listener.start();
|
||||
socket.readyState = 1;
|
||||
socket.emit('open');
|
||||
socket.emit('message', JSON.stringify({
|
||||
id: 'ready-leader-route',
|
||||
type: 'event',
|
||||
session_id: 'session-leader-route',
|
||||
epoch: 1,
|
||||
to: 'bot:leader-route:listener',
|
||||
payload: { event: 'session.ready' }
|
||||
}));
|
||||
socket.emit('message', JSON.stringify({
|
||||
id: 'leader-route-message-1',
|
||||
type: 'event',
|
||||
from: 'channel:wechat:leader-route-user',
|
||||
payload: { text: '查询今天的任务' }
|
||||
}));
|
||||
|
||||
await waitFor(() => observed.length === 1 && ingested === 1);
|
||||
assert.deepEqual(observed, [{
|
||||
organizationId: 'org-1',
|
||||
leaderUserId: 'leader-route-user',
|
||||
channelId: 'channel-leader-route',
|
||||
recipientAddress: 'channel:wechat:leader-route-user',
|
||||
conversationId: 'agentbus:channel:wechat:leader-route-user'
|
||||
}]);
|
||||
});
|
||||
|
||||
test('AgentBus accepted delivery payloads keep final ownership metadata server-only', () => {
|
||||
const waitingPayload = createAgentBusAcceptedDeliveryPayload({
|
||||
text: AGENTBUS_ROSTER_WAITING_TEXT,
|
||||
|
||||
@@ -1231,8 +1231,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
|
||||
const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
assert.match(index, /id="loginPanel"/);
|
||||
assert.match(index, /id="workbench"[^>]*hidden/);
|
||||
assert.match(index, /styles\.css\?v=20260907-leader-summary-1/);
|
||||
assert.match(index, /app\.js\?v=20260907-leader-summary-1/);
|
||||
assert.match(index, /styles\.css\?v=20260907-leader-summary-2/);
|
||||
assert.match(index, /app\.js\?v=20260907-leader-summary-2/);
|
||||
assert.match(index, /id="statusDetailsPopover"/);
|
||||
assert.match(index, /id="statusDetailsRefresh"/);
|
||||
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
|
||||
|
||||
@@ -6,9 +6,11 @@ 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 () => {
|
||||
test('migration keeps the automatic feature fail-closed in storage and 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/);
|
||||
// The runtime reconciler is the only component that activates rows. A raw
|
||||
// insert must remain inert when identity or routing checks have not run.
|
||||
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\)/);
|
||||
@@ -25,7 +27,28 @@ test('migration creates a default-off encrypted outbox separate from employee re
|
||||
assert.doesNotMatch(sql, /payload\s+jsonb/iu);
|
||||
});
|
||||
|
||||
test('projection is organization-scoped, future-only, role-safe and source-selective', async () => {
|
||||
test('automatic reconciliation derives enabled subscriptions from leader identity and AgentBus routing', async () => {
|
||||
const service = await source('../src/leader-notification-service.ts');
|
||||
const channels = await source('../src/agentbus-channels.ts');
|
||||
assert.match(service, /reconcileAutomaticSubscriptions/);
|
||||
assert.match(service, /leader\.role = 'team_lead'/);
|
||||
assert.match(service, /leader\.is_active = true/);
|
||||
assert.match(service, /leader\.erp_account IS NOT NULL/);
|
||||
assert.match(service, /channel\.owner_user_id = leader\.id/);
|
||||
assert.match(service, /latest_route\.inbound_from/);
|
||||
assert.match(service, /channel\.external_user_ref/);
|
||||
assert.match(service, /route_task\.assigned_user_id = leader\.id/);
|
||||
assert.match(service, /source: inboundRecipient \? 'agentbus_inbound' : 'channel_external_user_ref'/);
|
||||
assert.match(service, /`agentbus:\$\{recipientAddress\}`/);
|
||||
assert.match(service, /VALUES \(\$1, \$2, \$3, true, true, true, now\(\), 0/);
|
||||
assert.match(service, /target_verified_at = now\(\)/);
|
||||
assert.match(service, /automatic_disabled/);
|
||||
assert.match(service, /channel_disabled.*route_unavailable.*role_or_channel_invalid/s);
|
||||
assert.doesNotMatch(service, /upsertSubscription|expectedRevision|leader_summary_target_unverified/);
|
||||
assert.match(channels, /ownerChanged[\s\S]*?\? null[\s\S]*?: currentExternalUserRef/);
|
||||
});
|
||||
|
||||
test('projection remains organization-scoped, future-only, role-safe and encrypted', 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/);
|
||||
@@ -43,20 +66,19 @@ test('projection is organization-scoped, future-only, role-safe and source-selec
|
||||
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.match(service, /delivery_status = 'cancelled'.*旧路由待发送摘要已取消/s);
|
||||
assert.doesNotMatch(service, /reply_to/);
|
||||
});
|
||||
|
||||
test('HTTP configuration is administrator-only and has no live test-send endpoint', async () => {
|
||||
test('HTTP exposes administrator status only and no manual mutation or 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(routes, /app\.(put|post|patch|delete)/);
|
||||
assert.doesNotMatch(server, /leaderSummarySubscriptionSchema|upsertSubscription/);
|
||||
assert.doesNotMatch(server, /leader-summary-subscriptions.*test-send|leader-summary-subscriptions.*test\/send/s);
|
||||
});
|
||||
|
||||
@@ -80,17 +102,22 @@ test('AgentBus sends summaries as reserved low-priority proactive events without
|
||||
assert.match(leaderDelivery, /recipient_fingerprint/);
|
||||
assert.match(leaderDelivery, /conversation_fingerprint/);
|
||||
assert.doesNotMatch(leaderDelivery, /this\.logFrame/);
|
||||
assert.match(agentbus, /ownerRole === 'team_lead'/);
|
||||
assert.match(agentbus, /observeLeaderRoute/);
|
||||
assert.match(agentbus, /recipientAddress: text\(frame\.from\),\s*conversationId/s);
|
||||
});
|
||||
|
||||
test('administrator UI explains safety boundaries and never asks the API to backfill', async () => {
|
||||
test('administrator UI is read-only and explains role-driven automatic delivery', 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(html, /只处理自动启用后的新结果,不补发历史任务/);
|
||||
assert.match(html, /已经到达微信的消息无法撤回/);
|
||||
assert.match(app, /目标已核对/);
|
||||
assert.match(app, /expected_revision/);
|
||||
assert.match(app, /消息正文不含原始指令、客户\/游客资料或技术错误/);
|
||||
assert.doesNotMatch(html, /leaderSummary(Form|ChannelId|RecipientAddress|ConversationId|TargetVerified|Enabled|Save)/);
|
||||
assert.match(app, /自动推送/);
|
||||
assert.match(app, /等待路由/);
|
||||
assert.match(app, /无需维护收件地址或微信会话 ID/);
|
||||
assert.doesNotMatch(app, /leader-summary-subscriptions\/\$\{|expected_revision: subscription\.revision/);
|
||||
assert.doesNotMatch(app, /leader-summary-subscriptions[^'"\n]*backfill/);
|
||||
});
|
||||
Reference in new issue
Block a user