pi-oracle 0.7.4 → 0.7.6

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 (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +53 -18
  3. package/docs/ORACLE_DESIGN.md +16 -8
  4. package/docs/platform-smoke.md +156 -0
  5. package/extensions/oracle/index.ts +10 -4
  6. package/extensions/oracle/lib/config.ts +53 -27
  7. package/extensions/oracle/lib/jobs.ts +9 -5
  8. package/extensions/oracle/lib/poller.ts +1 -0
  9. package/extensions/oracle/lib/runtime.ts +107 -32
  10. package/extensions/oracle/lib/tools.ts +138 -12
  11. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  12. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  13. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  14. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  15. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  16. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  17. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  18. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  19. package/extensions/oracle/worker/run-job.mjs +107 -25
  20. package/package.json +30 -9
  21. package/platform-smoke.config.mjs +66 -0
  22. package/scripts/oracle-real-smoke.mjs +500 -0
  23. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  24. package/scripts/platform-smoke/artifacts.mjs +87 -0
  25. package/scripts/platform-smoke/assertions.mjs +34 -0
  26. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  27. package/scripts/platform-smoke/doctor.mjs +239 -0
  28. package/scripts/platform-smoke/invariants.mjs +124 -0
  29. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  30. package/scripts/platform-smoke/targets.mjs +434 -0
  31. package/scripts/platform-smoke.mjs +152 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Thin Crabbox CLI wrapper for pi-oracle's local platform smoke targets.
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+
7
+ const CRABBOX_BIN = process.env.PI_ORACLE_SMOKE_CRABBOX || process.env.PLATFORM_SMOKE_CRABBOX || "crabbox";
8
+
9
+ function env(name) {
10
+ return process.env[name] ?? "";
11
+ }
12
+
13
+ export function execCrabbox(args, opts = {}) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn(CRABBOX_BIN, args, {
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false", ...opts.env },
18
+ ...opts.spawnOpts,
19
+ });
20
+ const stdoutChunks = [];
21
+ const stderrChunks = [];
22
+ let timeout;
23
+ let killTimeout;
24
+ if (opts.timeout && opts.timeout > 0) {
25
+ timeout = setTimeout(() => {
26
+ stderrChunks.push(Buffer.from(`\n[platform-smoke] crabbox command timed out after ${opts.timeout}ms\n`));
27
+ try { child.kill("SIGTERM"); } catch {}
28
+ killTimeout = setTimeout(() => {
29
+ try { child.kill("SIGKILL"); } catch {}
30
+ }, 10_000);
31
+ }, opts.timeout);
32
+ }
33
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
34
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
35
+ child.on("error", (error) => {
36
+ if (timeout) clearTimeout(timeout);
37
+ if (killTimeout) clearTimeout(killTimeout);
38
+ resolve({
39
+ stdout: Buffer.concat(stdoutChunks).toString(),
40
+ stderr: `${Buffer.concat(stderrChunks).toString()}\n${error.message}`.trim(),
41
+ code: 1,
42
+ signal: null,
43
+ });
44
+ });
45
+ child.on("close", (code, signal) => {
46
+ if (timeout) clearTimeout(timeout);
47
+ if (killTimeout) clearTimeout(killTimeout);
48
+ resolve({
49
+ stdout: Buffer.concat(stdoutChunks).toString(),
50
+ stderr: Buffer.concat(stderrChunks).toString(),
51
+ code: code ?? (signal ? 1 : 0),
52
+ signal,
53
+ });
54
+ });
55
+ });
56
+ }
57
+
58
+ export function buildTargetBaseArgs(targetName, config = {}) {
59
+ switch (targetName) {
60
+ case "macos": {
61
+ const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
62
+ const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
63
+ const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName ?? "pi-oracle"}`;
64
+ return [
65
+ "--provider", "ssh",
66
+ "--target", "macos",
67
+ "--static-host", host,
68
+ "--static-user", user,
69
+ "--static-port", "22",
70
+ "--static-work-root", workRoot,
71
+ ];
72
+ }
73
+ case "ubuntu": {
74
+ const image = env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24";
75
+ return [
76
+ "--provider", "local-container",
77
+ "--target", "linux",
78
+ "--local-container-image", image,
79
+ ];
80
+ }
81
+ case "windows-native": {
82
+ const vm = env("PI_ORACLE_SMOKE_WINDOWS_VM") || env("PLATFORM_SMOKE_WINDOWS_VM") || config.windowsParallels?.sourceVm || "pi-extension-windows-template";
83
+ const snapshot = env("PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT") || env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config.windowsParallels?.snapshot || "crabbox-ready";
84
+ const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
85
+ const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName ?? "pi-oracle"}`;
86
+ return [
87
+ "--provider", "parallels",
88
+ "--target", "windows",
89
+ "--windows-mode", "normal",
90
+ "--parallels-source", vm,
91
+ "--parallels-source-snapshot", snapshot,
92
+ "--parallels-user", user,
93
+ "--parallels-work-root", workRoot,
94
+ ];
95
+ }
96
+ default:
97
+ throw new Error(`unknown target: ${targetName}`);
98
+ }
99
+ }
100
+
101
+ function parseLeaseId(output) {
102
+ return output.match(/\bleased\s+(\S+)/)?.[1]
103
+ ?? output.match(/\blease=(\S+)/)?.[1]
104
+ ?? null;
105
+ }
106
+
107
+ export async function warmupLease(config, targetName, slug) {
108
+ const args = ["warmup", ...buildTargetBaseArgs(targetName, config), "--slug", slug, "--keep"];
109
+ if (targetName === "macos") args.push("--reclaim");
110
+ console.log(` [crabbox] ${args.join(" ")}`);
111
+ const result = await execCrabbox(args, { timeout: 300_000 });
112
+ return {
113
+ ok: result.code === 0,
114
+ ...result,
115
+ leaseId: parseLeaseId(result.stdout) ?? parseLeaseId(result.stderr) ?? slug,
116
+ };
117
+ }
118
+
119
+ export async function runOnLease(config, targetName, leaseId, command, opts = {}) {
120
+ const args = ["run", ...buildTargetBaseArgs(targetName, config), "--id", leaseId];
121
+ if (targetName === "macos") args.push("--reclaim");
122
+ for (const name of opts.allowEnvNames ?? []) args.push("--allow-env", name);
123
+ if (opts.sync === false) args.push("--no-sync");
124
+ else args.push("--fresh-sync");
125
+ if (opts.shell) args.push("--shell", command);
126
+ else args.push("--", ...(Array.isArray(command) ? command : command.split(" ")));
127
+ console.log(` [crabbox] run ${targetName} ${opts.sync === false ? "--no-sync" : "--fresh-sync"} ...`);
128
+ return execCrabbox(args, { timeout: opts.timeout ?? 900_000, env: opts.env });
129
+ }
130
+
131
+ export async function stopLease(config, targetName, leaseId) {
132
+ const args = ["stop", ...buildTargetBaseArgs(targetName, config), "--id", leaseId];
133
+ console.log(` [crabbox] ${args.join(" ")}`);
134
+ return execCrabbox(args, { timeout: 60_000 });
135
+ }
@@ -0,0 +1,239 @@
1
+ // Preflight checks for the pi-oracle Crabbox platform smoke gate.
2
+
3
+ import { execFileSync, execSync } from "node:child_process";
4
+ import { accessSync, constants, existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ let failures = 0;
8
+
9
+ function ok(label) { console.log(` ✓ ${label}`); }
10
+ function warn(label) { console.log(` ⚠ ${label}`); }
11
+ function fail(label) { console.error(` ✗ ${label}`); failures += 1; }
12
+ function env(name) { return process.env[name] ?? ""; }
13
+ function truthy(value) { return /^(1|true|yes|on)$/i.test(String(value ?? "")); }
14
+
15
+ function silent(command, args, opts = {}) {
16
+ try { return execFileSync(command, args, { timeout: 20_000, stdio: "pipe", ...opts }).toString().trim(); }
17
+ catch { return null; }
18
+ }
19
+
20
+ function shell(command, opts = {}) {
21
+ try { return execSync(command, { timeout: 20_000, stdio: "pipe", ...opts }).toString().trim(); }
22
+ catch { return null; }
23
+ }
24
+
25
+ function resolveCommand(command) {
26
+ if (!command) return null;
27
+ if (command.includes("/") || command.includes("\\")) return command;
28
+ return shell(`command -v ${command}`) ?? command;
29
+ }
30
+
31
+ function compareVersions(actual, required) {
32
+ const a = String(actual).split(".").map((part) => Number.parseInt(part, 10));
33
+ const b = String(required).split(".").map((part) => Number.parseInt(part, 10));
34
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
35
+ const av = Number.isFinite(a[i]) ? a[i] : 0;
36
+ const bv = Number.isFinite(b[i]) ? b[i] : 0;
37
+ if (av > bv) return 1;
38
+ if (av < bv) return -1;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ function realSmokeProvider(config) {
44
+ return process.env.PI_ORACLE_REAL_TEST_PROVIDER || config.realSmoke?.defaultProvider || "zai";
45
+ }
46
+
47
+ function windowsVmName(config) {
48
+ return env("PI_ORACLE_SMOKE_WINDOWS_VM") || env("PLATFORM_SMOKE_WINDOWS_VM") || config.windowsParallels?.sourceVm || "pi-extension-windows-template";
49
+ }
50
+
51
+ function windowsSnapshotName(config) {
52
+ return env("PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT") || env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config.windowsParallels?.snapshot || "crabbox-ready";
53
+ }
54
+
55
+ function requiredRealSmokeAuthEnv(config) {
56
+ if (!(config.requiredSuites ?? []).includes("real-extension")) return [];
57
+ if (!truthy(process.env.PI_ORACLE_REAL_TEST_MODEL_AGENT)) return [];
58
+ const provider = realSmokeProvider(config);
59
+ return config.realSmoke?.authEnvByProvider?.[provider] ?? [];
60
+ }
61
+
62
+ function parseJson(text) {
63
+ try { return JSON.parse(text); } catch { return undefined; }
64
+ }
65
+
66
+ function targetBaseArgs(targetName, config) {
67
+ if (targetName === "macos") {
68
+ const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
69
+ const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
70
+ const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName}`;
71
+ return ["--provider", "ssh", "--target", "macos", "--static-host", host, "--static-user", user, "--static-port", "22", "--static-work-root", workRoot];
72
+ }
73
+ if (targetName === "ubuntu") {
74
+ const image = env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24";
75
+ return ["--provider", "local-container", "--target", "linux", "--local-container-image", image];
76
+ }
77
+ const vm = windowsVmName(config);
78
+ const snapshot = windowsSnapshotName(config);
79
+ const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
80
+ const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName}`;
81
+ return ["--provider", "parallels", "--target", "windows", "--windows-mode", "normal", "--parallels-source", vm, "--parallels-source-snapshot", snapshot, "--parallels-user", user, "--parallels-work-root", workRoot];
82
+ }
83
+
84
+ function runCrabboxDoctor(cbox, label, args, timeout = 120_000) {
85
+ const output = silent(cbox, ["doctor", ...args, "--json"], { env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" }, timeout });
86
+ if (!output) {
87
+ fail(`crabbox doctor ${label} failed`);
88
+ return;
89
+ }
90
+ const parsed = parseJson(output);
91
+ if (!parsed) fail(`could not parse ${label} Crabbox doctor JSON`);
92
+ else if (parsed.ok) ok(`${label} provider OK`);
93
+ else fail(`${label} doctor failed: ${parsed.error ?? "not ok"}`);
94
+ }
95
+
96
+ function runTargetToolProbe(cbox, targetName, config) {
97
+ const required = targetName === "windows-native"
98
+ ? ["node", "npm", "git", "tar", "zstd", "agent-browser"]
99
+ : ["node", "npm", "git", "tar", "rsync", "zstd", "agent-browser"];
100
+ const command = targetName === "windows-native"
101
+ ? `cmd.exe /c "where node && where npm && where git && where tar && where zstd && where agent-browser && zstd --version && agent-browser --version && echo tools-ok"`
102
+ : `missing=""; for tool in ${required.join(" ")}; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"; done; if [ -n "$missing" ]; then echo "missing=$missing"; exit 1; fi; zstd --version >/dev/null && agent-browser --version >/dev/null && echo tools-ok`;
103
+ const baseArgs = targetBaseArgs(targetName, config);
104
+ if (targetName === "macos") baseArgs.push("--reclaim");
105
+ const result = silent(cbox, ["run", ...baseArgs, "--no-sync", "--shell", command], {
106
+ env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
107
+ timeout: targetName === "windows-native" ? 300_000 : 180_000,
108
+ });
109
+ result?.includes("tools-ok") ? ok(`${targetName} required target tools OK`) : fail(`${targetName} required target tools missing or probe failed${result ? `: ${result.split(/\r?\n/).slice(-3).join(" | ")}` : ""}`);
110
+ }
111
+
112
+ function verifyWindowsTemplate(config) {
113
+ const vm = windowsVmName(config);
114
+ const snapshot = windowsSnapshotName(config);
115
+ const prlctl = resolveCommand("prlctl");
116
+ if (!prlctl) {
117
+ fail("prlctl not found on host PATH");
118
+ return;
119
+ }
120
+ ok(`prlctl: ${prlctl}`);
121
+ const list = silent("prlctl", ["list", "--all", "-j"], { timeout: 30_000 });
122
+ const vms = parseJson(list);
123
+ const vmRecord = Array.isArray(vms) ? vms.find((entry) => entry.name === vm || entry.ID === vm) : undefined;
124
+ if (!vmRecord) fail(`Windows source VM not found: ${vm}`);
125
+ else if (vmRecord.status !== "stopped") fail(`Windows source VM ${vm} must be stopped; status=${vmRecord.status}`);
126
+ else ok(`Windows source VM stopped: ${vm}`);
127
+
128
+ const snapshotsText = silent("prlctl", ["snapshot-list", vm, "-j"], { timeout: 30_000 });
129
+ const snapshots = parseJson(snapshotsText);
130
+ const snapshotRecord = snapshots ? Object.values(snapshots).find((entry) => entry?.name === snapshot) : undefined;
131
+ if (!snapshotRecord) fail(`Windows snapshot not found: ${snapshot}`);
132
+ else if (snapshotRecord.state !== "poweroff") fail(`Windows snapshot ${snapshot} must be poweroff; state=${snapshotRecord.state}`);
133
+ else ok(`Windows snapshot poweroff/forkable: ${snapshot}`);
134
+ }
135
+
136
+ function verifyPackageExclusions() {
137
+ const forbiddenPatterns = [/^\.env(?:\.|$)/, /^\.artifacts(?:\/|$)/, /^\.crabbox(?:\/|$)/, /^\.debug(?:\/|$)/, /^\.platform-smoke-runs(?:\/|$)/, /\.tgz$/];
138
+ const output = shell("npm pack --dry-run --json", { timeout: 120_000 });
139
+ const parsed = parseJson(output);
140
+ if (!Array.isArray(parsed) || !parsed[0]?.files) {
141
+ warn("could not inspect npm pack file list");
142
+ return;
143
+ }
144
+ const files = parsed[0].files.map((file) => file.path);
145
+ const forbidden = files.filter((file) => forbiddenPatterns.some((pattern) => pattern.test(file)));
146
+ forbidden.length ? fail(`forbidden files in package: ${forbidden.join(", ")}`) : ok("package excludes local env/artifact/debug state");
147
+ }
148
+
149
+ function verifyRepoForbiddenFiles() {
150
+ const forbidden = shell("find . -maxdepth 3 \\( -name '.env' -o -name '.env.*' -o -name '*.tgz' \\) -print 2>/dev/null");
151
+ forbidden ? fail(`forbidden local secret/package artifact(s): ${forbidden}`) : ok("no .env/.env.* or package tarballs at repo root depth");
152
+ for (const dir of [".artifacts", ".crabbox", ".debug", ".platform-smoke-runs"]) {
153
+ const ignored = shell(`git check-ignore -q ${dir}/probe && echo ignored`);
154
+ ignored ? ok(`${dir}/ is gitignored`) : fail(`${dir}/ must be gitignored`);
155
+ }
156
+ const trackedForbidden = shell("git ls-files '.env*' '*.tgz' '.artifacts/*' '.crabbox/*' '.debug/*' '.platform-smoke-runs/*'");
157
+ trackedForbidden ? fail(`forbidden tracked local state: ${trackedForbidden}`) : ok("no forbidden local state is tracked");
158
+ verifyPackageExclusions();
159
+ }
160
+
161
+ export async function runDoctor(config) {
162
+ failures = 0;
163
+ console.log("\n── Environment ──");
164
+ const cbox = env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") || "crabbox";
165
+ ok(`Crabbox binary = ${resolveCommand(cbox) ?? cbox}${env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") ? " (env override)" : " (PATH)"}`);
166
+ ok(`PI_ORACLE_SMOKE_MAC_HOST = ${env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost"}`);
167
+ ok(`PI_ORACLE_SMOKE_MAC_USER = ${env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER")}`);
168
+ ok(`PI_ORACLE_SMOKE_UBUNTU_IMAGE = ${env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24"}`);
169
+ ok(`PI_ORACLE_SMOKE_WINDOWS_VM = ${windowsVmName(config)}`);
170
+ ok(`PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT = ${windowsSnapshotName(config)}`);
171
+
172
+ console.log("\n── Crabbox ──");
173
+ const resolvedCbox = resolveCommand(cbox);
174
+ try { accessSync(resolvedCbox ?? cbox, constants.X_OK); ok(`binary executable: ${resolvedCbox ?? cbox}`); }
175
+ catch { fail(`${resolvedCbox ?? cbox} is not executable`); }
176
+ const version = silent(cbox, ["--version"]);
177
+ const versionLine = version?.split("\n")[0] ?? "";
178
+ version ? ok(`version: ${versionLine}`) : fail("could not read Crabbox version");
179
+ const requiredVersion = config.requiredCrabbox?.minVersion ?? config.requiredCrabbox?.version;
180
+ if (requiredVersion && versionLine) {
181
+ compareVersions(versionLine, requiredVersion) >= 0 ? ok(`required version: ${requiredVersion}+`) : fail(`Crabbox version mismatch: need ${requiredVersion}+, got ${versionLine}`);
182
+ }
183
+ const providers = silent(cbox, ["providers"], { timeout: 30_000 }) ?? "";
184
+ for (const provider of ["ssh", "local-container", "parallels"]) {
185
+ new RegExp(`^${provider}$`, "m").test(providers) ? ok(`provider available: ${provider}`) : fail(`crabbox providers missing ${provider}`);
186
+ }
187
+ runCrabboxDoctor(cbox, "macOS static SSH", targetBaseArgs("macos", config), 120_000);
188
+ runCrabboxDoctor(cbox, "Windows native Parallels", targetBaseArgs("windows-native", config), 180_000);
189
+ runCrabboxDoctor(cbox, "local-container", targetBaseArgs("ubuntu", config), 120_000);
190
+
191
+ console.log("\n── Host tools ──");
192
+ for (const [name, command] of [["Docker", "docker info --format '{{.ServerVersion}}'"], ["Node", "node --version"], ["npm", "npm --version"], ["git", "git --version"], ["tar", "tar --version"], ["rsync", "rsync --version"]]) {
193
+ const output = shell(command);
194
+ output ? ok(`${name}: ${output.split("\n")[0]}`) : fail(`${name} not found or unavailable`);
195
+ }
196
+ const nodeVersion = shell("node --version");
197
+ if (nodeVersion) {
198
+ const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
199
+ if (major < (config.nodeValidationMajor ?? 24)) fail(`Node ${nodeVersion}; need ${config.nodeValidationMajor ?? 24}+ for smoke validation`);
200
+ }
201
+ verifyWindowsTemplate(config);
202
+
203
+ console.log("\n── Auth environment ──");
204
+ const requiredAuth = requiredRealSmokeAuthEnv(config);
205
+ if (requiredAuth.length === 0) ok("no real-smoke auth env required by configured suites");
206
+ for (const name of requiredAuth) {
207
+ env(name) ? ok(`${name} = (present, redacted)`) : fail(`${name} missing for required real-extension suite`);
208
+ }
209
+
210
+ console.log("\n── Target tools ──");
211
+ for (const target of config.requiredTargets ?? []) runTargetToolProbe(cbox, target, config);
212
+
213
+ console.log("\n── Artifact root ──");
214
+ const artifactRoot = resolve(process.cwd(), config.artifactRoot ?? ".artifacts/platform-smoke");
215
+ try {
216
+ mkdirSync(artifactRoot, { recursive: true });
217
+ const probe = resolve(artifactRoot, ".doctor-write-test");
218
+ writeFileSync(probe, "ok");
219
+ unlinkSync(probe);
220
+ ok(`writable: ${artifactRoot}`);
221
+ } catch (error) {
222
+ fail(`cannot write artifact root: ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+
225
+ console.log("\n── Git/package hygiene ──");
226
+ const branch = shell("git branch --show-current");
227
+ branch ? ok(`branch: ${branch}`) : warn("could not determine branch");
228
+ const status = shell("git status --short");
229
+ status ? warn(`${status.split(/\r?\n/).filter(Boolean).length} uncommitted change(s) under test`) : ok("clean worktree");
230
+ verifyRepoForbiddenFiles();
231
+
232
+ console.log(`\n=== Results: ${failures} failure(s) ===`);
233
+ if (failures > 0) {
234
+ process.exitCode = 1;
235
+ console.log("Fix the failures above before running npm run smoke:platform:all.");
236
+ } else {
237
+ console.log("Ready for npm run smoke:platform:all.");
238
+ }
239
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ // Cheap invariant tests for the platform-smoke harness.
3
+
4
+ import assert from "node:assert/strict";
5
+ import { spawnSync } from "node:child_process";
6
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import config from "../../platform-smoke.config.mjs";
11
+ import { writeManifest } from "./artifacts.mjs";
12
+ import { buildPlatformBuildCommand, buildRealExtensionCommand } from "./targets.mjs";
13
+
14
+ const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
15
+
16
+ function runNode(args) {
17
+ return spawnSync(process.execPath, args, { cwd: repoRoot, encoding: "utf8" });
18
+ }
19
+
20
+ function testHelpTextIncludesTargetsAndExamples() {
21
+ const result = runNode(["scripts/platform-smoke.mjs", "--help"]);
22
+ assert.equal(result.status, 0, `help should exit cleanly: ${result.stderr}`);
23
+ assert.match(result.stdout, /Supported: macos,ubuntu,windows-native/, "help should list supported targets");
24
+ assert.match(result.stdout, /--suite\s+Suite name\. Supported: platform-build,real-extension/, "help should list supported suites");
25
+ assert.match(result.stdout, /npm run release:check/, "help should name the full release gate");
26
+ assert.match(result.stdout, /PLATFORM_SMOKE_CRABBOX/, "help should document reusable platform-smoke env knobs");
27
+ }
28
+
29
+ function testTargetSelection() {
30
+ const result = runNode(["scripts/platform-smoke.mjs", "run", "--target", "not-a-target", "--suite", "platform-build"]);
31
+ assert.notEqual(result.status, 0, "unsupported targets should fail before Crabbox runs");
32
+ assert.ifError(result.error);
33
+ assert.match(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, /unsupported target: not-a-target/);
34
+ }
35
+
36
+ function testPackedInstallCommandRendering() {
37
+ const command = buildPlatformBuildCommand("ubuntu", config.packageName, config.nodeValidationMajor);
38
+ assert.match(command, /verify:oracle:platform/, "platform-build should run the platform-focused verification gate");
39
+ assert.doesNotMatch(command, /npm test/, "platform-build should not run the full local iteration gate on every target");
40
+ assert.match(command, /npm pack --silent/, "platform-build should pack the package");
41
+ assert.match(command, /npm install --no-save/, "platform-build should install the packed tarball");
42
+ assert.match(command, /install -l \.\/node_modules\/pi-oracle/, "platform-build should install through pi's package path");
43
+ assert.doesNotMatch(command, /\bpi\s+(?:-e|--extension)\s+\./, "release proof must not use pi -e/--extension source shortcuts");
44
+ }
45
+
46
+ function testRealExtensionPackedInstallRendering() {
47
+ const command = buildRealExtensionCommand("ubuntu", config);
48
+ assert.match(command, /smoke:real:packed/, "required real-extension suite should run packed real smoke");
49
+ assert.doesNotMatch(command, /smoke:real:source|extensions\/oracle\/index\.ts|\bpi\s+-e\b/, "required real-extension suite must not use source extension loading");
50
+ }
51
+
52
+ function testManifestFailure() {
53
+ const dir = mkdtempSync(join(tmpdir(), "pi-oracle-manifest-test-"));
54
+ try {
55
+ writeFileSync(join(dir, "present.txt"), "ok\n");
56
+ const manifest = writeManifest(dir, ["present.txt", "missing.txt"]);
57
+ assert.deepEqual(manifest.missing, ["missing.txt"], "missing manifest entries should be reported");
58
+ assert(manifest.expected.includes("artifact-manifest.json"), "manifest should require itself as an artifact");
59
+ } finally {
60
+ rmSync(dir, { recursive: true, force: true });
61
+ }
62
+ }
63
+
64
+ function testCleanupFailureInvariant() {
65
+ const source = readFileSync(new URL("./targets.mjs", import.meta.url), "utf8");
66
+ assert.match(source, /recordStopResultOnSuite/, "multi-suite runs should record stop evidence on suite artifacts");
67
+ assert.match(source, /lease-stop/, "cleanup results should be asserted as lease-stop");
68
+ assert.match(source, /stop exit/, "cleanup failure should surface a stop exit error");
69
+ }
70
+
71
+ function testPackageExclusion() {
72
+ const result = spawnSync("npm", ["pack", "--dry-run", "--json"], { cwd: repoRoot, encoding: "utf8", shell: process.platform === "win32" });
73
+ assert.equal(result.status, 0, `npm pack dry-run failed: ${result.stderr}`);
74
+ const files = JSON.parse(result.stdout)[0].files.map((file) => file.path);
75
+ for (const forbidden of [".artifacts/", ".crabbox/", ".debug/", ".platform-smoke-runs/", ".env", "context.md"]) {
76
+ assert(!files.some((file) => file === forbidden || file.startsWith(forbidden)), `package should exclude ${forbidden}`);
77
+ }
78
+ }
79
+
80
+ function testSourceSmokeExplicitlyDebugOnly() {
81
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
82
+ assert.equal(pkg.scripts["smoke:real"], "npm run smoke:real:packed", "default real smoke should be packed-release proof");
83
+ assert.match(pkg.scripts["smoke:real:source"], /--mode source/, "source real smoke should be explicitly named");
84
+ }
85
+
86
+ function testCanonicalWorkflowConfig() {
87
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
88
+ assert.deepEqual(config.workflows?.everyday?.commands, ["npm run verify:oracle"], "everyday workflow should use the local verification gate");
89
+ assert(config.workflows?.platformSensitive?.commands?.includes("npm run smoke:platform:doctor"), "platform-sensitive workflow should start with doctor");
90
+ assert(config.workflows?.platformSensitive?.commands?.some((command) => command.includes("--target <target> --suite <suite>")), "platform-sensitive workflow should document focused target/suite runs");
91
+ assert.deepEqual(config.workflows?.platformMatrix?.commands, ["npm run smoke:platform:all"], "platform matrix workflow should use the full target matrix");
92
+ assert.deepEqual(config.workflows?.release?.commands, ["npm run release:check"], "release workflow should use the full local-plus-platform release gate");
93
+ assert.equal(config.requiredCrabbox?.minVersion, "0.26.0", "Crabbox baseline should match the documented provider contract");
94
+ assert.equal(pkg.scripts["smoke:platform:all"], "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native", "full platform smoke should remain doctor-first and cover all required targets");
95
+ assert.match(pkg.scripts["release:check"], /npm run verify:oracle && npm run smoke:platform:all/, "release check should combine local verification and full platform smoke");
96
+ const runnerSource = readFileSync(new URL("./crabbox-runner.mjs", import.meta.url), "utf8");
97
+ assert.match(runnerSource, /PLATFORM_SMOKE_CRABBOX/, "runner should honor reusable Crabbox binary override");
98
+ assert.match(runnerSource, /PLATFORM_SMOKE_MAC_WORK_ROOT/, "runner should honor reusable macOS work-root override");
99
+ assert.match(runnerSource, /PLATFORM_SMOKE_WINDOWS_WORK_ROOT/, "runner should honor reusable Windows work-root override");
100
+ }
101
+
102
+ function testRealSmokeExpensiveAgentPathsAreOptIn() {
103
+ const source = readFileSync(new URL("../oracle-real-smoke.mjs", import.meta.url), "utf8");
104
+ assert.match(source, /runDirectOracleSubmit/, "default real smoke should use deterministic installed-tool execution");
105
+ assert.match(source, /PI_ORACLE_REAL_TEST_MODEL_AGENT/, "real smoke should expose the optional model-agent toggle");
106
+ const targetsSource = readFileSync(new URL("./targets.mjs", import.meta.url), "utf8");
107
+ assert.match(targetsSource, /PI_ORACLE_REAL_TEST_MODEL_AGENT/, "platform real-extension command should forward the optional model-agent toggle");
108
+ assert.match(targetsSource, /realSmokeUsesModelAgent\(\) \? config\.realSmoke\?\.authEnvByProvider/, "platform smoke should allow provider auth env only for the optional model-agent path");
109
+ assert.match(source, /truthy\(env\("PI_ORACLE_REAL_TEST_MODEL_AGENT"\)\)/, "model-agent real smoke path should be opt-in");
110
+ assert.match(source, /PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK/, "real smoke should expose the optional negative symlink toggle");
111
+ assert.match(source, /truthy\(env\("PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK"\)\)/, "negative symlink real-agent check should be opt-in");
112
+ }
113
+
114
+ testHelpTextIncludesTargetsAndExamples();
115
+ testTargetSelection();
116
+ testPackedInstallCommandRendering();
117
+ testRealExtensionPackedInstallRendering();
118
+ testManifestFailure();
119
+ testCleanupFailureInvariant();
120
+ testPackageExclusion();
121
+ testSourceSmokeExplicitlyDebugOnly();
122
+ testCanonicalWorkflowConfig();
123
+ testRealSmokeExpensiveAgentPathsAreOptIn();
124
+ console.log("platform-smoke invariant checks passed");