pi-agent-browser-native 0.2.37 → 0.2.39

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.
@@ -0,0 +1,103 @@
1
+ param(
2
+ [Parameter(Mandatory=$true)][string]$PackageName,
3
+ [Parameter(Mandatory=$true)][int]$NodeValidationMajor
4
+ )
5
+
6
+ $ErrorActionPreference = "Continue"
7
+ $SourceRoot = (Get-Location).Path
8
+ $RunRoot = Join-Path ".platform-smoke-runs" ("platform-build-{0}-{1}" -f ((Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ")), $PID)
9
+ $PackDir = Join-Path $SourceRoot (Join-Path $RunRoot "pack")
10
+ $PiProject = Join-Path $SourceRoot (Join-Path $RunRoot "pi-project")
11
+ New-Item -ItemType Directory -Force -Path $PackDir, $PiProject | Out-Null
12
+
13
+ function Write-Section($Name, $Path) {
14
+ Write-Output "--- $Name START ---"
15
+ if (Test-Path $Path) { Get-Content -Raw $Path }
16
+ Write-Output "--- $Name END ---"
17
+ }
18
+
19
+ Write-Output "Starting platform-build in $SourceRoot at $((Get-Date).ToUniversalTime().ToString('o'))"
20
+ Write-Output "PLATFORM_RUN_ROOT=$RunRoot"
21
+
22
+ $NodeVersion = (& node --version 2>$null)
23
+ $NodeMajor = 0
24
+ if ($NodeVersion -match '^v?(\d+)\.') { $NodeMajor = [int]$Matches[1] }
25
+ Write-Output "PLATFORM_NODE_VERSION=$NodeVersion"
26
+ $NodeVersionExit = if ($NodeMajor -ge $NodeValidationMajor) { 0 } else { 1 }
27
+ Write-Output "PLATFORM_NODE_VERSION_EXIT=$NodeVersionExit"
28
+
29
+ & npm ci 2>&1
30
+ $NpmCiExit = $LASTEXITCODE
31
+ Write-Output "PLATFORM_NPM_CI_EXIT=$NpmCiExit"
32
+
33
+ & npm run verify -- platform-target 2>&1
34
+ $VerifyExit = $LASTEXITCODE
35
+ Write-Output "PLATFORM_VERIFY_EXIT=$VerifyExit"
36
+
37
+ $PackStderr = Join-Path $PackDir "npm-pack.stderr.txt"
38
+ $PackOutput = (& npm pack --silent --pack-destination $PackDir 2>$PackStderr)
39
+ $PackExit = $LASTEXITCODE
40
+ $PackTarball = ($PackOutput | Select-Object -Last 1)
41
+ $PackFile = if ($PackTarball) { Join-Path $PackDir $PackTarball } else { "" }
42
+ if (Test-Path $PackStderr) { Get-Content -Raw $PackStderr }
43
+ Write-Output "PLATFORM_NPM_PACK_EXIT=$PackExit"
44
+ Write-Output "PLATFORM_PACKED_TARBALL=$PackFile"
45
+
46
+ $PiCli = Join-Path $SourceRoot "node_modules/.bin/pi.cmd"
47
+ if (-not (Test-Path $PiCli)) { $PiCli = Join-Path $SourceRoot "node_modules/.bin/pi" }
48
+ if (-not (Test-Path $PiCli)) { $PiCli = "pi" }
49
+ Write-Output "PLATFORM_PI_CLI=$PiCli"
50
+
51
+ $PackedNodeInstallStdout = Join-Path $PackDir "packed-node-install.stdout.txt"
52
+ $PackedNodeInstallStderr = Join-Path $PackDir "packed-node-install.stderr.txt"
53
+ if ($PackFile -and (Test-Path $PackFile)) {
54
+ Push-Location $PiProject
55
+ & npm init -y >$PackedNodeInstallStdout 2>$PackedNodeInstallStderr
56
+ if ($LASTEXITCODE -eq 0) {
57
+ & npm install --no-save $PackFile >>$PackedNodeInstallStdout 2>>$PackedNodeInstallStderr
58
+ }
59
+ $PackedNodeInstallExit = $LASTEXITCODE
60
+ Pop-Location
61
+ } else {
62
+ "missing tarball" | Set-Content $PackedNodeInstallStderr
63
+ $PackedNodeInstallExit = 1
64
+ }
65
+ Write-Output "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PackedNodeInstallExit"
66
+ Write-Section "PACKED_NODE_INSTALL_STDOUT" $PackedNodeInstallStdout
67
+ Write-Section "PACKED_NODE_INSTALL_STDERR" $PackedNodeInstallStderr
68
+
69
+ $PiInstallStdout = Join-Path $PackDir "pi-install.stdout.txt"
70
+ $PiInstallStderr = Join-Path $PackDir "pi-install.stderr.txt"
71
+ if ($PackedNodeInstallExit -eq 0) {
72
+ Push-Location $PiProject
73
+ $env:PI_OFFLINE = "1"
74
+ & $PiCli install -l ".\node_modules\$PackageName" >$PiInstallStdout 2>$PiInstallStderr
75
+ $PiInstallExit = $LASTEXITCODE
76
+ Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue
77
+ Pop-Location
78
+ } else {
79
+ "packed npm install failed" | Set-Content $PiInstallStderr
80
+ $PiInstallExit = 1
81
+ }
82
+ Write-Output "PLATFORM_PI_INSTALL_EXIT=$PiInstallExit"
83
+ Write-Section "PI_INSTALL_STDOUT" $PiInstallStdout
84
+ Write-Section "PI_INSTALL_STDERR" $PiInstallStderr
85
+
86
+ $PiListStdout = Join-Path $PackDir "pi-list.stdout.txt"
87
+ $PiListStderr = Join-Path $PackDir "pi-list.stderr.txt"
88
+ Push-Location $PiProject
89
+ $env:PI_OFFLINE = "1"
90
+ & $PiCli list >$PiListStdout 2>$PiListStderr
91
+ $PiListExit = $LASTEXITCODE
92
+ Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue
93
+ Pop-Location
94
+ Write-Output "PLATFORM_PI_LIST_EXIT=$PiListExit"
95
+ Write-Section "PI_LIST_STDOUT" $PiListStdout
96
+ Write-Section "PI_LIST_STDERR" $PiListStderr
97
+
98
+ if ($NodeVersionExit -ne 0 -or $NpmCiExit -ne 0 -or $VerifyExit -ne 0 -or $PackExit -ne 0 -or $PackedNodeInstallExit -ne 0 -or $PiInstallExit -ne 0 -or $PiListExit -ne 0) {
99
+ Write-Output "PLATFORM_BUILD_FAILED"
100
+ exit 1
101
+ }
102
+
103
+ Write-Output "PLATFORM_BUILD_OK"
@@ -0,0 +1,471 @@
1
+ /** Target/suite runner for pi-agent-browser-native platform smoke. */
2
+
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+
6
+ import {
7
+ collectSecretValues,
8
+ createSuiteDir,
9
+ redactSecrets,
10
+ scanArtifactTextFiles,
11
+ scanForSecrets,
12
+ writeCommand,
13
+ writeExitCode,
14
+ writeManifest,
15
+ writeSummary,
16
+ } from "./artifacts.mjs";
17
+ import { cleanupStaleTargetState, runOnLease, stopLease, warmupLease } from "./crabbox-runner.mjs";
18
+
19
+ export function platformFor(targetName) {
20
+ return targetName === "windows-native" ? "powershell" : "posix";
21
+ }
22
+
23
+ function makeRunId() {
24
+ return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
25
+ }
26
+
27
+ function shellQuote(value) {
28
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
29
+ }
30
+
31
+ function psSingleQuote(value) {
32
+ return `'${String(value).replace(/'/g, "''")}'`;
33
+ }
34
+
35
+ function authEnvAllowList(config = {}) {
36
+ const raw = process.env.PLATFORM_SMOKE_AUTH_ENV;
37
+ const names = raw ? raw.split(",") : (config.defaultAuthEnv ?? []);
38
+ return names.map((name) => String(name).trim()).filter(Boolean);
39
+ }
40
+
41
+ function writeRedacted(path, text, secretValues) {
42
+ writeFileSync(path, redactSecrets(text ?? "", secretValues));
43
+ }
44
+
45
+ function section(text, name) {
46
+ const start = `--- ${name} START ---`;
47
+ const end = `--- ${name} END ---`;
48
+ const startIndex = text.indexOf(start);
49
+ if (startIndex === -1) return "";
50
+ const contentStart = startIndex + start.length;
51
+ const endIndex = text.indexOf(end, contentStart);
52
+ return (endIndex === -1 ? text.slice(contentStart) : text.slice(contentStart, endIndex)).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
53
+ }
54
+
55
+ function marker(text, name) {
56
+ return text.match(new RegExp(`^${name}=(.*)$`, "m"))?.[1]?.trim() ?? "";
57
+ }
58
+
59
+ function parseJsonObject(text) {
60
+ const trimmed = String(text ?? "").trim();
61
+ if (!trimmed) return {};
62
+ try {
63
+ return JSON.parse(trimmed);
64
+ } catch {
65
+ const first = trimmed.indexOf("{");
66
+ const last = trimmed.lastIndexOf("}");
67
+ if (first !== -1 && last > first) {
68
+ try {
69
+ return JSON.parse(trimmed.slice(first, last + 1));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+ return {};
75
+ }
76
+ }
77
+
78
+ function writePlatformExtracts(suiteDir, stdout, secretValues = []) {
79
+ writeFileSync(resolve(suiteDir, "node-version.txt"), `${marker(stdout, "PLATFORM_NODE_VERSION")}\n`);
80
+ writeRedacted(resolve(suiteDir, "packed-tarball.txt"), `${marker(stdout, "PLATFORM_PACKED_TARBALL")}\n`, secretValues);
81
+ writeRedacted(resolve(suiteDir, "packed-node-install.stdout.txt"), section(stdout, "PACKED_NODE_INSTALL_STDOUT"), secretValues);
82
+ writeRedacted(resolve(suiteDir, "packed-node-install.stderr.txt"), section(stdout, "PACKED_NODE_INSTALL_STDERR"), secretValues);
83
+ writeRedacted(resolve(suiteDir, "pi-install.stdout.txt"), section(stdout, "PI_INSTALL_STDOUT"), secretValues);
84
+ writeRedacted(resolve(suiteDir, "pi-install.stderr.txt"), section(stdout, "PI_INSTALL_STDERR"), secretValues);
85
+ writeRedacted(resolve(suiteDir, "pi-list.stdout.txt"), section(stdout, "PI_LIST_STDOUT"), secretValues);
86
+ writeRedacted(resolve(suiteDir, "pi-list.stderr.txt"), section(stdout, "PI_LIST_STDERR"), secretValues);
87
+ }
88
+
89
+ function writeDogfoodExtracts(suiteDir, stdout, secretValues = []) {
90
+ writeFileSync(resolve(suiteDir, "node-version.txt"), `${marker(stdout, "PLATFORM_NODE_VERSION")}\n`);
91
+ writeRedacted(resolve(suiteDir, "dogfood-artifacts.txt"), `${marker(stdout, "PLATFORM_DOGFOOD_ARTIFACT_DIR")}\n`, secretValues);
92
+ const dogfoodStdout = section(stdout, "DOGFOOD_STDOUT");
93
+ writeRedacted(resolve(suiteDir, "dogfood.stdout.txt"), dogfoodStdout, secretValues);
94
+ writeRedacted(resolve(suiteDir, "dogfood.stderr.txt"), section(stdout, "DOGFOOD_STDERR"), secretValues);
95
+ const report = parseJsonObject(dogfoodStdout);
96
+ writeRedacted(resolve(suiteDir, "dogfood-report.json"), JSON.stringify(report, null, 2), secretValues);
97
+ return report;
98
+ }
99
+
100
+ function assertionsFromChecks(checks) {
101
+ const evaluated = checks.map((check) => {
102
+ let ok = false;
103
+ let error = check.error;
104
+ try {
105
+ ok = check.fn() === true;
106
+ } catch (err) {
107
+ error = err.message;
108
+ }
109
+ return { id: check.id, ok, ...(ok ? {} : { error: error ?? `${check.id} failed` }) };
110
+ });
111
+ return { ok: evaluated.every((check) => check.ok), checks: evaluated, writtenAt: new Date().toISOString() };
112
+ }
113
+
114
+ function writeAssertions(suiteDir, checks) {
115
+ const assertions = assertionsFromChecks(checks);
116
+ writeFileSync(resolve(suiteDir, "assertions.json"), JSON.stringify(assertions, null, 2));
117
+ if (!assertions.ok) {
118
+ writeFileSync(resolve(suiteDir, "failures.md"), [
119
+ "# Platform smoke failures",
120
+ "",
121
+ ...assertions.checks.filter((check) => !check.ok).map((check) => `- ${check.id}: ${check.error ?? "failed"}`),
122
+ "",
123
+ "Inspect command.txt, crabbox.stdout.txt, and crabbox.stderr.txt in this suite directory.",
124
+ "",
125
+ ].join("\n"));
126
+ }
127
+ return assertions;
128
+ }
129
+
130
+ function finalizeSuite(suiteDir, checks, summary, expectedFiles) {
131
+ const assertions = writeAssertions(suiteDir, checks);
132
+ writeSummary(suiteDir, { ...summary, ok: assertions.ok });
133
+ const expected = assertions.ok ? expectedFiles : [...expectedFiles, "failures.md"];
134
+ const manifest = writeManifest(suiteDir, expected);
135
+ if (manifest.missing.length === 0) return { assertions, manifest };
136
+ const finalAssertions = writeAssertions(suiteDir, [
137
+ ...checks,
138
+ { id: "artifact-manifest-complete", fn: () => false, error: `missing required artifact(s): ${manifest.missing.join(", ")}` },
139
+ ]);
140
+ writeSummary(suiteDir, { ...summary, ok: false });
141
+ return { assertions: finalAssertions, manifest: writeManifest(suiteDir, [...expectedFiles, "failures.md"]) };
142
+ }
143
+
144
+ export function createLeaseCleanupResult(config, targetName, leaseId, stopResult, staleCleanupResult = null) {
145
+ const suiteName = "lease-cleanup";
146
+ const runId = makeRunId();
147
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
148
+ const secretValues = collectSecretValues(authEnvAllowList(config));
149
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform: platformFor(targetName), runId, slug: `${config.packageName}-${targetName}` }, null, 2));
150
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, leaseId, modelCalls: 0 }, null, 2));
151
+ writeCommand(suiteDir, `crabbox stop ${targetName} --id ${leaseId}`);
152
+ writeExitCode(suiteDir, stopResult.code, stopResult.signal);
153
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stdout.txt"), stopResult.stdout ?? "", secretValues);
154
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stderr.txt"), stopResult.stderr ?? "", secretValues);
155
+ writeFileSync(resolve(suiteDir, "crabbox.stop.exit-code.txt"), `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`);
156
+ if (staleCleanupResult) {
157
+ writeRedacted(resolve(suiteDir, "crabbox.cleanup.stdout.txt"), staleCleanupResult.stdout ?? "", secretValues);
158
+ writeRedacted(resolve(suiteDir, "crabbox.cleanup.stderr.txt"), staleCleanupResult.stderr ?? "", secretValues);
159
+ writeFileSync(resolve(suiteDir, "crabbox.cleanup.exit-code.txt"), `code=${staleCleanupResult.code}\nsignal=${staleCleanupResult.signal ?? "none"}\n`);
160
+ }
161
+ const secretViolations = [
162
+ ...scanForSecrets(`${stopResult.stdout ?? ""}\n${stopResult.stderr ?? ""}`, secretValues),
163
+ ...scanArtifactTextFiles(suiteDir, secretValues).map((finding) => `${finding.file}: ${finding.violation}`),
164
+ ];
165
+ const { assertions } = finalizeSuite(
166
+ suiteDir,
167
+ [
168
+ { id: "lease-cleanup", fn: () => stopResult.code === 0, error: `Crabbox stop failed with exit ${stopResult.code}` },
169
+ { id: "stale-cleanup", fn: () => !staleCleanupResult || staleCleanupResult.code === 0, error: `Crabbox cleanup failed with exit ${staleCleanupResult?.code}` },
170
+ { id: "no-secret-artifacts", fn: () => secretViolations.length === 0, error: secretViolations.join(", ") },
171
+ ],
172
+ { target: targetName, suite: suiteName, exitCode: stopResult.code, signal: stopResult.signal, elapsedMs: 0 },
173
+ [
174
+ "summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt",
175
+ "crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt",
176
+ ...(staleCleanupResult ? ["crabbox.cleanup.stdout.txt", "crabbox.cleanup.stderr.txt", "crabbox.cleanup.exit-code.txt"] : []),
177
+ "assertions.json",
178
+ ],
179
+ );
180
+ return { ok: assertions.ok, suiteDir, assertions };
181
+ }
182
+
183
+ export function createLeaseCleanupFailureResult(config, targetName, leaseId, stopResult) {
184
+ return createLeaseCleanupResult(config, targetName, leaseId, stopResult);
185
+ }
186
+
187
+ export function createLeaseWarmupFailureResult(config, targetName, warmupResult) {
188
+ const suiteName = "lease-warmup";
189
+ const runId = makeRunId();
190
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
191
+ const secretValues = collectSecretValues(authEnvAllowList(config));
192
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform: platformFor(targetName), runId, slug: `${config.packageName}-${targetName}` }, null, 2));
193
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, modelCalls: 0 }, null, 2));
194
+ writeCommand(suiteDir, `crabbox warmup ${targetName}`);
195
+ writeExitCode(suiteDir, warmupResult.code, warmupResult.signal);
196
+ writeRedacted(resolve(suiteDir, "crabbox.stdout.txt"), warmupResult.stdout ?? "", secretValues);
197
+ writeRedacted(resolve(suiteDir, "crabbox.stderr.txt"), warmupResult.stderr ?? "", secretValues);
198
+ const secretViolations = [
199
+ ...scanForSecrets(`${warmupResult.stdout ?? ""}\n${warmupResult.stderr ?? ""}`, secretValues),
200
+ ...scanArtifactTextFiles(suiteDir, secretValues).map((finding) => `${finding.file}: ${finding.violation}`),
201
+ ];
202
+ const { assertions } = finalizeSuite(
203
+ suiteDir,
204
+ [
205
+ { id: "lease-warmup", fn: () => false, error: `Crabbox warmup failed with exit ${warmupResult.code}` },
206
+ { id: "no-secret-artifacts", fn: () => secretViolations.length === 0, error: secretViolations.join(", ") },
207
+ ],
208
+ { target: targetName, suite: suiteName, exitCode: warmupResult.code, signal: warmupResult.signal, elapsedMs: 0, ok: false },
209
+ ["summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stdout.txt", "crabbox.stderr.txt", "assertions.json"],
210
+ );
211
+ return { ok: false, suiteDir, assertions };
212
+ }
213
+
214
+ export function buildPlatformBuildCommand(targetName, packageName = "pi-agent-browser-native", nodeValidationMajor = 22) {
215
+ if (platformFor(targetName) === "powershell") {
216
+ return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${psSingleQuote(packageName)} -NodeValidationMajor ${nodeValidationMajor}`;
217
+ }
218
+
219
+ const lines = [];
220
+ lines.push(`echo "Starting platform-build in $(pwd) at $(date -u +%Y-%m-%dT%H:%M:%SZ)"`);
221
+ lines.push(`RUN_ROOT=".platform-smoke-runs/platform-build-$(date -u +%Y%m%dT%H%M%SZ)-$$"`);
222
+ lines.push(`SOURCE_ROOT="$(pwd)"`);
223
+ lines.push(`PACK_DIR="$SOURCE_ROOT/$RUN_ROOT/pack"`);
224
+ lines.push(`PI_PROJECT="$SOURCE_ROOT/$RUN_ROOT/pi-project"`);
225
+ lines.push(`mkdir -p "$PACK_DIR" "$PI_PROJECT"`);
226
+ lines.push(`echo "PLATFORM_RUN_ROOT=$RUN_ROOT"`);
227
+ lines.push(`NODE_VERSION=$(node --version)`);
228
+ lines.push(`NODE_MAJOR="${"${NODE_VERSION#v}"}"`);
229
+ lines.push(`NODE_MAJOR="${"${NODE_MAJOR%%.*}"}"`);
230
+ lines.push(`echo "PLATFORM_NODE_VERSION=$NODE_VERSION"`);
231
+ lines.push(`if [ "$NODE_MAJOR" -ge ${nodeValidationMajor} ]; then NODE_VERSION_EXIT=0; else NODE_VERSION_EXIT=1; fi`);
232
+ lines.push(`echo "PLATFORM_NODE_VERSION_EXIT=$NODE_VERSION_EXIT"`);
233
+ lines.push(`npm ci 2>&1`);
234
+ lines.push(`NPM_CI_EXIT=$?`);
235
+ lines.push(`echo "PLATFORM_NPM_CI_EXIT=$NPM_CI_EXIT"`);
236
+ lines.push(`npm run verify -- platform-target 2>&1`);
237
+ lines.push(`VERIFY_EXIT=$?`);
238
+ lines.push(`echo "PLATFORM_VERIFY_EXIT=$VERIFY_EXIT"`);
239
+ lines.push(`PACK_TARBALL=$(npm pack --silent --pack-destination "$PACK_DIR" 2>"$PACK_DIR/npm-pack.stderr.txt")`);
240
+ lines.push(`PACK_EXIT=$?`);
241
+ lines.push(`cat "$PACK_DIR/npm-pack.stderr.txt"`);
242
+ lines.push(`PACK_FILE="$PACK_DIR/$PACK_TARBALL"`);
243
+ lines.push(`echo "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"`);
244
+ lines.push(`echo "PLATFORM_PACKED_TARBALL=$PACK_FILE"`);
245
+ lines.push(`PI_CLI="$SOURCE_ROOT/node_modules/.bin/pi"`);
246
+ lines.push(`if [ ! -x "$PI_CLI" ]; then PI_CLI="$(command -v pi || true)"; fi`);
247
+ lines.push(`echo "PLATFORM_PI_CLI=$PI_CLI"`);
248
+ lines.push(`if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_FILE" ]; 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_FILE" >>"$PACK_DIR/packed-node-install.stdout.txt" 2>>"$PACK_DIR/packed-node-install.stderr.txt"); PACKED_NODE_INSTALL_EXIT=$?; else echo "missing tarball" >"$PACK_DIR/packed-node-install.stderr.txt"; PACKED_NODE_INSTALL_EXIT=1; fi`);
249
+ lines.push(`echo "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"`);
250
+ lines.push(`echo "--- PACKED_NODE_INSTALL_STDOUT START ---"; cat "$PACK_DIR/packed-node-install.stdout.txt" 2>/dev/null || true; echo "--- PACKED_NODE_INSTALL_STDOUT END ---"`);
251
+ lines.push(`echo "--- PACKED_NODE_INSTALL_STDERR START ---"; cat "$PACK_DIR/packed-node-install.stderr.txt" 2>/dev/null || true; echo "--- PACKED_NODE_INSTALL_STDERR END ---"`);
252
+ 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 "missing pi cli or packed install" >"$PACK_DIR/pi-install.stderr.txt"; PI_INSTALL_EXIT=1; fi`);
253
+ lines.push(`echo "PLATFORM_PI_INSTALL_EXIT=$PI_INSTALL_EXIT"`);
254
+ lines.push(`echo "--- PI_INSTALL_STDOUT START ---"; cat "$PACK_DIR/pi-install.stdout.txt" 2>/dev/null || true; echo "--- PI_INSTALL_STDOUT END ---"`);
255
+ lines.push(`echo "--- PI_INSTALL_STDERR START ---"; cat "$PACK_DIR/pi-install.stderr.txt" 2>/dev/null || true; echo "--- PI_INSTALL_STDERR END ---"`);
256
+ 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`);
257
+ lines.push(`echo "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"`);
258
+ lines.push(`echo "--- PI_LIST_STDOUT START ---"; cat "$PACK_DIR/pi-list.stdout.txt" 2>/dev/null || true; echo "--- PI_LIST_STDOUT END ---"`);
259
+ lines.push(`echo "--- PI_LIST_STDERR START ---"; cat "$PACK_DIR/pi-list.stderr.txt" 2>/dev/null || true; echo "--- PI_LIST_STDERR END ---"`);
260
+ lines.push(`if [ "$NODE_VERSION_EXIT" -ne 0 ] || [ "$NPM_CI_EXIT" -ne 0 ] || [ "$VERIFY_EXIT" -ne 0 ] || [ "$PACK_EXIT" -ne 0 ] || [ "$PACKED_NODE_INSTALL_EXIT" -ne 0 ] || [ "$PI_INSTALL_EXIT" -ne 0 ] || [ "$PI_LIST_EXIT" -ne 0 ]; then echo "PLATFORM_BUILD_FAILED"; exit 1; fi`);
261
+ lines.push(`echo "PLATFORM_BUILD_OK"`);
262
+ return lines.join("\n");
263
+ }
264
+
265
+ export function buildBrowserDogfoodCommand(targetName, agentBrowserVersion = "0.27.1") {
266
+ if (platformFor(targetName) === "powershell") {
267
+ return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\browser-dogfood-windows.ps1 -AgentBrowserVersion ${psSingleQuote(agentBrowserVersion)}`;
268
+ }
269
+
270
+ const lines = [];
271
+ lines.push(`echo "Starting browser-dogfood-smoke in $(pwd) at $(date -u +%Y-%m-%dT%H:%M:%SZ)"`);
272
+ lines.push(`RUN_ROOT=".platform-smoke-runs/browser-dogfood-$(date -u +%Y%m%dT%H%M%SZ)-$$"`);
273
+ lines.push(`SOURCE_ROOT="$(pwd)"`);
274
+ lines.push(`DOGFOOD_DIR="$SOURCE_ROOT/$RUN_ROOT/dogfood"`);
275
+ lines.push(`DOGFOOD_ARTIFACT_DIR="$DOGFOOD_DIR/artifacts"`);
276
+ lines.push(`mkdir -p "$DOGFOOD_ARTIFACT_DIR"`);
277
+ lines.push(`echo "PLATFORM_RUN_ROOT=$RUN_ROOT"`);
278
+ lines.push(`echo "PLATFORM_DOGFOOD_ARTIFACT_DIR=$DOGFOOD_ARTIFACT_DIR"`);
279
+ lines.push(`NODE_VERSION=$(node --version)`);
280
+ lines.push(`NODE_MAJOR="${"${NODE_VERSION#v}"}"`);
281
+ lines.push(`NODE_MAJOR="${"${NODE_MAJOR%%.*}"}"`);
282
+ lines.push(`echo "PLATFORM_NODE_VERSION=$NODE_VERSION"`);
283
+ lines.push(`EXPECTED_AGENT_BROWSER_VERSION=${shellQuote(`agent-browser ${agentBrowserVersion}`)}`);
284
+ lines.push(`AGENT_BROWSER_VERSION_OUTPUT=$(agent-browser --version 2>&1)`);
285
+ lines.push(`AGENT_BROWSER_VERSION_COMMAND_EXIT=$?`);
286
+ lines.push(`echo "PLATFORM_AGENT_BROWSER_VERSION=$AGENT_BROWSER_VERSION_OUTPUT"`);
287
+ lines.push(`if [ "$AGENT_BROWSER_VERSION_COMMAND_EXIT" -eq 0 ] && [ "$AGENT_BROWSER_VERSION_OUTPUT" = "$EXPECTED_AGENT_BROWSER_VERSION" ]; then AGENT_BROWSER_READY_EXIT=0; else AGENT_BROWSER_READY_EXIT=1; fi`);
288
+ lines.push(`echo "PLATFORM_AGENT_BROWSER_READY_EXIT=$AGENT_BROWSER_READY_EXIT"`);
289
+ lines.push(`npm ci 2>&1`);
290
+ lines.push(`NPM_CI_EXIT=$?`);
291
+ lines.push(`echo "PLATFORM_NPM_CI_EXIT=$NPM_CI_EXIT"`);
292
+ lines.push(`TSX_CLI="$SOURCE_ROOT/node_modules/.bin/tsx"`);
293
+ lines.push(`if [ ! -x "$TSX_CLI" ]; then TSX_CLI="$(command -v tsx || true)"; fi`);
294
+ lines.push(`echo "PLATFORM_TSX_CLI=$TSX_CLI"`);
295
+ lines.push(`if [ "$NPM_CI_EXIT" -eq 0 ] && [ "$AGENT_BROWSER_READY_EXIT" -eq 0 ] && [ -n "$TSX_CLI" ]; then "$TSX_CLI" scripts/verify-agent-browser-dogfood.ts --artifact-dir "$DOGFOOD_ARTIFACT_DIR" --json >"$DOGFOOD_DIR/dogfood.stdout.txt" 2>"$DOGFOOD_DIR/dogfood.stderr.txt"; DOGFOOD_EXIT=$?; else echo "missing tsx, npm ci failed, or agent-browser baseline mismatch" >"$DOGFOOD_DIR/dogfood.stderr.txt"; DOGFOOD_EXIT=1; fi`);
296
+ lines.push(`echo "PLATFORM_DOGFOOD_EXIT=$DOGFOOD_EXIT"`);
297
+ lines.push(`echo "--- DOGFOOD_STDOUT START ---"; cat "$DOGFOOD_DIR/dogfood.stdout.txt" 2>/dev/null || true; echo "--- DOGFOOD_STDOUT END ---"`);
298
+ lines.push(`echo "--- DOGFOOD_STDERR START ---"; cat "$DOGFOOD_DIR/dogfood.stderr.txt" 2>/dev/null || true; echo "--- DOGFOOD_STDERR END ---"`);
299
+ lines.push(`if [ "$NPM_CI_EXIT" -ne 0 ] || [ "$AGENT_BROWSER_READY_EXIT" -ne 0 ] || [ "$DOGFOOD_EXIT" -ne 0 ]; then echo "PLATFORM_BROWSER_DOGFOOD_FAILED"; exit 1; fi`);
300
+ lines.push(`echo "PLATFORM_BROWSER_DOGFOOD_OK"`);
301
+ return lines.join("\n");
302
+ }
303
+
304
+ async function runBrowserDogfoodSuite(config, targetName, suiteName, leaseSession) {
305
+ const runId = makeRunId();
306
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
307
+ const startedAt = Date.now();
308
+ const platform = platformFor(targetName);
309
+ const slug = `${config.packageName}-${targetName}`;
310
+ const command = buildBrowserDogfoodCommand(targetName, config.agentBrowserVersion);
311
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform, runId, slug }, null, 2));
312
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, modelCalls: 0, realBrowser: true }, null, 2));
313
+ writeCommand(suiteDir, command);
314
+
315
+ let lease = leaseSession;
316
+ const ownsLease = !lease;
317
+ if (!lease) lease = await warmupLease(targetName, slug, config);
318
+ if (!lease.ok) {
319
+ writeExitCode(suiteDir, lease.code, lease.signal);
320
+ writeFileSync(resolve(suiteDir, "crabbox.stdout.txt"), lease.stdout ?? "");
321
+ writeFileSync(resolve(suiteDir, "crabbox.stderr.txt"), lease.stderr ?? "");
322
+ const { assertions } = finalizeSuite(suiteDir, [{ id: "crabbox-warmup", fn: () => false, error: "Crabbox warmup failed" }], { target: targetName, suite: suiteName, elapsedMs: Date.now() - startedAt }, ["summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stdout.txt", "crabbox.stderr.txt", "assertions.json"]);
323
+ return { ok: false, suiteDir, assertions };
324
+ }
325
+
326
+ const secretValues = collectSecretValues(authEnvAllowList(config));
327
+ const result = await runOnLease(targetName, lease.leaseId, command, { timeout: 900_000, sync: leaseSession?.sync, config });
328
+ const elapsedMs = Date.now() - startedAt;
329
+ writeRedacted(resolve(suiteDir, "crabbox.stdout.txt"), result.stdout, secretValues);
330
+ writeRedacted(resolve(suiteDir, "crabbox.stderr.txt"), result.stderr, secretValues);
331
+ writeFileSync(resolve(suiteDir, "crabbox.timing.json"), JSON.stringify({ elapsedMs, code: result.code, signal: result.signal }, null, 2));
332
+ writeExitCode(suiteDir, result.code, result.signal);
333
+ const dogfoodReport = writeDogfoodExtracts(suiteDir, result.stdout, secretValues);
334
+ let stopResult;
335
+ if (ownsLease) {
336
+ stopResult = await stopLease(targetName, lease.leaseId, config);
337
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stdout.txt"), stopResult.stdout, secretValues);
338
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stderr.txt"), stopResult.stderr, secretValues);
339
+ writeFileSync(resolve(suiteDir, "crabbox.stop.exit-code.txt"), `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`);
340
+ }
341
+
342
+ const secretViolations = [
343
+ ...scanForSecrets(`${result.stdout}\n${result.stderr}`, secretValues),
344
+ ...scanArtifactTextFiles(suiteDir, secretValues).map((finding) => `${finding.file}: ${finding.violation}`),
345
+ ];
346
+ const reportIds = new Set((dogfoodReport.reports ?? []).map((report) => report.id));
347
+ const checks = [
348
+ { id: "command-exit-zero", fn: () => result.code === 0, error: `exit ${result.code}` },
349
+ { id: "browser-dogfood-marker", fn: () => result.stdout.includes("PLATFORM_BROWSER_DOGFOOD_OK") },
350
+ { id: "npm-ci", fn: () => /PLATFORM_NPM_CI_EXIT=0/.test(result.stdout) },
351
+ { id: "agent-browser-baseline", fn: () => /PLATFORM_AGENT_BROWSER_READY_EXIT=0/.test(result.stdout) },
352
+ { id: "agent-browser-browser-cache", fn: () => platform !== "powershell" || /PLATFORM_AGENT_BROWSER_BROWSER_CACHE_EXIT=0/.test(result.stdout) },
353
+ { id: "agent-browser-prewarm", fn: () => platform !== "powershell" || /PLATFORM_AGENT_BROWSER_PREWARM_EXIT=0/.test(result.stdout) },
354
+ { id: "dogfood-exit-zero", fn: () => /PLATFORM_DOGFOOD_EXIT=0/.test(result.stdout) },
355
+ { id: "dogfood-report", fn: () => Array.isArray(dogfoodReport.reports) && dogfoodReport.reports.length >= 5 },
356
+ { id: "dogfood-qa", fn: () => reportIds.has("qa-url") },
357
+ { id: "dogfood-session-close", fn: () => reportIds.has("close-session") },
358
+ { id: "no-secret-artifacts", fn: () => secretViolations.length === 0, error: secretViolations.join(", ") },
359
+ ];
360
+ if (stopResult) checks.push({ id: "lease-cleanup", fn: () => stopResult.code === 0, error: `stop exit ${stopResult.code}` });
361
+ const expectedFiles = [
362
+ "summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stdout.txt", "crabbox.stderr.txt", "crabbox.timing.json",
363
+ "node-version.txt", "dogfood-artifacts.txt", "dogfood.stdout.txt", "dogfood.stderr.txt", "dogfood-report.json", "assertions.json",
364
+ ];
365
+ if (stopResult) expectedFiles.push("crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt");
366
+ const { assertions } = finalizeSuite(suiteDir, checks, { target: targetName, suite: suiteName, elapsedMs, exitCode: result.code, signal: result.signal }, expectedFiles);
367
+ return { ok: assertions.ok, suiteDir, assertions };
368
+ }
369
+
370
+ async function runPlatformBuildSuite(config, targetName, suiteName, leaseSession) {
371
+ const runId = makeRunId();
372
+ const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
373
+ const startedAt = Date.now();
374
+ const platform = platformFor(targetName);
375
+ const slug = `${config.packageName}-${targetName}`;
376
+ const command = buildPlatformBuildCommand(targetName, config.packageName, config.nodeValidationMajor);
377
+ mkdirSync(dirname(suiteDir), { recursive: true });
378
+ writeFileSync(resolve(suiteDir, "target.json"), JSON.stringify({ targetName, platform, runId, slug }, null, 2));
379
+ writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, modelCalls: 0 }, null, 2));
380
+ writeCommand(suiteDir, command);
381
+
382
+ let lease = leaseSession;
383
+ const ownsLease = !lease;
384
+ if (!lease) lease = await warmupLease(targetName, slug, config);
385
+ if (!lease.ok) {
386
+ writeExitCode(suiteDir, lease.code, lease.signal);
387
+ writeFileSync(resolve(suiteDir, "crabbox.stdout.txt"), lease.stdout ?? "");
388
+ writeFileSync(resolve(suiteDir, "crabbox.stderr.txt"), lease.stderr ?? "");
389
+ const { assertions } = finalizeSuite(suiteDir, [{ id: "crabbox-warmup", fn: () => false, error: "Crabbox warmup failed" }], { target: targetName, suite: suiteName, elapsedMs: Date.now() - startedAt }, ["summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stdout.txt", "crabbox.stderr.txt", "assertions.json"]);
390
+ return { ok: false, suiteDir, assertions };
391
+ }
392
+
393
+ const secretValues = collectSecretValues(authEnvAllowList(config));
394
+ const result = await runOnLease(targetName, lease.leaseId, command, { timeout: 1_500_000, sync: leaseSession?.sync, config });
395
+ const elapsedMs = Date.now() - startedAt;
396
+ writeRedacted(resolve(suiteDir, "crabbox.stdout.txt"), result.stdout, secretValues);
397
+ writeRedacted(resolve(suiteDir, "crabbox.stderr.txt"), result.stderr, secretValues);
398
+ writeFileSync(resolve(suiteDir, "crabbox.timing.json"), JSON.stringify({ elapsedMs, code: result.code, signal: result.signal }, null, 2));
399
+ writeExitCode(suiteDir, result.code, result.signal);
400
+ writePlatformExtracts(suiteDir, result.stdout, secretValues);
401
+ let stopResult;
402
+ if (ownsLease) {
403
+ stopResult = await stopLease(targetName, lease.leaseId, config);
404
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stdout.txt"), stopResult.stdout, secretValues);
405
+ writeRedacted(resolve(suiteDir, "crabbox.stop.stderr.txt"), stopResult.stderr, secretValues);
406
+ writeFileSync(resolve(suiteDir, "crabbox.stop.exit-code.txt"), `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`);
407
+ }
408
+
409
+ const stdout = result.stdout;
410
+ const listOutput = section(stdout, "PI_LIST_STDOUT");
411
+ const nodeMajor = Number(marker(stdout, "PLATFORM_NODE_VERSION").replace(/^v/, "").split(".")[0] ?? 0);
412
+ const secretViolations = [
413
+ ...scanForSecrets(`${result.stdout}\n${result.stderr}`, secretValues),
414
+ ...scanArtifactTextFiles(suiteDir, secretValues).map((finding) => `${finding.file}: ${finding.violation}`),
415
+ ];
416
+ const checks = [
417
+ { id: "command-exit-zero", fn: () => result.code === 0, error: `exit ${result.code}` },
418
+ { id: "platform-marker", fn: () => stdout.includes("PLATFORM_BUILD_OK") },
419
+ { id: "node-version", fn: () => nodeMajor >= (config.nodeValidationMajor ?? 22), error: `Node major ${nodeMajor}` },
420
+ { id: "npm-ci", fn: () => /PLATFORM_NPM_CI_EXIT=0/.test(stdout) },
421
+ { id: "npm-run-verify", fn: () => /PLATFORM_VERIFY_EXIT=0/.test(stdout) },
422
+ { id: "npm-pack", fn: () => /PLATFORM_NPM_PACK_EXIT=0/.test(stdout) && marker(stdout, "PLATFORM_PACKED_TARBALL").length > 0 },
423
+ { id: "packed-node-install", fn: () => /PLATFORM_PACKED_NODE_INSTALL_EXIT=0/.test(stdout) },
424
+ { id: "pi-install-local-package", fn: () => /PLATFORM_PI_INSTALL_EXIT=0/.test(stdout) },
425
+ { id: "pi-list-local-package", fn: () => /PLATFORM_PI_LIST_EXIT=0/.test(stdout) && listOutput.includes(config.packageName) },
426
+ { id: "no-source-extension-shortcut", fn: () => !/\bpi\s+(?:-e|--extension)\s+\./.test(stdout) },
427
+ { id: "no-secret-artifacts", fn: () => secretViolations.length === 0, error: secretViolations.join(", ") },
428
+ ];
429
+ if (stopResult) checks.push({ id: "lease-cleanup", fn: () => stopResult.code === 0, error: `stop exit ${stopResult.code}` });
430
+ const expectedFiles = [
431
+ "summary.json", "artifact-manifest.json", "target.json", "suite.json", "command.txt", "exit-code.txt", "crabbox.stdout.txt", "crabbox.stderr.txt", "crabbox.timing.json",
432
+ "node-version.txt", "packed-tarball.txt", "packed-node-install.stdout.txt", "packed-node-install.stderr.txt", "pi-install.stdout.txt", "pi-install.stderr.txt", "pi-list.stdout.txt", "pi-list.stderr.txt", "assertions.json",
433
+ ];
434
+ if (stopResult) expectedFiles.push("crabbox.stop.stdout.txt", "crabbox.stop.stderr.txt", "crabbox.stop.exit-code.txt");
435
+ const { assertions } = finalizeSuite(suiteDir, checks, { target: targetName, suite: suiteName, elapsedMs, exitCode: result.code, signal: result.signal }, expectedFiles);
436
+ return { ok: assertions.ok, suiteDir, assertions };
437
+ }
438
+
439
+ export async function runTargetSuite(config, targetName, suiteName, leaseSession) {
440
+ if (suiteName === "platform-build") return await runPlatformBuildSuite(config, targetName, suiteName, leaseSession);
441
+ if (suiteName === "browser-dogfood-smoke") return await runBrowserDogfoodSuite(config, targetName, suiteName, leaseSession);
442
+ throw new Error(`unknown suite: ${suiteName}`);
443
+ }
444
+
445
+ export async function runTargetSuites(config, targetName, suiteNames) {
446
+ const slug = `${config.packageName}-${targetName}`;
447
+ const lease = await warmupLease(targetName, slug, config);
448
+ if (!lease.ok) {
449
+ const warmupFailure = createLeaseWarmupFailureResult(config, targetName, lease);
450
+ return { ok: false, results: [warmupFailure] };
451
+ }
452
+ const results = [];
453
+ let stopResult;
454
+ let staleCleanupResult;
455
+ try {
456
+ let sync = true;
457
+ for (const suiteName of suiteNames) {
458
+ const result = await runTargetSuite(config, targetName, suiteName, { ...lease, sync });
459
+ results.push(result);
460
+ sync = false;
461
+ if (!result.ok) break;
462
+ }
463
+ } finally {
464
+ stopResult = await stopLease(targetName, lease.leaseId, config);
465
+ staleCleanupResult = await cleanupStaleTargetState(targetName, config);
466
+ }
467
+ if (stopResult) {
468
+ results.push(createLeaseCleanupResult(config, targetName, lease.leaseId, stopResult, staleCleanupResult));
469
+ }
470
+ return { ok: results.every((result) => result.ok), results };
471
+ }