@fitchmultz/pi-subagents 0.52.10 → 0.52.11
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 +13 -0
- package/dist/build-info.js +1 -1
- package/package.json +1 -1
- package/scripts/archive-retired-history.mjs +19 -12
- package/scripts/build.mjs +54 -27
- package/scripts/check-compat.mjs +18 -9
- package/scripts/compat-host.mjs +22 -10
- package/scripts/intercom-bridge-credentials.mjs +69 -25
- package/scripts/intercom-bridge-wake-preflight.mjs +9 -8
- package/scripts/intercom-remote.mjs +68 -31
- package/scripts/local-install-smoke.mjs +10 -5
- package/scripts/measure-size.mjs +5 -1
- package/scripts/native-package-smoke-assertions.mjs +84 -0
- package/scripts/native-package-smoke.mjs +122 -120
- package/scripts/package-smoke.mjs +44 -14
- package/scripts/prepare.mjs +20 -12
- package/scripts/real-pi-live.mjs +259 -69
- package/scripts/real-pi-processes.mjs +117 -54
- package/scripts/real-pi-smoke.mjs +56 -22
- package/scripts/run-tests.mjs +45 -17
- package/tsconfig.json +1 -1
|
@@ -9,9 +9,40 @@ import {
|
|
|
9
9
|
trySignalChildTree,
|
|
10
10
|
} from "../src/shared/post-exit-stdio-guard.ts";
|
|
11
11
|
|
|
12
|
+
// oxlint-disable-next-line typescript/strict-void-return
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} cwd
|
|
17
|
+
* @param {NodeJS.ProcessEnv} env
|
|
18
|
+
* @returns {import("node:child_process").ExecFileOptionsWithStringEncoding & { detached: boolean }}
|
|
19
|
+
*/
|
|
20
|
+
function spawnOptions(cwd, env) {
|
|
21
|
+
return { cwd, env, encoding: "utf8", detached: true, maxBuffer: 16 * 1024 * 1024 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A smoke-owned process: a direct execFile child or a discovered detached worker.
|
|
26
|
+
* @typedef {object} ProcessEntry
|
|
27
|
+
* @property {import("node:child_process").ChildProcess | { pid: number, kill(signal?: NodeJS.Signals | number): true }} child
|
|
28
|
+
* @property {boolean | undefined} [broker]
|
|
29
|
+
* @property {boolean | undefined} [exited]
|
|
30
|
+
* @property {number | undefined} [stoppedAt]
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} SmokeProcessOptions
|
|
34
|
+
* @property {string} cwd
|
|
35
|
+
* @property {NodeJS.ProcessEnv} env
|
|
36
|
+
* @property {number} timeoutMs
|
|
37
|
+
* @property {AbortSignal} signal
|
|
38
|
+
* @property {string} root
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} dir
|
|
43
|
+
* @returns {Generator<string, void, void>}
|
|
44
|
+
*/
|
|
45
|
+
export function* filesIn(/** @type {string} */ dir) {
|
|
15
46
|
if (!existsSync(dir)) {
|
|
16
47
|
return;
|
|
17
48
|
}
|
|
@@ -25,24 +56,29 @@ export function* filesIn(dir) {
|
|
|
25
56
|
}
|
|
26
57
|
}
|
|
27
58
|
|
|
28
|
-
export function processRef(pid) {
|
|
59
|
+
export function processRef(/** @type {number} */ pid) {
|
|
29
60
|
if (!Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) {
|
|
30
61
|
throw new Error("Invalid smoke-owned process ID");
|
|
31
62
|
}
|
|
32
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
pid,
|
|
65
|
+
kill: (/** @type {NodeJS.Signals | number | undefined} */ signal) => process.kill(pid, signal),
|
|
66
|
+
};
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
// This attempt owns only PIDs discovered from private run files or known direct children.
|
|
36
70
|
// Credentials and artifacts must remain available until all owned workers and the broker exit.
|
|
37
71
|
export class SmokeProcesses {
|
|
38
|
-
|
|
39
|
-
#
|
|
72
|
+
// oxlint-disable-next-line typescript/prefer-readonly -- JSDoc class fields cannot express readonly
|
|
73
|
+
/** @type {Map<number, ProcessEntry>} */ #processes = new Map();
|
|
74
|
+
// oxlint-disable-next-line typescript/prefer-readonly -- JSDoc class fields cannot express readonly
|
|
75
|
+
/** @type {SmokeProcessOptions} */ #options;
|
|
40
76
|
|
|
41
|
-
constructor({ cwd, env, timeoutMs, signal, root }) {
|
|
77
|
+
constructor(/** @type {SmokeProcessOptions} */ { cwd, env, timeoutMs, signal, root }) {
|
|
42
78
|
this.#options = { cwd, env, timeoutMs, signal, root };
|
|
43
79
|
}
|
|
44
80
|
|
|
45
|
-
#remember(pid, broker = false) {
|
|
81
|
+
#remember(/** @type {number | undefined} */ pid, /** @type {boolean} */ broker = false) {
|
|
46
82
|
if (pid === undefined) {
|
|
47
83
|
return;
|
|
48
84
|
}
|
|
@@ -50,17 +86,30 @@ export class SmokeProcesses {
|
|
|
50
86
|
this.#processes.set(pid, { child: processRef(pid) });
|
|
51
87
|
}
|
|
52
88
|
if (broker) {
|
|
53
|
-
this.#processes.get(pid)
|
|
89
|
+
const remembered = this.#processes.get(pid);
|
|
90
|
+
if (remembered) {
|
|
91
|
+
remembered.broker = true;
|
|
92
|
+
}
|
|
54
93
|
}
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} label
|
|
98
|
+
* @param {string} command
|
|
99
|
+
* @param {string[]} args
|
|
100
|
+
* @param {string | ((child: import("node:child_process").ChildProcess) => void)} [input]
|
|
101
|
+
* @returns {Promise<string>}
|
|
102
|
+
*/
|
|
57
103
|
run(label, command, args, input) {
|
|
58
104
|
const { cwd, env, timeoutMs, signal } = this.#options;
|
|
59
105
|
signal.throwIfAborted();
|
|
60
106
|
return new Promise((resolve, reject) => {
|
|
61
|
-
let discovery;
|
|
62
|
-
let discoveryError;
|
|
63
|
-
const finish = async (
|
|
107
|
+
/** @type {Promise<void> | undefined} */ let discovery;
|
|
108
|
+
/** @type {string | undefined} */ let discoveryError;
|
|
109
|
+
const finish = async (
|
|
110
|
+
/** @type {Error | undefined} */ error,
|
|
111
|
+
/** @type {string} */ output = "",
|
|
112
|
+
) => {
|
|
64
113
|
clearTimeout(timer);
|
|
65
114
|
clearInterval(watcher);
|
|
66
115
|
signal.removeEventListener("abort", cancel);
|
|
@@ -71,25 +120,23 @@ export class SmokeProcesses {
|
|
|
71
120
|
resolve(output);
|
|
72
121
|
}
|
|
73
122
|
};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
);
|
|
123
|
+
const failure = (
|
|
124
|
+
/** @type {import("node:child_process").ExecFileException | null} */ error,
|
|
125
|
+
/** @type {string} */ output,
|
|
126
|
+
) =>
|
|
127
|
+
error === null
|
|
128
|
+
? undefined
|
|
129
|
+
: new Error(
|
|
130
|
+
`${label} failed with ${error.code ?? "spawn error"}\nCommand: ${command} ${args.join(" ")}\n${output}`,
|
|
131
|
+
);
|
|
132
|
+
const child = execFile(command, args, spawnOptions(cwd, env), (error, stdout, stderr) => {
|
|
133
|
+
const output = `${stdout}${stderr}`;
|
|
134
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- finish settles the outer promise by design
|
|
135
|
+
void finish(failure(error, output), output);
|
|
136
|
+
});
|
|
90
137
|
attachChildProcessLifecycle(child);
|
|
91
|
-
if (child.pid) {
|
|
92
|
-
const entry = { child };
|
|
138
|
+
if (child.pid !== undefined) {
|
|
139
|
+
/** @type {ProcessEntry} */ const entry = { child };
|
|
93
140
|
this.#processes.set(child.pid, entry);
|
|
94
141
|
child.once("close", () => {
|
|
95
142
|
entry.exited = !isChildTreeAlive(child);
|
|
@@ -98,11 +145,12 @@ export class SmokeProcesses {
|
|
|
98
145
|
// Discover detached startup children even if their parent's startup fails.
|
|
99
146
|
const watcher = setInterval(() => {
|
|
100
147
|
discovery ??= this.#collect()
|
|
101
|
-
.catch((error) => {
|
|
102
|
-
|
|
103
|
-
|
|
148
|
+
.catch((/** @type {unknown} */ error) => {
|
|
149
|
+
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
150
|
+
if (message !== discoveryError) {
|
|
151
|
+
console.error(`[real-pi-smoke] process discovery: ${message}`);
|
|
104
152
|
}
|
|
105
|
-
discoveryError =
|
|
153
|
+
discoveryError = message;
|
|
106
154
|
})
|
|
107
155
|
.finally(() => {
|
|
108
156
|
discovery = undefined;
|
|
@@ -110,6 +158,7 @@ export class SmokeProcesses {
|
|
|
110
158
|
}, 100);
|
|
111
159
|
// The outer finally stops parents AND detached workers; rejecting must not kill parents first.
|
|
112
160
|
const timer = setTimeout(() => {
|
|
161
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- finish settles the outer promise by design
|
|
113
162
|
void finish(
|
|
114
163
|
new Error(
|
|
115
164
|
`${label} timed out after ${timeoutMs}ms\nCommand: ${command} ${args.join(" ")}`,
|
|
@@ -117,36 +166,47 @@ export class SmokeProcesses {
|
|
|
117
166
|
);
|
|
118
167
|
}, timeoutMs);
|
|
119
168
|
const cancel = () => {
|
|
120
|
-
|
|
169
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion, typescript/no-floating-promises -- signal.reason is any; finish settles the outer promise
|
|
170
|
+
void finish(/** @type {Error | undefined} */ (signal.reason));
|
|
121
171
|
};
|
|
122
172
|
signal.addEventListener("abort", cancel, { once: true });
|
|
123
|
-
child.stdin
|
|
173
|
+
child.stdin?.on("error", () => {
|
|
124
174
|
// A command may exit without reading RPC input; execFile reports its exit failure.
|
|
125
175
|
});
|
|
126
176
|
// A function input owns a persistent RPC stdin and closes it to request orderly shutdown.
|
|
127
177
|
if (typeof input === "function") {
|
|
128
178
|
input(child);
|
|
129
179
|
} else {
|
|
130
|
-
child.stdin
|
|
180
|
+
child.stdin?.end(input);
|
|
131
181
|
}
|
|
132
182
|
});
|
|
133
183
|
}
|
|
134
184
|
|
|
135
185
|
#readRunPids() {
|
|
136
186
|
const { env } = this.#options;
|
|
137
|
-
const
|
|
187
|
+
const agentDir = env.PI_CODING_AGENT_DIR;
|
|
188
|
+
const runtimeDir = env.PI_INTERCOM_RUNTIME_DIR;
|
|
189
|
+
if (agentDir === undefined || runtimeDir === undefined) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"PI_CODING_AGENT_DIR and PI_INTERCOM_RUNTIME_DIR are required for ownership discovery",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const runs = join(agentDir, "sessions", "agent-runs");
|
|
138
195
|
for (const file of filesIn(runs)) {
|
|
139
196
|
if (basename(file) === "process.json" && basename(dirname(file)) === "actor.lock") {
|
|
140
|
-
|
|
197
|
+
/** @type {{ pid: number }} */
|
|
198
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
199
|
+
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
200
|
+
this.#remember(record.pid);
|
|
141
201
|
}
|
|
142
202
|
}
|
|
143
|
-
const brokerPid = join(
|
|
203
|
+
const brokerPid = join(runtimeDir, "broker.pid");
|
|
144
204
|
if (existsSync(brokerPid)) {
|
|
145
205
|
this.#remember(Number(readFileSync(brokerPid, "utf8").split("\n")[0]), true);
|
|
146
206
|
}
|
|
147
207
|
}
|
|
148
208
|
|
|
149
|
-
async #discoverChildren(child) {
|
|
209
|
+
async #discoverChildren(/** @type {{ pid?: number | undefined }} */ child) {
|
|
150
210
|
try {
|
|
151
211
|
const { stdout } = await execFileAsync("pgrep", ["-P", String(child.pid)], {
|
|
152
212
|
env: this.#options.env,
|
|
@@ -157,7 +217,7 @@ export class SmokeProcesses {
|
|
|
157
217
|
this.#remember(Number(pid));
|
|
158
218
|
}
|
|
159
219
|
} catch (error) {
|
|
160
|
-
if (error.code !== 1) {
|
|
220
|
+
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== 1) {
|
|
161
221
|
throw error;
|
|
162
222
|
}
|
|
163
223
|
// pgrep exit 1 means there are no matching children.
|
|
@@ -168,10 +228,11 @@ export class SmokeProcesses {
|
|
|
168
228
|
this.#readRunPids();
|
|
169
229
|
// Visit newly remembered grandchildren too, before signalling parents. Never scan argv globally.
|
|
170
230
|
for (const entry of this.#processes.values()) {
|
|
171
|
-
if (
|
|
231
|
+
if (entry.exited !== true) {
|
|
172
232
|
entry.exited = !isChildTreeAlive(entry.child);
|
|
173
233
|
}
|
|
174
|
-
|
|
234
|
+
// oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- exited stays optional under tsc
|
|
235
|
+
if (entry.exited !== true) {
|
|
175
236
|
// The Map expands during discovery; sequential traversal covers newly found grandchildren.
|
|
176
237
|
// oxlint-disable-next-line no-await-in-loop
|
|
177
238
|
await this.#discoverChildren(entry.child);
|
|
@@ -180,23 +241,24 @@ export class SmokeProcesses {
|
|
|
180
241
|
}
|
|
181
242
|
|
|
182
243
|
#alive() {
|
|
183
|
-
const alive = [];
|
|
244
|
+
/** @type {ProcessEntry[]} */ const alive = [];
|
|
184
245
|
for (const entry of this.#processes.values()) {
|
|
185
|
-
if (
|
|
246
|
+
if (entry.exited !== true) {
|
|
186
247
|
entry.exited = !isChildTreeAlive(entry.child);
|
|
187
248
|
}
|
|
188
|
-
|
|
249
|
+
// oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- exited stays optional under tsc
|
|
250
|
+
if (entry.exited !== true) {
|
|
189
251
|
alive.push(entry);
|
|
190
252
|
}
|
|
191
253
|
}
|
|
192
254
|
return alive;
|
|
193
255
|
}
|
|
194
256
|
|
|
195
|
-
#signal(alive) {
|
|
196
|
-
const workers = alive.filter((entry) =>
|
|
257
|
+
#signal(/** @type {ProcessEntry[]} */ alive) {
|
|
258
|
+
const workers = alive.filter((entry) => entry.broker !== true);
|
|
197
259
|
// Stop the broker last so exiting workers cannot restart it during cleanup.
|
|
198
|
-
for (const entry of workers.length ? workers : alive) {
|
|
199
|
-
if (
|
|
260
|
+
for (const entry of workers.length > 0 ? workers : alive) {
|
|
261
|
+
if (entry.stoppedAt === undefined) {
|
|
200
262
|
entry.stoppedAt = Date.now();
|
|
201
263
|
trySignalChildTree(entry.child, "SIGTERM");
|
|
202
264
|
} else if (Date.now() - entry.stoppedAt >= 3000) {
|
|
@@ -206,7 +268,7 @@ export class SmokeProcesses {
|
|
|
206
268
|
}
|
|
207
269
|
|
|
208
270
|
async stop() {
|
|
209
|
-
let lastError;
|
|
271
|
+
/** @type {string | undefined} */ let lastError;
|
|
210
272
|
while (true) {
|
|
211
273
|
let collected = false;
|
|
212
274
|
try {
|
|
@@ -216,10 +278,11 @@ export class SmokeProcesses {
|
|
|
216
278
|
collected = true;
|
|
217
279
|
} catch (error) {
|
|
218
280
|
// Retain the controller and credentials if ownership cannot yet be read.
|
|
219
|
-
|
|
220
|
-
|
|
281
|
+
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
282
|
+
if (message !== lastError) {
|
|
283
|
+
console.error(`[real-pi-smoke] waiting for owned-process cleanup: ${message}`);
|
|
221
284
|
}
|
|
222
|
-
lastError =
|
|
285
|
+
lastError = message;
|
|
223
286
|
}
|
|
224
287
|
const alive = this.#alive();
|
|
225
288
|
if (collected && alive.length === 0) {
|
|
@@ -13,7 +13,7 @@ function usage() {
|
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function parsePositiveInteger(value, source) {
|
|
16
|
+
function parsePositiveInteger(/** @type {string} */ value, /** @type {string} */ source) {
|
|
17
17
|
if (!/^\d+$/.test(value)) {
|
|
18
18
|
throw new Error(`${source} must be a positive integer, got ${value}`);
|
|
19
19
|
}
|
|
@@ -24,10 +24,23 @@ function parsePositiveInteger(value, source) {
|
|
|
24
24
|
return parsed;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @param {string[]} argv
|
|
29
|
+
* @param {number} index
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function requiredValue(argv, index) {
|
|
33
|
+
const value = argv[index];
|
|
34
|
+
if (value === undefined || value === "") {
|
|
35
|
+
throw new Error("--timeout-ms requires a value");
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(/** @type {string[]} */ argv) {
|
|
28
41
|
const options = { llm: false, llmFull: false, keepTemp: false, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
29
42
|
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
-
const arg = argv[index];
|
|
43
|
+
const arg = argv[index] ?? "";
|
|
31
44
|
if (arg === "--help" || arg === "-h") {
|
|
32
45
|
usage();
|
|
33
46
|
process.exit(0);
|
|
@@ -46,11 +59,7 @@ function parseArgs(argv) {
|
|
|
46
59
|
continue;
|
|
47
60
|
}
|
|
48
61
|
if (arg === "--timeout-ms") {
|
|
49
|
-
|
|
50
|
-
if (!value) {
|
|
51
|
-
throw new Error("--timeout-ms requires a value");
|
|
52
|
-
}
|
|
53
|
-
options.timeoutMs = parsePositiveInteger(value, "--timeout-ms");
|
|
62
|
+
options.timeoutMs = parsePositiveInteger(requiredValue(argv, index + 1), "--timeout-ms");
|
|
54
63
|
index += 1;
|
|
55
64
|
continue;
|
|
56
65
|
}
|
|
@@ -63,7 +72,7 @@ function parseArgs(argv) {
|
|
|
63
72
|
return options;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
function isolatedEnv(root, agentDir) {
|
|
75
|
+
function isolatedEnv(/** @type {string} */ root, /** @type {string} */ agentDir) {
|
|
67
76
|
const home = join(root, "home");
|
|
68
77
|
const env = { ...process.env };
|
|
69
78
|
for (const key of Object.keys(env)) {
|
|
@@ -85,11 +94,18 @@ function isolatedEnv(root, agentDir) {
|
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} label
|
|
99
|
+
* @param {string[]} args
|
|
100
|
+
* @param {{ processes: import("./real-pi-processes.mjs").SmokeProcesses, input?: string | ((child: import("node:child_process").ChildProcess) => void) }} options
|
|
101
|
+
*/
|
|
88
102
|
function runPi(label, args, options) {
|
|
89
103
|
return options.processes.run(label, "pi", args, options.input);
|
|
90
104
|
}
|
|
91
105
|
|
|
92
|
-
async function verifyBundledResources(
|
|
106
|
+
async function verifyBundledResources(
|
|
107
|
+
/** @type {{ processes: import("./real-pi-processes.mjs").SmokeProcesses, input?: string | ((child: import("node:child_process").ChildProcess) => void), root: string }} */ options,
|
|
108
|
+
) {
|
|
93
109
|
const marker = join(options.root, "resources.json");
|
|
94
110
|
const observer = join(options.root, "observe-resources.mjs");
|
|
95
111
|
writeFileSync(
|
|
@@ -107,6 +123,8 @@ async function verifyBundledResources(options) {
|
|
|
107
123
|
throw new Error(`bundled Pi resources did not register ${name}:\n${output}`);
|
|
108
124
|
}
|
|
109
125
|
}
|
|
126
|
+
/** @type {string[]} */
|
|
127
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
110
128
|
const tools = JSON.parse(readFileSync(marker, "utf8"));
|
|
111
129
|
for (const name of ["delegate", "agent_runs", "intercom"]) {
|
|
112
130
|
if (!tools.includes(name)) {
|
|
@@ -126,12 +144,19 @@ async function verifyBundledResources(options) {
|
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} output
|
|
149
|
+
* @returns {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } } | undefined}
|
|
150
|
+
*/
|
|
129
151
|
function commandResponse(output) {
|
|
152
|
+
/** @type {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } } | undefined} */
|
|
130
153
|
let response;
|
|
131
154
|
for (const line of output.split(/\r?\n/)) {
|
|
132
155
|
try {
|
|
156
|
+
/** @type {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } }} */
|
|
157
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
133
158
|
const parsed = JSON.parse(line);
|
|
134
|
-
if (parsed
|
|
159
|
+
if (parsed.type === "response" && parsed.id === "commands") {
|
|
135
160
|
response = parsed;
|
|
136
161
|
}
|
|
137
162
|
} catch {
|
|
@@ -141,13 +166,18 @@ function commandResponse(output) {
|
|
|
141
166
|
return response;
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
function registeredCommands(
|
|
145
|
-
|
|
146
|
-
|
|
169
|
+
function registeredCommands(
|
|
170
|
+
/** @type {{ success?: boolean, data?: { commands?: { name?: string }[] } } | undefined} */ response,
|
|
171
|
+
) {
|
|
172
|
+
return response !== undefined &&
|
|
173
|
+
response.success === true &&
|
|
174
|
+
response.data !== undefined &&
|
|
175
|
+
Array.isArray(response.data.commands)
|
|
176
|
+
? response.data.commands.map((command) => command.name)
|
|
147
177
|
: [];
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
function createProject(root) {
|
|
180
|
+
function createProject(/** @type {string} */ root) {
|
|
151
181
|
const projectRoot = join(root, "project");
|
|
152
182
|
mkdirSync(projectRoot, { recursive: true });
|
|
153
183
|
return projectRoot;
|
|
@@ -158,7 +188,9 @@ async function main() {
|
|
|
158
188
|
try {
|
|
159
189
|
options = parseArgs(process.argv.slice(2));
|
|
160
190
|
} catch (error) {
|
|
161
|
-
console.error(
|
|
191
|
+
console.error(
|
|
192
|
+
`[real-pi-smoke] ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
193
|
+
);
|
|
162
194
|
usage();
|
|
163
195
|
process.exit(2);
|
|
164
196
|
}
|
|
@@ -169,19 +201,19 @@ async function main() {
|
|
|
169
201
|
const projectRoot = createProject(root);
|
|
170
202
|
const env = isolatedEnv(root, agentDir);
|
|
171
203
|
const cancellation = new AbortController();
|
|
172
|
-
const cancel = (
|
|
204
|
+
const cancel = (/** @type {NodeJS.Signals} */ signal) =>
|
|
205
|
+
cancellation.abort(new Error(`Cancelled by ${signal}`));
|
|
173
206
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
174
207
|
process.on(signal, cancel);
|
|
175
208
|
}
|
|
176
|
-
const
|
|
209
|
+
const smokeOptions = {
|
|
177
210
|
cwd: projectRoot,
|
|
178
211
|
env,
|
|
179
212
|
timeoutMs: options.timeoutMs,
|
|
180
213
|
root,
|
|
181
214
|
signal: cancellation.signal,
|
|
182
215
|
};
|
|
183
|
-
|
|
184
|
-
runOptions.processes = new SmokeProcesses(runOptions);
|
|
216
|
+
const runOptions = { ...smokeOptions, processes: new SmokeProcesses(smokeOptions) };
|
|
185
217
|
|
|
186
218
|
try {
|
|
187
219
|
await runPi("pi install pi-subagents", ["install", repoRoot, "--approve"], runOptions);
|
|
@@ -223,7 +255,9 @@ async function main() {
|
|
|
223
255
|
}
|
|
224
256
|
}
|
|
225
257
|
|
|
226
|
-
main().catch((error) => {
|
|
227
|
-
console.error(
|
|
258
|
+
main().catch((/** @type {unknown} */ error) => {
|
|
259
|
+
console.error(
|
|
260
|
+
`[real-pi-smoke] ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
261
|
+
);
|
|
228
262
|
process.exitCode = 1;
|
|
229
263
|
});
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function usage() {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function parsePositiveInteger(value, source) {
|
|
17
|
+
function parsePositiveInteger(/** @type {string} */ value, /** @type {string} */ source) {
|
|
18
18
|
if (!/^\d+$/.test(value)) {
|
|
19
19
|
console.error(`${source} must be a positive integer number of milliseconds, got: ${value}`);
|
|
20
20
|
process.exit(2);
|
|
@@ -29,25 +29,39 @@ function parsePositiveInteger(value, source) {
|
|
|
29
29
|
return parsed;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/**
|
|
33
|
+
* @param {string[]} argv
|
|
34
|
+
* @param {number} index
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function requiredValue(argv, index) {
|
|
38
|
+
const value = argv[index];
|
|
39
|
+
if (value === undefined || value === "") {
|
|
40
|
+
console.error("--timeout-ms requires a value");
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initialTimeout() {
|
|
47
|
+
const env = process.env.PI_TEST_TIMEOUT_MS;
|
|
48
|
+
return env !== undefined && env !== ""
|
|
49
|
+
? parsePositiveInteger(env, "PI_TEST_TIMEOUT_MS")
|
|
36
50
|
: undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(/** @type {string[]} */ argv) {
|
|
54
|
+
let mode = "unit";
|
|
55
|
+
let timeoutMs = initialTimeout();
|
|
37
56
|
|
|
38
57
|
for (let index = 0; index < argv.length; index += 1) {
|
|
39
|
-
const arg = argv[index];
|
|
58
|
+
const arg = argv[index] ?? "";
|
|
40
59
|
if (arg === "--help" || arg === "-h") {
|
|
41
60
|
usage();
|
|
42
61
|
process.exit(0);
|
|
43
62
|
}
|
|
44
63
|
if (arg === "--timeout-ms") {
|
|
45
|
-
|
|
46
|
-
if (!value) {
|
|
47
|
-
console.error("--timeout-ms requires a value");
|
|
48
|
-
process.exit(2);
|
|
49
|
-
}
|
|
50
|
-
timeoutMs = parsePositiveInteger(value, "--timeout-ms");
|
|
64
|
+
timeoutMs = parsePositiveInteger(requiredValue(argv, index + 1), "--timeout-ms");
|
|
51
65
|
index += 1;
|
|
52
66
|
continue;
|
|
53
67
|
}
|
|
@@ -71,7 +85,7 @@ function parseArgs(argv) {
|
|
|
71
85
|
return { mode, timeoutMs };
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
function testFiles(dir) {
|
|
88
|
+
function testFiles(/** @type {string} */ dir) {
|
|
75
89
|
return readdirSync(dir)
|
|
76
90
|
.filter((name) => name.endsWith(".test.ts"))
|
|
77
91
|
.sort()
|
|
@@ -88,8 +102,20 @@ function sanitizedEnv() {
|
|
|
88
102
|
return env;
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} label
|
|
107
|
+
* @param {string[]} files
|
|
108
|
+
* @param {number} timeoutMs
|
|
109
|
+
* @param {number} [concurrency]
|
|
110
|
+
*/
|
|
91
111
|
function runNodeTest(label, files, timeoutMs, concurrency) {
|
|
92
|
-
const args = [
|
|
112
|
+
const args = [
|
|
113
|
+
...(concurrency !== undefined && concurrency !== 0
|
|
114
|
+
? [`--test-concurrency=${concurrency}`]
|
|
115
|
+
: []),
|
|
116
|
+
"--test",
|
|
117
|
+
...files,
|
|
118
|
+
];
|
|
93
119
|
const startedAt = Date.now();
|
|
94
120
|
const tempRoot = mkdtempSync(
|
|
95
121
|
join(process.platform === "android" ? tmpdir() : "/tmp", "pi-subagents-"),
|
|
@@ -112,12 +138,14 @@ function runNodeTest(label, files, timeoutMs, concurrency) {
|
|
|
112
138
|
try {
|
|
113
139
|
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
114
140
|
} catch (error) {
|
|
115
|
-
console.error(
|
|
141
|
+
console.error(
|
|
142
|
+
`Could not remove ${tempRoot}: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
143
|
+
);
|
|
116
144
|
}
|
|
117
145
|
}
|
|
118
146
|
const elapsedMs = Date.now() - startedAt;
|
|
119
147
|
if (result.error) {
|
|
120
|
-
if (result.error.code === "ETIMEDOUT") {
|
|
148
|
+
if (/** @type {NodeJS.ErrnoException} */ (result.error).code === "ETIMEDOUT") {
|
|
121
149
|
console.error(`${label} timed out after ${timeoutMs}ms (elapsed ${elapsedMs}ms).`);
|
|
122
150
|
console.error(`Command: ${process.execPath} ${args.join(" ")}`);
|
|
123
151
|
console.error(
|
|
@@ -128,7 +156,7 @@ function runNodeTest(label, files, timeoutMs, concurrency) {
|
|
|
128
156
|
console.error(`Failed to start ${label}: ${result.error.message}`);
|
|
129
157
|
return 1;
|
|
130
158
|
}
|
|
131
|
-
if (result.signal) {
|
|
159
|
+
if (result.signal !== null) {
|
|
132
160
|
console.error(`${label} exited due to signal ${result.signal} after ${elapsedMs}ms.`);
|
|
133
161
|
console.error(`Command: ${process.execPath} ${args.join(" ")}`);
|
|
134
162
|
return 1;
|