@abdwhb-png/pi-test-harness 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +673 -0
  4. package/dist/diagnostics.d.ts +11 -0
  5. package/dist/diagnostics.d.ts.map +1 -0
  6. package/dist/diagnostics.js +61 -0
  7. package/dist/diagnostics.js.map +1 -0
  8. package/dist/events.d.ts +6 -0
  9. package/dist/events.d.ts.map +1 -0
  10. package/dist/events.js +33 -0
  11. package/dist/events.js.map +1 -0
  12. package/dist/index.d.ts +14 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +19 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/mock-pi-script.mjs +176 -0
  17. package/dist/mock-pi.d.ts +32 -0
  18. package/dist/mock-pi.d.ts.map +1 -0
  19. package/dist/mock-pi.js +150 -0
  20. package/dist/mock-pi.js.map +1 -0
  21. package/dist/mock-tools.d.ts +51 -0
  22. package/dist/mock-tools.d.ts.map +1 -0
  23. package/dist/mock-tools.js +192 -0
  24. package/dist/mock-tools.js.map +1 -0
  25. package/dist/mock-ui.d.ts +13 -0
  26. package/dist/mock-ui.d.ts.map +1 -0
  27. package/dist/mock-ui.js +159 -0
  28. package/dist/mock-ui.js.map +1 -0
  29. package/dist/pi-loader-parity.d.ts +36 -0
  30. package/dist/pi-loader-parity.d.ts.map +1 -0
  31. package/dist/pi-loader-parity.js +60 -0
  32. package/dist/pi-loader-parity.js.map +1 -0
  33. package/dist/playbook.d.ts +44 -0
  34. package/dist/playbook.d.ts.map +1 -0
  35. package/dist/playbook.js +143 -0
  36. package/dist/playbook.js.map +1 -0
  37. package/dist/sandbox.d.ts +27 -0
  38. package/dist/sandbox.d.ts.map +1 -0
  39. package/dist/sandbox.js +269 -0
  40. package/dist/sandbox.js.map +1 -0
  41. package/dist/session.d.ts +13 -0
  42. package/dist/session.d.ts.map +1 -0
  43. package/dist/session.js +187 -0
  44. package/dist/session.js.map +1 -0
  45. package/dist/types.d.ts +171 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +5 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/utils.d.ts +32 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +46 -0
  52. package/dist/utils.js.map +1 -0
  53. package/package.json +84 -0
  54. package/skills/pi-test-harness/SKILL.md +451 -0
  55. package/skills/pi-test-harness/evals/evals.json +26 -0
  56. package/skills/pi-test-harness/references/api-reference.md +480 -0
  57. package/skills/pi-test-harness/references/mock-pi-cli.md +135 -0
  58. package/skills/pi-test-harness/references/mock-tools.md +176 -0
  59. package/skills/pi-test-harness/references/mock-ui.md +170 -0
  60. package/skills/pi-test-harness/references/playbook-dsl.md +209 -0
  61. package/skills/pi-test-harness/references/sandbox-install.md +113 -0
  62. package/src/diagnostics.ts +90 -0
  63. package/src/events.ts +43 -0
  64. package/src/index.ts +42 -0
  65. package/src/mock-pi-script.mjs +176 -0
  66. package/src/mock-pi.ts +169 -0
  67. package/src/mock-tools.ts +252 -0
  68. package/src/mock-ui.ts +196 -0
  69. package/src/pi-loader-parity.ts +61 -0
  70. package/src/playbook.ts +189 -0
  71. package/src/sandbox.ts +334 -0
  72. package/src/session.ts +249 -0
  73. package/src/types.ts +203 -0
  74. package/src/utils.ts +46 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Playbook diagnostic messages — clear errors when things diverge.
