@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.
- package/CHANGELOG.md +161 -0
- package/LICENSE +21 -0
- package/README.md +673 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +61 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/events.d.ts +6 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +33 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-pi-script.mjs +176 -0
- package/dist/mock-pi.d.ts +32 -0
- package/dist/mock-pi.d.ts.map +1 -0
- package/dist/mock-pi.js +150 -0
- package/dist/mock-pi.js.map +1 -0
- package/dist/mock-tools.d.ts +51 -0
- package/dist/mock-tools.d.ts.map +1 -0
- package/dist/mock-tools.js +192 -0
- package/dist/mock-tools.js.map +1 -0
- package/dist/mock-ui.d.ts +13 -0
- package/dist/mock-ui.d.ts.map +1 -0
- package/dist/mock-ui.js +159 -0
- package/dist/mock-ui.js.map +1 -0
- package/dist/pi-loader-parity.d.ts +36 -0
- package/dist/pi-loader-parity.d.ts.map +1 -0
- package/dist/pi-loader-parity.js +60 -0
- package/dist/pi-loader-parity.js.map +1 -0
- package/dist/playbook.d.ts +44 -0
- package/dist/playbook.d.ts.map +1 -0
- package/dist/playbook.js +143 -0
- package/dist/playbook.js.map +1 -0
- package/dist/sandbox.d.ts +27 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +269 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +187 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +32 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +46 -0
- package/dist/utils.js.map +1 -0
- package/package.json +84 -0
- package/skills/pi-test-harness/SKILL.md +451 -0
- package/skills/pi-test-harness/evals/evals.json +26 -0
- package/skills/pi-test-harness/references/api-reference.md +480 -0
- package/skills/pi-test-harness/references/mock-pi-cli.md +135 -0
- package/skills/pi-test-harness/references/mock-tools.md +176 -0
- package/skills/pi-test-harness/references/mock-ui.md +170 -0
- package/skills/pi-test-harness/references/playbook-dsl.md +209 -0
- package/skills/pi-test-harness/references/sandbox-install.md +113 -0
- package/src/diagnostics.ts +90 -0
- package/src/events.ts +43 -0
- package/src/index.ts +42 -0
- package/src/mock-pi-script.mjs +176 -0
- package/src/mock-pi.ts +169 -0
- package/src/mock-tools.ts +252 -0
- package/src/mock-ui.ts +196 -0
- package/src/pi-loader-parity.ts +61 -0
- package/src/playbook.ts +189 -0
- package/src/sandbox.ts +334 -0
- package/src/session.ts +249 -0
- package/src/types.ts +203 -0
- 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
|
+
}
|