From 48683a7f71b003ad0c1d480fa9549639f9e50f2b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 11:03:52 +0100 Subject: [PATCH] ci: split auto-reply reply routing shard --- scripts/lib/ci-node-test-plan.mjs | 11 +--- ...erver.agent.gateway-server-agent-a.test.ts | 49 +++++++++------- ...erver.agent.gateway-server-agent-b.test.ts | 56 ++++++++++++++----- test/scripts/ci-node-test-plan.test.ts | 10 +++- 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index ca064927d8a..8d73b2ea0b4 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -67,16 +67,7 @@ function createAutoReplyReplySplitShards() { } } - const mergedGroups = { - "auto-reply-reply-agent-runner": groups["auto-reply-reply-agent-runner"], - "auto-reply-reply-dispatch": groups["auto-reply-reply-dispatch"], - "auto-reply-reply-commands-state-routing": [ - ...groups["auto-reply-reply-commands"], - ...groups["auto-reply-reply-state-routing"], - ], - }; - - return Object.entries(mergedGroups) + return Object.entries(groups) .map(([groupName, includePatterns]) => ({ configs: ["test/vitest/vitest.auto-reply-reply.config.ts"], includePatterns, diff --git a/src/gateway/server.agent.gateway-server-agent-a.test.ts b/src/gateway/server.agent.gateway-server-agent-a.test.ts index 0562dddf61f..a776a81dd1e 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import { createChannelTestPluginBase } from "../test-utils/channel-plugins.js"; import { setRegistry } from "./server.agent.gateway-server-agent.mocks.js"; @@ -61,9 +61,18 @@ async function setTestSessionStore(params: { }); } -function latestAgentCall(): AgentCommandCall { +async function waitForAgentCall(runId: string): Promise { + await vi.waitFor(() => + expect( + (vi.mocked(agentCommand).mock.calls as unknown as Array<[AgentCommandCall]>).some( + ([call]) => call.runId === runId, + ), + ).toBe(true), + ); const calls = vi.mocked(agentCommand).mock.calls as unknown as Array<[unknown]>; - return calls.at(-1)?.[0] as AgentCommandCall; + return calls.find( + ([call]) => (call as AgentCommandCall).runId === runId, + )?.[0] as AgentCommandCall; } async function runMainAgentDeliveryWithSession(params: { @@ -89,7 +98,7 @@ async function runMainAgentDeliveryWithSession(params: { ...params.request, }); expect(res.ok).toBe(true); - return latestAgentCall(); + return await waitForAgentCall(String(params.request.idempotencyKey)); } finally { testState.allowFrom = undefined; } @@ -160,8 +169,19 @@ const defaultRegistry = createRegistry([ ]); describe("gateway server agent", () => { - test("agent marks implicit delivery when lastTo is stale", async () => { + beforeEach(() => { + vi.mocked(agentCommand).mockClear(); + testState.agentsConfig = undefined; + testState.allowFrom = undefined; setRegistry(defaultRegistry); + }); + + afterEach(() => { + testState.agentsConfig = undefined; + testState.allowFrom = undefined; + }); + + test("agent marks implicit delivery when lastTo is stale", async () => { testState.allowFrom = ["+436769770569"]; await setTestSessionStore({ entries: { @@ -182,7 +202,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const call = latestAgentCall(); + const call = await waitForAgentCall("idem-agent-last-stale"); expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliveryTargetMode).toBe("implicit"); @@ -191,7 +211,6 @@ describe("gateway server agent", () => { }); test("agent forwards sessionKey to agentCommand", async () => { - setRegistry(defaultRegistry); await setTestSessionStore({ entries: { "agent:main:subagent:abc": { @@ -207,7 +226,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const call = latestAgentCall(); + const call = await waitForAgentCall("idem-agent-subkey"); expect(call.sessionKey).toBe("agent:main:subagent:abc"); expect(call.sessionId).toBe("sess-sub"); expectChannels(call, "webchat"); @@ -216,7 +235,6 @@ describe("gateway server agent", () => { }); test("agent preserves spawnDepth on subagent sessions", async () => { - setRegistry(defaultRegistry); await setTestSessionStore({ entries: { "agent:main:subagent:depth": { @@ -245,7 +263,6 @@ describe("gateway server agent", () => { }); test("agent derives sessionKey from agentId", async () => { - setRegistry(defaultRegistry); await setTestSessionStore({ agentId: "ops", entries: { @@ -263,13 +280,12 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const call = latestAgentCall(); + const call = await waitForAgentCall("idem-agent-id"); expect(call.sessionKey).toBe("agent:ops:main"); expect(call.sessionId).toBe("sess-ops"); }); test("agent rejects unknown reply channel", async () => { - setRegistry(defaultRegistry); const res = await rpcReq(ws, "agent", { message: "hi", replyChannel: "unknown-channel", @@ -283,7 +299,6 @@ describe("gateway server agent", () => { }); test("agent rejects mismatched agentId and sessionKey", async () => { - setRegistry(defaultRegistry); testState.agentsConfig = { list: [{ id: "ops" }] }; const res = await rpcReq(ws, "agent", { message: "hi", @@ -299,7 +314,6 @@ describe("gateway server agent", () => { }); test("agent rejects malformed agent-prefixed session keys", async () => { - setRegistry(defaultRegistry); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "agent:main", @@ -391,7 +405,6 @@ describe("gateway server agent", () => { }); test("agent forwards image attachments as images[]", async () => { - setRegistry(defaultRegistry); await setTestSessionStore({ entries: { main: { @@ -414,7 +427,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const call = latestAgentCall(); + const call = await waitForAgentCall("idem-agent-attachments"); expect(call.sessionKey).toBe("agent:main:main"); expectChannels(call, "webchat"); expect(typeof call.message).toBe("string"); @@ -429,7 +442,6 @@ describe("gateway server agent", () => { }); test("agent errors when delivery requested and no last channel exists", async () => { - setRegistry(defaultRegistry); testState.allowFrom = ["+1555"]; try { await setTestSessionStore({ @@ -493,7 +505,6 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-last-signal", }, ])("agent routes main last-channel $name", async (tc) => { - setRegistry(defaultRegistry); await setTestSessionStore({ entries: { main: { @@ -513,7 +524,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const call = latestAgentCall(); + const call = await waitForAgentCall(tc.idempotencyKey); expectChannels(call, tc.lastChannel); expect(call.to).toBe(tc.lastTo); expect(call.deliver).toBe(true); diff --git a/src/gateway/server.agent.gateway-server-agent-b.test.ts b/src/gateway/server.agent.gateway-server-agent-b.test.ts index a417793b5ac..ffd4cfed3e3 100644 --- a/src/gateway/server.agent.gateway-server-agent-b.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-b.test.ts @@ -111,18 +111,30 @@ function expectChannels(call: Record, channel: string) { expect(call.messageChannel).toBe(channel); } -function readAgentCommandCall(fromEnd = 1) { +async function readAgentCommandCall(params: { runId?: string; fromEnd?: number } = {}) { + if (params.runId) { + await vi.waitFor(() => + expect( + (vi.mocked(agentCommand).mock.calls as unknown as Array<[Record]>).some( + ([call]) => call.runId === params.runId, + ), + ).toBe(true), + ); + const calls = vi.mocked(agentCommand).mock.calls as unknown as Array<[Record]>; + return calls.find(([call]) => call.runId === params.runId)?.[0] ?? {}; + } const calls = vi.mocked(agentCommand).mock.calls; - return (calls.at(-fromEnd)?.[0] ?? {}) as Record; + return (calls.at(-(params.fromEnd ?? 1))?.[0] ?? {}) as Record; } -function expectAgentRoutingCall(params: { +async function expectAgentRoutingCall(params: { channel: string; deliver: boolean; to?: string; fromEnd?: number; + runId?: string; }) { - const call = readAgentCommandCall(params.fromEnd); + const call = await readAgentCommandCall({ runId: params.runId, fromEnd: params.fromEnd }); expectChannels(call, params.channel); if ("to" in params) { expect(call.to).toBe(params.to); @@ -186,10 +198,13 @@ async function useTempSessionStorePath() { describe("gateway server agent", () => { beforeEach(() => { + vi.mocked(agentCommand).mockClear(); + testState.allowFrom = undefined; setRegistry(defaultRegistry); }); afterEach(() => { + testState.allowFrom = undefined; setRegistry(emptyRegistry); }); @@ -215,11 +230,11 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-last-msteams", }); expect(res.ok).toBe(true); - expectAgentRoutingCall({ + await expectAgentRoutingCall({ channel: "msteams", deliver: true, to: "conversation:teams-123", - fromEnd: 1, + runId: "idem-agent-last-msteams", }); }); @@ -308,10 +323,11 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-imsg", }); expect(resIMessage.ok).toBe(true); - await vi.waitFor(() => { - expect(vi.mocked(agentCommand)).toHaveBeenCalled(); + await expectAgentRoutingCall({ + channel: "imessage", + deliver: true, + runId: "idem-agent-imsg", }); - expectAgentRoutingCall({ channel: "imessage", deliver: true, fromEnd: 1 }); }); test("agent accepts plugin channel alias (teams)", async () => { @@ -333,11 +349,11 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-teams", }); expect(resTeams.ok).toBe(true); - expectAgentRoutingCall({ + await expectAgentRoutingCall({ channel: "msteams", deliver: false, to: "conversation:teams-abc", - fromEnd: 1, + runId: "idem-agent-teams", }); }); @@ -389,7 +405,11 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-webchat-best-effort", }); expect(res.ok).toBe(true); - expectAgentRoutingCall({ channel: "webchat", deliver: false }); + await expectAgentRoutingCall({ + channel: "webchat", + deliver: false, + runId: "idem-agent-webchat-best-effort", + }); }); test("agent downgrades to session-only when multiple channels are configured but no external target resolves", async () => { @@ -417,7 +437,11 @@ describe("gateway server agent", () => { idempotencyKey: "idem-agent-multi-configured-best-effort", }); expect(res.ok).toBe(true); - expectAgentRoutingCall({ channel: "webchat", deliver: false }); + await expectAgentRoutingCall({ + channel: "webchat", + deliver: false, + runId: "idem-agent-multi-configured-best-effort", + }); }); test("agent uses webchat for internal runs when last provider is webchat", async () => { @@ -435,7 +459,11 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - expectAgentRoutingCall({ channel: "webchat", deliver: false }); + await expectAgentRoutingCall({ + channel: "webchat", + deliver: false, + runId: "idem-agent-webchat-internal", + }); }); test("write-scoped callers cannot reset conversations via agent", async () => { diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 990511ff8d3..c8b0d2336b3 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -328,6 +328,12 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { requiresDist: false, shardName: "auto-reply-reply-agent-runner", }, + { + checkName: "checks-node-auto-reply-reply-commands", + configs: ["test/vitest/vitest.auto-reply-reply.config.ts"], + requiresDist: false, + shardName: "auto-reply-reply-commands", + }, { checkName: "checks-node-auto-reply-reply-dispatch", configs: ["test/vitest/vitest.auto-reply-reply.config.ts"], @@ -335,10 +341,10 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { shardName: "auto-reply-reply-dispatch", }, { - checkName: "checks-node-auto-reply-reply-commands-state-routing", + checkName: "checks-node-auto-reply-reply-state-routing", configs: ["test/vitest/vitest.auto-reply-reply.config.ts"], requiresDist: false, - shardName: "auto-reply-reply-commands-state-routing", + shardName: "auto-reply-reply-state-routing", }, ]); });