diff --git a/src/gateway/server.agent.subagent-delivery-context.test.ts b/src/gateway/server.agent.subagent-delivery-context.test.ts index 8e63a8686b9..14cadbcf293 100644 --- a/src/gateway/server.agent.subagent-delivery-context.test.ts +++ b/src/gateway/server.agent.subagent-delivery-context.test.ts @@ -81,6 +81,14 @@ type StoredEntry = { lastAccountId?: string; }; +type StoreEntries = Parameters[0]["entries"]; + +async function prepareSessionStore(entries: StoreEntries = {}): Promise { + setRegistry(defaultRegistry); + testState.sessionStorePath = sessionStorePath; + await writeSessionStore({ entries }); +} + function readStoredEntry(stored: Record, key: string): StoredEntry { const entry = stored[key]; if (!entry) { @@ -96,34 +104,50 @@ function readDeliveryContext(entry: StoredEntry): NonNullable { + const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< + string, + StoredEntry + >; + return readStoredEntry(stored, key); +} + +async function sendAgentRequest(params: Record): Promise { + const res = await rpcReq(ws, "agent", { + deliver: false, + ...params, + }); + expect(res.ok).toBe(true); +} + +function expectDeliveryContextFields(entry: StoredEntry, expected: Record): void { + const deliveryContext = readDeliveryContext(entry); + for (const [key, value] of Object.entries(expected)) { + expect(deliveryContext[key as keyof typeof deliveryContext]).toBe(value); + } +} + describe("subagent session deliveryContext from spawn request params", () => { test("new subagent session inherits deliveryContext from request channel/to/threadId", async () => { - setRegistry(defaultRegistry); - testState.sessionStorePath = sessionStorePath; - await writeSessionStore({ entries: {} }); + await prepareSessionStore(); - const res = await rpcReq(ws, "agent", { + await sendAgentRequest({ message: "[Subagent Task]: analyze data", sessionKey: "agent:main:subagent:test-delivery-ctx", channel: "slack", to: "channel:C0AF8TW48UQ", accountId: "default", threadId: "1774374945.091819", - deliver: false, idempotencyKey: "idem-subagent-delivery-ctx-1", }); - expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< - string, - StoredEntry - >; - const entry = readStoredEntry(stored, "agent:main:subagent:test-delivery-ctx"); - const deliveryContext = readDeliveryContext(entry); - expect(deliveryContext.channel).toBe("slack"); - expect(deliveryContext.to).toBe("channel:C0AF8TW48UQ"); - expect(deliveryContext.threadId).toBe("1774374945.091819"); - expect(deliveryContext.accountId).toBe("default"); + const entry = await readStoredSessionEntry("agent:main:subagent:test-delivery-ctx"); + expectDeliveryContextFields(entry, { + channel: "slack", + to: "channel:C0AF8TW48UQ", + threadId: "1774374945.091819", + accountId: "default", + }); expect(entry.route).toEqual({ channel: "slack", accountId: "default", @@ -135,103 +159,84 @@ describe("subagent session deliveryContext from spawn request params", () => { }); test("existing session deliveryContext is NOT overwritten by request params", async () => { - setRegistry(defaultRegistry); - testState.sessionStorePath = sessionStorePath; - await writeSessionStore({ - entries: { - "agent:main:subagent:existing-ctx": { - sessionId: "sess-existing", - updatedAt: Date.now(), - deliveryContext: { - channel: "slack", - to: "user:U09U1LV7JDN", - accountId: "default", - threadId: "1771242986.529939", - }, - lastChannel: "slack", - lastTo: "user:U09U1LV7JDN", - lastAccountId: "default", - lastThreadId: "1771242986.529939", + await prepareSessionStore({ + "agent:main:subagent:existing-ctx": { + sessionId: "sess-existing", + updatedAt: Date.now(), + deliveryContext: { + channel: "slack", + to: "user:U09U1LV7JDN", + accountId: "default", + threadId: "1771242986.529939", }, + lastChannel: "slack", + lastTo: "user:U09U1LV7JDN", + lastAccountId: "default", + lastThreadId: "1771242986.529939", }, }); - const res = await rpcReq(ws, "agent", { + await sendAgentRequest({ message: "follow-up", sessionKey: "agent:main:subagent:existing-ctx", channel: "slack", to: "channel:C0AF8TW48UQ", threadId: "9999999999.000000", - deliver: false, idempotencyKey: "idem-subagent-delivery-ctx-2", }); - expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< - string, - StoredEntry - >; - const entry = readStoredEntry(stored, "agent:main:subagent:existing-ctx"); - const deliveryContext = readDeliveryContext(entry); + const entry = await readStoredSessionEntry("agent:main:subagent:existing-ctx"); // The ORIGINAL deliveryContext should be preserved (primary wins in merge). - expect(deliveryContext.to).toBe("user:U09U1LV7JDN"); - expect(deliveryContext.threadId).toBe("1771242986.529939"); + expectDeliveryContextFields(entry, { + to: "user:U09U1LV7JDN", + threadId: "1771242986.529939", + }); expect(entry.lastTo).toBe("user:U09U1LV7JDN"); }); test("existing session route metadata survives agent request delivery normalization", async () => { - setRegistry(defaultRegistry); - testState.sessionStorePath = sessionStorePath; - await writeSessionStore({ - entries: { - "agent:main:subagent:existing-route-metadata": { - sessionId: "sess-existing-route", - updatedAt: Date.now(), - route: { - channel: "slack", - accountId: "default", - target: { - to: "channel:C0AF8TW48UQ", - rawTo: "slack://C0AF8TW48UQ", - chatType: "channel", - }, - thread: { - id: "1771242986.529939", - kind: "thread", - source: "target", - }, - }, - deliveryContext: { - channel: "slack", + await prepareSessionStore({ + "agent:main:subagent:existing-route-metadata": { + sessionId: "sess-existing-route", + updatedAt: Date.now(), + route: { + channel: "slack", + accountId: "default", + target: { to: "channel:C0AF8TW48UQ", - accountId: "default", - threadId: "1771242986.529939", + rawTo: "slack://C0AF8TW48UQ", + chatType: "channel", + }, + thread: { + id: "1771242986.529939", + kind: "thread", + source: "target", }, - lastChannel: "slack", - lastTo: "channel:C0AF8TW48UQ", - lastAccountId: "default", - lastThreadId: "1771242986.529939", }, + deliveryContext: { + channel: "slack", + to: "channel:C0AF8TW48UQ", + accountId: "default", + threadId: "1771242986.529939", + }, + lastChannel: "slack", + lastTo: "channel:C0AF8TW48UQ", + lastAccountId: "default", + lastThreadId: "1771242986.529939", }, }); - const res = await rpcReq(ws, "agent", { + await sendAgentRequest({ message: "follow-up", sessionKey: "agent:main:subagent:existing-route-metadata", channel: "slack", to: "channel:C0AF8TW48UQ", accountId: "default", threadId: "1771242986.529939", - deliver: false, idempotencyKey: "idem-subagent-delivery-route-metadata", }); - expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< - string, - StoredEntry - >; - const entry = readStoredEntry(stored, "agent:main:subagent:existing-route-metadata"); + const entry = await readStoredSessionEntry("agent:main:subagent:existing-route-metadata"); expect(entry.route).toEqual({ channel: "slack", accountId: "default", @@ -253,41 +258,32 @@ describe("subagent session deliveryContext from spawn request params", () => { // first (to set spawnDepth, spawnedBy, etc.), then calls callSubagentGateway({method: "agent"}). // The sessions.patch creates a partial entry without deliveryContext. // The agent handler must seed deliveryContext from the request params. - setRegistry(defaultRegistry); - testState.sessionStorePath = sessionStorePath; - await writeSessionStore({ - entries: { - "agent:main:subagent:pre-patched": { - sessionId: "sess-pre-patched", - updatedAt: Date.now(), - spawnDepth: 1, - spawnedBy: "agent:main:slack:direct:u07fdr83w6n:thread:1775577152.364109", - }, + await prepareSessionStore({ + "agent:main:subagent:pre-patched": { + sessionId: "sess-pre-patched", + updatedAt: Date.now(), + spawnDepth: 1, + spawnedBy: "agent:main:slack:direct:u07fdr83w6n:thread:1775577152.364109", }, }); - const res = await rpcReq(ws, "agent", { + await sendAgentRequest({ message: "[Subagent Task]: investigate data", sessionKey: "agent:main:subagent:pre-patched", channel: "slack", to: "user:U07FDR83W6N", accountId: "default", threadId: "1775577152.364109", - deliver: false, idempotencyKey: "idem-subagent-delivery-ctx-prepatched", }); - expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< - string, - StoredEntry - >; - const entry = readStoredEntry(stored, "agent:main:subagent:pre-patched"); - const deliveryContext = readDeliveryContext(entry); - expect(deliveryContext.channel).toBe("slack"); - expect(deliveryContext.to).toBe("user:U07FDR83W6N"); - expect(deliveryContext.threadId).toBe("1775577152.364109"); - expect(deliveryContext.accountId).toBe("default"); + const entry = await readStoredSessionEntry("agent:main:subagent:pre-patched"); + expectDeliveryContextFields(entry, { + channel: "slack", + to: "user:U07FDR83W6N", + threadId: "1775577152.364109", + accountId: "default", + }); expect(entry.route).toEqual({ channel: "slack", accountId: "default", @@ -298,26 +294,18 @@ describe("subagent session deliveryContext from spawn request params", () => { }); test("request without to/threadId does not inject empty values", async () => { - setRegistry(defaultRegistry); - testState.sessionStorePath = sessionStorePath; - await writeSessionStore({ entries: {} }); + await prepareSessionStore(); - const res = await rpcReq(ws, "agent", { + await sendAgentRequest({ message: "internal task", sessionKey: "agent:main:subagent:no-routing", channel: "slack", - deliver: false, idempotencyKey: "idem-subagent-delivery-ctx-3", }); - expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(sessionStorePath, "utf-8")) as Record< - string, - StoredEntry - >; - const entry = readStoredEntry(stored, "agent:main:subagent:no-routing"); + const entry = await readStoredSessionEntry("agent:main:subagent:no-routing"); + expectDeliveryContextFields(entry, { channel: "slack" }); const deliveryContext = readDeliveryContext(entry); - expect(deliveryContext.channel).toBe("slack"); expect(deliveryContext.to).toBeUndefined(); expect(deliveryContext.threadId).toBeUndefined(); });