Skip to content

Commit 463b3d3

Browse files
committed
test: prove spawned child executes inherited tools
1 parent 6f98aa5 commit 463b3d3

2 files changed

Lines changed: 147 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111111
[0.3.0]: https://github.com/agenticoding/pi-agenticoding/compare/v0.2.0...v0.3.0
112112
[0.2.0]: https://github.com/agenticoding/pi-agenticoding/compare/v0.1.0...v0.2.0
113113
[0.1.0]: https://github.com/agenticoding/pi-agenticoding/releases/tag/v0.1.0
114-
115-
## [Unreleased]
116-
117-
### Added
118-
119-
- No changes yet.

agenticoding.test.ts

Lines changed: 147 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import test, { after } from "node:test";
22
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";
57
import { Text } from "@earendil-works/pi-tui";
68
import { registerHandoffCommand } from "./handoff/command.js";
79
import { registerHandoffTool } from "./handoff/tool.js";
@@ -175,6 +177,43 @@ class MockPi {
175177
}
176178
}
177179

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+
178217
// ── TUI indicator tests ───────────────────────────────────────────────
179218

180219
function makeTUICtx(
@@ -895,39 +934,117 @@ test("nested spawn rerenders when stats become unavailable", () => {
895934
});
896935

897936
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");
902941
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;
904946

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"));
912991
}
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-
};
920992

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+
);
9291029

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+
}
9311048
});
9321049

9331050
test("spawn execute passes broad active registered tool formula to child session", async () => {

0 commit comments

Comments
 (0)