
























@@ -146,6 +146,14 @@ function assistantMessage(text: string, timestamp: number) {
146146};
147147}
148148149+function userMessage(text: string, timestamp: number) {
150+return {
151+role: "user" as const,
152+content: [{ type: "text" as const, text }],
153+ timestamp,
154+};
155+}
156+149157function createAppServerHarness(
150158requestImpl: (method: string, params: unknown) => Promise<unknown>,
151159options: {
@@ -752,6 +760,34 @@ describe("runCodexAppServerAttempt", () => {
752760);
753761});
754762763+it("projects mirrored history when starting Codex without a native thread binding", async () => {
764+const sessionFile = path.join(tempDir, "session.jsonl");
765+const workspaceDir = path.join(tempDir, "workspace");
766+const sessionManager = SessionManager.open(sessionFile);
767+sessionManager.appendMessage(userMessage("we are fixing the Opik default project", Date.now()));
768+sessionManager.appendMessage(assistantMessage("Opik default project context", Date.now() + 1));
769+const harness = createStartedThreadHarness();
770+const params = createParams(sessionFile, workspaceDir);
771+params.prompt = "make the default webpage openclaw";
772+773+const run = runCodexAppServerAttempt(params);
774+await harness.waitForMethod("turn/start");
775+await new Promise<void>((resolve) => setImmediate(resolve));
776+await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
777+await run;
778+779+const turnStart = harness.requests.find((request) => request.method === "turn/start");
780+const inputText =
781+(turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ??
782+"";
783+784+expect(inputText).toContain("OpenClaw assembled context for this turn:");
785+expect(inputText).toContain("we are fixing the Opik default project");
786+expect(inputText).toContain("Opik default project context");
787+expect(inputText).toContain("Current user request:");
788+expect(inputText).toContain("make the default webpage openclaw");
789+});
790+755791it("passes OpenClaw bootstrap files through Codex config instructions", async () => {
756792const sessionFile = path.join(tempDir, "session.jsonl");
757793const workspaceDir = path.join(tempDir, "workspace");
@@ -2048,6 +2084,90 @@ describe("runCodexAppServerAttempt", () => {
20482084expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
20492085});
205020862087+it("resumes a bound Codex thread when dynamic tools are reordered", async () => {
2088+const sessionFile = path.join(tempDir, "session.jsonl");
2089+const workspaceDir = path.join(tempDir, "workspace");
2090+const params = createParams(sessionFile, workspaceDir);
2091+const appServer = createThreadLifecycleAppServerOptions();
2092+const request = vi.fn(async (method: string) => {
2093+if (method === "thread/start") {
2094+return threadStartResult("thread-existing");
2095+}
2096+if (method === "thread/resume") {
2097+return threadStartResult("thread-existing");
2098+}
2099+throw new Error(`unexpected method: ${method}`);
2100+});
2101+2102+await startOrResumeThread({
2103+client: { request } as never,
2104+ params,
2105+cwd: workspaceDir,
2106+dynamicTools: [createNamedDynamicTool("wiki_status"), createNamedDynamicTool("diffs")],
2107+ appServer,
2108+});
2109+const binding = await startOrResumeThread({
2110+client: { request } as never,
2111+ params,
2112+cwd: workspaceDir,
2113+dynamicTools: [createNamedDynamicTool("diffs"), createNamedDynamicTool("wiki_status")],
2114+ appServer,
2115+});
2116+2117+expect(binding.threadId).toBe("thread-existing");
2118+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
2119+});
2120+2121+it("keeps the previous dynamic tool fingerprint for transient no-tool maintenance turns", async () => {
2122+const sessionFile = path.join(tempDir, "session.jsonl");
2123+const workspaceDir = path.join(tempDir, "workspace");
2124+const params = createParams(sessionFile, workspaceDir);
2125+const appServer = createThreadLifecycleAppServerOptions();
2126+let nextThread = 1;
2127+const request = vi.fn(async (method: string) => {
2128+if (method === "thread/start") {
2129+return threadStartResult(`thread-${nextThread++}`);
2130+}
2131+if (method === "thread/resume") {
2132+return threadStartResult("thread-1");
2133+}
2134+throw new Error(`unexpected method: ${method}`);
2135+});
2136+2137+await startOrResumeThread({
2138+client: { request } as never,
2139+ params,
2140+cwd: workspaceDir,
2141+dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
2142+ appServer,
2143+});
2144+const fingerprint = (await readCodexAppServerBinding(sessionFile))?.dynamicToolsFingerprint;
2145+await startOrResumeThread({
2146+client: { request } as never,
2147+ params,
2148+cwd: workspaceDir,
2149+dynamicTools: [],
2150+ appServer,
2151+});
2152+await startOrResumeThread({
2153+client: { request } as never,
2154+ params,
2155+cwd: workspaceDir,
2156+dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
2157+ appServer,
2158+});
2159+2160+await expect(readCodexAppServerBinding(sessionFile)).resolves.toMatchObject({
2161+dynamicToolsFingerprint: fingerprint,
2162+threadId: "thread-1",
2163+});
2164+expect(request.mock.calls.map(([method]) => method)).toEqual([
2165+"thread/start",
2166+"thread/start",
2167+"thread/resume",
2168+]);
2169+});
2170+20512171it("preserves the binding when the app-server closes during thread resume", async () => {
20522172const sessionFile = path.join(tempDir, "session.jsonl");
20532173const workspaceDir = path.join(tempDir, "workspace");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。