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,168 @@
1
+ param(
2
+ [string]$PackageName = "pi-oracle",
3
+ [int]$NodeValidationMajor = 24
4
+ )
5
+
6
+ $ErrorActionPreference = "Continue"
7
+
8
+ function Exit-CodeFromLastCommand {
9
+ if ($null -ne $global:LASTEXITCODE) { return [int]$global:LASTEXITCODE }
10
+ if ($?) { return 0 }
11
+ return 1
12
+ }
13
+
14
+ function Write-SectionFile {
15
+ param([string]$Name, [string]$Path)
16
+ Write-Output "--- $Name START ---"
17
+ if (Test-Path -LiteralPath $Path) {
18
+ Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue
19
+ }
20
+ Write-Output "--- $Name END ---"
21
+ }
22
+
23
+ Write-Output "Starting pi-oracle platform-build in $(Get-Location) at $(Get-Date -Format o)"
24
+ $SourceRoot = (Get-Location).Path
25
+ $RunRoot = Join-Path $SourceRoot (Join-Path ".platform-smoke-runs" ("platform-build-" + (Get-Date -Format "yyyyMMddTHHmmssZ") + "-" + $PID))
26
+ $PackDir = Join-Path $RunRoot "pack"
27
+ $TestWorkspace = Join-Path $RunRoot "test-workspace"
28
+ $PiProject = Join-Path $RunRoot "pi-project"
29
+ New-Item -ItemType Directory -Force -Path $PackDir, $TestWorkspace, $PiProject | Out-Null
30
+ Write-Output "PLATFORM_RUN_ROOT=$RunRoot"
31
+ Write-Output "PLATFORM_TEST_WORKSPACE=$TestWorkspace"
32
+ Write-Output "PLATFORM_PI_PROJECT=$PiProject"
33
+
34
+ $NodeVersion = (& node.exe --version).Trim()
35
+ $NodeMajor = [int](($NodeVersion -replace '^v', '').Split('.')[0])
36
+ if ($NodeMajor -ge $NodeValidationMajor) { $NODE_VERSION_EXIT = 0 } else { $NODE_VERSION_EXIT = 1 }
37
+ Write-Output "PLATFORM_NODE_VERSION=$NodeVersion"
38
+ Write-Output "PLATFORM_NODE_VERSION_EXIT=$NODE_VERSION_EXIT"
39
+
40
+ Write-Output "=== npm ci ==="
41
+ & npm.cmd ci *>&1
42
+ $CI_EXIT = Exit-CodeFromLastCommand
43
+ Write-Output "PLATFORM_NPM_CI_EXIT=$CI_EXIT"
44
+
45
+ Write-Output "=== platform dependencies ==="
46
+ $DEPS_EXIT = 0
47
+ $ZstdCommand = Get-Command zstd -ErrorAction SilentlyContinue
48
+ if ($ZstdCommand) { $ZSTD_INSTALL_EXIT = 0 } else { Write-Output "zstd missing on Windows target PATH; update pi-extension-windows-template/crabbox-ready"; $ZSTD_INSTALL_EXIT = 1 }
49
+ if ($ZSTD_INSTALL_EXIT -ne 0) { $DEPS_EXIT = 1 }
50
+ $AgentBrowserCommand = Get-Command agent-browser -ErrorAction SilentlyContinue
51
+ if (-not $AgentBrowserCommand) { $AgentBrowserCommand = Get-Command agent-browser.cmd -ErrorAction SilentlyContinue }
52
+ if ($AgentBrowserCommand) { $AGENT_BROWSER_INSTALL_EXIT = 0; $AgentBrowserPath = $AgentBrowserCommand.Source } else { Write-Output "agent-browser missing on Windows target PATH; update pi-extension-windows-template/crabbox-ready"; $AGENT_BROWSER_INSTALL_EXIT = 1; $AgentBrowserPath = "" }
53
+ if ($AGENT_BROWSER_INSTALL_EXIT -ne 0) { $DEPS_EXIT = 1 }
54
+ Write-Output "PLATFORM_ZSTD_INSTALL_EXIT=$ZSTD_INSTALL_EXIT"
55
+ Write-Output "PLATFORM_AGENT_BROWSER_INSTALL_EXIT=$AGENT_BROWSER_INSTALL_EXIT"
56
+ Write-Output "PLATFORM_DEPS_EXIT=$DEPS_EXIT"
57
+ if ($ZstdCommand) { Write-Output $ZstdCommand.Source }
58
+ if ($AgentBrowserPath) { Write-Output $AgentBrowserPath }
59
+
60
+ Write-Output "=== platform verification ==="
61
+ $PreviousAgentBrowserPath = $env:AGENT_BROWSER_PATH
62
+ $PreviousSanityProgress = $env:PI_ORACLE_SANITY_PROGRESS
63
+ $env:AGENT_BROWSER_PATH = $AgentBrowserPath
64
+ $env:PI_ORACLE_SANITY_PROGRESS = "1"
65
+ & npm.cmd run verify:oracle:platform *>&1
66
+ $TEST_EXIT = Exit-CodeFromLastCommand
67
+ if ($null -eq $PreviousAgentBrowserPath) { Remove-Item Env:\AGENT_BROWSER_PATH -ErrorAction SilentlyContinue } else { $env:AGENT_BROWSER_PATH = $PreviousAgentBrowserPath }
68
+ if ($null -eq $PreviousSanityProgress) { Remove-Item Env:\PI_ORACLE_SANITY_PROGRESS -ErrorAction SilentlyContinue } else { $env:PI_ORACLE_SANITY_PROGRESS = $PreviousSanityProgress }
69
+ Write-Output "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"
70
+
71
+ Write-Output "=== npm pack ==="
72
+ $NpmPackErr = Join-Path $PackDir "npm-pack.stderr.txt"
73
+ $PackOutput = @(& npm.cmd pack --silent 2> $NpmPackErr)
74
+ $PACK_EXIT = Exit-CodeFromLastCommand
75
+ Get-Content -LiteralPath $NpmPackErr -ErrorAction SilentlyContinue
76
+ $PackTarball = ($PackOutput | Select-Object -First 1)
77
+ if ($PackTarball) { $PackTarball = $PackTarball.Trim() }
78
+ Write-Output "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"
79
+ if ($PackTarball -and (Test-Path -LiteralPath $PackTarball)) {
80
+ Move-Item -LiteralPath $PackTarball -Destination (Join-Path $PackDir $PackTarball) -Force
81
+ }
82
+ Write-Output "PLATFORM_PACKED_TARBALL=$PackTarball"
83
+ Set-Content -Path (Join-Path $PackDir "packed-tarball.txt") -Value $PackTarball
84
+
85
+ Write-Output "=== fixture workspace ==="
86
+ Copy-Item -LiteralPath package.json, README.md -Destination $TestWorkspace -ErrorAction SilentlyContinue
87
+ Copy-Item -LiteralPath extensions, prompts, docs -Destination $TestWorkspace -Recurse -ErrorAction SilentlyContinue
88
+ if ((Test-Path -LiteralPath (Join-Path $TestWorkspace "package.json")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "README.md")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "extensions")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "prompts")) -and (Test-Path -LiteralPath (Join-Path $TestWorkspace "docs"))) {
89
+ $FIXTURE_EXIT = 0
90
+ } else {
91
+ $FIXTURE_EXIT = 1
92
+ }
93
+ Write-Output "PLATFORM_FIXTURE_EXIT=$FIXTURE_EXIT"
94
+
95
+ $PiCli = Join-Path (Get-Location) "node_modules\.bin\pi.cmd"
96
+ if (-not (Test-Path -LiteralPath $PiCli)) { $PiCli = Join-Path (Get-Location) "node_modules\.bin\pi" }
97
+ if (-not (Test-Path -LiteralPath $PiCli)) {
98
+ $Command = Get-Command pi -ErrorAction SilentlyContinue
99
+ $PiCli = $Command.Source
100
+ }
101
+ Write-Output "PLATFORM_PI_CLI=$PiCli"
102
+
103
+ $PackedNodeInstallOut = Join-Path $PackDir "packed-node-install.stdout.txt"
104
+ $PackedNodeInstallErr = Join-Path $PackDir "packed-node-install.stderr.txt"
105
+ $PiInstallOut = Join-Path $PackDir "pi-install.stdout.txt"
106
+ $PiInstallErr = Join-Path $PackDir "pi-install.stderr.txt"
107
+ $PiListOut = Join-Path $PackDir "pi-list.stdout.txt"
108
+ $PiListErr = Join-Path $PackDir "pi-list.stderr.txt"
109
+
110
+ Write-Output "=== pi install packed package ==="
111
+ $TarballPath = Join-Path $PackDir $PackTarball
112
+ if ($PackTarball -and $PiCli -and (Test-Path -LiteralPath $TarballPath)) {
113
+ Push-Location $PiProject
114
+ & npm.cmd init -y 1> $PackedNodeInstallOut 2> $PackedNodeInstallErr
115
+ $NPM_INIT_EXIT = Exit-CodeFromLastCommand
116
+ if ($NPM_INIT_EXIT -eq 0) {
117
+ & npm.cmd install --no-save $TarballPath 1>> $PackedNodeInstallOut 2>> $PackedNodeInstallErr
118
+ $PACKED_NODE_INSTALL_EXIT = Exit-CodeFromLastCommand
119
+ } else {
120
+ $PACKED_NODE_INSTALL_EXIT = $NPM_INIT_EXIT
121
+ }
122
+ if ($PACKED_NODE_INSTALL_EXIT -eq 0) {
123
+ $PreviousPiOffline = $env:PI_OFFLINE
124
+ $env:PI_OFFLINE = "1"
125
+ & $PiCli install -l (Join-Path ".\node_modules" $PackageName) 1> $PiInstallOut 2> $PiInstallErr
126
+ $PI_INSTALL_EXIT = Exit-CodeFromLastCommand
127
+ if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
128
+ } else {
129
+ Set-Content -LiteralPath $PiInstallErr -Value "packed npm install failed"
130
+ $PI_INSTALL_EXIT = 1
131
+ }
132
+ Pop-Location
133
+ } else {
134
+ Set-Content -LiteralPath $PackedNodeInstallErr -Value "missing pi cli or tarball"
135
+ Set-Content -LiteralPath $PiInstallErr -Value "missing pi cli or tarball"
136
+ $PACKED_NODE_INSTALL_EXIT = 1
137
+ $PI_INSTALL_EXIT = 1
138
+ }
139
+ Write-Output "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"
140
+ Write-SectionFile "PACKED_NODE_INSTALL_STDOUT" $PackedNodeInstallOut
141
+ Write-SectionFile "PACKED_NODE_INSTALL_STDERR" $PackedNodeInstallErr
142
+ Write-Output "PLATFORM_PI_INSTALL_EXIT=$PI_INSTALL_EXIT"
143
+ Write-SectionFile "PI_INSTALL_STDOUT" $PiInstallOut
144
+ Write-SectionFile "PI_INSTALL_STDERR" $PiInstallErr
145
+
146
+ Write-Output "=== pi list ==="
147
+ if ($PiCli) {
148
+ Push-Location $PiProject
149
+ $PreviousPiOffline = $env:PI_OFFLINE
150
+ $env:PI_OFFLINE = "1"
151
+ & $PiCli list 1> $PiListOut 2> $PiListErr
152
+ $PI_LIST_EXIT = Exit-CodeFromLastCommand
153
+ if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
154
+ Pop-Location
155
+ } else {
156
+ Set-Content -LiteralPath $PiListErr -Value "missing pi cli"
157
+ $PI_LIST_EXIT = 1
158
+ }
159
+ Write-Output "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"
160
+ Write-SectionFile "PI_LIST_STDOUT" $PiListOut
161
+ Write-SectionFile "PI_LIST_STDERR" $PiListErr
162
+
163
+ Write-Output "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"
164
+ if ($NODE_VERSION_EXIT -ne 0 -or $CI_EXIT -ne 0 -or $DEPS_EXIT -ne 0 -or $TEST_EXIT -ne 0 -or $PACK_EXIT -ne 0 -or $FIXTURE_EXIT -ne 0 -or $PACKED_NODE_INSTALL_EXIT -ne 0 -or $PI_INSTALL_EXIT -ne 0 -or $PI_LIST_EXIT -ne 0) {
165
+ Write-Output "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"
166
+ exit 1
167
+ }
168
+ Write-Output "PLATFORM_BUILD_OK"
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Cross-platform smoke suites for pi-oracle.
3
+ * The suites prove the package builds, packs, installs, loads, and runs 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
+ }