ci: split auto-reply reply routing shard

This commit is contained in:
Peter Steinberger
2026-04-29 11:03:52 +01:00
parent af548bb07d
commit 48683a7f71
4 changed files with 81 additions and 45 deletions

View File

@@ -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,

View File

@@ -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<AgentCommandCall> {
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);

View File

@@ -111,18 +111,30 @@ function expectChannels(call: Record<string, unknown>, 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<string, unknown>]>).some(
([call]) => call.runId === params.runId,
),
).toBe(true),
);
const calls = vi.mocked(agentCommand).mock.calls as unknown as Array<[Record<string, unknown>]>;
return calls.find(([call]) => call.runId === params.runId)?.[0] ?? {};
}
const calls = vi.mocked(agentCommand).mock.calls;
return (calls.at(-fromEnd)?.[0] ?? {}) as Record<string, unknown>;
return (calls.at(-(params.fromEnd ?? 1))?.[0] ?? {}) as Record<string, unknown>;
}
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 () => {

View File

@@ -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",
},
]);
});