pi-oracle 0.7.4 → 0.7.5

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 (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +51 -17
  3. package/docs/ORACLE_DESIGN.md +12 -5
  4. package/docs/platform-smoke.md +153 -0
  5. package/extensions/oracle/lib/config.ts +53 -27
  6. package/extensions/oracle/lib/jobs.ts +9 -5
  7. package/extensions/oracle/lib/runtime.ts +107 -32
  8. package/extensions/oracle/lib/tools.ts +138 -12
  9. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  10. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  11. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  12. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  13. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  14. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  15. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  16. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  17. package/extensions/oracle/worker/run-job.mjs +107 -25
  18. package/package.json +27 -6
  19. package/platform-smoke.config.mjs +59 -0
  20. package/scripts/oracle-real-smoke.mjs +497 -0
  21. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  22. package/scripts/platform-smoke/artifacts.mjs +87 -0
  23. package/scripts/platform-smoke/assertions.mjs +34 -0
  24. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  25. package/scripts/platform-smoke/doctor.mjs +239 -0
  26. package/scripts/platform-smoke/invariants.mjs +108 -0
  27. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  28. package/scripts/platform-smoke/targets.mjs +434 -0
  29. package/scripts/platform-smoke.mjs +149 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Ubuntu platform-build suite for pi-oracle.
3
+ * The suite proves the PR's package builds/tests on Linux and installs through pi's package path.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+
9
+ import { runAssertions } from "./assertions.mjs";
10
+ import { createSuiteDir, scanArtifacts, scanForSecrets, writeCommand, writeExitCode, writeManifest, writeSummary } from "./artifacts.mjs";
11
+ import { runOnLease, stopLease, warmupLease } from "./crabbox-runner.mjs";
12
+
13
+ function makeRunId() {
14
+ return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+
17
+ function platformForTarget(targetName) {
18
+ if (targetName === "macos") return "darwin";
19
+ if (targetName === "windows-native") return "win32";
20
+ return "linux";
21
+ }
22
+
23
+ function section(text, name) {
24
+ const start = `--- ${name} START ---`;
25
+ const end = `--- ${name} END ---`;
26
+ const startIndex = text.indexOf(start);
27
+ if (startIndex === -1) return "";
28
+ const contentStart = startIndex + start.length;
29
+ const endIndex = text.indexOf(end, contentStart);
30
+ const raw = endIndex === -1 ? text.slice(contentStart) : text.slice(contentStart, endIndex);
31
+ return raw.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
32
+ }
33
+
34
+ function markerValue(text, name) {
35
+ return text.match(new RegExp(`^${name}=(.*)$`, "m"))?.[1]?.trim() ?? "";
36
+ }
37
+
38
+ function writeExtracts(suiteDir, stdout) {
39
+ writeFileSync(resolve(suiteDir, "packed-tarball.txt"), `${markerValue(stdout, "PLATFORM_PACKED_TARBALL")}\n`);
40
+ writeFileSync(resolve(suiteDir, "packed-node-install.stdout.txt"), section(stdout, "PACKED_NODE_INSTALL_STDOUT"));
41
+ writeFileSync(resolve(suiteDir, "packed-node-install.stderr.txt"), section(stdout, "PACKED_NODE_INSTALL_STDERR"));
42
+ writeFileSync(resolve(suiteDir, "pi-install.stdout.txt"), section(stdout, "PI_INSTALL_STDOUT"));
43
+ writeFileSync(resolve(suiteDir, "pi-install.stderr.txt"), section(stdout, "PI_INSTALL_STDERR"));
44
+ writeFileSync(resolve(suiteDir, "pi-list.stdout.txt"), section(stdout, "PI_LIST_STDOUT"));
45
+ writeFileSync(resolve(suiteDir, "pi-list.stderr.txt"), section(stdout, "PI_LIST_STDERR"));
46
+ }
47
+
48
+ function posixSection(name, command) {
49
+ return [`echo "--- ${name} START ---"`, command, `echo "--- ${name} END ---"`];
50
+ }
51
+
52
+ function realSmokeProvider(config = {}) {
53
+ return process.env.PI_ORACLE_REAL_TEST_PROVIDER || config.realSmoke?.defaultProvider || "zai";
54
+ }
55
+
56
+ function realSmokeModel(config = {}) {
57
+ return process.env.PI_ORACLE_REAL_TEST_MODEL || config.realSmoke?.defaultModel || "glm-5.1";
58
+ }
59
+
60
+ function truthy(value) {
61
+ return /^(1|true|yes|on)$/i.test(String(value ?? ""));
62
+ }
63
+
64
+ function realSmokeUsesModelAgent() {
65
+ return truthy(process.env.PI_ORACLE_REAL_TEST_MODEL_AGENT);
66
+ }
67
+
68
+ function realSmokeAllowedEnvNames(config = {}) {
69
+ const provider = realSmokeProvider(config);
70
+ const authNames = realSmokeUsesModelAgent() ? config.realSmoke?.authEnvByProvider?.[provider] ?? [] : [];
71
+ return [
72
+ "PI_ORACLE_REAL_TEST_PROVIDER",
73
+ "PI_ORACLE_REAL_TEST_MODEL",
74
+ "PI_ORACLE_REAL_TEST_MODEL_AGENT",
75
+ "PI_ORACLE_REAL_TEST_TIMEOUT_MS",
76
+ ...authNames,
77
+ ];
78
+ }
79
+
80
+ function shellQuote(value) {
81
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
82
+ }
83
+
84
+ function powershellSingleQuote(value) {
85
+ return `'${String(value).replaceAll("'", "''")}'`;
86
+ }
87
+
88
+ export function buildRealExtensionCommand(targetName = "ubuntu", config = {}) {
89
+ const provider = realSmokeProvider(config);
90
+ const model = realSmokeModel(config);
91
+ const useModelAgent = realSmokeUsesModelAgent();
92
+ if (targetName === "windows-native") {
93
+ return [
94
+ "$ErrorActionPreference = 'Continue'",
95
+ `$env:PI_ORACLE_REAL_TEST_PROVIDER = ${powershellSingleQuote(provider)}`,
96
+ `$env:PI_ORACLE_REAL_TEST_MODEL = ${powershellSingleQuote(model)}`,
97
+ `$env:PI_ORACLE_REAL_TEST_MODEL_AGENT = ${powershellSingleQuote(useModelAgent ? "1" : "")}`,
98
+ "$env:PI_ORACLE_REAL_TEST_TIMEOUT_MS = '360000'",
99
+ "if (-not (Test-Path -LiteralPath 'node_modules')) { npm.cmd ci; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } }",
100
+ "npm.cmd run smoke:real:doctor",
101
+ "if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }",
102
+ "npm.cmd run smoke:real:packed",
103
+ "exit $LASTEXITCODE",
104
+ ].join("; ");
105
+ }
106
+ return [
107
+ "set -o pipefail",
108
+ `export PI_ORACLE_REAL_TEST_PROVIDER=${shellQuote(provider)}`,
109
+ `export PI_ORACLE_REAL_TEST_MODEL=${shellQuote(model)}`,
110
+ `export PI_ORACLE_REAL_TEST_MODEL_AGENT=${shellQuote(useModelAgent ? "1" : "")}`,
111
+ 'if [ ! -d node_modules ]; then npm ci; fi',
112
+ "npm run smoke:real:doctor",
113
+ "npm run smoke:real:packed",
114
+ ].join("\n");
115
+ }
116
+
117
+ export function buildPlatformBuildCommand(targetName = "ubuntu", packageName = "pi-oracle", nodeValidationMajor = 24) {
118
+ if (targetName === "windows-native") {
119
+ return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${packageName} -NodeValidationMajor ${nodeValidationMajor}`;
120
+ }
121
+ const lines = [];
122
+ lines.push("set -o pipefail");
123
+ if (targetName === "macos") {
124
+ lines.push('if [ -d "$HOME/.local/share/mise/installs/node/24/bin" ]; then export PATH="$HOME/.local/share/mise/installs/node/24/bin:$PATH"; fi');
125
+ }
126
+ lines.push('echo "Starting pi-oracle platform-build in $(pwd) at $(date -u +%Y-%m-%dT%H:%M:%SZ)"');
127
+ lines.push('RUN_ROOT=".platform-smoke-runs/platform-build-$(date -u +%Y%m%dT%H%M%SZ)-$$"');
128
+ lines.push('SOURCE_ROOT="$(pwd)"');
129
+ lines.push('PACK_DIR="$SOURCE_ROOT/$RUN_ROOT/pack"');
130
+ lines.push('TEST_WORKSPACE="$SOURCE_ROOT/$RUN_ROOT/test-workspace"');
131
+ lines.push('PI_PROJECT="$SOURCE_ROOT/$RUN_ROOT/pi-project"');
132
+ lines.push('mkdir -p "$PACK_DIR" "$TEST_WORKSPACE" "$PI_PROJECT"');
133
+ lines.push('echo "PLATFORM_RUN_ROOT=$RUN_ROOT"');
134
+ lines.push('echo "PLATFORM_TEST_WORKSPACE=$TEST_WORKSPACE"');
135
+ lines.push('echo "PLATFORM_PI_PROJECT=$PI_PROJECT"');
136
+ lines.push("");
137
+ lines.push('NODE_VERSION=$(node --version)');
138
+ lines.push('NODE_MAJOR=${NODE_VERSION#v}');
139
+ lines.push('NODE_MAJOR=${NODE_MAJOR%%.*}');
140
+ lines.push('echo "PLATFORM_NODE_VERSION=$NODE_VERSION"');
141
+ lines.push(`if [ "$NODE_MAJOR" -ge ${nodeValidationMajor} ]; then NODE_VERSION_EXIT=0; else NODE_VERSION_EXIT=1; fi`);
142
+ lines.push('echo "PLATFORM_NODE_VERSION_EXIT=$NODE_VERSION_EXIT"');
143
+ lines.push("");
144
+ lines.push('echo "=== npm ci ==="');
145
+ lines.push('npm ci 2>&1');
146
+ lines.push('CI_EXIT=$?');
147
+ lines.push('echo "PLATFORM_NPM_CI_EXIT=$CI_EXIT"');
148
+ lines.push("");
149
+ lines.push('echo "=== platform dependencies ==="');
150
+ lines.push('DEPS_EXIT=0');
151
+ if (targetName === "ubuntu") {
152
+ lines.push('if command -v zstd >/dev/null 2>&1; then ZSTD_INSTALL_EXIT=0; else echo "zstd missing on Ubuntu target; use a smoke image with zstd installed before running platform smoke"; ZSTD_INSTALL_EXIT=1; fi');
153
+ } else {
154
+ lines.push('if command -v zstd >/dev/null 2>&1; then ZSTD_INSTALL_EXIT=0; else echo "zstd missing on macOS target; install it on the host before running platform smoke"; ZSTD_INSTALL_EXIT=1; fi');
155
+ }
156
+ lines.push('if [ "$ZSTD_INSTALL_EXIT" -ne 0 ]; then DEPS_EXIT=1; fi');
157
+ lines.push('AGENT_BROWSER_BIN="$(command -v agent-browser || true)"');
158
+ lines.push('if [ -n "$AGENT_BROWSER_BIN" ]; then AGENT_BROWSER_INSTALL_EXIT=0; else echo "agent-browser missing on target PATH; install it in target setup before running platform smoke"; AGENT_BROWSER_INSTALL_EXIT=1; fi');
159
+ lines.push('if [ "$AGENT_BROWSER_INSTALL_EXIT" -ne 0 ]; then DEPS_EXIT=1; fi');
160
+ lines.push('echo "PLATFORM_ZSTD_INSTALL_EXIT=$ZSTD_INSTALL_EXIT"');
161
+ lines.push('echo "PLATFORM_AGENT_BROWSER_INSTALL_EXIT=$AGENT_BROWSER_INSTALL_EXIT"');
162
+ lines.push('echo "PLATFORM_DEPS_EXIT=$DEPS_EXIT"');
163
+ lines.push('command -v zstd || true');
164
+ lines.push('if [ -n "$AGENT_BROWSER_BIN" ]; then echo "$AGENT_BROWSER_BIN"; fi');
165
+ lines.push("");
166
+ lines.push('echo "=== platform verification ==="');
167
+ lines.push('AGENT_BROWSER_PATH="$AGENT_BROWSER_BIN" npm run verify:oracle:platform 2>&1');
168
+ lines.push('TEST_EXIT=$?');
169
+ lines.push('echo "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"');
170
+ lines.push("");
171
+ lines.push('echo "=== npm pack ==="');
172
+ lines.push('PACK_TARBALL=$(npm pack --silent 2>"$PACK_DIR/npm-pack.stderr.txt")');
173
+ lines.push('PACK_EXIT=$?');
174
+ lines.push('cat "$PACK_DIR/npm-pack.stderr.txt"');
175
+ lines.push('echo "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"');
176
+ lines.push('if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_TARBALL" ]; then mv "$PACK_TARBALL" "$PACK_DIR/$PACK_TARBALL"; fi');
177
+ lines.push('echo "PLATFORM_PACKED_TARBALL=$PACK_TARBALL"');
178
+ lines.push('printf "%s\\n" "$PACK_TARBALL" > "$PACK_DIR/packed-tarball.txt"');
179
+ lines.push("");
180
+ lines.push('echo "=== fixture workspace ==="');
181
+ lines.push('cp package.json README.md "$TEST_WORKSPACE"/ 2>"$PACK_DIR/fixture.stderr.txt"');
182
+ lines.push('FIXTURE_COPY_EXIT=$?');
183
+ lines.push('cp -R extensions prompts docs "$TEST_WORKSPACE"/ 2>>"$PACK_DIR/fixture.stderr.txt"');
184
+ lines.push('TREE_COPY_EXIT=$?');
185
+ lines.push('if [ "$FIXTURE_COPY_EXIT" -eq 0 ] && [ "$TREE_COPY_EXIT" -eq 0 ]; then FIXTURE_EXIT=0; else FIXTURE_EXIT=1; fi');
186
+ lines.push('cat "$PACK_DIR/fixture.stderr.txt"');
187
+ lines.push('echo "PLATFORM_FIXTURE_EXIT=$FIXTURE_EXIT"');
188
+ lines.push("");
189
+ lines.push('echo "=== pi install packed package ==="');
190
+ lines.push('PI_CLI="$(pwd)/node_modules/.bin/pi"');
191
+ lines.push('if [ ! -x "$PI_CLI" ]; then PI_CLI="$(command -v pi || true)"; fi');
192
+ lines.push('echo "PLATFORM_PI_CLI=$PI_CLI"');
193
+ lines.push('if [ -n "$PACK_TARBALL" ] && [ -n "$PI_CLI" ] && [ -f "$PACK_DIR/$PACK_TARBALL" ]; then (cd "$PI_PROJECT" && npm init -y >"$PACK_DIR/packed-node-install.stdout.txt" 2>"$PACK_DIR/packed-node-install.stderr.txt" && npm install --no-save "$PACK_DIR/$PACK_TARBALL" >>"$PACK_DIR/packed-node-install.stdout.txt" 2>>"$PACK_DIR/packed-node-install.stderr.txt"); PACKED_NODE_INSTALL_EXIT=$?; else echo "missing pi cli or tarball" >"$PACK_DIR/packed-node-install.stderr.txt"; PACKED_NODE_INSTALL_EXIT=1; fi');
194
+ lines.push('echo "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"');
195
+ lines.push(...posixSection("PACKED_NODE_INSTALL_STDOUT", 'cat "$PACK_DIR/packed-node-install.stdout.txt" 2>/dev/null || true'));
196
+ lines.push(...posixSection("PACKED_NODE_INSTALL_STDERR", 'cat "$PACK_DIR/packed-node-install.stderr.txt" 2>/dev/null || true'));
197
+ lines.push(`if [ "$PACKED_NODE_INSTALL_EXIT" -eq 0 ] && [ -n "$PI_CLI" ]; then (cd "$PI_PROJECT" && PI_OFFLINE=1 "$PI_CLI" install -l ./node_modules/${packageName} >"$PACK_DIR/pi-install.stdout.txt" 2>"$PACK_DIR/pi-install.stderr.txt"); PI_INSTALL_EXIT=$?; else echo "packed npm install failed or missing pi cli" >"$PACK_DIR/pi-install.stderr.txt"; PI_INSTALL_EXIT=1; fi`);
198
+ lines.push('echo "PLATFORM_PI_INSTALL_EXIT=$PI_INSTALL_EXIT"');
199
+ lines.push(...posixSection("PI_INSTALL_STDOUT", 'cat "$PACK_DIR/pi-install.stdout.txt" 2>/dev/null || true'));
200
+ lines.push(...posixSection("PI_INSTALL_STDERR", 'cat "$PACK_DIR/pi-install.stderr.txt" 2>/dev/null || true'));
201
+ lines.push("");
202
+ lines.push('echo "=== pi list ==="');
203
+ lines.push('if [ -n "$PI_CLI" ]; then (cd "$PI_PROJECT" && PI_OFFLINE=1 "$PI_CLI" list >"$PACK_DIR/pi-list.stdout.txt" 2>"$PACK_DIR/pi-list.stderr.txt"); PI_LIST_EXIT=$?; else echo "missing pi cli" >"$PACK_DIR/pi-list.stderr.txt"; PI_LIST_EXIT=1; fi');
204
+ lines.push('echo "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"');
205
+ lines.push(...posixSection("PI_LIST_STDOUT", 'cat "$PACK_DIR/pi-list.stdout.txt" 2>/dev/null || true'));
206
+ lines.push(...posixSection("PI_LIST_STDERR", 'cat "$PACK_DIR/pi-list.stderr.txt" 2>/dev/null || true'));
207
+ lines.push("");
208
+ lines.push('echo "node=$NODE_VERSION_EXIT ci=$CI_EXIT deps=$DEPS_EXIT test=$TEST_EXIT pack=$PACK_EXIT fixture=$FIXTURE_EXIT packedNodeInstall=$PACKED_NODE_INSTALL_EXIT install=$PI_INSTALL_EXIT list=$PI_LIST_EXIT"');
209
+ lines.push('if [ "$NODE_VERSION_EXIT" -ne 0 ] || [ "$CI_EXIT" -ne 0 ] || [ "$DEPS_EXIT" -ne 0 ] || [ "$TEST_EXIT" -ne 0 ] || [ "$PACK_EXIT" -ne 0 ] || [ "$FIXTURE_EXIT" -ne 0 ] || [ "$PACKED_NODE_INSTALL_EXIT" -ne 0 ] || [ "$PI_INSTALL_EXIT" -ne 0 ] || [ "$PI_LIST_EXIT" -ne 0 ]; then');
210
+ lines.push(' echo "PLATFORM_BUILD_FAILED: node=$NODE_VERSION_EXIT ci=$CI_EXIT deps=$DEPS_EXIT test=$TEST_EXIT pack=$PACK_EXIT fixture=$FIXTURE_EXIT packedNodeInstall=$PACKED_NODE_INSTALL_EXIT install=$PI_INSTALL_EXIT list=$PI_LIST_EXIT"');
211
+ lines.push(' exit 1');
212
+ lines.push('fi');
213
+ lines.push('echo "PLATFORM_BUILD_OK"');
214
+ return lines.join("\n");
215
+ }
216
+
217
+ export async function runTargetSuite(config, targetName, suiteName, leaseSession) {
218
+ if (!["macos", "ubuntu", "windows-native"].includes(targetName)) throw new Error(`unknown target: ${targetName}`);
219
+ if (!["platform-build", "real-extension"].includes(suiteName)) throw new Error(`unknown suite: ${suiteName}`);
220
+ const runId = makeRunId();
221
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
222
+ const slug = `${config.packageName ?? "pi-oracle"}-${targetName}`;
223
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform: platformForTarget(targetName), slug, runId, writtenAt: new Date().toISOString() }, null, 2));
224
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, writtenAt: new Date().toISOString() }, null, 2));
225
+
226
+ const command = suiteName === "platform-build"
227
+ ? buildPlatformBuildCommand(targetName, config.packageName ?? "pi-oracle", config.nodeValidationMajor ?? 24)
228
+ : buildRealExtensionCommand(targetName, config);
229
+ writeCommand(suiteDir, command);
230
+
231
+ let warmup = leaseSession;
232
+ const ownsLease = !warmup;
233
+ if (!warmup) {
234
+ console.log(` warmup ${targetName}...`);
235
+ warmup = await warmupLease(config, targetName, slug);
236
+ if (!warmup.ok) return failTransportSuite(suiteDir, targetName, suiteName, warmup, "warmup");
237
+ }
238
+
239
+ const startedAt = Date.now();
240
+ console.log(` executing ${suiteName} on ${targetName}...`);
241
+ const result = await runOnLease(config, targetName, warmup.leaseId, command, {
242
+ shell: true,
243
+ timeout: suiteName === "real-extension" ? 900_000 : 900_000,
244
+ sync: leaseSession?.sync,
245
+ allowEnvNames: suiteName === "real-extension" ? realSmokeAllowedEnvNames(config) : undefined,
246
+ });
247
+ const elapsedMs = Date.now() - startedAt;
248
+
249
+ writeFileSync(resolve(suiteDir, "crabbox.stdout.txt"), result.stdout);
250
+ writeFileSync(resolve(suiteDir, "crabbox.stderr.txt"), result.stderr);
251
+ writeFileSync(resolve(suiteDir, "crabbox.timing.json"), JSON.stringify({ startedAt: new Date(startedAt).toISOString(), elapsedMs, code: result.code, signal: result.signal }, null, 2));
252
+ writeExitCode(suiteDir, result.code, result.signal);
253
+ if (suiteName === "platform-build") writeExtracts(suiteDir, result.stdout);
254
+
255
+ let stopResult;
256
+ if (ownsLease) {
257
+ stopResult = await stopLease(config, targetName, warmup.leaseId);
258
+ writeStopArtifacts(suiteDir, stopResult);
259
+ }
260
+
261
+ const violations = [
262
+ ...scanForSecrets(`${result.stdout}\n${result.stderr}`),
263
+ ...scanArtifacts(suiteDir).map((finding) => `${finding.file}: ${finding.violation}`),
264
+ ];
265
+ if (violations.length > 0) writeFileSync(resolve(suiteDir, "redaction-violations.json"), JSON.stringify(violations, null, 2));
266
+
267
+ const stdout = result.stdout;
268
+ const packageName = config.packageName ?? "pi-oracle";
269
+ let checks;
270
+ let expectedFiles;
271
+ if (suiteName === "platform-build") {
272
+ const listOutput = section(stdout, "PI_LIST_STDOUT");
273
+ const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
274
+ const packageInstallPattern = new RegExp(`node_modules[\\\\/]${escapedPackageName}`);
275
+ const nodeMajor = Number(stdout.match(/PLATFORM_NODE_VERSION=v?(\d+)\./)?.[1] ?? 0);
276
+ checks = [
277
+ { id: "build-exit-zero", fn: () => result.code === 0, error: `exit ${result.code}` },
278
+ { id: "build-marker", fn: () => stdout.includes("PLATFORM_BUILD_OK") },
279
+ { id: "node-version", fn: () => nodeMajor >= (config.nodeValidationMajor ?? 24) },
280
+ { id: "npm-ci", fn: () => /PLATFORM_NPM_CI_EXIT=0/.test(stdout) },
281
+ { id: "platform-dependencies", fn: () => /PLATFORM_DEPS_EXIT=0/.test(stdout) },
282
+ { id: "platform-verification", fn: () => /PLATFORM_NPM_TEST_EXIT=0/.test(stdout) },
283
+ { id: "npm-pack", fn: () => /PLATFORM_NPM_PACK_EXIT=0/.test(stdout) && /PLATFORM_PACKED_TARBALL=\S+/.test(stdout) },
284
+ { id: "fixture-workspace", fn: () => /PLATFORM_FIXTURE_EXIT=0/.test(stdout) },
285
+ { id: "packed-node-install", fn: () => /PLATFORM_PACKED_NODE_INSTALL_EXIT=0/.test(stdout) },
286
+ { id: "pi-install", fn: () => /PLATFORM_PI_INSTALL_EXIT=0/.test(stdout) },
287
+ { id: "pi-list", fn: () => /PLATFORM_PI_LIST_EXIT=0/.test(stdout) && listOutput.includes(packageName) && packageInstallPattern.test(listOutput) },
288
+ { id: "no-source-extension-path", fn: () => !/\bpi\s+(?:-e|--extension)\s+\./.test(stdout) },
289
+ { id: "no-secrets", fn: () => violations.length === 0, error: "redaction violations found" },
290
+ ];
291
+ expectedFiles = [
292
+ "summary.json", "target.json", "suite.json", "command.txt", "exit-code.txt",
293
+ "crabbox.stdout.txt", "crabbox.stderr.txt", "crabbox.timing.json",
294
+ "packed-tarball.txt", "packed-node-install.stdout.txt", "packed-node-install.stderr.txt",
295
+ "pi-install.stdout.txt", "pi-install.stderr.txt", "pi-list.stdout.txt", "pi-list.stderr.txt",
296
+ "assertions.json",
297
+ ];
298
+ } else {
299
+ const provider = process.env.PI_ORACLE_REAL_TEST_PROVIDER || "zai";
300
+ checks = [
301
+ { id: "real-smoke-exit-zero", fn: () => result.code === 0, error: `exit ${result.code}` },
302
+ { id: "real-smoke-doctor", fn: () => stdout.includes("Oracle real smoke doctor") && stdout.includes(`provider: ${provider}`) },
303
+ { id: "real-smoke-marker", fn: () => stdout.includes("Oracle real smoke passed:") },
304
+ { id: "real-smoke-packed-install", fn: () => stdout.includes("mode=packed") && stdout.includes("extension=./node_modules/pi-oracle") },
305
+ { id: "real-smoke-no-source-extension", fn: () => !stdout.includes("extensions/oracle/index.ts") && !/\bpi\s+(?:-e|--extension)\s+extensions\/oracle/.test(stdout) },
306
+ { id: "no-secrets", fn: () => violations.length === 0, error: "redaction violations found" },
307
+ ];
308
+ expectedFiles = [
309
+ "summary.json", "target.json", "suite.json", "command.txt", "exit-code.txt",
310
+ "crabbox.stdout.txt", "crabbox.stderr.txt", "crabbox.timing.json", "assertions.json",
311
+ ];
312
+ }
313
+ if (stopResult) checks.push({ id: "lease-stop", fn: () => stopResult.code === 0, error: `stop exit ${stopResult.code}` });
314
+ if (stopResult) expectedFiles.push("crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt");
315
+ const assertions = finalizeSuiteArtifacts(suiteDir, checks, { target: targetName, suite: suiteName, exitCode: result.code, signal: result.signal, elapsedMs }, expectedFiles);
316
+ console.log(` ${assertions.ok ? "PASS" : "FAIL"} ${suiteName} on ${targetName} (${elapsedMs}ms)`);
317
+ return { ok: assertions.ok, suiteDir, assertions };
318
+ }
319
+
320
+ export async function runTargetSuites(config, targetName, suiteNames) {
321
+ const slug = `${config.packageName ?? "pi-oracle"}-${targetName}`;
322
+ console.log(` warmup ${targetName}...`);
323
+ const warmup = await warmupLease(config, targetName, slug);
324
+ if (!warmup.ok) {
325
+ const result = createWarmupFailureResult(config, targetName, suiteNames[0] ?? "platform-build", warmup);
326
+ return { ok: false, results: [result] };
327
+ }
328
+
329
+ const results = [];
330
+ let sync = true;
331
+ let stopResult;
332
+ try {
333
+ for (const suiteName of suiteNames) {
334
+ const result = await runTargetSuite(config, targetName, suiteName, { ...warmup, sync });
335
+ results.push(result);
336
+ sync = false;
337
+ if (!result.ok) break;
338
+ }
339
+ } finally {
340
+ stopResult = await stopLease(config, targetName, warmup.leaseId);
341
+ for (const result of results) recordStopResultOnSuite(result.suiteDir, stopResult);
342
+ }
343
+ if (stopResult?.code !== 0) results.push(createStopFailureResult(config, targetName, warmup.leaseId, stopResult));
344
+ return { ok: results.every((result) => result.ok), results };
345
+ }
346
+
347
+ function writeStopArtifacts(suiteDir, stopResult) {
348
+ writeFileSync(resolve(suiteDir, "crabbox.stop.stdout.txt"), stopResult.stdout ?? "");
349
+ writeFileSync(resolve(suiteDir, "crabbox.stop.stderr.txt"), stopResult.stderr ?? "");
350
+ writeFileSync(resolve(suiteDir, "crabbox.stop.exit-code.txt"), `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`);
351
+ }
352
+
353
+ function recordStopResultOnSuite(suiteDir, stopResult) {
354
+ if (!suiteDir || !stopResult) return;
355
+ writeStopArtifacts(suiteDir, stopResult);
356
+ const assertionsPath = resolve(suiteDir, "assertions.json");
357
+ const summaryPath = resolve(suiteDir, "summary.json");
358
+ const manifestPath = resolve(suiteDir, "artifact-manifest.json");
359
+ const assertions = JSON.parse(readFileSync(assertionsPath, "utf8"));
360
+ const summary = JSON.parse(readFileSync(summaryPath, "utf8"));
361
+ const stopCheck = { id: "lease-stop", ok: stopResult.code === 0, ...(stopResult.code === 0 ? {} : { error: `stop exit ${stopResult.code}` }) };
362
+ assertions.checks = [...(assertions.checks ?? []).filter((check) => check.id !== "lease-stop"), stopCheck];
363
+ assertions.ok = assertions.checks.every((check) => check.ok);
364
+ writeFileSync(assertionsPath, JSON.stringify({ ...assertions, writtenAt: new Date().toISOString() }, null, 2));
365
+ if (!assertions.ok) {
366
+ const failed = assertions.checks.filter((check) => !check.ok);
367
+ writeFileSync(resolve(suiteDir, "failures.md"), `# Assertion Failures\n\n${failed.map((check) => `- **${check.id}**: ${check.error ?? "failed"}`).join("\n")}\n\nTotal: ${failed.length} failure(s)\n`);
368
+ }
369
+ writeSummary(suiteDir, { ...summary, ok: Boolean(summary.ok && assertions.ok) });
370
+ const previousManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
371
+ const expected = [...new Set([...(previousManifest.expected ?? []), "crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt", "artifact-manifest.json"])]
372
+ writeManifest(suiteDir, expected);
373
+ }
374
+
375
+ function finalizeSuiteArtifacts(suiteDir, checks, summary, expectedFiles) {
376
+ let assertions = runAssertions(suiteDir, checks);
377
+ writeSummary(suiteDir, { ...summary, ok: assertions.ok });
378
+ let manifest = writeManifest(suiteDir, assertions.ok ? expectedFiles : [...expectedFiles, "failures.md"]);
379
+ if (manifest.missing.length > 0) {
380
+ assertions = runAssertions(suiteDir, [
381
+ ...checks,
382
+ { id: "artifact-manifest-complete", fn: () => false, error: `missing required artifact(s): ${manifest.missing.join(", ")}` },
383
+ ]);
384
+ writeSummary(suiteDir, { ...summary, ok: false });
385
+ manifest = writeManifest(suiteDir, [...expectedFiles, "failures.md"]);
386
+ }
387
+ void manifest;
388
+ return assertions;
389
+ }
390
+
391
+ function failTransportSuite(suiteDir, targetName, suiteName, result, phase) {
392
+ writeFileSync(resolve(suiteDir, `crabbox.${phase}.stdout.txt`), result.stdout ?? "");
393
+ writeFileSync(resolve(suiteDir, `crabbox.${phase}.stderr.txt`), result.stderr ?? "");
394
+ writeExitCode(suiteDir, result.code ?? 1, result.signal);
395
+ const assertions = finalizeSuiteArtifacts(
396
+ suiteDir,
397
+ [{ id: phase, fn: () => false, error: `Crabbox ${phase} failed: ${String(result.stderr || result.stdout || "unknown").slice(-500)}` }],
398
+ { target: targetName, suite: suiteName, exitCode: result.code ?? 1, signal: result.signal, phase },
399
+ ["summary.json", "target.json", "suite.json", "command.txt", "exit-code.txt", `crabbox.${phase}.stdout.txt`, `crabbox.${phase}.stderr.txt`, "assertions.json"],
400
+ );
401
+ return { ok: false, suiteDir, assertions };
402
+ }
403
+
404
+ function createWarmupFailureResult(config, targetName, suiteName, warmup) {
405
+ const runId = makeRunId();
406
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
407
+ const slug = `${config.packageName ?? "pi-oracle"}-${targetName}`;
408
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform: platformForTarget(targetName), slug, runId, writtenAt: new Date().toISOString() }, null, 2));
409
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, writtenAt: new Date().toISOString() }, null, 2));
410
+ writeCommand(suiteDir, `crabbox warmup ${targetName}`);
411
+ return failTransportSuite(suiteDir, targetName, suiteName, warmup, "warmup");
412
+ }
413
+
414
+ function createStopFailureResult(config, targetName, leaseId, stopResult) {
415
+ const suiteName = "lease-cleanup";
416
+ const runId = makeRunId();
417
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
418
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform: platformForTarget(targetName), leaseId, runId, writtenAt: new Date().toISOString() }, null, 2));
419
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, writtenAt: new Date().toISOString() }, null, 2));
420
+ writeCommand(suiteDir, `crabbox stop ${targetName} --id ${leaseId}`);
421
+ writeExitCode(suiteDir, stopResult.code, stopResult.signal);
422
+ writeStopArtifacts(suiteDir, stopResult);
423
+ const assertions = finalizeSuiteArtifacts(
424
+ suiteDir,
425
+ [{ id: "lease-stop", fn: () => false, error: `stop exit ${stopResult.code}` }],
426
+ { target: targetName, suite: suiteName, exitCode: stopResult.code, signal: stopResult.signal },
427
+ ["summary.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt", "assertions.json"],
428
+ );
429
+ return { ok: false, suiteDir, assertions };
430
+ }
431
+
432
+ export function readSuiteSummary(suiteDir) {
433
+ return JSON.parse(readFileSync(resolve(suiteDir, "summary.json"), "utf8"));
434
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "node:module";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const repoRoot = resolve(__dirname, "..");
10
+ const require = createRequire(import.meta.url);
11
+
12
+ let config;
13
+ try {
14
+ config = require(resolve(repoRoot, "platform-smoke.config.mjs"));
15
+ if (config.default) config = config.default;
16
+ } catch (error) {
17
+ config = null;
18
+ }
19
+
20
+ function printHelp() {
21
+ console.log(`Usage: node scripts/platform-smoke.mjs <command> [options]
22
+
23
+ Commands:
24
+ doctor Run mandatory Crabbox, host, target-tool, auth, and package preflight checks
25
+ run --target <names> Run one or more comma-separated targets concurrently
26
+ run --suite <name> Run one suite on the configured target(s)
27
+
28
+ Options:
29
+ --target Comma-separated target names. Supported: macos,ubuntu,windows-native
30
+ --suite Suite name. Supported: platform-build,real-extension
31
+ --help, -h Show this help
32
+
33
+ Examples:
34
+ node scripts/platform-smoke.mjs doctor
35
+ node scripts/platform-smoke.mjs run --target macos
36
+ node scripts/platform-smoke.mjs run --target ubuntu
37
+ node scripts/platform-smoke.mjs run --target windows-native
38
+ node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native --suite platform-build
39
+ node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native --suite real-extension
40
+
41
+ Canonical workflows:
42
+ Everyday local iteration: npm run verify:oracle
43
+ Platform-sensitive changes: npm run smoke:platform:doctor, then focused run --target <target> --suite <suite>
44
+ Publish/release proof: npm run smoke:platform:all
45
+
46
+ Environment:
47
+ PI_ORACLE_SMOKE_CRABBOX Optional Crabbox binary override (defaults to PATH)
48
+ PI_ORACLE_SMOKE_MAC_HOST macOS SSH host (default: localhost)
49
+ PI_ORACLE_SMOKE_MAC_USER macOS SSH user (default: $USER)
50
+ PI_ORACLE_SMOKE_MAC_WORK_ROOT macOS Crabbox work root
51
+ PI_ORACLE_SMOKE_UBUNTU_IMAGE Optional local-container image override
52
+ PI_ORACLE_SMOKE_WINDOWS_VM Parallels source VM (default from config)
53
+ PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT Parallels snapshot (default from config)
54
+ PI_ORACLE_SMOKE_WINDOWS_USER Windows SSH user (default: $USER)
55
+ PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows work root
56
+ PI_ORACLE_REAL_TEST_PROVIDER Real smoke provider (default: zai)
57
+ PI_ORACLE_REAL_TEST_MODEL Real smoke model (default: glm-5.1)
58
+ ZAI_API_KEY Default real-smoke provider API key
59
+ `);
60
+ }
61
+
62
+ function parseArgs(argv) {
63
+ const args = { command: null, target: null, suite: null };
64
+ for (let i = 2; i < argv.length; i += 1) {
65
+ const arg = argv[i];
66
+ if (arg === "--help" || arg === "-h") {
67
+ args.command = "help";
68
+ return args;
69
+ }
70
+ if (arg === "doctor" || arg === "run") {
71
+ args.command = arg;
72
+ continue;
73
+ }
74
+ if (arg === "--target" && i + 1 < argv.length) {
75
+ args.target = argv[i + 1];
76
+ i += 1;
77
+ continue;
78
+ }
79
+ if (arg === "--suite" && i + 1 < argv.length) {
80
+ args.suite = argv[i + 1];
81
+ i += 1;
82
+ continue;
83
+ }
84
+ throw new Error(`unknown argument: ${arg}`);
85
+ }
86
+ return args;
87
+ }
88
+
89
+ function splitCsv(value) {
90
+ return value.split(",").map((part) => part.trim()).filter(Boolean);
91
+ }
92
+
93
+ function validateSelection(targets, suites) {
94
+ const supportedTargets = new Set(config.requiredTargets ?? ["ubuntu"]);
95
+ const supportedSuites = new Set(config.requiredSuites ?? ["platform-build"]);
96
+ for (const target of targets) {
97
+ if (!supportedTargets.has(target)) throw new Error(`unsupported target: ${target}`);
98
+ }
99
+ for (const suite of suites) {
100
+ if (!supportedSuites.has(suite)) throw new Error(`unsupported suite: ${suite}`);
101
+ }
102
+ }
103
+
104
+ async function runDoctor() {
105
+ const { runDoctor } = await import("./platform-smoke/doctor.mjs");
106
+ await runDoctor(config);
107
+ }
108
+
109
+ async function runTarget(targetName, suites, singleSuite) {
110
+ const { runTargetSuite, runTargetSuites } = await import("./platform-smoke/targets.mjs");
111
+ if (singleSuite) return runTargetSuite(config, targetName, suites[0]);
112
+ return runTargetSuites(config, targetName, suites);
113
+ }
114
+
115
+ async function main() {
116
+ const args = parseArgs(process.argv);
117
+ if (!args.command || args.command === "help") {
118
+ printHelp();
119
+ process.exit(args.command === "help" ? 0 : 1);
120
+ }
121
+ if (!config) throw new Error("platform-smoke.config.mjs not found or failed to load");
122
+
123
+ if (args.command === "doctor") {
124
+ await runDoctor();
125
+ return;
126
+ }
127
+
128
+ if (args.command === "run") {
129
+ const targets = args.target ? splitCsv(args.target) : config.requiredTargets;
130
+ const suites = args.suite ? [args.suite] : config.requiredSuites;
131
+ validateSelection(targets, suites);
132
+ const results = await Promise.all(targets.map(async (target) => {
133
+ console.log(`\n=== Target: ${target} ===`);
134
+ return { target, result: await runTarget(target, suites, Boolean(args.suite)) };
135
+ }));
136
+ if (results.some(({ result }) => !result.ok)) {
137
+ console.log("\nOne or more platform smoke suites failed. Check .artifacts/platform-smoke/ for details.");
138
+ process.exit(1);
139
+ }
140
+ return;
141
+ }
142
+
143
+ throw new Error(`unknown command: ${args.command}`);
144
+ }
145
+
146
+ main().catch((error) => {
147
+ console.error(error instanceof Error ? error.message : String(error));
148
+ process.exit(1);
149
+ });