* Gateway: resolve agent.wait for chat.send runs * Discord: harden ACP thread binding + listener timeout * ACPX: handle already-exited child wait * Gateway/Discord: address PR review findings * Discord: keep ACP error-state thread bindings on startup * gateway: make agent.wait dedupe bridge event-driven * discord: harden ACP probe classification and cap startup fan-out * discord: add cooperative timeout cancellation * discord: fix startup probe concurrency helper typing * plugin-sdk: avoid Windows root-alias shard timeout * plugin-sdk: keep root alias reflection path non-blocking * discord+gateway: resolve remaining PR review findings * gateway+discord: fix codex review regressions * Discord/Gateway: address Codex review findings * Gateway: keep agent.wait lifecycle active with shared run IDs * Discord: clean up status reactions on aborted runs * fix: add changelog note for ACP/Discord startup hardening (#33699) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
324 lines
7.0 KiB
TypeScript
324 lines
7.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
__testing,
|
|
readTerminalSnapshotFromGatewayDedupe,
|
|
setGatewayDedupeEntry,
|
|
waitForTerminalGatewayDedupe,
|
|
} from "./agent-wait-dedupe.js";
|
|
|
|
describe("agent wait dedupe helper", () => {
|
|
beforeEach(() => {
|
|
__testing.resetWaiters();
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
__testing.resetWaiters();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("unblocks waiters when a terminal chat dedupe entry is written", async () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-chat-terminal";
|
|
const waiter = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
await Promise.resolve();
|
|
expect(__testing.getWaiterCount(runId)).toBe(1);
|
|
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "ok",
|
|
startedAt: 100,
|
|
endedAt: 200,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(waiter).resolves.toEqual({
|
|
status: "ok",
|
|
startedAt: 100,
|
|
endedAt: 200,
|
|
error: undefined,
|
|
});
|
|
expect(__testing.getWaiterCount(runId)).toBe(0);
|
|
});
|
|
|
|
it("keeps stale chat dedupe blocked while agent dedupe is in-flight", async () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-stale-chat";
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "ok",
|
|
},
|
|
},
|
|
});
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `agent:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "accepted",
|
|
},
|
|
},
|
|
});
|
|
|
|
const snapshot = readTerminalSnapshotFromGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
});
|
|
expect(snapshot).toBeNull();
|
|
|
|
const blockedWait = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 25,
|
|
});
|
|
await vi.advanceTimersByTimeAsync(30);
|
|
await expect(blockedWait).resolves.toBeNull();
|
|
expect(__testing.getWaiterCount(runId)).toBe(0);
|
|
});
|
|
|
|
it("uses newer terminal chat snapshot when agent entry is non-terminal", () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-nonterminal-agent-with-newer-chat";
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `agent:${runId}`,
|
|
entry: {
|
|
ts: 100,
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "accepted",
|
|
},
|
|
},
|
|
});
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: 200,
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "ok",
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(
|
|
readTerminalSnapshotFromGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
}),
|
|
).toEqual({
|
|
status: "ok",
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
error: undefined,
|
|
});
|
|
});
|
|
|
|
it("ignores stale agent snapshots when waiting for an active chat run", async () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-chat-active-ignore-agent";
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `agent:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "ok",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(
|
|
readTerminalSnapshotFromGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
ignoreAgentTerminalSnapshot: true,
|
|
}),
|
|
).toBeNull();
|
|
|
|
const wait = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 1_000,
|
|
ignoreAgentTerminalSnapshot: true,
|
|
});
|
|
await Promise.resolve();
|
|
expect(__testing.getWaiterCount(runId)).toBe(1);
|
|
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: {
|
|
runId,
|
|
status: "ok",
|
|
startedAt: 123,
|
|
endedAt: 456,
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(wait).resolves.toEqual({
|
|
status: "ok",
|
|
startedAt: 123,
|
|
endedAt: 456,
|
|
error: undefined,
|
|
});
|
|
});
|
|
|
|
it("prefers the freshest terminal snapshot when agent/chat dedupe keys collide", () => {
|
|
const runId = "run-collision";
|
|
const dedupe = new Map();
|
|
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `agent:${runId}`,
|
|
entry: {
|
|
ts: 100,
|
|
ok: true,
|
|
payload: { runId, status: "ok", startedAt: 10, endedAt: 20 },
|
|
},
|
|
});
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: 200,
|
|
ok: false,
|
|
payload: { runId, status: "error", startedAt: 30, endedAt: 40, error: "chat failed" },
|
|
},
|
|
});
|
|
|
|
expect(
|
|
readTerminalSnapshotFromGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
}),
|
|
).toEqual({
|
|
status: "error",
|
|
startedAt: 30,
|
|
endedAt: 40,
|
|
error: "chat failed",
|
|
});
|
|
|
|
const dedupeReverse = new Map();
|
|
setGatewayDedupeEntry({
|
|
dedupe: dedupeReverse,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: 100,
|
|
ok: true,
|
|
payload: { runId, status: "ok", startedAt: 1, endedAt: 2 },
|
|
},
|
|
});
|
|
setGatewayDedupeEntry({
|
|
dedupe: dedupeReverse,
|
|
key: `agent:${runId}`,
|
|
entry: {
|
|
ts: 200,
|
|
ok: true,
|
|
payload: { runId, status: "timeout", startedAt: 3, endedAt: 4, error: "still running" },
|
|
},
|
|
});
|
|
|
|
expect(
|
|
readTerminalSnapshotFromGatewayDedupe({
|
|
dedupe: dedupeReverse,
|
|
runId,
|
|
}),
|
|
).toEqual({
|
|
status: "timeout",
|
|
startedAt: 3,
|
|
endedAt: 4,
|
|
error: "still running",
|
|
});
|
|
});
|
|
|
|
it("resolves multiple waiters for the same run id", async () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-multi";
|
|
const first = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 1_000,
|
|
});
|
|
const second = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
await Promise.resolve();
|
|
expect(__testing.getWaiterCount(runId)).toBe(2);
|
|
|
|
setGatewayDedupeEntry({
|
|
dedupe,
|
|
key: `chat:${runId}`,
|
|
entry: {
|
|
ts: Date.now(),
|
|
ok: true,
|
|
payload: { runId, status: "ok" },
|
|
},
|
|
});
|
|
|
|
await expect(first).resolves.toEqual(
|
|
expect.objectContaining({
|
|
status: "ok",
|
|
}),
|
|
);
|
|
await expect(second).resolves.toEqual(
|
|
expect.objectContaining({
|
|
status: "ok",
|
|
}),
|
|
);
|
|
expect(__testing.getWaiterCount(runId)).toBe(0);
|
|
});
|
|
|
|
it("cleans up waiter registration on timeout", async () => {
|
|
const dedupe = new Map();
|
|
const runId = "run-timeout";
|
|
const wait = waitForTerminalGatewayDedupe({
|
|
dedupe,
|
|
runId,
|
|
timeoutMs: 20,
|
|
});
|
|
|
|
await Promise.resolve();
|
|
expect(__testing.getWaiterCount(runId)).toBe(1);
|
|
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
await expect(wait).resolves.toBeNull();
|
|
expect(__testing.getWaiterCount(runId)).toBe(0);
|
|
});
|
|
});
|