

























@@ -10,6 +10,15 @@ import type { agentCommand as AgentCommand } from "./agent.js";
10101111const loadConfig = vi.hoisted(() => vi.fn());
1212const callGateway = vi.hoisted(() => vi.fn());
13+const isGatewayTransportError = vi.hoisted(() =>
14+vi.fn((value: unknown) => {
15+if (!(value instanceof Error) || value.name !== "GatewayTransportError") {
16+return false;
17+}
18+const kind = (value as { kind?: unknown }).kind;
19+return kind === "closed" || kind === "timeout";
20+}),
21+);
1322const agentCommand = vi.hoisted(() => vi.fn());
14231524const runtime: RuntimeEnv = {
@@ -78,9 +87,24 @@ function mockLocalAgentReply(text = "local") {
7887});
7988}
808990+function createGatewayTimeoutError() {
91+const err = new Error("gateway timeout after 90000ms");
92+err.name = "GatewayTransportError";
93+return Object.assign(err, {
94+kind: "timeout",
95+timeoutMs: 90_000,
96+connectionDetails: {
97+url: "ws://127.0.0.1:18789",
98+urlSource: "local loopback",
99+message: "Gateway target: ws://127.0.0.1:18789",
100+},
101+});
102+}
103+81104vi.mock("../config/config.js", () => ({ getRuntimeConfig: loadConfig, loadConfig }));
82105vi.mock("../gateway/call.js", () => ({
83106 callGateway,
107+ isGatewayTransportError,
84108randomIdempotencyKey: () => "idem-1",
85109}));
86110vi.mock("./agent.js", () => ({ agentCommand }));
@@ -182,6 +206,73 @@ describe("agentCliCommand", () => {
182206});
183207});
184208209+it("uses a fresh embedded session when gateway agent times out", async () => {
210+await withTempStore(async () => {
211+callGateway.mockRejectedValue(createGatewayTimeoutError());
212+mockLocalAgentReply();
213+214+await agentCliCommand(
215+{
216+message: "hi",
217+sessionId: "locked-session",
218+runId: "locked-run",
219+},
220+runtime,
221+);
222+223+expect(callGateway).toHaveBeenCalledTimes(1);
224+expect(agentCommand).toHaveBeenCalledTimes(1);
225+const fallbackOpts = agentCommand.mock.calls[0]?.[0] as {
226+sessionId?: string;
227+sessionKey?: string;
228+runId?: string;
229+resultMetaOverrides?: unknown;
230+};
231+expect(fallbackOpts.sessionId).toMatch(/^gateway-fallback-/);
232+expect(fallbackOpts.sessionId).not.toBe("locked-session");
233+expect(fallbackOpts.sessionKey).toBe(`agent:main:explicit:${fallbackOpts.sessionId}`);
234+expect(fallbackOpts.runId).toBe(fallbackOpts.sessionId);
235+expect(fallbackOpts.resultMetaOverrides).toMatchObject({
236+transport: "embedded",
237+fallbackFrom: "gateway",
238+fallbackReason: "gateway_timeout",
239+fallbackSessionId: fallbackOpts.sessionId,
240+fallbackSessionKey: fallbackOpts.sessionKey,
241+});
242+expect(runtime.error).toHaveBeenCalledWith(
243+expect.stringContaining(
244+"Gateway agent timed out; running embedded agent with fresh session",
245+),
246+);
247+expect(runtime.log).toHaveBeenCalledWith("local");
248+});
249+});
250+251+it("keeps timeout fallback from replacing the routed conversation session key", async () => {
252+await withTempStore(async () => {
253+callGateway.mockRejectedValue(createGatewayTimeoutError());
254+mockLocalAgentReply();
255+256+await agentCliCommand(
257+{
258+message: "hi",
259+to: "+1555",
260+},
261+runtime,
262+);
263+264+const fallbackOpts = agentCommand.mock.calls[0]?.[0] as {
265+sessionId?: string;
266+sessionKey?: string;
267+to?: string;
268+};
269+expect(fallbackOpts.to).toBe("+1555");
270+expect(fallbackOpts.sessionId).toMatch(/^gateway-fallback-/);
271+expect(fallbackOpts.sessionKey).toBe(`agent:main:explicit:${fallbackOpts.sessionId}`);
272+expect(fallbackOpts.sessionKey).not.toBe("agent:main:+1555");
273+});
274+});
275+185276it("passes fallback metadata into JSON embedded fallback output", async () => {
186277await withTempStore(async () => {
187278callGateway.mockRejectedValue(new Error("gateway not connected"));
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。