diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 7a186b532f8..427dea289ca 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -73,21 +73,51 @@ async function invokeWrappedTestStream( return await Promise.resolve(wrappedFn({} as never, {} as never, {} as never)); } +function requireRecord(value: unknown, label: string): Record { + expect(value, label).toBeTypeOf("object"); + expect(value, label).not.toBeNull(); + return value as Record; +} + +function requireContentItem( + content: Array<{ type?: string; text?: string; name?: string }> | unknown[], + index = 0, +) { + return requireRecord(content[index], `content item ${index}`); +} + +function expectSingleTextContent( + content: Array<{ type?: string; text?: string }> | unknown[], + textFragment: string, +) { + expect(content).toHaveLength(1); + const item = requireContentItem(content); + expect(item.type).toBe("text"); + expect(item.text).toContain(textFragment); +} + +function expectSingleToolCallContent( + content: Array<{ type?: string; name?: string }> | unknown[], + name: string, +) { + expect(content).toHaveLength(1); + const item = requireContentItem(content); + expect(item.type).toBe("toolCall"); + expect(item.name).toBe(name); +} + describe("buildEmbeddedAttemptToolRunContext", () => { it("carries runtime toolsAllow into coding tool construction", () => { - expect( - buildEmbeddedAttemptToolRunContext({ - trigger: "manual", - jobId: "job-1", - memoryFlushWritePath: "memory/log.md", - toolsAllow: ["memory_search", "memory_get"], - }), - ).toMatchObject({ + const context = buildEmbeddedAttemptToolRunContext({ trigger: "manual", jobId: "job-1", memoryFlushWritePath: "memory/log.md", - runtimeToolAllowlist: ["memory_search", "memory_get"], + toolsAllow: ["memory_search", "memory_get"], }); + expect(context.trigger).toBe("manual"); + expect(context.jobId).toBe("job-1"); + expect(context.memoryFlushWritePath).toBe("memory/log.md"); + expect(context.runtimeToolAllowlist).toEqual(["memory_search", "memory_get"]); }); }); @@ -280,15 +310,9 @@ describe("normalizeMessagesForLlmBoundary", () => { ) as unknown as Array>; expect(output).toHaveLength(3); - expect(output).not.toEqual( - expect.arrayContaining([expect.objectContaining({ content: "old secret runtime context" })]), - ); - expect(output).toEqual( - expect.arrayContaining([expect.objectContaining({ content: "secret runtime context" })]), - ); - expect(output).toEqual( - expect.arrayContaining([expect.objectContaining({ customType: "other-extension-context" })]), - ); + expect(output.some((item) => item.content === "old secret runtime context")).toBe(false); + expect(output.some((item) => item.content === "secret runtime context")).toBe(true); + expect(output.some((item) => item.customType === "other-extension-context")).toBe(true); }); it("keeps only safe blocked metadata at the LLM boundary", () => { @@ -720,10 +744,8 @@ describe("mergeOrphanedTrailingUserPrompt", () => { } as never, }); - expect(result).toMatchObject({ - merged: true, - removeLeaf: true, - }); + expect(result.merged).toBe(true); + expect(result.removeLeaf).toBe(true); expect(result.prompt).toContain("please inspect this inline image"); expect(result.prompt).toContain("[image_url] inline data URI (image/png, 4118 chars)"); expect(result.prompt).not.toContain("base64"); @@ -748,10 +770,8 @@ describe("mergeOrphanedTrailingUserPrompt", () => { } as never, }); - expect(result).toMatchObject({ - merged: true, - removeLeaf: true, - }); + expect(result.merged).toBe(true); + expect(result.removeLeaf).toBe(true); expect(result.prompt).toContain("[value] inline data URI (image/png, 10022 chars)"); expect(result.prompt).toContain("bbbb"); expect(result.prompt).toContain("(2000 chars)"); @@ -826,11 +846,12 @@ describe("resolveEmbeddedAgentStreamFn", () => { }, }); - await expect( - streamFn({ provider: "demo-provider", id: "demo-model" } as never, {} as never, {}), - ).resolves.toMatchObject({ - apiKey: "demo-runtime-key", - }); + const streamOptions = await streamFn( + { provider: "demo-provider", id: "demo-model" } as never, + {} as never, + {}, + ); + expect(requireRecord(streamOptions, "stream options").apiKey).toBe("demo-runtime-key"); expect(providerStreamFn).toHaveBeenCalledTimes(1); }); @@ -847,17 +868,16 @@ describe("resolveEmbeddedAgentStreamFn", () => { } as never, }); - await expect( - streamFn( - { provider: "demo-provider", id: "demo-model" } as never, - { - systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`, - } as never, - {}, - ), - ).resolves.toMatchObject({ - systemPrompt: "Stable prefix\nDynamic suffix", - }); + const context = await streamFn( + { provider: "demo-provider", id: "demo-model" } as never, + { + systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`, + } as never, + {}, + ); + expect(requireRecord(context, "stream context").systemPrompt).toBe( + "Stable prefix\nDynamic suffix", + ); expect(providerStreamFn).toHaveBeenCalledTimes(1); }); it("routes supported default streamSimple fallbacks through boundary-aware transports", () => { @@ -1154,10 +1174,9 @@ describe("wrapStreamFnTrimToolCallNames", () => { for (let i = 0; i < 10; i += 1) { const stream = await Promise.resolve(wrappedFn({} as never, {} as never, {} as never)); const result = await stream.result(); - expect(result).toMatchObject({ - role: "assistant", - content: [{ type: "toolCall", name: "exec" }], - }); + const message = requireRecord(result, "result message"); + expect(message.role).toBe("assistant"); + expectSingleToolCallContent(message.content as unknown[], "exec"); } const blockedStream = await Promise.resolve(wrappedFn({} as never, {} as never, {} as never)); @@ -1167,12 +1186,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(blockedResult.role).toBe("assistant"); - expect(blockedResult.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"exec"'), - }), - ]); + expectSingleTextContent(blockedResult.content, '"exec"'); }); it("leaves repeated unavailable tool calls alone when the unknown-tool guard is disabled", async () => { @@ -1190,10 +1204,9 @@ describe("wrapStreamFnTrimToolCallNames", () => { for (let i = 0; i < 11; i += 1) { const stream = await Promise.resolve(wrappedFn({} as never, {} as never, {} as never)); const result = await stream.result(); - expect(result).toMatchObject({ - role: "assistant", - content: [{ type: "toolCall", name: "exec" }], - }); + const message = requireRecord(result, "result message"); + expect(message.role).toBe("assistant"); + expectSingleToolCallContent(message.content as unknown[], "exec"); } }); @@ -1221,7 +1234,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { expect(partialToolCall.name).toBe("exec"); expect(messageToolCall.name).toBe("exec"); - expect(result.content).toEqual([expect.objectContaining({ type: "toolCall", name: "exec" })]); + expectSingleToolCallContent(result.content, "exec"); }); it("does not reset the unavailable-tool streak on partial-only stream chunks", async () => { @@ -1256,12 +1269,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(secondResult.role).toBe("assistant"); - expect(secondResult.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"exec"'), - }), - ]); + expectSingleTextContent(secondResult.content, '"exec"'); }); it("counts the final unknown-tool retry when streamed messages omit the tool name", async () => { @@ -1296,12 +1304,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(secondResult.role).toBe("assistant"); - expect(secondResult.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"exec"'), - }), - ]); + expectSingleTextContent(secondResult.content, '"exec"'); }); it("resets a provisional streamed unknown-tool retry when later chunks resolve to an allowed tool", async () => { @@ -1351,12 +1354,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(secondResult.role).toBe("assistant"); - expect(secondResult.content).toEqual([ - expect.objectContaining({ - type: "toolCall", - name: "ex", - }), - ]); + expectSingleToolCallContent(secondResult.content, "ex"); }); it("keeps processing later streamed messages after one streamed unknown-tool retry was counted", async () => { @@ -1406,12 +1404,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(secondResult.role).toBe("assistant"); - expect(secondResult.content).toEqual([ - expect.objectContaining({ - type: "toolCall", - name: "re", - }), - ]); + expectSingleToolCallContent(secondResult.content, "re"); }); it("resets a stale unknown-tool streak when a streamed message mixes allowed and unknown tools", async () => { @@ -1475,12 +1468,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { }; expect(thirdResult.role).toBe("assistant"); - expect(thirdResult.content).toEqual([ - expect.objectContaining({ - type: "toolCall", - name: "ex", - }), - ]); + expectSingleToolCallContent(thirdResult.content, "ex"); }); it("infers tool names from malformed toolCallId variants when allowlist is present", async () => { @@ -1626,12 +1614,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { content: Array<{ type: string; text?: string }>; }; - expect(result.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"blank tool name"'), - }), - ]); + expectSingleTextContent(result.content, '"blank tool name"'); expect(finalToolCall.name).toBe(""); expect(finalToolCall.id).toBe("call_auto_1"); }); @@ -1764,12 +1747,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { content: Array<{ type: string; text?: string }>; }; - expect(result.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"blank tool name"'), - }), - ]); + expectSingleTextContent(result.content, '"blank tool name"'); expect(finalToolCall.name).toBe(""); }); it("leaves provisional blank streamed names recoverable while stopping final blank dispatch", async () => { @@ -1790,12 +1768,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { content: Array<{ type: string; text?: string }>; }; - expect(result.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"blank tool name"'), - }), - ]); + expectSingleTextContent(result.content, '"blank tool name"'); expect(partialToolCall.name).toBe(" "); expect(finalToolCall.name).toBe("\t "); expect(baseFn).toHaveBeenCalledTimes(1); @@ -1816,12 +1789,7 @@ describe("wrapStreamFnTrimToolCallNames", () => { content: Array<{ type: string; text?: string }>; }; - expect(result.content).toEqual([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining('"blank tool name"'), - }), - ]); + expectSingleTextContent(result.content, '"blank tool name"'); expect(finalToolCall.name).toBe(""); }); @@ -2300,32 +2268,30 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { expect(baseFn).toHaveBeenCalledTimes(1); const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] }; - expect(seenContext.messages).toEqual([ + expect(seenContext.messages).toHaveLength(3); + expect(seenContext.messages[0]).toEqual({ + role: "assistant", + content: [ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "call_1", name: "read", arguments: {} }, + ], + }); + const repairedToolResult = requireRecord(seenContext.messages[1], "repaired tool result"); + expect(repairedToolResult.role).toBe("toolResult"); + expect(repairedToolResult.toolCallId).toBe("call_1"); + expect(repairedToolResult.toolName).toBe("read"); + expect(repairedToolResult.content).toEqual([ { - role: "assistant", - content: [ - { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], - }, - { - role: "toolResult", - toolCallId: "call_1", - toolName: "read", - content: [ - { - type: "text", - text: "[openclaw] missing tool result in session history; inserted synthetic error result for transcript repair.", - }, - ], - isError: true, - timestamp: expect.any(Number), - }, - { - role: "user", - content: [{ type: "text", text: "retry" }], + type: "text", + text: "[openclaw] missing tool result in session history; inserted synthetic error result for transcript repair.", }, ]); + expect(repairedToolResult.isError).toBe(true); + expect(repairedToolResult.timestamp).toBeTypeOf("number"); + expect(seenContext.messages[2]).toEqual({ + role: "user", + content: [{ type: "text", text: "retry" }], + }); }); it("preserves sessions_spawn attachment payloads on replay", async () => { @@ -3482,15 +3448,16 @@ describe("buildAfterTurnRuntimeContext", () => { activeAgentId: "main", }); - expect(legacy.activeProcessSessions).toEqual([ - expect.objectContaining({ - sessionId: "sess-session-id", - command: "sleep 600", - pid: 1234, - }), - ]); - expect(legacy.activeProcessSessions).not.toEqual( - expect.arrayContaining([expect.objectContaining({ sessionId: "sess-other" })]), + const activeProcessSessions = legacy.activeProcessSessions as + | Array<{ sessionId?: string; command?: string; pid?: number }> + | undefined; + expect(activeProcessSessions).toHaveLength(1); + const activeSession = requireRecord(activeProcessSessions?.[0], "active process session"); + expect(activeSession.sessionId).toBe("sess-session-id"); + expect(activeSession.command).toBe("sleep 600"); + expect(activeSession.pid).toBe(1234); + expect(activeProcessSessions?.some((session) => session.sessionId === "sess-other")).toBe( + false, ); } finally { resetProcessRegistryForTests(); @@ -3519,10 +3486,8 @@ describe("buildAfterTurnRuntimeContext", () => { agentDir: "/tmp/agent", }); - expect(legacy).toMatchObject({ - provider: "openai-codex", - model: "gpt-5.4", - }); + expect(legacy.provider).toBe("openai-codex"); + expect(legacy.model).toBe("gpt-5.4"); }); it("resolves compaction.model override in runtime context so all context engines use the correct model", () => { @@ -3558,12 +3523,10 @@ describe("buildAfterTurnRuntimeContext", () => { // buildEmbeddedCompactionRuntimeContext now resolves the override eagerly // so that context engines (including third-party ones) receive the correct // compaction model in the runtime context. - expect(legacy).toMatchObject({ - provider: "openrouter", - model: "anthropic/claude-sonnet-4-5", - // Auth profile dropped because provider changed from openai-codex to openrouter - authProfileId: undefined, - }); + expect(legacy.provider).toBe("openrouter"); + expect(legacy.model).toBe("anthropic/claude-sonnet-4-5"); + // Auth profile dropped because provider changed from openai-codex to openrouter. + expect(legacy.authProfileId).toBeUndefined(); }); it("includes resolved auth profile fields for context-engine afterTurn compaction", () => { const promptCache = buildContextEnginePromptCacheInfo({ @@ -3599,20 +3562,14 @@ describe("buildAfterTurnRuntimeContext", () => { promptCache, }); - expect(legacy).toMatchObject({ - authProfileId: "openai:p1", - provider: "openai-codex", - model: "gpt-5.4", - workspaceDir: "/tmp/workspace", - agentDir: "/tmp/agent", - tokenBudget: 1050000, - currentTokenCount: 52, - promptCache: { - lastCallUsage: { - total: 57, - }, - }, - }); + expect(legacy.authProfileId).toBe("openai:p1"); + expect(legacy.provider).toBe("openai-codex"); + expect(legacy.model).toBe("gpt-5.4"); + expect(legacy.workspaceDir).toBe("/tmp/workspace"); + expect(legacy.agentDir).toBe("/tmp/agent"); + expect(legacy.tokenBudget).toBe(1050000); + expect(legacy.currentTokenCount).toBe(52); + expect(legacy.promptCache?.lastCallUsage?.total).toBe(57); }); it("derives afterTurn token count from the current assistant usage snapshot", () => { @@ -3648,14 +3605,8 @@ describe("buildAfterTurnRuntimeContext", () => { promptCache, }); - expect(legacy).toMatchObject({ - currentTokenCount: 52, - promptCache: { - lastCallUsage: { - total: 57, - }, - }, - }); + expect(legacy.currentTokenCount).toBe(52); + expect(legacy.promptCache?.lastCallUsage?.total).toBe(57); }); it("preserves sender and channel routing context for scoped compaction discovery", () => { @@ -3684,11 +3635,9 @@ describe("buildAfterTurnRuntimeContext", () => { agentDir: "/tmp/agent", }); - expect(legacy).toMatchObject({ - senderId: "user-123", - currentChannelId: "C123", - currentThreadTs: "thread-9", - currentMessageId: "msg-42", - }); + expect(legacy.senderId).toBe("user-123"); + expect(legacy.currentChannelId).toBe("C123"); + expect(legacy.currentThreadTs).toBe("thread-9"); + expect(legacy.currentMessageId).toBe("msg-42"); }); });