@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
package/src/types.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Shared types for pi-test-harness.
3
+ */
4
+
5
+ import type {
6
+ AgentSession,
7
+ AgentSessionEvent,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
10
+
11
+ // ── Playbook types ──────────────────────────────────────────
12
+
13
+ export interface PlaybookAction {
14
+ type: "call" | "say";
15
+ /** For "call": tool name */
16
+ toolName?: string;
17
+ /** For "call": static or late-bound params */
18
+ params?: Record<string, unknown> | (() => Record<string, unknown>);
19
+ /** For "say": text content */
20
+ text?: string;
21
+ /** Optional callback after tool execution */
22
+ thenCallback?: (result: ToolResultRecord) => void;
23
+ }
24
+
25
+ export interface Turn {
26
+ prompt: string;
27
+ actions: PlaybookAction[];
28
+ }
29
+
30
+ // ── Mock types ──────────────────────────────────────────────
31
+
32
+ export interface ToolResult {
33
+ content: Array<{ type: string; text: string }>;
34
+ details?: unknown;
35
+ isError?: boolean;
36
+ }
37
+
38
+ export type MockToolHandler =
39
+ | string
40
+ | ToolResult
41
+ | ((params: Record<string, unknown>) => string | ToolResult);
42
+
43
+ export interface MockUIConfig {
44
+ confirm?: boolean | ((title: string, message: string) => boolean);
45
+ select?:
46
+ | number
47
+ | string
48
+ | ((title: string, items: string[]) => string | undefined);
49
+ input?:
50
+ | string
51
+ | ((title: string, placeholder?: string) => string | undefined);
52
+ editor?: string | ((title: string, prefilled?: string) => string | undefined);
53
+ }
54
+
55
+ // ── Event collection types ──────────────────────────────────
56
+
57
+ export interface ToolCallRecord {
58
+ step: number;
59
+ toolName: string;
60
+ input: Record<string, unknown>;
61
+ blocked: boolean;
62
+ blockReason?: string;
63
+ }
64
+
65
+ export interface ToolResultRecord {
66
+ step: number;
67
+ toolName: string;
68
+ toolCallId: string;
69
+ text: string;
70
+ content: Array<{ type: string; text?: string }>;
71
+ isError: boolean;
72
+ details?: unknown;
73
+ mocked: boolean;
74
+ }
75
+
76
+ export interface UICallRecord {
77
+ method: string;
78
+ args: unknown[];
79
+ returnValue?: unknown;
80
+ }
81
+
82
+ export interface TestEvents {
83
+ all: AgentSessionEvent[];
84
+ toolCalls: ToolCallRecord[];
85
+ toolResults: ToolResultRecord[];
86
+ messages: AgentMessage[];
87
+ ui: UICallRecord[];
88
+
89
+ toolCallsFor(name: string): ToolCallRecord[];
90
+ toolResultsFor(name: string): ToolResultRecord[];
91
+ blockedCalls(): ToolCallRecord[];
92
+ uiCallsFor(method: string): UICallRecord[];
93
+ /** Ordered list of tool names as they were called */
94
+ toolSequence(): string[];
95
+ }
96
+
97
+ // ── Session types ───────────────────────────────────────────
98
+
99
+ export interface TestSessionOptions {
100
+ /** Extension file paths to load */
101
+ extensions?: string[];
102
+ /** Extension factory functions (inline) */
103
+ extensionFactories?: Array<(pi: any) => void>;
104
+ /** Working directory (auto temp dir if omitted, cleaned on dispose) */
105
+ cwd?: string;
106
+ /** System prompt override */
107
+ systemPrompt?: string;
108
+
109
+ /** Mock tool execution (intercepts tool.execute()) */
110
+ mockTools?: Record<string, MockToolHandler>;
111
+ /** Mock UI responses */
112
+ mockUI?: MockUIConfig;
113
+
114
+ /** Abort on real tool throw (default: true) */
115
+ propagateErrors?: boolean;
116
+ }
117
+
118
+ export interface TestSession {
119
+ /** Run a conversation script */
120
+ run(...turns: Turn[]): Promise<void>;
121
+ /** Real session underneath */
122
+ session: AgentSession;
123
+ /** Working directory */
124
+ cwd: string;
125
+ /** Collected events */
126
+ events: TestEvents;
127
+ /** Playbook consumption state */
128
+ playbook: { consumed: number; remaining: number };
129
+ /** Cleanup */
130
+ dispose(): void;
131
+ }
132
+
133
+ // ── MockPi types ────────────────────────────────────────────
134
+
135
+ /** Configuration for a single mock pi invocation response. */
136
+ export interface MockPiCall {
137
+ /** Text output from the mock agent */
138
+ output?: string;
139
+ /** Exit code (default: 0) */
140
+ exitCode?: number;
141
+ /** Stderr output */
142
+ stderr?: string;
143
+ /** Delay in ms before responding */
144
+ delay?: number;
145
+ /** Raw JSONL events to emit instead of default message_end */
146
+ jsonl?: object[];
147
+ /** Files to write before exiting (path → content) */
148
+ writeFiles?: Record<string, string>;
149
+ }
150
+
151
+ /** Mock pi CLI for testing extensions that spawn pi as a subprocess. */
152
+ export interface MockPi {
153
+ /** Create temp dir with pi shim, prepend to PATH */
154
+ install(): void;
155
+ /** Remove from PATH, delete temp dir */
156
+ uninstall(): void;
157
+ /** Queue a response for the next pi invocation */
158
+ onCall(response: MockPiCall): void;
159
+ /** Clear the response queue and reset the call counter */
160
+ reset(): void;
161
+ /** Number of times the mock pi has been invoked */
162
+ callCount(): number;
163
+ /** The temporary directory containing the queue and shim */
164
+ dir: string;
165
+ }
166
+
167
+ // ── Sandbox types ───────────────────────────────────────────
168
+
169
+ export interface SandboxOptions {
170
+ /** Package directory (runs npm pack) */
171
+ packageDir: string;
172
+ /** Expected resources after install */
173
+ expect?: {
174
+ extensions?: number;
175
+ tools?: string[];
176
+ skills?: number;
177
+ };
178
+ /** Optional smoke test in the sandbox */
179
+ smoke?: {
180
+ mockTools?: Record<string, MockToolHandler>;
181
+ script: Turn[];
182
+ };
183
+ /**
184
+ * Custom npm command and arguments for pack/install.
185
+ * Default: [process.platform === "win32" ? "npm.cmd" : "npm"]
186
+ * Use ["sfw", "npm"] to run through Socket Firewall, or provide
187
+ * an array like ["/usr/local/bin/node", "/path/to/npm_cli.js"]
188
+ * for a non-standard npm executable.
189
+ */
190
+ npmCommand?: string[];
191
+ }
192
+
193
+ export interface SandboxResult {
194
+ loaded: {
195
+ extensions: number;
196
+ extensionErrors: string[];
197
+ tools: string[];
198
+ skills: number;
199
+ };
200
+ smoke?: {
201
+ events: TestEvents;
202
+ };
203
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Utility helpers for pi-test-harness consumers.
3
+ */
4
+
5
+ import { rmSync } from "node:fs";
6
+
7
+ /**
8
+ * Remove a file, silently ignoring EPERM/EBUSY errors.
9
+ *
10
+ * **Why this exists**: On Windows, pi extensions that open SQLite databases
11
+ * do so in the `session_start` event handler. The corresponding close happens
12
+ * in `session_shutdown` — but that event fires at Node.js **process exit**,
13
+ * NOT when `session.dispose()` is called. This means DB files remain locked
14
+ * for the lifetime of the test runner process.
15
+ *
16
+ * Safe pattern in afterEach:
17
+ * ```ts
18
+ * afterEach(() => {
19
+ * safeRmSync(dbPath);
20
+ * safeRmSync(dbPath + "-wal");
21
+ * safeRmSync(dbPath + "-shm");
22
+ * });
23
+ * ```
24
+ *
25
+ * Files are cleaned up by the OS when the process exits (or on next run).
26
+ * Using unique DB paths per test ensures isolation.
27
+ */
28
+ /**
29
+ * Returns true for errno codes that represent a Windows file-lock condition.
30
+ * Exported for unit testing — not part of the public API contract.
31
+ * @internal
32
+ */
33
+ export function _isLockedFileError(err: unknown): boolean {
34
+ const code = (err as NodeJS.ErrnoException).code;
35
+ return code === "EPERM" || code === "EBUSY";
36
+ }
37
+
38
+ export function safeRmSync(filePath: string): void {
39
+ try {
40
+ rmSync(filePath, { force: true });
41
+ } catch (err) {
42
+ // Only swallow Windows file-lock errors. Everything else (permissions,
43
+ // bad path type, disk full) should still propagate so failures are visible.
44
+ if (!_isLockedFileError(err)) throw err;
45
+ }
46
+ }