Files
openclaw/src/gateway/server-methods/agent-wait-dedupe.test.ts
Bob 61f7cea48b fix: kill stuck ACP child processes on startup and harden sessions in discord threads (#33699)
* 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>
2026-03-04 10:52:28 +01:00

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);
});
});