3
+ */
4
+ function formatAction(action) {
5
+ if (action.type === "say") {
6
+ return `says("${action.text?.slice(0, 60)}${(action.text?.length ?? 0) > 60 ? "..." : ""}")`;
7
+ }
8
+ if (action.type === "call") {
9
+ const params = typeof action.params === "function" ? "<late-bound>" : JSON.stringify(action.params);
10
+ const truncated = params.length > 80 ? params.slice(0, 80) + "..." : params;
11
+ return `calls("${action.toolName}", ${truncated})`;
12
+ }
13
+ return `unknown(${action.type})`;
14
+ }
15
+ export function formatPlaybookDiagnostic(type, state, remainingActions) {
16
+ if (type === "exhausted") {
17
+ const last = state.consumedActions[state.consumedActions.length - 1];
18
+ const lines = [
19
+ `Playbook exhausted unexpectedly.`,
20
+ ` Consumed ${state.consumed} action(s).`,
21
+ ];
22
+ if (last) {
23
+ lines.push(` Last consumed: ${formatAction(last)} at step ${state.consumed}`);
24
+ }
25
+ lines.push("", " The agent loop called streamFn but no more playbook actions were available.", " This usually means a tool call produced an unexpected result that caused", " additional streamFn calls (retries, error handling).");
26
+ return lines.join("\n");
27
+ }
28
+ if (type === "remaining" && remainingActions) {
29
+ const lines = [
30
+ `Playbook not fully consumed after run() completed.`,
31
+ ` Consumed ${state.consumed} of ${state.consumed + remainingActions.length} action(s).`,
32
+ ` Remaining:`,
33
+ ];
34
+ for (const action of remainingActions.slice(0, 5)) {
35
+ lines.push(` - ${formatAction(action)}`);
36
+ }
37
+ if (remainingActions.length > 5) {
38
+ lines.push(` ... +${remainingActions.length - 5} more`);
39
+ }
40
+ lines.push("", " The agent loop ended before all playbook actions were used.", " This usually means a tool was blocked by a hook or returned early,", " causing fewer streamFn calls than expected.");
41
+ return lines.join("\n");
42
+ }
43
+ return "Unknown playbook diagnostic.";
44
+ }
45
+ export function formatToolError(step, toolName, error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ const stack = error instanceof Error ? error.stack : undefined;
48
+ const lines = [
49
+ `Error during tool execution at playbook step ${step} (call "${toolName}"):`,
50
+ ` ${message}`,
51
+ ];
52
+ if (stack) {
53
+ const stackLines = stack.split("\n").slice(1, 4);
54
+ for (const line of stackLines) {
55
+ lines.push(` ${line.trim()}`);
56
+ }
57
+ }
58
+ lines.push("", "This error was thrown by the real tool execution, not by the playbook.", "To capture errors as tool results instead of aborting, set:", " createTestSession({ propagateErrors: false })");
59
+ return lines.join("\n");
60
+ }
61
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Event collection — passively collects all events during a test run.
3
+ */
4
+ import type { TestEvents } from "./types.js";
5
+ export declare function createEventCollector(): TestEvents;
6
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,YAAY,CAAC;AAE7F,wBAAgB,oBAAoB,IAAI,UAAU,CAkCjD"}
package/dist/events.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Event collection — passively collects all events during a test run.
3
+ */
4
+ export function createEventCollector() {
5
+ const all = [];
6
+ const toolCalls = [];
7
+ const toolResults = [];
8
+ const messages = [];
9
+ const ui = [];
10
+ return {
11
+ all,
12
+ toolCalls,
13
+ toolResults,
14
+ messages,
15
+ ui,
16
+ toolCallsFor(name) {
17
+ return toolCalls.filter((tc) => tc.toolName === name);
18
+ },
19
+ toolResultsFor(name) {
20
+ return toolResults.filter((tr) => tr.toolName === name);
21
+ },
22
+ blockedCalls() {
23
+ return toolCalls.filter((tc) => tc.blocked);
24
+ },
25
+ uiCallsFor(method) {
26
+ return ui.filter((u) => u.method === method);
27
+ },
28
+ toolSequence() {
29
+ return toolCalls.map((tc) => tc.toolName);
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,UAAU,oBAAoB;IACnC,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,MAAM,SAAS,GAAqB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,EAAE,GAAmB,EAAE,CAAC;IAE9B,OAAO;QACN,GAAG;QACH,SAAS;QACT,WAAW;QACX,QAAQ;QACR,EAAE;QAEF,YAAY,CAAC,IAAY;YACxB,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QACvD,CAAC;QAED,cAAc,CAAC,IAAY;YAC1B,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QACzD,CAAC;QAED,YAAY;YACX,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;QAC7C,CAAC;QAED,UAAU,CAAC,MAAc;YACxB,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;QAC9C,CAAC;QAED,YAAY;YACX,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QAC3C,CAAC;KACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @abdwhb-png/pi-test-harness
3
+ *
4
+ * Test harness for pi extensions — playbook-based model mocking,
5
+ * session testing, sandbox install verification.
6
+ */
7
+ export { when, calls, says } from "./playbook.js";
8
+ export { createTestSession } from "./session.js";
9
+ export { verifySandboxInstall } from "./sandbox.js";
10
+ export { createMockPi } from "./mock-pi.js";
11
+ export type { TestSession, TestSessionOptions, TestEvents, ToolCallRecord, ToolResultRecord, UICallRecord, MockToolHandler, MockUIConfig, SandboxOptions, SandboxResult, MockPi, MockPiCall, Turn, PlaybookAction, } from "./types.js";
12
+ export { ToolBlockedError } from "./mock-tools.js";
13
+ export { safeRmSync } from "./utils.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,YAAY,EACX,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,cAAc,EACd,aAAa,EACb,MAAM,EACN,UAAU,EACV,IAAI,EACJ,cAAc,GACd,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @abdwhb-png/pi-test-harness
3
+ *
4
+ * Test harness for pi extensions — playbook-based model mocking,
5
+ * session testing, sandbox install verification.
6
+ */
7
+ // DSL builders
8
+ export { when, calls, says } from "./playbook.js";
9
+ // Session
10
+ export { createTestSession } from "./session.js";
11
+ // Sandbox
12
+ export { verifySandboxInstall } from "./sandbox.js";
13
+ // Mock Pi
14
+ export { createMockPi } from "./mock-pi.js";
15
+ // Errors
16
+ export { ToolBlockedError } from "./mock-tools.js";
17
+ // Utilities
18
+ export { safeRmSync } from "./utils.js";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe;AACf,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAElD,UAAU;AACV,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,UAAU;AACV,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,UAAU;AACV,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAoB5C,SAAS;AACT,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,YAAY;AACZ,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mock pi CLI for integration tests.
4
+ *
5
+ * Reads response queue from MOCK_PI_QUEUE_DIR (set by the shim created by createMockPi).
6
+ * Each invocation consumes the next entry from the queue. When the queue is exhausted,
7
+ * the last entry repeats. If no entries are queued, outputs a default echo response.
8
+ *
9
+ * Queue protocol:
10
+ * {queueDir}/queue.json — JSON array of MockPiCall objects
11
+ * {queueDir}/counter — integer: current call index (auto-incremented)
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+
17
+ // Safety timeout — prevent hanging tests if something goes wrong
18
+ const TIMEOUT_MS = 30_000;
19
+ setTimeout(() => {
20
+ process.stderr.write("mock-pi-script: timeout after 30s\n");
21
+ process.exit(124);
22
+ }, TIMEOUT_MS).unref();
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Queue directory (set by the shim script)
26
+ // ---------------------------------------------------------------------------
27
+ const queueDir = process.env.MOCK_PI_QUEUE_DIR;
28
+ if (!queueDir) {
29
+ process.stderr.write("mock-pi-script: MOCK_PI_QUEUE_DIR not set\n");
30
+ process.exit(99);
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Parse CLI arguments (matches what pi-subagents passes to pi)
35
+ // ---------------------------------------------------------------------------
36
+ const args = process.argv.slice(2);
37
+ let task = "";
38
+ let sessionDir = null;
39
+
40
+ let i = 0;
41
+ while (i < args.length) {
42
+ const arg = args[i];
43
+
44
+ // Flags with a value — skip the value
45
+ if (
46
+ arg === "--session-dir" ||
47
+ arg === "--mode" ||
48
+ arg === "--models" ||
49
+ arg === "--tools" ||
50
+ arg === "--extension" ||
51
+ arg === "--append-system-prompt"
52
+ ) {
53
+ if (arg === "--session-dir") sessionDir = args[i + 1] ?? null;
54
+ i += 2;
55
+ continue;
56
+ }
57
+
58
+ // Flags without a value
59
+ if (arg === "-p" || arg === "--no-session" || arg === "--no-extensions") {
60
+ i++;
61
+ continue;
62
+ }
63
+
64
+ // @file — read task from file
65
+ if (arg?.startsWith("@")) {
66
+ try {
67
+ task = fs.readFileSync(arg.slice(1), "utf-8");
68
+ } catch {
69
+ task = "(could not read " + arg.slice(1) + ")";
70
+ }
71
+ i++;
72
+ continue;
73
+ }
74
+
75
+ // Positional — treat as task text
76
+ if (arg && !arg.startsWith("-")) {
77
+ task = arg;
78
+ }
79
+ i++;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Read queue and counter
84
+ // ---------------------------------------------------------------------------
85
+ let queue = [];
86
+ const queueFile = path.join(queueDir, "queue.json");
87
+ if (fs.existsSync(queueFile)) {
88
+ try {
89
+ queue = JSON.parse(fs.readFileSync(queueFile, "utf-8"));
90
+ } catch {
91
+ // Malformed queue — treat as empty
92
+ }
93
+ }
94
+
95
+ const counterFile = path.join(queueDir, "counter");
96
+ let counter = 0;
97
+ if (fs.existsSync(counterFile)) {
98
+ try {
99
+ counter = parseInt(fs.readFileSync(counterFile, "utf-8").trim(), 10) || 0;
100
+ } catch {
101
+ // Missing or unreadable — start at 0
102
+ }
103
+ }
104
+
105
+ // Increment counter for next invocation
106
+ fs.writeFileSync(counterFile, String(counter + 1));
107
+
108
+ // Get the current entry (repeat last when exhausted, null if no queue)
109
+ const entry =
110
+ queue.length > 0 ? queue[Math.min(counter, queue.length - 1)] : null;
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Delay
114
+ // ---------------------------------------------------------------------------
115
+ if (entry?.delay > 0) {
116
+ await new Promise((r) => setTimeout(r, entry.delay));
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Stderr
121
+ // ---------------------------------------------------------------------------
122
+ if (entry?.stderr) {
123
+ process.stderr.write(entry.stderr + "\n");
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Write files (for chain_dir output simulation)
128
+ // ---------------------------------------------------------------------------
129
+ if (entry?.writeFiles) {
130
+ for (const [filePath, content] of Object.entries(entry.writeFiles)) {
131
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
132
+ fs.writeFileSync(filePath, content);
133
+ }
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // JSONL output
138
+ // ---------------------------------------------------------------------------
139
+ if (entry?.jsonl) {
140
+ for (const event of entry.jsonl) {
141
+ console.log(typeof event === "string" ? event : JSON.stringify(event));
142
+ }
143
+ } else {
144
+ const taskClean = task.replace(/^Task:\s*/i, "").slice(0, 500);
145
+ const output = entry?.output ?? "Mock output for: " + taskClean;
146
+ console.log(
147
+ JSON.stringify({
148
+ type: "message_end",
149
+ message: {
150
+ role: "assistant",
151
+ content: [{ type: "text", text: output }],
152
+ model: "mock/test-model",
153
+ usage: {
154
+ input: 100,
155
+ output: 50,
156
+ cacheRead: 0,
157
+ cacheWrite: 0,
158
+ cost: { total: 0.001 },
159
+ },
160
+ },
161
+ }),
162
+ );
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Session file (if requested)
167
+ // ---------------------------------------------------------------------------
168
+ if (sessionDir) {
169
+ fs.mkdirSync(sessionDir, { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(sessionDir, "session-" + Date.now() + ".jsonl"),
172
+ JSON.stringify({ type: "session_start" }) + "\n",
173
+ );
174
+ }
175
+
176
+ process.exit(entry?.exitCode ?? 0);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * createMockPi — mock pi CLI for testing extensions that spawn pi as a subprocess.
3
+ *
4
+ * Creates a temp directory with a `pi` shim (`.cmd` on Windows, shell script on Linux)
5
+ * that prepends to PATH. The shim invokes a mock-pi-script.mjs that reads queued
6
+ * responses from a file-based queue. Responses are consumed in order; the last one
7
+ * repeats when the queue is exhausted.
8
+ *
9
+ * ```ts
10
+ * const mockPi = createMockPi();
11
+ * mockPi.install();
12
+ *
13
+ * mockPi.onCall({ output: "Hello from agent" });
14
+ * mockPi.onCall({ stderr: "crashed", exitCode: 1 });
15
+ *
16
+ * // ... test code that spawns pi ...
17
+ *
18
+ * mockPi.reset();
19
+ * mockPi.uninstall();
20
+ * ```
21
+ */
22
+ import type { MockPi } from "./types.js";
23
+ /**
24
+ * Create a mock pi CLI for testing extensions that spawn pi as a subprocess.
25
+ *
26
+ * **Concurrency constraint**: Designed for serial subprocess spawns within a single test.
27
+ * If your test spawns multiple pi processes concurrently, responses may be consumed
28
+ * out of order. Use separate `createMockPi()` instances for concurrent scenarios,
29
+ * or ensure your test logic doesn't depend on response ordering.
30
+ */
31
+ export declare function createMockPi(): MockPi;
32
+ //# sourceMappingURL=mock-pi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-pi.d.ts","sourceRoot":"","sources":["../src/mock-pi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,YAAY,CAAC;AAoCrD;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAkGrC"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * createMockPi — mock pi CLI for testing extensions that spawn pi as a subprocess.
3
+ *
4
+ * Creates a temp directory with a `pi` shim (`.cmd` on Windows, shell script on Linux)
5
+ * that prepends to PATH. The shim invokes a mock-pi-script.mjs that reads queued
6
+ * responses from a file-based queue. Responses are consumed in order; the last one
7
+ * repeats when the queue is exhausted.
8
+ *
9
+ * ```ts
10
+ * const mockPi = createMockPi();
11
+ * mockPi.install();
12
+ *
13
+ * mockPi.onCall({ output: "Hello from agent" });
14
+ * mockPi.onCall({ stderr: "crashed", exitCode: 1 });
15
+ *
16
+ * // ... test code that spawns pi ...
17
+ *
18
+ * mockPi.reset();
19
+ * mockPi.uninstall();
20
+ * ```
21
+ */
22
+ import * as fs from "node:fs";
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ /** Valid keys for MockPiCall — used for runtime validation. */
28
+ const VALID_MOCK_PI_CALL_KEYS = new Set([
29
+ "output",
30
+ "exitCode",
31
+ "stderr",
32
+ "delay",
33
+ "jsonl",
34
+ "writeFiles",
35
+ ]);
36
+ /**
37
+ * Resolve the mock-pi-script.mjs path.
38
+ *
39
+ * During dev (vitest): src/mock-pi.ts → sibling src/mock-pi-script.mjs
40
+ * After build (dist): dist/mock-pi.js → ../src/mock-pi-script.mjs
41
+ */
42
+ function findMockPiScript() {
43
+ // Direct sibling (running from src/ via vitest)
44
+ const sibling = path.join(__dirname, "mock-pi-script.mjs");
45
+ if (fs.existsSync(sibling))
46
+ return sibling;
47
+ // One level up to package root, then into src/ (running from dist/)
48
+ const fromDist = path.join(__dirname, "..", "src", "mock-pi-script.mjs");
49
+ if (fs.existsSync(fromDist))
50
+ return fromDist;
51
+ throw new Error("Could not find mock-pi-script.mjs. Searched:\n" +
52
+ ` ${sibling}\n` +
53
+ ` ${fromDist}`);
54
+ }
55
+ /**
56
+ * Create a mock pi CLI for testing extensions that spawn pi as a subprocess.
57
+ *
58
+ * **Concurrency constraint**: Designed for serial subprocess spawns within a single test.
59
+ * If your test spawns multiple pi processes concurrently, responses may be consumed
60
+ * out of order. Use separate `createMockPi()` instances for concurrent scenarios,
61
+ * or ensure your test logic doesn't depend on response ordering.
62
+ */
63
+ export function createMockPi() {
64
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-mock-"));
65
+ let originalPath;
66
+ let installed = false;
67
+ const queueFile = path.join(dir, "queue.json");
68
+ const counterFile = path.join(dir, "counter");
69
+ // Initialize empty queue
70
+ fs.writeFileSync(queueFile, "[]");
71
+ fs.writeFileSync(counterFile, "0");
72
+ const scriptPath = findMockPiScript();
73
+ const nodeExe = process.execPath;
74
+ // Safety net: restore PATH if process exits without uninstall()
75
+ const exitHandler = () => {
76
+ if (originalPath !== undefined) {
77
+ process.env.PATH = originalPath;
78
+ }
79
+ };
80
+ return {
81
+ dir,
82
+ install() {
83
+ if (installed)
84
+ return;
85
+ originalPath = process.env.PATH;
86
+ if (process.platform === "win32") {
87
+ // Windows: .cmd batch file shim
88
+ const cmd = [
89
+ "@echo off",
90
+ `set "MOCK_PI_QUEUE_DIR=${dir}"`,
91
+ `"${nodeExe}" "${scriptPath}" %*`,
92
+ ].join("\r\n") + "\r\n";
93
+ fs.writeFileSync(path.join(dir, "pi.cmd"), cmd);
94
+ }
95
+ else {
96
+ // Linux/macOS: shell script shim
97
+ const sh = [
98
+ "#!/bin/sh",
99
+ `MOCK_PI_QUEUE_DIR="${dir}" exec "${nodeExe}" "${scriptPath}" "$@"`,
100
+ ].join("\n") + "\n";
101
+ const piPath = path.join(dir, "pi");
102
+ fs.writeFileSync(piPath, sh);
103
+ fs.chmodSync(piPath, 0o755);
104
+ }
105
+ process.env.PATH = `${dir}${path.delimiter}${originalPath}`;
106
+ process.on("exit", exitHandler);
107
+ installed = true;
108
+ },
109
+ uninstall() {
110
+ if (!installed)
111
+ return;
112
+ process.removeListener("exit", exitHandler);
113
+ if (originalPath !== undefined) {
114
+ process.env.PATH = originalPath;
115
+ originalPath = undefined;
116
+ }
117
+ try {
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ }
120
+ catch {
121
+ // Best-effort cleanup
122
+ }
123
+ installed = false;
124
+ },
125
+ onCall(response) {
126
+ // Validate keys to catch typos early
127
+ const unknown = Object.keys(response).filter((k) => !VALID_MOCK_PI_CALL_KEYS.has(k));
128
+ if (unknown.length > 0) {
129
+ throw new Error(`Unknown MockPiCall key(s): ${unknown.join(", ")}. ` +
130
+ `Valid keys: ${[...VALID_MOCK_PI_CALL_KEYS].join(", ")}`);
131
+ }
132
+ const queue = JSON.parse(fs.readFileSync(queueFile, "utf-8"));
133
+ queue.push(response);
134
+ fs.writeFileSync(queueFile, JSON.stringify(queue));
135
+ },
136
+ reset() {
137
+ fs.writeFileSync(queueFile, "[]");
138
+ fs.writeFileSync(counterFile, "0");
139
+ },
140
+ callCount() {
141
+ try {
142
+ return parseInt(fs.readFileSync(counterFile, "utf-8").trim(), 10) || 0;
143
+ }
144
+ catch {
145
+ return 0;
146
+ }
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=mock-pi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-pi.js","sourceRoot":"","sources":["../src/mock-pi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tool execution interceptor — wraps tool.execute() for tools in mockTools.
3
+ *
4
+ * For mocked tools, the mock replaces tool.execute() and returns controlled
5
+ * values. Extension hooks (tool_call / tool_result) are handled by
6
+ * AgentSession 0.83's internal beforeToolCall/afterToolCall — the mock
7
+ * must NOT re-emit them.
8
+ *
9
+ * For non-mocked tools, the real execute() is called and results are
10
+ * collected for event queries.
11
+ */
12
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
13
+ import type { MockToolHandler } from "./types.js";
14
+ import type { PlaybookState } from "./playbook.js";
15
+ /**
16
+ * Thrown when an extension hook blocks a tool call.
17
+ * Exported for test assertions — no longer thrown by the mock itself since
18
+ * AgentSession 0.83's beforeToolCall handles blocking before execute().
19
+ */
20
+ export declare class ToolBlockedError extends Error {
21
+ readonly toolBlocked: true;
22
+ constructor(reason: string);
23
+ }
24
+ /**
25
+ * Returns true if `err` represents a hook-based tool block.
26
+ *
27
+ * Kept for consumers that catch errors from tool execution flows, though
28
+ * AgentSession 0.83's beforeToolCall blocks before execute() is reached.
29
+ */
30
+ export declare function isBlockedError(err: unknown): boolean;
31
+ /**
32
+ * Intercept tool execution for mocked tools.
33
+ *
34
+ * Unlike the old approach, this does NOT emit tool_call/tool_result hooks
35
+ * manually — AgentSession 0.83's beforeToolCall/afterToolCall handles that.
36
+ * The mock only replaces execute() to return controlled values. Result
37
+ * recording is handled by the session subscriber from tool_execution_end
38
+ * events (which carry the final afterToolCall-modified result).
39
+ *
40
+ * Returns a Set of mocked tool names for the session subscriber to set the
41
+ * mocked flag on recorded results, plus a Set of toolCallIds whose mock
42
+ * returned a ToolResult with isError:true. Pi 0.84's agent loop hardcodes
43
+ * successful execute() as non-error (isError:false), so the subscriber must
44
+ * consult this set to preserve the mock's error intent in collected records.
45
+ */
46
+ export declare function interceptToolExecution(tools: AgentTool[], mockTools: Record<string, MockToolHandler>, playbookState: PlaybookState, propagateErrors: boolean): {
47
+ tools: AgentTool[];
48
+ mockedNames: ReadonlySet<string>;
49
+ mockedErrorToolCallIds: ReadonlySet<string>;
50
+ };
51
+ //# sourceMappingURL=mock-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-tools.d.ts","sourceRoot":"","sources":["../src/mock-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,WAAW,EAAG,IAAI,CAAU;gBAEzB,MAAM,EAAE,MAAM;CAI1B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAWpD;AA0BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAC1C,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,OAAO,GACtB;IACF,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C,CAiDA"}