|
1 | 1 | import test, { after } from "node:test"; |
2 | 2 | import assert from "node:assert/strict"; |
3 | | -import { readFile } from "node:fs/promises"; |
4 | | -import type { Theme } from "@earendil-works/pi-coding-agent"; |
| 3 | +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import { AuthStorage, ModelRegistry, type Theme } from "@earendil-works/pi-coding-agent"; |
5 | 7 | import { Text } from "@earendil-works/pi-tui"; |
6 | 8 | import { registerHandoffCommand } from "./handoff/command.js"; |
7 | 9 | import { registerHandoffTool } from "./handoff/tool.js"; |
@@ -175,6 +177,43 @@ class MockPi { |
175 | 177 | } |
176 | 178 | } |
177 | 179 |
|
| 180 | +const EMPTY_USAGE = { |
| 181 | + input: 0, |
| 182 | + output: 0, |
| 183 | + cacheRead: 0, |
| 184 | + cacheWrite: 0, |
| 185 | + totalTokens: 0, |
| 186 | + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, |
| 187 | +}; |
| 188 | + |
| 189 | +function createTestAssistantMessage(model: any, content: any[], stopReason = "stop") { |
| 190 | + return { |
| 191 | + role: "assistant", |
| 192 | + content, |
| 193 | + api: model.api, |
| 194 | + provider: model.provider, |
| 195 | + model: model.id, |
| 196 | + usage: EMPTY_USAGE, |
| 197 | + stopReason, |
| 198 | + timestamp: Date.now(), |
| 199 | + }; |
| 200 | +} |
| 201 | + |
| 202 | +function createTestAssistantStream(message: any): any { |
| 203 | + return { |
| 204 | + async *[Symbol.asyncIterator]() { |
| 205 | + yield { type: "done", reason: message.stopReason, message }; |
| 206 | + }, |
| 207 | + result: async () => message, |
| 208 | + }; |
| 209 | +} |
| 210 | + |
| 211 | +function messageText(message: any): string { |
| 212 | + return (message.content ?? []) |
| 213 | + .map((block: any) => block.type === "text" ? block.text : JSON.stringify(block)) |
| 214 | + .join("\n"); |
| 215 | +} |
| 216 | + |
178 | 217 | // ── TUI indicator tests ─────────────────────────────────────────────── |
179 | 218 |
|
180 | 219 | function makeTUICtx( |
@@ -895,39 +934,117 @@ test("nested spawn rerenders when stats become unavailable", () => { |
895 | 934 | }); |
896 | 935 |
|
897 | 936 | test("agentic e2e spawn child can use active registered non-builtin tool", async () => { |
898 | | - const pi = new MockPi(); |
899 | | - pi.setToolSource("agentic_e2e_probe", "project"); |
900 | | - pi.setActiveTools(["read", "agentic_e2e_probe", "spawn"]); |
901 | | - const state = createState(); |
| 937 | + const tempRoot = await mkdtemp(join(tmpdir(), "pi-agenticoding-a10-")); |
| 938 | + const tempCwd = join(tempRoot, "project"); |
| 939 | + const tempAgentDir = join(tempRoot, "agent"); |
| 940 | + const extensionDir = join(tempCwd, ".pi", "extensions"); |
902 | 941 | const sentinel = "AGENTIC_E2E_PROBE_OK"; |
903 | | - const childPrompt = `Use the agentic_e2e_probe tool and return ${sentinel}.`; |
| 942 | + const oldAgentDir = process.env.PI_CODING_AGENT_DIR; |
| 943 | + const oldOpenAiApiKey = process.env.OPENAI_API_KEY; |
| 944 | + const parentRegistry = ModelRegistry.inMemory(AuthStorage.inMemory()); |
| 945 | + let streamCallCount = 0; |
904 | 946 |
|
905 | | - const mockFactory = async (config: any) => { |
906 | | - const session = { |
907 | | - messages: [] as any[], |
908 | | - prompt: async (prompt: string) => { |
909 | | - assert.match(prompt, /agentic_e2e_probe/); |
910 | | - if (!config.tools.includes("agentic_e2e_probe")) { |
911 | | - throw new Error("Child could not find tool agentic_e2e_probe"); |
| 947 | + try { |
| 948 | + await mkdir(extensionDir, { recursive: true }); |
| 949 | + await mkdir(tempAgentDir, { recursive: true }); |
| 950 | + await writeFile(join(tempCwd, "package.json"), JSON.stringify({ type: "module" })); |
| 951 | + await writeFile( |
| 952 | + join(extensionDir, "agentic-e2e-probe.js"), |
| 953 | + ` |
| 954 | +export default function(pi) { |
| 955 | + pi.registerTool({ |
| 956 | + name: "agentic_e2e_probe", |
| 957 | + label: "Agentic E2E Probe", |
| 958 | + description: "Return the deterministic Story 04 A10 sentinel.", |
| 959 | + promptSnippet: "Call agentic_e2e_probe to return the Story 04 A10 sentinel.", |
| 960 | + parameters: { type: "object", properties: {}, additionalProperties: false }, |
| 961 | + async execute() { |
| 962 | + globalThis.__agenticE2eProbeCalls = (globalThis.__agenticE2eProbeCalls ?? 0) + 1; |
| 963 | + return { |
| 964 | + content: [{ type: "text", text: "${sentinel}" }], |
| 965 | + details: { sentinel: "${sentinel}" }, |
| 966 | + }; |
| 967 | + }, |
| 968 | + }); |
| 969 | +} |
| 970 | +`, |
| 971 | + ); |
| 972 | + |
| 973 | + process.env.PI_CODING_AGENT_DIR = tempAgentDir; |
| 974 | + process.env.OPENAI_API_KEY = "test-openai-key"; |
| 975 | + (globalThis as any).__agenticE2eProbeCalls = 0; |
| 976 | + |
| 977 | + parentRegistry.registerProvider("openai", { |
| 978 | + name: "Agentic E2E OpenAI-compatible provider", |
| 979 | + api: "agentic-e2e-api", |
| 980 | + apiKey: "test-openai-key", |
| 981 | + baseUrl: "http://localhost:0", |
| 982 | + streamSimple: (model: any, context: any) => { |
| 983 | + streamCallCount += 1; |
| 984 | + if (streamCallCount === 1) { |
| 985 | + const promptText = context.messages.map(messageText).join("\n"); |
| 986 | + assert.match(promptText, /agentic_e2e_probe/); |
| 987 | + assert.match(promptText, new RegExp(sentinel)); |
| 988 | + return createTestAssistantStream(createTestAssistantMessage(model, [ |
| 989 | + { type: "toolCall", id: "probe-call-1", name: "agentic_e2e_probe", arguments: {} }, |
| 990 | + ], "tool_calls")); |
912 | 991 | } |
913 | | - session.messages = [{ role: "assistant", content: [{ type: "text", text: sentinel }] }]; |
914 | | - }, |
915 | | - abort: async () => {}, |
916 | | - getSessionStats: () => undefined, |
917 | | - }; |
918 | | - return { session: session as any }; |
919 | | - }; |
920 | 992 |
|
921 | | - registerSpawnTool(pi as any, state, mockFactory as any); |
922 | | - const result = await pi.tools.get("spawn").execute( |
923 | | - "spawn-e2e", |
924 | | - { prompt: childPrompt, thinking: "medium" }, |
925 | | - undefined, |
926 | | - undefined, |
927 | | - { model: { id: "mock-model" }, cwd: "/tmp" }, |
928 | | - ); |
| 993 | + const probeResult = context.messages.find((message: any) => |
| 994 | + message.role === "toolResult" && |
| 995 | + message.toolName === "agentic_e2e_probe" && |
| 996 | + messageText(message).includes(sentinel) |
| 997 | + ); |
| 998 | + const text = probeResult ? sentinel : "AGENTIC_E2E_PROBE_MISSING"; |
| 999 | + return createTestAssistantStream(createTestAssistantMessage(model, [{ type: "text", text }])); |
| 1000 | + }, |
| 1001 | + models: [{ |
| 1002 | + id: "agentic-e2e-model", |
| 1003 | + name: "Agentic E2E Model", |
| 1004 | + reasoning: false, |
| 1005 | + input: ["text"], |
| 1006 | + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, |
| 1007 | + contextWindow: 128000, |
| 1008 | + maxTokens: 1024, |
| 1009 | + }], |
| 1010 | + }); |
| 1011 | + const model = parentRegistry.find("openai", "agentic-e2e-model"); |
| 1012 | + assert.ok(model); |
| 1013 | + |
| 1014 | + const pi = new MockPi(); |
| 1015 | + pi.setToolSource("agentic_e2e_probe", "project"); |
| 1016 | + pi.setActiveTools(["read", "agentic_e2e_probe", "spawn"]); |
| 1017 | + pi.setAllTools(["read", "agentic_e2e_probe", "spawn"]); |
| 1018 | + const state = createState(); |
| 1019 | + const childPrompt = `Use the agentic_e2e_probe tool and return ${sentinel}.`; |
| 1020 | + |
| 1021 | + registerSpawnTool(pi as any, state); |
| 1022 | + const result = await pi.tools.get("spawn").execute( |
| 1023 | + "spawn-e2e", |
| 1024 | + { prompt: childPrompt, thinking: "medium" }, |
| 1025 | + undefined, |
| 1026 | + undefined, |
| 1027 | + { model, cwd: tempCwd }, |
| 1028 | + ); |
929 | 1029 |
|
930 | | - assert.equal(result.content[0].text, sentinel); |
| 1030 | + assert.equal(result.content[0].text, sentinel); |
| 1031 | + assert.equal((globalThis as any).__agenticE2eProbeCalls, 1); |
| 1032 | + assert.equal(streamCallCount, 2); |
| 1033 | + } finally { |
| 1034 | + parentRegistry.unregisterProvider("openai"); |
| 1035 | + if (oldAgentDir === undefined) { |
| 1036 | + delete process.env.PI_CODING_AGENT_DIR; |
| 1037 | + } else { |
| 1038 | + process.env.PI_CODING_AGENT_DIR = oldAgentDir; |
| 1039 | + } |
| 1040 | + if (oldOpenAiApiKey === undefined) { |
| 1041 | + delete process.env.OPENAI_API_KEY; |
| 1042 | + } else { |
| 1043 | + process.env.OPENAI_API_KEY = oldOpenAiApiKey; |
| 1044 | + } |
| 1045 | + delete (globalThis as any).__agenticE2eProbeCalls; |
| 1046 | + await rm(tempRoot, { recursive: true, force: true }); |
| 1047 | + } |
931 | 1048 | }); |
932 | 1049 |
|
933 | 1050 | test("spawn execute passes broad active registered tool formula to child session", async () => { |
|
0 commit comments