





















@@ -187,6 +187,39 @@ describe("CLI attempt execution", () => {
187187});
188188}
189189190+async function writeClaudeCliAssistantTranscript(cliSessionId: string) {
191+const homeDir = path.join(tmpDir, `home-${cliSessionId}`);
192+const projectsDir = path.join(homeDir, ".claude", "projects", "demo-workspace");
193+process.env.HOME = homeDir;
194+await fs.mkdir(projectsDir, { recursive: true });
195+await fs.writeFile(
196+path.join(projectsDir, `${cliSessionId}.jsonl`),
197+`${JSON.stringify({
198+ type: "assistant",
199+ message: { role: "assistant", content: [{ type: "text", text: "old reply" }] },
200+ })}\n`,
201+"utf-8",
202+);
203+}
204+205+function makeClaudeCliSessionEntry(
206+openclawSessionId: string,
207+cliSessionId: string,
208+): SessionEntry {
209+return {
210+sessionId: openclawSessionId,
211+updatedAt: Date.now(),
212+cliSessionBindings: {
213+"claude-cli": {
214+sessionId: cliSessionId,
215+authProfileId: "anthropic:claude-cli",
216+},
217+},
218+cliSessionIds: { "claude-cli": cliSessionId },
219+claudeCliSessionId: cliSessionId,
220+};
221+}
222+190223it("clears stale Claude CLI session IDs before retrying after session expiration", async () => {
191224const sessionKey = "agent:main:subagent:cli-expired";
192225const homeDir = path.join(tmpDir, "home");
@@ -265,6 +298,73 @@ describe("CLI attempt execution", () => {
265298expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined();
266299});
267300301+it("clears reused Claude CLI session IDs after AbortError without retrying", async () => {
302+const sessionKey = "agent:main:direct:cli-abort";
303+const cliSessionId = "abort-poisoned-session";
304+await writeClaudeCliAssistantTranscript(cliSessionId);
305+const sessionEntry = makeClaudeCliSessionEntry("session-cli-abort", cliSessionId);
306+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
307+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
308+const abortError = Object.assign(new Error("aborted"), { name: "AbortError" });
309+runCliAgentMock.mockRejectedValueOnce(abortError);
310+311+await expect(
312+runClaudeCliAttempt({
313+ sessionKey,
314+ sessionEntry,
315+ sessionStore,
316+body: "resume after abort",
317+runId: "run-cli-abort",
318+}),
319+).rejects.toMatchObject({ name: "AbortError" });
320+321+expect(runCliAgentMock).toHaveBeenCalledTimes(1);
322+expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId);
323+expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
324+expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
325+expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined();
326+327+const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
328+string,
329+SessionEntry
330+>;
331+expect(persisted[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
332+expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
333+expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined();
334+});
335+336+it("clears reused Claude CLI session IDs after non-expired failover without retrying", async () => {
337+const sessionKey = "agent:main:direct:cli-timeout";
338+const cliSessionId = "timeout-poisoned-session";
339+await writeClaudeCliAssistantTranscript(cliSessionId);
340+const sessionEntry = makeClaudeCliSessionEntry("session-cli-timeout", cliSessionId);
341+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
342+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
343+runCliAgentMock.mockRejectedValueOnce(
344+new FailoverError("CLI produced no output for 60s", {
345+reason: "timeout",
346+provider: "claude-cli",
347+model: "opus",
348+}),
349+);
350+351+await expect(
352+runClaudeCliAttempt({
353+ sessionKey,
354+ sessionEntry,
355+ sessionStore,
356+body: "resume after timeout",
357+runId: "run-cli-timeout",
358+}),
359+).rejects.toMatchObject({ name: "FailoverError", reason: "timeout" });
360+361+expect(runCliAgentMock).toHaveBeenCalledTimes(1);
362+expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId);
363+expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
364+expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
365+expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined();
366+});
367+268368it("does not pass --resume when the stored Claude CLI transcript is missing", async () => {
269369const sessionKey = "agent:main:direct:claude-missing-transcript";
270370const homeDir = path.join(tmpDir, "home");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。