pi-cursor-sdk 0.1.40 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +12 -9
- package/docs/cursor-dogfood-checklist.md +6 -0
- package/docs/cursor-live-smoke-checklist.md +4 -4
- package/docs/cursor-model-ux-spec.md +6 -6
- package/docs/cursor-native-tool-replay.md +11 -7
- package/docs/cursor-native-tool-visual-audit.md +2 -2
- package/docs/cursor-testing-lessons.md +1 -1
- package/docs/cursor-tool-surfaces.md +4 -0
- package/docs/platform-smoke.md +9 -1
- package/package.json +8 -5
- package/scripts/lib/cursor-visual-manifest.d.mts +3 -0
- package/scripts/lib/cursor-visual-manifest.mjs +82 -0
- package/scripts/platform-smoke/artifacts.mjs +147 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/doctor.mjs +53 -8
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke.mjs +69 -7
- package/scripts/visual-tui-smoke-self-test.mjs +229 -0
- package/scripts/visual-tui-smoke.mjs +45 -179
- package/src/context.ts +25 -10
- package/src/cursor-active-tools.ts +7 -0
- package/src/cursor-compact-tool-summary.ts +81 -0
- package/src/cursor-native-tool-display-registration.ts +31 -21
- package/src/cursor-native-tool-display-replay.ts +13 -2
- package/src/cursor-native-tool-display-state.ts +13 -4
- package/src/cursor-pi-tool-bridge-run.ts +6 -3
- package/src/cursor-pi-tool-bridge-types.ts +2 -2
- package/src/cursor-provider-errors.ts +2 -1
- package/src/cursor-provider-live-run-drain.ts +1 -1
- package/src/cursor-provider-turn-prepare.ts +1 -1
- package/src/cursor-provider-turn-send.ts +2 -0
- package/src/cursor-question-tool.ts +2 -1
- package/src/cursor-replay-activity-builders.ts +12 -4
- package/src/cursor-replay-summary-args.ts +21 -2
- package/src/cursor-sdk-event-debug.ts +3 -1
- package/src/cursor-skill-tool.ts +2 -1
- package/src/cursor-task-presentation.ts +77 -0
- package/src/cursor-tool-manifest.ts +2 -1
- package/src/cursor-tool-presentation-registry.ts +16 -2
- package/src/cursor-tool-result-display-readers.ts +13 -8
- package/src/cursor-transcript-tool-formatters.ts +5 -5
- package/src/cursor-usage-accounting.ts +5 -4
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Artifact management — directory layout, manifest, redaction scanning, packaging.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from "node:fs";
|
|
6
|
-
import { resolve, relative, basename } from "node:path";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, renameSync } from "node:fs";
|
|
6
|
+
import { resolve, relative, basename, dirname } from "node:path";
|
|
7
7
|
|
|
8
8
|
const PLATFORM_SMOKE_RUN_DIR_PATTERN = /^run-(\d+)-[a-z0-9]+$/i;
|
|
9
9
|
const HOURS_TO_MS = 60 * 60 * 1000;
|
|
10
10
|
const DAYS_TO_MS = 24 * HOURS_TO_MS;
|
|
11
|
+
const LATEST_INDEX_NAME = "latest.json";
|
|
11
12
|
|
|
12
13
|
function finiteNonNegativeNumber(value) {
|
|
13
14
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -178,6 +179,150 @@ export function writeSummary(dir, data) {
|
|
|
178
179
|
}, null, 2));
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
function readJsonFile(path) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectFiles(root) {
|
|
191
|
+
const files = [];
|
|
192
|
+
function walk(dir) {
|
|
193
|
+
let entries;
|
|
194
|
+
try {
|
|
195
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
const path = resolve(dir, entry.name);
|
|
201
|
+
if (entry.isDirectory()) walk(path);
|
|
202
|
+
else if (entry.isFile()) files.push(path);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (existsSync(root)) walk(root);
|
|
206
|
+
files.sort();
|
|
207
|
+
return files;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function existingPath(path) {
|
|
211
|
+
return existsSync(path) ? path : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function providerDebugPathFields(debugRoot) {
|
|
215
|
+
if (!existsSync(debugRoot)) return {};
|
|
216
|
+
const providerDebugArtifacts = collectFiles(debugRoot);
|
|
217
|
+
const keyArtifacts = providerDebugArtifacts.filter((path) => /(?:^|[\\/])(?:session|summary|timeline|provider-events|bridge-events|wait-result)\.(?:json|jsonl)$/i.test(path));
|
|
218
|
+
const capped = keyArtifacts.slice(0, 40);
|
|
219
|
+
return {
|
|
220
|
+
providerDebugRoot: debugRoot,
|
|
221
|
+
providerDebugArtifactCount: providerDebugArtifacts.length,
|
|
222
|
+
providerDebugArtifacts: capped,
|
|
223
|
+
...(providerDebugArtifacts.length > capped.length ? { providerDebugArtifactsTruncated: true } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function pathFields(suiteDir) {
|
|
228
|
+
const artifactsDir = resolve(suiteDir, "artifacts");
|
|
229
|
+
const debugRoot = resolve(suiteDir, "cursor-sdk-events");
|
|
230
|
+
const paths = {
|
|
231
|
+
artifactManifest: existingPath(resolve(suiteDir, "artifact-manifest.json")),
|
|
232
|
+
summary: existingPath(resolve(suiteDir, "summary.json")),
|
|
233
|
+
assertions: existingPath(resolve(suiteDir, "assertions.json")),
|
|
234
|
+
failures: existingPath(resolve(suiteDir, "failures.md")),
|
|
235
|
+
terminalHtml: existingPath(resolve(artifactsDir, "terminal.html")),
|
|
236
|
+
terminalFullPng: existingPath(resolve(artifactsDir, "terminal.full.png")),
|
|
237
|
+
terminalFinalViewportPng: existingPath(resolve(artifactsDir, "terminal.final-viewport.png")),
|
|
238
|
+
visualEvidence: existingPath(resolve(artifactsDir, "visual-evidence.json")),
|
|
239
|
+
sessionJsonl: existingPath(resolve(artifactsDir, "session.jsonl")),
|
|
240
|
+
jsonlToolResults: existingPath(resolve(artifactsDir, "jsonl-tool-results.json")),
|
|
241
|
+
...providerDebugPathFields(debugRoot),
|
|
242
|
+
};
|
|
243
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
244
|
+
if (value === undefined) delete paths[key];
|
|
245
|
+
}
|
|
246
|
+
return paths;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function suiteIndexFromResult(result, artifactRoot) {
|
|
250
|
+
if (!result?.suiteDir) return undefined;
|
|
251
|
+
const suiteDir = resolve(result.suiteDir);
|
|
252
|
+
const summary = readJsonFile(resolve(suiteDir, "summary.json"));
|
|
253
|
+
const target = readJsonFile(resolve(suiteDir, "target.json"));
|
|
254
|
+
const suite = readJsonFile(resolve(suiteDir, "suite.json"));
|
|
255
|
+
const rel = relative(resolve(process.cwd(), artifactRoot), suiteDir).split(/[\\/]/);
|
|
256
|
+
return {
|
|
257
|
+
target: summary?.target ?? target?.targetName ?? rel.at(-2),
|
|
258
|
+
suite: summary?.suite ?? suite?.suiteName ?? rel.at(-1),
|
|
259
|
+
runId: target?.runId ?? rel.at(-3),
|
|
260
|
+
ok: result.ok === true,
|
|
261
|
+
artifactDir: suiteDir,
|
|
262
|
+
paths: pathFields(suiteDir),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function targetIndexesFromRun(targetName, result, artifactRoot) {
|
|
267
|
+
const suiteResults = Array.isArray(result?.results) ? result.results : [result];
|
|
268
|
+
const suites = suiteResults.map((suiteResult) => suiteIndexFromResult(suiteResult, artifactRoot)).filter(Boolean);
|
|
269
|
+
const runIds = [...new Set(suites.map((suite) => suite.runId).filter(Boolean))];
|
|
270
|
+
return {
|
|
271
|
+
target: targetName,
|
|
272
|
+
ok: result?.ok === true,
|
|
273
|
+
...(result?.error ? { error: redactSecrets(result.error) } : {}),
|
|
274
|
+
runId: runIds.length === 1 ? runIds[0] : undefined,
|
|
275
|
+
runIds,
|
|
276
|
+
suites,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Build a stable, agent-readable platform-smoke latest index from target run results. */
|
|
281
|
+
export function buildLatestPlatformSmokeIndex(config, runResults, metadata = {}) {
|
|
282
|
+
const artifactRoot = resolve(process.cwd(), config?.artifactRoot ?? ".artifacts/platform-smoke");
|
|
283
|
+
const targets = runResults.map(({ targetName, result }) => targetIndexesFromRun(targetName, result, artifactRoot));
|
|
284
|
+
const runIds = [...new Set(targets.flatMap((target) => target.runIds).filter(Boolean))].sort();
|
|
285
|
+
const newestRunId = runIds
|
|
286
|
+
.map((runId) => ({ runId, match: PLATFORM_SMOKE_RUN_DIR_PATTERN.exec(runId) }))
|
|
287
|
+
.filter((entry) => entry.match)
|
|
288
|
+
.sort((a, b) => Number(b.match[1]) - Number(a.match[1]))[0]?.runId ?? runIds.at(-1);
|
|
289
|
+
return {
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
kind: "platform-smoke-latest",
|
|
292
|
+
runId: runIds.length === 1 ? runIds[0] : newestRunId,
|
|
293
|
+
runIds,
|
|
294
|
+
artifactRoot,
|
|
295
|
+
startedAt: metadata.startedAt,
|
|
296
|
+
finishedAt: metadata.finishedAt,
|
|
297
|
+
command: metadata.command,
|
|
298
|
+
pid: process.pid,
|
|
299
|
+
ok: targets.every((target) => target.ok),
|
|
300
|
+
targets,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Atomically write .artifacts/platform-smoke/latest.json. */
|
|
305
|
+
export function writeLatestPlatformSmokeIndex(config, runResults, metadata = {}) {
|
|
306
|
+
const index = buildLatestPlatformSmokeIndex(config, runResults, metadata);
|
|
307
|
+
mkdirSync(index.artifactRoot, { recursive: true });
|
|
308
|
+
const outPath = resolve(index.artifactRoot, LATEST_INDEX_NAME);
|
|
309
|
+
const tmpPath = resolve(dirname(outPath), `.${LATEST_INDEX_NAME}.${process.pid}.${Date.now()}.tmp`);
|
|
310
|
+
writeFileSync(tmpPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
311
|
+
renameSync(tmpPath, outPath);
|
|
312
|
+
return { index, path: outPath };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Return concise existing evidence paths for a failed suite result. */
|
|
316
|
+
export function platformSmokeSuiteEvidence(result, artifactRoot) {
|
|
317
|
+
const suite = suiteIndexFromResult(result, artifactRoot ?? ".artifacts/platform-smoke");
|
|
318
|
+
if (!suite) return undefined;
|
|
319
|
+
return {
|
|
320
|
+
suite: suite.suite,
|
|
321
|
+
artifactDir: suite.artifactDir,
|
|
322
|
+
paths: suite.paths,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
181
326
|
/** Write command.txt recording the command that was executed. */
|
|
182
327
|
export function writeCommand(dir, cmd) {
|
|
183
328
|
writeFileSync(resolve(dir, "command.txt"), Array.isArray(cmd) ? cmd.join(" ") + "\n" : cmd + "\n");
|
|
@@ -18,7 +18,7 @@ const CARD_PATTERNS = [
|
|
|
18
18
|
{ id: "write", pattern: /^\s*\+.*beta\s*$/i },
|
|
19
19
|
{ id: "edit-diff", pattern: /^\s*\+.*gamma\s*$/i },
|
|
20
20
|
{ id: "shell-failure", pattern: /^\s*(?:native shell failure|Command exited with code 7)\s*$/i },
|
|
21
|
-
{ id: "bridge-read-success", pattern: /^\s*read
|
|
21
|
+
{ id: "bridge-read-success", pattern: /^\s*read (?:\.\/package\.json|.*[\\/]package\.json)\s*$/i },
|
|
22
22
|
{ id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
|
|
23
23
|
{ id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
|
|
24
24
|
{ id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { execSync, execFileSync } from "node:child_process";
|
|
10
|
-
import { accessSync, constants, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { accessSync, constants, existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { renderAll } from "./render-ansi.mjs";
|
|
12
13
|
|
|
13
14
|
let failures = 0;
|
|
14
15
|
|
|
@@ -126,6 +127,41 @@ function disposableWindowsSshProbe(cbox, config = {}) {
|
|
|
126
127
|
|
|
127
128
|
function hasBin(name) { return silent("which", [name]) !== null; }
|
|
128
129
|
|
|
130
|
+
async function runRenderProbe(artifactRoot) {
|
|
131
|
+
const probeDir = resolve(artifactRoot, `.doctor-render-${process.pid}-${Date.now()}`);
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(probeDir, { recursive: true });
|
|
134
|
+
const ansiPath = resolve(probeDir, "terminal.ansi");
|
|
135
|
+
writeFileSync(ansiPath, "\u001b[32mplatform smoke render probe\u001b[0m\n");
|
|
136
|
+
const rendered = await renderAll(ansiPath, probeDir, {
|
|
137
|
+
label: "doctor-render-probe",
|
|
138
|
+
model: "doctor",
|
|
139
|
+
mode: "doctor",
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
sessionId: "doctor-render-probe",
|
|
142
|
+
width: 80,
|
|
143
|
+
height: 10,
|
|
144
|
+
historyLines: 100,
|
|
145
|
+
});
|
|
146
|
+
const pngPath = resolve(probeDir, "terminal.full.png");
|
|
147
|
+
const pngOk = rendered.pngOk && existsSync(pngPath) && statSync(pngPath).size > 100;
|
|
148
|
+
if (!pngOk) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
message: `host-side xterm/Playwright render probe did not produce a PNG at ${pngPath}. Run: npx playwright install chromium`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, message: pngPath };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
message: `host-side xterm/Playwright render probe failed: ${error instanceof Error ? error.message : String(error)}. Run npm install, then: npx playwright install chromium`,
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(probeDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
129
165
|
function findGitRoot(startPath) {
|
|
130
166
|
let dir = startPath;
|
|
131
167
|
for (let i = 0; i < 8; i++) {
|
|
@@ -137,7 +173,7 @@ function findGitRoot(startPath) {
|
|
|
137
173
|
return null;
|
|
138
174
|
}
|
|
139
175
|
|
|
140
|
-
function runChecks(config) {
|
|
176
|
+
async function runChecks(config) {
|
|
141
177
|
// ── Phase 1: environment variables ──
|
|
142
178
|
console.log("\n── Environment variables ──");
|
|
143
179
|
const requiredVars = [
|
|
@@ -396,7 +432,16 @@ function runChecks(config) {
|
|
|
396
432
|
fail(`cannot write to ${artRoot}: ${e.message}`);
|
|
397
433
|
}
|
|
398
434
|
|
|
399
|
-
// ── Phase 10:
|
|
435
|
+
// ── Phase 10: Host-side visual render probe ──
|
|
436
|
+
console.log("\n── Host-side visual render probe ──");
|
|
437
|
+
const renderProbe = await runRenderProbe(artRoot);
|
|
438
|
+
if (renderProbe.ok) {
|
|
439
|
+
ok("xterm/Playwright Chromium render probe wrote a PNG");
|
|
440
|
+
} else {
|
|
441
|
+
fail(renderProbe.message);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Phase 11: Git status ──
|
|
400
445
|
console.log("\n── Git status ──");
|
|
401
446
|
const branch = shell("git branch --show-current");
|
|
402
447
|
branch ? ok(`branch: ${branch}`) : warn("could not determine branch");
|
|
@@ -408,7 +453,7 @@ function runChecks(config) {
|
|
|
408
453
|
ok("clean worktree");
|
|
409
454
|
}
|
|
410
455
|
|
|
411
|
-
// ── Phase
|
|
456
|
+
// ── Phase 12: Forbidden files ──
|
|
412
457
|
console.log("\n── Forbidden files ──");
|
|
413
458
|
let anyForbidden = false;
|
|
414
459
|
for (const pat of [".env", "*.tgz"]) {
|
|
@@ -428,7 +473,7 @@ function runChecks(config) {
|
|
|
428
473
|
}
|
|
429
474
|
ok("no tracked .env.*");
|
|
430
475
|
|
|
431
|
-
// ── Phase
|
|
476
|
+
// ── Phase 13: Cursor auth ──
|
|
432
477
|
console.log("\n── Cursor auth ──");
|
|
433
478
|
const key = env("CURSOR_API_KEY");
|
|
434
479
|
if (key && key.length > 10) {
|
|
@@ -439,7 +484,7 @@ function runChecks(config) {
|
|
|
439
484
|
fail("CURSOR_API_KEY missing — live Cursor suites will not run");
|
|
440
485
|
}
|
|
441
486
|
|
|
442
|
-
// ── Phase
|
|
487
|
+
// ── Phase 14: node-pty self-test ──
|
|
443
488
|
console.log("\n── node-pty self-test ──");
|
|
444
489
|
const ptyPath = resolve(process.cwd(), "node_modules", "node-pty");
|
|
445
490
|
if (existsSync(ptyPath)) {
|
|
@@ -458,7 +503,7 @@ function runChecks(config) {
|
|
|
458
503
|
warn("node-pty not installed — live PTY suites will not run. Run: npm ci");
|
|
459
504
|
}
|
|
460
505
|
|
|
461
|
-
// ── Phase
|
|
506
|
+
// ── Phase 15: Summary ──
|
|
462
507
|
console.log(`\n=== Results: ${failures} failure(s) ===`);
|
|
463
508
|
if (failures > 0) {
|
|
464
509
|
console.log("Fix failures above before running live Cursor suites.");
|
|
@@ -470,5 +515,5 @@ function runChecks(config) {
|
|
|
470
515
|
}
|
|
471
516
|
|
|
472
517
|
export async function runDoctor(config) {
|
|
473
|
-
runChecks(config);
|
|
518
|
+
await runChecks(config);
|
|
474
519
|
}
|
|
@@ -131,7 +131,7 @@ BRIDGE_MATRIX_OK bash_ok=<yes/no> read_ok=<yes/no> read_missing_error=<yes/no>`,
|
|
|
131
131
|
{ id: "bridge-shell-success", toolName: "bash", isError: false, contains: "bridge visual smoke" },
|
|
132
132
|
],
|
|
133
133
|
visualEvidence: [
|
|
134
|
-
{ id: "bridge-read-success", pattern: "^\\s*read
|
|
134
|
+
{ id: "bridge-read-success", pattern: "^\\s*read (?:\\./package\\.json|.*[\\\\/]package\\.json)", jsonlResultId: "bridge-read-success" },
|
|
135
135
|
{ id: "bridge-read-failure", pattern: "^\\s*read \\./definitely-missing-platform-smoke-file\\.txt|ENOENT: no such file", jsonlResultId: "bridge-read-failure" },
|
|
136
136
|
{ id: "bridge-shell-success", pattern: "^\\s*bridge visual smoke\\s*$", jsonlResultId: "bridge-shell-success" },
|
|
137
137
|
],
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { resolve, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
7
|
|
|
8
|
-
import { prunePlatformSmokeArtifacts } from "./platform-smoke/artifacts.mjs";
|
|
8
|
+
import { platformSmokeSuiteEvidence, prunePlatformSmokeArtifacts, redactSecrets, writeLatestPlatformSmokeIndex } from "./platform-smoke/artifacts.mjs";
|
|
9
9
|
|
|
10
10
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -104,6 +104,53 @@ function validateSelections(targets, suites) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function failedSuiteResults(result) {
|
|
108
|
+
if (!result) return [];
|
|
109
|
+
if (Array.isArray(result.results)) return result.results.filter((suiteResult) => suiteResult?.ok !== true);
|
|
110
|
+
return result.ok === true ? [] : [result];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatExistingPath(label, path) {
|
|
114
|
+
return path && existsSync(path) ? ` ${label}: ${path}` : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printFailureEvidence(results, artifactRoot) {
|
|
118
|
+
const failed = [];
|
|
119
|
+
for (const { targetName, result } of results) {
|
|
120
|
+
let targetEvidenceCount = 0;
|
|
121
|
+
for (const suiteResult of failedSuiteResults(result)) {
|
|
122
|
+
const evidence = platformSmokeSuiteEvidence(suiteResult, artifactRoot);
|
|
123
|
+
if (evidence) {
|
|
124
|
+
targetEvidenceCount++;
|
|
125
|
+
failed.push({ targetName, ...evidence });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (targetEvidenceCount === 0 && result?.ok !== true && result?.error) {
|
|
129
|
+
failed.push({ targetName, suite: "target", error: result.error });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (failed.length === 0) return;
|
|
133
|
+
console.log("\nFailed suite artifacts:");
|
|
134
|
+
for (const item of failed) {
|
|
135
|
+
const paths = item.paths ?? {};
|
|
136
|
+
console.log(`- Suite: ${item.targetName}/${item.suite}`);
|
|
137
|
+
if (item.error) console.log(` Target error: ${item.error}`);
|
|
138
|
+
const lines = [
|
|
139
|
+
formatExistingPath("Artifact dir", item.artifactDir),
|
|
140
|
+
formatExistingPath("Assertions", paths.assertions),
|
|
141
|
+
formatExistingPath("Failures", paths.failures),
|
|
142
|
+
formatExistingPath("Terminal HTML", paths.terminalHtml),
|
|
143
|
+
formatExistingPath("Terminal full PNG", paths.terminalFullPng),
|
|
144
|
+
formatExistingPath("Terminal final viewport PNG", paths.terminalFinalViewportPng),
|
|
145
|
+
formatExistingPath("Visual evidence", paths.visualEvidence),
|
|
146
|
+
formatExistingPath("Session JSONL", paths.sessionJsonl),
|
|
147
|
+
formatExistingPath("JSONL tool results", paths.jsonlToolResults),
|
|
148
|
+
formatExistingPath("Provider/Cursor debug artifacts", paths.providerDebugRoot),
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
for (const line of lines) console.log(line);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
// ── commands ───────────────────────────────────────────────────────────────
|
|
108
155
|
async function runDoctor() {
|
|
109
156
|
try {
|
|
@@ -125,8 +172,9 @@ async function runSuite(targetName, suiteName) {
|
|
|
125
172
|
const result = await runTargetSuite(config, targetName, suiteName);
|
|
126
173
|
return result;
|
|
127
174
|
} catch (err) {
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
const message = redactSecrets(err.message);
|
|
176
|
+
console.error(`suite ${suiteName} on ${targetName} exception:`, message);
|
|
177
|
+
return { ok: false, error: message };
|
|
130
178
|
}
|
|
131
179
|
}
|
|
132
180
|
|
|
@@ -135,8 +183,9 @@ async function runTarget(targetName, suites) {
|
|
|
135
183
|
const { runTargetSuites } = await import("./platform-smoke/targets.mjs");
|
|
136
184
|
return await runTargetSuites(config, targetName, suites);
|
|
137
185
|
} catch (err) {
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
const message = redactSecrets(err.message);
|
|
187
|
+
console.error(`target ${targetName} exception:`, message);
|
|
188
|
+
return { ok: false, error: message };
|
|
140
189
|
}
|
|
141
190
|
}
|
|
142
191
|
|
|
@@ -179,6 +228,7 @@ async function main() {
|
|
|
179
228
|
console.log(`Pruned ${pruneResult.removed.length} old platform smoke artifact run(s) from ${pruneResult.root}`);
|
|
180
229
|
}
|
|
181
230
|
|
|
231
|
+
const startedAt = new Date().toISOString();
|
|
182
232
|
const targetRuns = targets.map(async (targetName) => {
|
|
183
233
|
console.log(`\n=== Target: ${targetName} ===`);
|
|
184
234
|
const result = args.suite
|
|
@@ -187,9 +237,21 @@ async function main() {
|
|
|
187
237
|
return { targetName, result };
|
|
188
238
|
});
|
|
189
239
|
const results = await Promise.all(targetRuns);
|
|
240
|
+
const finishedAt = new Date().toISOString();
|
|
241
|
+
const latest = writeLatestPlatformSmokeIndex(config, results, {
|
|
242
|
+
startedAt,
|
|
243
|
+
finishedAt,
|
|
244
|
+
command: {
|
|
245
|
+
cwd: process.cwd(),
|
|
246
|
+
targets,
|
|
247
|
+
suites,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
console.log(`\nArtifact index: ${latest.path}`);
|
|
190
251
|
const anyFailed = results.some(({ result }) => !result.ok);
|
|
191
252
|
if (anyFailed) {
|
|
192
|
-
|
|
253
|
+
printFailureEvidence(results, config.artifactRoot);
|
|
254
|
+
console.log("\nOne or more suites failed.");
|
|
193
255
|
process.exit(1);
|
|
194
256
|
}
|
|
195
257
|
return;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, chmodSync, utimesSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { delimiter } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
function assertSelfTest(condition, message) {
|
|
7
|
+
if (!condition) throw new Error(`self-test failed: ${message}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function envMap(assignments) {
|
|
11
|
+
return new Map(assignments.map(([name, value]) => [name, value]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseEnvCapture(path) {
|
|
15
|
+
return new Map(
|
|
16
|
+
readFileSync(path, "utf8")
|
|
17
|
+
.split("\n")
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map((line) => {
|
|
20
|
+
const index = line.indexOf("=");
|
|
21
|
+
return index === -1 ? [line, ""] : [line.slice(0, index), line.slice(index + 1)];
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runVisualSmokeSelfTest(deps) {
|
|
27
|
+
const { ROOT, DEFAULT_MODE, DEFAULT_MODEL, DEFAULT_SETTING_SOURCES, DEBUG_ENV_NAMES, shellQuote, parseArgs, snapshotJsonlMtimes, findLatestJsonl, sealedNodePath, resolveCommand, requireNode, requireCommand, buildLaunchPlan, run, runVisualSmoke } = deps;
|
|
28
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pi-cursor-sdk-visual-self-test-"));
|
|
29
|
+
try {
|
|
30
|
+
const binDir = join(tempDir, "bin");
|
|
31
|
+
mkdirSync(binDir, { recursive: true });
|
|
32
|
+
const fakePi = join(binDir, "pi");
|
|
33
|
+
const fakeNode = join(binDir, "node");
|
|
34
|
+
const fakeNodeMarker = join(tempDir, "fake-node-used");
|
|
35
|
+
const envCapture = join(tempDir, "fake-pi.env");
|
|
36
|
+
writeFileSync(
|
|
37
|
+
fakePi,
|
|
38
|
+
`#!/usr/bin/env node\nconst { writeFileSync } = require("node:fs");\nwriteFileSync(${JSON.stringify(envCapture)}, Object.entries(process.env).map(([key, value]) => key + "=" + (value ?? "")).join("\\n") + "\\n", "utf8");\n`,
|
|
39
|
+
"utf8",
|
|
40
|
+
);
|
|
41
|
+
writeFileSync(fakeNode, `#!/bin/sh\necho fake-node-used > ${shellQuote(fakeNodeMarker)}\nexit 99\n`, "utf8");
|
|
42
|
+
chmodSync(fakePi, 0o755);
|
|
43
|
+
chmodSync(fakeNode, 0o755);
|
|
44
|
+
|
|
45
|
+
const promptFile = join(tempDir, "prompt.txt");
|
|
46
|
+
writeFileSync(promptFile, "file prompt", "utf8");
|
|
47
|
+
assertSelfTest(parseArgs(["--label", "prompt-order", "--prompt-file", promptFile, "--prompt", "inline prompt"]).prompt === "inline prompt", "--prompt should override an earlier --prompt-file");
|
|
48
|
+
assertSelfTest(parseArgs(["--label", "prompt-dash", "--prompt", "--starts-with-dash"]).prompt === "--starts-with-dash", "--prompt should accept dash-prefixed free-form text");
|
|
49
|
+
assertSelfTest(parseArgs(["--label", "prompt-order", "--prompt", "inline prompt", "--prompt-file", promptFile]).prompt === "file prompt", "--prompt-file should override an earlier --prompt");
|
|
50
|
+
|
|
51
|
+
const jsonlDir = join(tempDir, "jsonl-filter");
|
|
52
|
+
mkdirSync(jsonlDir, { recursive: true });
|
|
53
|
+
const staleJsonl = join(jsonlDir, "stale.jsonl");
|
|
54
|
+
const freshJsonl = join(jsonlDir, "fresh.jsonl");
|
|
55
|
+
writeFileSync(staleJsonl, "{}\n", "utf8");
|
|
56
|
+
utimesSync(staleJsonl, new Date(1_000), new Date(1_000));
|
|
57
|
+
const previousJsonlMtimes = snapshotJsonlMtimes(jsonlDir);
|
|
58
|
+
writeFileSync(freshJsonl, "{}\n", "utf8");
|
|
59
|
+
utimesSync(freshJsonl, new Date(3_000), new Date(3_000));
|
|
60
|
+
assertSelfTest(findLatestJsonl(jsonlDir, { sinceMs: 2_000, previousMtimes: previousJsonlMtimes }) === freshJsonl, "JSONL discovery should ignore unchanged stale files before run start");
|
|
61
|
+
assertSelfTest(findLatestJsonl(jsonlDir, { sinceMs: 4_000, previousMtimes: snapshotJsonlMtimes(jsonlDir) }) === undefined, "JSONL discovery should not return stale evidence when current run has no changed JSONL");
|
|
62
|
+
|
|
63
|
+
assertSelfTest(!sealedNodePath(process.execPath, "").includes(delimiter), "empty inherited PATH must not leave an empty PATH segment");
|
|
64
|
+
const hostilePath = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
65
|
+
const sealedHostilePath = sealedNodePath(process.execPath, hostilePath);
|
|
66
|
+
assertSelfTest(resolveCommand("pi", hostilePath) === fakePi, "direct PATH resolver did not prefer fake PATH head");
|
|
67
|
+
assertSelfTest(requireNode() === process.execPath, "node resolver must use process.execPath");
|
|
68
|
+
assertSelfTest(requireCommand("pi", { envPath: hostilePath, env: { ...process.env, PATH: sealedHostilePath } }) === fakePi, "pi prereq should use sealed PATH when executing the shim");
|
|
69
|
+
assertSelfTest(!existsSync(fakeNodeMarker), "pi prereq should not use hostile fake node");
|
|
70
|
+
|
|
71
|
+
const baseOptions = {
|
|
72
|
+
ext: ROOT,
|
|
73
|
+
cwd: ROOT,
|
|
74
|
+
mode: DEFAULT_MODE,
|
|
75
|
+
model: DEFAULT_MODEL,
|
|
76
|
+
outDir: tempDir,
|
|
77
|
+
safeLabel: "self-test",
|
|
78
|
+
sessionDir: join(tempDir, "session"),
|
|
79
|
+
sessionId: "self-test",
|
|
80
|
+
settingSources: DEFAULT_SETTING_SOURCES,
|
|
81
|
+
bridge: false,
|
|
82
|
+
exposeBuiltinTools: false,
|
|
83
|
+
eventDebug: false,
|
|
84
|
+
};
|
|
85
|
+
const plan = buildLaunchPlan(baseOptions, { pi: fakePi, node: process.execPath, sealedPath: sealedHostilePath }, "/bin/sh");
|
|
86
|
+
const defaults = envMap(plan.envAssignments);
|
|
87
|
+
assertSelfTest(defaults.get("PI_CURSOR_NATIVE_TOOL_DISPLAY") === "1", "native display must be forced on");
|
|
88
|
+
assertSelfTest(defaults.get("PI_CURSOR_REGISTER_NATIVE_TOOLS") === "1", "native tool registration must be forced on");
|
|
89
|
+
assertSelfTest(defaults.get("PI_CURSOR_SETTING_SOURCES") === "none", "setting sources must default to none");
|
|
90
|
+
assertSelfTest(defaults.get("PI_CURSOR_PI_TOOL_BRIDGE") === "0", "bridge must default off");
|
|
91
|
+
assertSelfTest(defaults.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "0", "built-in exposure must default off");
|
|
92
|
+
for (const name of DEBUG_ENV_NAMES) {
|
|
93
|
+
assertSelfTest(plan.clearEnvNames.includes(name), `${name} must be cleared by default`);
|
|
94
|
+
}
|
|
95
|
+
assertSelfTest(plan.script.includes(shellQuote(fakePi)), "launch script must use resolved pi path");
|
|
96
|
+
assertSelfTest(!plan.script.includes(" exec pi "), "launch script must not use bare pi");
|
|
97
|
+
const hostileEnv = {
|
|
98
|
+
...process.env,
|
|
99
|
+
...Object.fromEntries(DEBUG_ENV_NAMES.map((name) => [name, join(tempDir, name)])),
|
|
100
|
+
PATH: hostilePath,
|
|
101
|
+
PI_CURSOR_REGISTER_NATIVE_TOOLS: "0",
|
|
102
|
+
PI_CURSOR_SETTING_SOURCES: "all",
|
|
103
|
+
PI_CURSOR_PI_TOOL_BRIDGE: "1",
|
|
104
|
+
PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "1",
|
|
105
|
+
};
|
|
106
|
+
const probe = run("/bin/sh", ["-c", plan.script], { env: hostileEnv });
|
|
107
|
+
assertSelfTest(probe.status === 0, `fake-pi env capture exited ${probe.status}: ${probe.stderr?.toString() ?? ""}`);
|
|
108
|
+
const capturedEnv = parseEnvCapture(envCapture);
|
|
109
|
+
assertSelfTest(!existsSync(fakeNodeMarker), "launch PATH should force the resolved node before hostile fake node");
|
|
110
|
+
assertSelfTest((capturedEnv.get("PATH") ?? "").split(delimiter)[0] === dirname(process.execPath), "captured PATH should start with resolved node directory");
|
|
111
|
+
assertSelfTest(capturedEnv.get("PI_CURSOR_NATIVE_TOOL_DISPLAY") === "1", "captured env should force native display on");
|
|
112
|
+
assertSelfTest(capturedEnv.get("PI_CURSOR_REGISTER_NATIVE_TOOLS") === "1", "captured env should force native registration on");
|
|
113
|
+
assertSelfTest(capturedEnv.get("PI_CURSOR_SETTING_SOURCES") === "none", "captured env should force settings off");
|
|
114
|
+
assertSelfTest(capturedEnv.get("PI_CURSOR_PI_TOOL_BRIDGE") === "0", "captured env should force bridge off");
|
|
115
|
+
assertSelfTest(capturedEnv.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "0", "captured env should force built-in exposure off");
|
|
116
|
+
for (const name of DEBUG_ENV_NAMES) {
|
|
117
|
+
assertSelfTest(!capturedEnv.has(name), `${name} should be absent from captured env by default`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const optInPlan = buildLaunchPlan(
|
|
121
|
+
{ ...baseOptions, settingSources: "all", bridge: true, exposeBuiltinTools: true, eventDebug: true },
|
|
122
|
+
{ pi: fakePi, node: process.execPath, sealedPath: sealedHostilePath },
|
|
123
|
+
"/bin/sh",
|
|
124
|
+
);
|
|
125
|
+
const optIns = envMap(optInPlan.envAssignments);
|
|
126
|
+
assertSelfTest(optIns.get("PI_CURSOR_SETTING_SOURCES") === "all", "setting source opt-in must be reflected");
|
|
127
|
+
assertSelfTest(optIns.get("PI_CURSOR_PI_TOOL_BRIDGE") === "1", "bridge opt-in must be reflected");
|
|
128
|
+
assertSelfTest(optIns.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "1", "built-in exposure opt-in must be reflected");
|
|
129
|
+
assertSelfTest(optIns.get("PI_CURSOR_SDK_EVENT_DEBUG") === "1", "event debug opt-in must be reflected");
|
|
130
|
+
assertSelfTest(optIns.get("PI_CURSOR_SDK_EVENT_DEBUG_DIR") === join(tempDir, "self-test.cursor-sdk-events"), "event debug dir must be deterministic under out-dir");
|
|
131
|
+
for (const name of DEBUG_ENV_NAMES) {
|
|
132
|
+
assertSelfTest(optInPlan.clearEnvNames.includes(name), `${name} must be cleared even when event debug is explicit`);
|
|
133
|
+
}
|
|
134
|
+
const eventDebugProbe = run("/bin/sh", ["-c", optInPlan.script], { env: hostileEnv });
|
|
135
|
+
assertSelfTest(eventDebugProbe.status === 0, `fake-pi event-debug env capture exited ${eventDebugProbe.status}: ${eventDebugProbe.stderr?.toString() ?? ""}`);
|
|
136
|
+
const capturedEventDebugEnv = parseEnvCapture(envCapture);
|
|
137
|
+
assertSelfTest(capturedEventDebugEnv.get("PI_CURSOR_SDK_EVENT_DEBUG") === "1", "event debug should be explicitly enabled");
|
|
138
|
+
assertSelfTest(capturedEventDebugEnv.get("PI_CURSOR_SDK_EVENT_DEBUG_DIR") === join(tempDir, "self-test.cursor-sdk-events"), "event debug dir should be deterministic under out-dir");
|
|
139
|
+
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR"), "stale event debug run dir should be cleared");
|
|
140
|
+
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_SESSION_DIR"), "stale event debug session dir should be cleared");
|
|
141
|
+
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_STDERR"), "stale event debug stderr flag should be cleared");
|
|
142
|
+
|
|
143
|
+
const fakeTmux = join(binDir, "tmux");
|
|
144
|
+
const deleteBufferMarker = join(tempDir, "delete-buffer-called");
|
|
145
|
+
writeFileSync(
|
|
146
|
+
fakeTmux,
|
|
147
|
+
`#!/bin/sh\ncase "$1" in\n -V) echo 'tmux fake'; exit 0 ;;\n new-session) exit 0 ;;\n load-buffer) cat >/dev/null; exit 0 ;;\n paste-buffer) exit 77 ;;\n delete-buffer) echo deleted > ${shellQuote(deleteBufferMarker)}; exit 0 ;;\n kill-session) exit 0 ;;\n *) echo "unexpected tmux command: $*" >&2; exit 64 ;;\nesac\n`,
|
|
148
|
+
"utf8",
|
|
149
|
+
);
|
|
150
|
+
chmodSync(fakeTmux, 0o755);
|
|
151
|
+
const originalPath = process.env.PATH;
|
|
152
|
+
try {
|
|
153
|
+
process.env.PATH = hostilePath;
|
|
154
|
+
let pasteFailed = false;
|
|
155
|
+
try {
|
|
156
|
+
runVisualSmoke({
|
|
157
|
+
...baseOptions,
|
|
158
|
+
prompt: "buffer cleanup prompt",
|
|
159
|
+
startupMs: 1,
|
|
160
|
+
waitMs: 1,
|
|
161
|
+
width: 80,
|
|
162
|
+
height: 24,
|
|
163
|
+
historyLines: 100,
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
pasteFailed = /paste-buffer failed/.test(error instanceof Error ? error.message : String(error));
|
|
167
|
+
}
|
|
168
|
+
assertSelfTest(pasteFailed, "fake tmux paste failure should exercise prompt-buffer cleanup path");
|
|
169
|
+
assertSelfTest(existsSync(deleteBufferMarker), "prompt tmux buffer should be deleted when paste/send fails");
|
|
170
|
+
} finally {
|
|
171
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
172
|
+
else process.env.PATH = originalPath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
writeFileSync(
|
|
176
|
+
fakeTmux,
|
|
177
|
+
`#!/bin/sh
|
|
178
|
+
case "$1" in
|
|
179
|
+
-V) echo 'tmux fake'; exit 0 ;;
|
|
180
|
+
new-session) exit 0 ;;
|
|
181
|
+
load-buffer) cat >/dev/null; exit 0 ;;
|
|
182
|
+
paste-buffer) exit 0 ;;
|
|
183
|
+
send-keys) exit 0 ;;
|
|
184
|
+
delete-buffer) exit 0 ;;
|
|
185
|
+
capture-pane) echo 'captured visual smoke output'; exit 0 ;;
|
|
186
|
+
kill-session) exit 0 ;;
|
|
187
|
+
*) echo "unexpected tmux command: $*" >&2; exit 64 ;;
|
|
188
|
+
esac
|
|
189
|
+
`,
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
chmodSync(fakeTmux, 0o755);
|
|
193
|
+
const noJsonlManifest = join(tempDir, "self-test-jsonl-missing.manifest.json");
|
|
194
|
+
try {
|
|
195
|
+
process.env.PATH = hostilePath;
|
|
196
|
+
let missingJsonlFailed = false;
|
|
197
|
+
let missingJsonlError = "";
|
|
198
|
+
try {
|
|
199
|
+
runVisualSmoke({
|
|
200
|
+
...baseOptions,
|
|
201
|
+
label: "self-test-jsonl-missing",
|
|
202
|
+
safeLabel: "self-test-jsonl-missing",
|
|
203
|
+
prompt: "jsonl failure prompt",
|
|
204
|
+
startupMs: 1,
|
|
205
|
+
waitMs: 1,
|
|
206
|
+
width: 80,
|
|
207
|
+
height: 24,
|
|
208
|
+
historyLines: 100,
|
|
209
|
+
sessionDir: join(tempDir, "missing-jsonl-session"),
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
missingJsonlError = error instanceof Error ? error.message : String(error);
|
|
213
|
+
missingJsonlFailed = /no current-run persisted \.jsonl/.test(missingJsonlError);
|
|
214
|
+
}
|
|
215
|
+
assertSelfTest(missingJsonlFailed, `missing JSONL should fail after partial visual artifacts are written: ${missingJsonlError || "no error"}`);
|
|
216
|
+
assertSelfTest(existsSync(noJsonlManifest), "missing JSONL should still write a failure manifest");
|
|
217
|
+
const manifest = JSON.parse(readFileSync(noJsonlManifest, "utf8"));
|
|
218
|
+
assertSelfTest(manifest.failure?.message?.includes("no current-run persisted .jsonl"), "failure manifest should record the missing JSONL reason");
|
|
219
|
+
assertSelfTest(manifest.paths?.html?.endsWith("self-test-jsonl-missing.html"), "failure manifest should point at partial HTML evidence");
|
|
220
|
+
} finally {
|
|
221
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
222
|
+
else process.env.PATH = originalPath;
|
|
223
|
+
}
|
|
224
|
+
console.log("[visual-smoke] self-test PASS");
|
|
225
|
+
} finally {
|
|
226
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|