pi-cursor-sdk 0.1.56 → 0.1.57
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 +45 -1
- package/README.md +100 -28
- package/docs/cursor-live-smoke-checklist.md +7 -7
- package/docs/cursor-model-ux-spec.md +45 -38
- package/docs/cursor-native-tool-replay.md +2 -2
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +20 -7
- package/docs/cursor-tool-surfaces.md +13 -2
- package/docs/platform-smoke-implementation.md +220 -0
- package/docs/platform-smoke.md +183 -247
- package/package.json +38 -6
- package/platform-smoke.config.mjs +5 -1
- package/scripts/cloud-runtime-smoke.d.mts +3 -0
- package/scripts/cloud-runtime-smoke.mjs +502 -0
- package/scripts/debug-provider-events.mjs +7 -2
- package/scripts/isolated-cursor-smoke.sh +4 -6
- package/scripts/lib/cursor-child-process.d.mts +1 -0
- package/scripts/lib/cursor-child-process.mjs +137 -7
- package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
- package/scripts/local-resume-cleanup-smoke.mjs +108 -0
- package/scripts/local-resume-smoke.d.mts +1 -0
- package/scripts/local-resume-smoke.mjs +642 -0
- package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
- package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
- package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
- package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
- package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
- package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
- package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
- package/scripts/platform-smoke/artifacts.mjs +293 -65
- package/scripts/platform-smoke/card-detect.mjs +16 -4
- package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
- package/scripts/platform-smoke/doctor.mjs +20 -10
- package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
- package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
- package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
- package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
- package/scripts/platform-smoke/scenarios.mjs +16 -2
- package/scripts/platform-smoke/target-runtime.mjs +206 -0
- package/scripts/platform-smoke/targets.mjs +33 -141
- package/scripts/platform-smoke/visual-evidence.mjs +6 -7
- package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
- package/scripts/platform-smoke.mjs +40 -27
- package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
- package/scripts/steering-rpc-smoke.mjs +12 -2
- package/scripts/tmux-live-smoke.sh +3 -5
- package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
- package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
- package/shared/cursor-sensitive-text.mjs +7 -1
- package/src/context.ts +5 -2
- package/src/cursor-agents-context-registration.ts +7 -0
- package/src/cursor-agents-context.ts +3 -1
- package/src/cursor-api-key.ts +15 -1
- package/src/cursor-bridge-contract.ts +3 -0
- package/src/cursor-cloud-lifecycle.ts +733 -0
- package/src/cursor-cloud-options.ts +206 -0
- package/src/cursor-cloud-reporting.ts +246 -0
- package/src/cursor-config.ts +659 -0
- package/src/cursor-display-only-trace.ts +14 -0
- package/src/cursor-display-text.ts +8 -2
- package/src/cursor-durable-fs.ts +49 -0
- package/src/cursor-fallback-models.generated.ts +2045 -485
- package/src/cursor-live-run-accounting.ts +7 -1
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-pi-tool-bridge-run.ts +14 -4
- package/src/cursor-provider-live-run-drain.ts +5 -0
- package/src/cursor-provider-run-finalizer.ts +36 -28
- package/src/cursor-provider-turn-finalize.ts +72 -6
- package/src/cursor-provider-turn-prepare.ts +228 -12
- package/src/cursor-provider-turn-runner.ts +41 -13
- package/src/cursor-provider-turn-send.ts +59 -16
- package/src/cursor-provider-turn-types.ts +44 -10
- package/src/cursor-runtime-state.ts +478 -0
- package/src/cursor-sdk-event-debug.ts +46 -6
- package/src/cursor-sdk-process-error-guard.ts +101 -30
- package/src/cursor-session-agent-cleanup.ts +328 -0
- package/src/cursor-session-agent-resume.ts +439 -0
- package/src/cursor-session-agent.ts +109 -13
- package/src/cursor-session-scope.ts +35 -2
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-skill-tool.ts +30 -11
- package/src/cursor-state.ts +112 -69
- package/src/cursor-usage-accounting.ts +14 -4
- package/src/index.ts +11 -2
- package/src/model-discovery.ts +10 -56
|
@@ -11,21 +11,18 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
|
-
import {
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { setTimeout as delay } from "node:timers/promises";
|
|
16
|
-
import { redactSecrets,
|
|
16
|
+
import { redactSecrets, writePlatformArtifactBundle } from "./artifacts.mjs";
|
|
17
17
|
import { extractContentText, jsonlHasAssistantFinalTextMarker } from "./jsonl-text.mjs";
|
|
18
18
|
import { getScenario, renderPrompt } from "./scenarios.mjs";
|
|
19
19
|
|
|
20
|
-
const BUNDLE_START = "PLATFORM_LIVE_BUNDLE_JSON_START";
|
|
21
|
-
const BUNDLE_END = "PLATFORM_LIVE_BUNDLE_JSON_END";
|
|
22
20
|
const DEFAULT_MODEL = "cursor/composer-2-5";
|
|
23
21
|
const DEFAULT_WAIT_MS = 240_000;
|
|
24
22
|
const READY_WAIT_MS = 45_000;
|
|
25
23
|
const SESSION_JSONL_WAIT_MS = 60_000;
|
|
26
24
|
const COLS = 150;
|
|
27
25
|
const ROWS = 45;
|
|
28
|
-
const MAX_BUNDLE_FILE_BYTES = 5 * 1024 * 1024;
|
|
29
26
|
|
|
30
27
|
function writeRedactedTextFile(path, text) {
|
|
31
28
|
writeFileSync(path, redactSecrets(text ?? ""));
|
|
@@ -44,6 +41,7 @@ Options:
|
|
|
44
41
|
--package-name <n> Packed package name. Default: pi-cursor-sdk.
|
|
45
42
|
--out-dir <dir> Remote artifact dir. Default: .platform-smoke-runs/live-<suite>-<timestamp>.
|
|
46
43
|
--prep-dir <dir> Optional shared packed-install prep dir reused by live suites on one target.
|
|
44
|
+
--prepare-only Prepare/reuse --prep-dir and print the packed package path without a live run.
|
|
47
45
|
--wait-ms <ms> Max wait for final marker. Default: ${DEFAULT_WAIT_MS}.
|
|
48
46
|
-h, --help Show help.
|
|
49
47
|
`);
|
|
@@ -74,13 +72,15 @@ function parseArgs(argv) {
|
|
|
74
72
|
case "--package-name": out.packageName = next(); break;
|
|
75
73
|
case "--out-dir": out.outDir = resolve(next()); break;
|
|
76
74
|
case "--prep-dir": out.prepDir = resolve(next()); break;
|
|
75
|
+
case "--prepare-only": out.prepareOnly = true; break;
|
|
77
76
|
case "--wait-ms": out.waitMs = Number(next()); break;
|
|
78
77
|
default: fail(`unknown argument: ${arg}`);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
if (out.help) return out;
|
|
82
|
-
if (!out.suite) fail("--suite is required");
|
|
81
|
+
if (!out.prepareOnly && !out.suite) fail("--suite is required");
|
|
83
82
|
if (!out.target) fail("--target is required");
|
|
83
|
+
if (out.prepareOnly && !out.prepDir) fail("--prepare-only requires --prep-dir");
|
|
84
84
|
if (!Number.isSafeInteger(out.waitMs) || out.waitMs <= 0) fail("--wait-ms must be a positive integer");
|
|
85
85
|
out.outDir ??= resolve(".platform-smoke-runs", `live-${out.suite}-${Date.now()}`);
|
|
86
86
|
return out;
|
|
@@ -477,7 +477,7 @@ function writeProcessSnapshot(logDir, name, platform) {
|
|
|
477
477
|
? spawnSync("powershell.exe", [
|
|
478
478
|
"-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
|
479
479
|
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress",
|
|
480
|
-
], { encoding: "utf8", timeout:
|
|
480
|
+
], { encoding: "utf8", timeout: 90_000 })
|
|
481
481
|
: spawnSync("sh", ["-lc", "ps -axo pid,ppid,comm,args"], { encoding: "utf8", timeout: 30_000 });
|
|
482
482
|
writeRedactedTextFile(join(logDir, `${name}.stdout.txt`), result.stdout ?? "");
|
|
483
483
|
writeRedactedTextFile(join(logDir, `${name}.stderr.txt`), result.stderr ?? "");
|
|
@@ -498,61 +498,22 @@ function assertNoAbortLeftover(logDir, platform) {
|
|
|
498
498
|
requireOk(result, "leftover process check");
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
function shouldBundleFile(root, path) {
|
|
502
|
-
const rel = relative(root, path).replace(/\\/g, "/");
|
|
503
|
-
if (/(^|\/)node_modules\//i.test(rel)) return false;
|
|
504
|
-
if (/\.env(?:\.|$)/i.test(rel)) return false;
|
|
505
|
-
if (/(^|\/)auth\.json$/i.test(rel)) return false;
|
|
506
|
-
if (/(^|\/)(?:id_rsa|id_ed25519|.*\.pem|.*\.key)$/i.test(rel)) return false;
|
|
507
|
-
return true;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function collectFiles(root) {
|
|
511
|
-
const files = [];
|
|
512
|
-
const findings = [];
|
|
513
|
-
function visit(dir) {
|
|
514
|
-
let entries;
|
|
515
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
516
|
-
for (const entry of entries) {
|
|
517
|
-
const path = join(dir, entry.name);
|
|
518
|
-
if (entry.isDirectory()) visit(path);
|
|
519
|
-
else if (entry.isFile() && shouldBundleFile(root, path)) {
|
|
520
|
-
const rel = relative(root, path).replace(/\\/g, "/");
|
|
521
|
-
const size = statSync(path).size;
|
|
522
|
-
if (size <= MAX_BUNDLE_FILE_BYTES) {
|
|
523
|
-
const content = readFileSync(path);
|
|
524
|
-
if (/\.(?:txt|json|jsonl|md|log|ansi|html|yml|yaml|js|mjs|ts)$/i.test(entry.name)) {
|
|
525
|
-
for (const violation of scanForSecrets(content.toString("utf8"))) findings.push({ file: rel, violation });
|
|
526
|
-
}
|
|
527
|
-
files.push({ path: rel, contentBase64: content.toString("base64"), size });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
visit(root);
|
|
533
|
-
return { files, findings };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function printBundle(root) {
|
|
537
|
-
const collected = collectFiles(root);
|
|
538
|
-
const files = collected.findings.length === 0
|
|
539
|
-
? collected.files
|
|
540
|
-
: [{
|
|
541
|
-
path: "artifacts/bundle-redaction-violations.json",
|
|
542
|
-
contentBase64: Buffer.from(JSON.stringify(collected.findings, null, 2)).toString("base64"),
|
|
543
|
-
size: Buffer.byteLength(JSON.stringify(collected.findings, null, 2)),
|
|
544
|
-
}];
|
|
545
|
-
const bundle = { root: basename(root), files };
|
|
546
|
-
const payload = `${BUNDLE_START}\n${JSON.stringify(bundle)}\n${BUNDLE_END}\n`;
|
|
547
|
-
return new Promise((resolvePromise) => process.stdout.write(payload, resolvePromise));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
501
|
async function main() {
|
|
551
502
|
const args = parseArgs(process.argv.slice(2));
|
|
552
503
|
if (args.help) {
|
|
553
504
|
usage();
|
|
554
505
|
return;
|
|
555
506
|
}
|
|
507
|
+
if (args.prepareOnly) {
|
|
508
|
+
const bootstrapDir = `${args.prepDir}-bootstrap`;
|
|
509
|
+
const logDir = join(bootstrapDir, "logs");
|
|
510
|
+
const artifactDir = join(bootstrapDir, "artifacts");
|
|
511
|
+
mkdirSync(logDir, { recursive: true });
|
|
512
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
513
|
+
const prep = prepareSharedPackedInstall(args.prepDir, logDir, artifactDir, args.packageName);
|
|
514
|
+
console.log(`PLATFORM_PACKED_PACKAGE_PATH=${prep.packagePath}`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
556
517
|
const scenario = getScenario(args.suite);
|
|
557
518
|
if (!scenario) fail(`unknown suite: ${args.suite}`);
|
|
558
519
|
const platform = platformForTarget(args.target);
|
|
@@ -663,7 +624,7 @@ async function main() {
|
|
|
663
624
|
status.error = error;
|
|
664
625
|
status.finishedAt = new Date().toISOString();
|
|
665
626
|
writeFileSync(join(artifactDir, "live-status.json"), JSON.stringify(status, null, 2));
|
|
666
|
-
|
|
627
|
+
writePlatformArtifactBundle(runRoot);
|
|
667
628
|
}
|
|
668
629
|
if (!ok) process.exitCode = 1;
|
|
669
630
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
extractPlatformArtifactBundle,
|
|
5
|
+
scanArtifacts,
|
|
6
|
+
scanForSecrets,
|
|
7
|
+
writeCommand,
|
|
8
|
+
writeExitCode,
|
|
9
|
+
} from "./artifacts.mjs";
|
|
10
|
+
import { stopLease, warmupLease } from "./crabbox-runner.mjs";
|
|
11
|
+
import { LOCAL_RESUME_SUITE_BY_NAME } from "./local-resume-suites.mjs";
|
|
12
|
+
import {
|
|
13
|
+
failSuite,
|
|
14
|
+
fetchPlatformArtifactBundle,
|
|
15
|
+
finalizeSuiteArtifacts,
|
|
16
|
+
platformFor,
|
|
17
|
+
runOnLeaseWithTransientRetry,
|
|
18
|
+
stopLeaseCheck,
|
|
19
|
+
writeRedactedFile,
|
|
20
|
+
writeStopLeaseArtifacts,
|
|
21
|
+
} from "./target-runtime.mjs";
|
|
22
|
+
|
|
23
|
+
export async function executeLocalResumeSuite(
|
|
24
|
+
config,
|
|
25
|
+
targetName,
|
|
26
|
+
suiteName,
|
|
27
|
+
suiteDir,
|
|
28
|
+
slug,
|
|
29
|
+
leaseSession,
|
|
30
|
+
) {
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
let warmup = leaseSession;
|
|
33
|
+
const ownsLease = !warmup;
|
|
34
|
+
const variant = LOCAL_RESUME_SUITE_BY_NAME.get(suiteName);
|
|
35
|
+
const prepDir = leaseSession?.livePrepDir ?? `.platform-smoke-runs/local-resume-prep-${Date.now()}-${targetName}`;
|
|
36
|
+
const command = buildLocalResumeSuiteCommand(
|
|
37
|
+
targetName,
|
|
38
|
+
variant.script,
|
|
39
|
+
prepDir,
|
|
40
|
+
config.packageName ?? "pi-cursor-sdk",
|
|
41
|
+
suiteName,
|
|
42
|
+
);
|
|
43
|
+
writeCommand(suiteDir, command);
|
|
44
|
+
|
|
45
|
+
if (!warmup) {
|
|
46
|
+
console.log(` warmup ${targetName}...`);
|
|
47
|
+
warmup = await warmupLease(targetName, slug, config);
|
|
48
|
+
if (!warmup.ok) {
|
|
49
|
+
writeExitCode(suiteDir, warmup.code, warmup.signal);
|
|
50
|
+
writeRedactedFile(
|
|
51
|
+
resolve(suiteDir, "crabbox.warmup.stdout.txt"),
|
|
52
|
+
warmup.stdout,
|
|
53
|
+
);
|
|
54
|
+
writeRedactedFile(
|
|
55
|
+
resolve(suiteDir, "crabbox.warmup.stderr.txt"),
|
|
56
|
+
warmup.stderr,
|
|
57
|
+
);
|
|
58
|
+
return failSuite(
|
|
59
|
+
suiteDir,
|
|
60
|
+
targetName,
|
|
61
|
+
suiteName,
|
|
62
|
+
`Crabbox warmup failed (exit ${warmup.code}): ${warmup.stderr.slice(-500)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(` executing local resume smoke on ${targetName}...`);
|
|
68
|
+
const result = await runOnLeaseWithTransientRetry(
|
|
69
|
+
suiteDir,
|
|
70
|
+
targetName,
|
|
71
|
+
warmup.leaseId,
|
|
72
|
+
command,
|
|
73
|
+
{
|
|
74
|
+
shell: true,
|
|
75
|
+
timeout: 900_000,
|
|
76
|
+
allowEnv: ["CURSOR_API_KEY"],
|
|
77
|
+
captureStdoutPath: resolve(suiteDir, ".crabbox.remote.stdout.raw"),
|
|
78
|
+
sync: leaseSession?.sync,
|
|
79
|
+
config,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
const bundleTransport = await fetchPlatformArtifactBundle(targetName, warmup.leaseId, result.stdout, config);
|
|
83
|
+
if (!bundleTransport.ok) result.stderr = `${result.stderr}\n[platform-smoke] ${bundleTransport.error}`.trim();
|
|
84
|
+
const elapsed = Date.now() - startedAt;
|
|
85
|
+
writeRedactedFile(resolve(suiteDir, "crabbox.stdout.txt"), result.stdout);
|
|
86
|
+
writeRedactedFile(resolve(suiteDir, "crabbox.stderr.txt"), result.stderr);
|
|
87
|
+
writeFileSync(
|
|
88
|
+
resolve(suiteDir, "crabbox.timing.json"),
|
|
89
|
+
JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
92
|
+
elapsedMs: elapsed,
|
|
93
|
+
code: result.code,
|
|
94
|
+
signal: result.signal,
|
|
95
|
+
},
|
|
96
|
+
null,
|
|
97
|
+
2,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
writeExitCode(suiteDir, result.code, result.signal);
|
|
101
|
+
|
|
102
|
+
const bundle = extractPlatformArtifactBundle(suiteDir, bundleTransport.stdout);
|
|
103
|
+
const evidenceRoot = resolve(suiteDir, "local-resume-evidence");
|
|
104
|
+
const packedPackagePath = result.stdout.match(/^PLATFORM_PACKED_PACKAGE_PATH=(.+)$/m)?.[1]?.trim();
|
|
105
|
+
const evidence = summarizeLocalResumeEvidence(evidenceRoot, packedPackagePath);
|
|
106
|
+
writeFileSync(resolve(suiteDir, "local-resume-evidence.json"), JSON.stringify(evidence, null, 2));
|
|
107
|
+
|
|
108
|
+
let stopResult;
|
|
109
|
+
if (ownsLease) {
|
|
110
|
+
console.log(` stopping lease ${warmup.leaseId}...`);
|
|
111
|
+
stopResult = await stopLease(targetName, warmup.leaseId, config);
|
|
112
|
+
writeStopLeaseArtifacts(suiteDir, stopResult);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const violations = [
|
|
116
|
+
...scanForSecrets(`${result.stdout}\n${result.stderr}`).map(
|
|
117
|
+
(violation) => ({ file: "process-output", violation }),
|
|
118
|
+
),
|
|
119
|
+
...bundle.violations,
|
|
120
|
+
...scanArtifacts(suiteDir),
|
|
121
|
+
];
|
|
122
|
+
if (violations.length > 0)
|
|
123
|
+
writeFileSync(
|
|
124
|
+
resolve(suiteDir, "redaction-violations.json"),
|
|
125
|
+
JSON.stringify(violations, null, 2),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const checks = [
|
|
129
|
+
{ id: "local-resume-exit-zero", fn: () => result.code === 0 },
|
|
130
|
+
{ id: "local-resume-bundle-extracted", fn: () => bundle.ok },
|
|
131
|
+
{ id: "local-resume-session-evidence", fn: () => evidence.sessionFiles > 0 },
|
|
132
|
+
{ id: "local-resume-debug-evidence", fn: () => evidence.debugFiles > 0 },
|
|
133
|
+
{ id: "local-resume-runtime-evidence", fn: () => evidence.runtimeFiles > 0 },
|
|
134
|
+
{ id: "local-resume-packed-extension", fn: () => evidence.packedExtensionPathMatched },
|
|
135
|
+
{
|
|
136
|
+
id: "local-resume-marker",
|
|
137
|
+
fn: () => result.stdout.includes(variant.marker),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "local-resume-stderr-evidence",
|
|
141
|
+
fn: () => variant.stderrPattern.test(result.stderr),
|
|
142
|
+
},
|
|
143
|
+
{ id: "no-secrets", fn: () => violations.length === 0 },
|
|
144
|
+
];
|
|
145
|
+
if (stopResult) checks.push(stopLeaseCheck(stopResult));
|
|
146
|
+
const expectedFiles = [
|
|
147
|
+
"summary.json",
|
|
148
|
+
"target.json",
|
|
149
|
+
"suite.json",
|
|
150
|
+
"command.txt",
|
|
151
|
+
"exit-code.txt",
|
|
152
|
+
"crabbox.stdout.txt",
|
|
153
|
+
"crabbox.stderr.txt",
|
|
154
|
+
"crabbox.timing.json",
|
|
155
|
+
"local-resume-evidence.json",
|
|
156
|
+
"local-resume-evidence/runtime-launches.jsonl",
|
|
157
|
+
"assertions.json",
|
|
158
|
+
];
|
|
159
|
+
if (stopResult)
|
|
160
|
+
expectedFiles.push(
|
|
161
|
+
"crabbox.stop.stdout.txt",
|
|
162
|
+
"crabbox.stop.stderr.txt",
|
|
163
|
+
"crabbox.stop.exit-code.txt",
|
|
164
|
+
);
|
|
165
|
+
const { assertions } = finalizeSuiteArtifacts(
|
|
166
|
+
suiteDir,
|
|
167
|
+
checks,
|
|
168
|
+
{
|
|
169
|
+
target: targetName,
|
|
170
|
+
suite: suiteName,
|
|
171
|
+
exitCode: result.code,
|
|
172
|
+
signal: result.signal,
|
|
173
|
+
elapsedMs: elapsed,
|
|
174
|
+
evidence,
|
|
175
|
+
},
|
|
176
|
+
expectedFiles,
|
|
177
|
+
);
|
|
178
|
+
console.log(
|
|
179
|
+
` ${assertions.ok ? "PASS" : "FAIL"} ${suiteName} on ${targetName} (${elapsed}ms)`,
|
|
180
|
+
);
|
|
181
|
+
return { ok: assertions.ok, suiteDir, assertions };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizedTargetPath(path) {
|
|
185
|
+
const normalized = String(path ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
186
|
+
return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function summarizeLocalResumeEvidence(root, packedPackagePath) {
|
|
190
|
+
const files = [];
|
|
191
|
+
function visit(dir) {
|
|
192
|
+
if (!existsSync(dir)) return;
|
|
193
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
194
|
+
const path = resolve(dir, entry.name);
|
|
195
|
+
if (entry.isDirectory()) visit(path);
|
|
196
|
+
else if (entry.isFile()) files.push(path.slice(root.length + 1).replace(/\\/g, "/"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
visit(root);
|
|
200
|
+
const runtimePath = resolve(root, "runtime-launches.jsonl");
|
|
201
|
+
let runtimeLaunchCount = 0;
|
|
202
|
+
let runtimeRecordsValid = false;
|
|
203
|
+
let runtimeExtensionPaths = [];
|
|
204
|
+
try {
|
|
205
|
+
const runtimeLines = readFileSync(runtimePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
206
|
+
runtimeLaunchCount = runtimeLines.length;
|
|
207
|
+
const parsedPaths = runtimeLines.map((line) => JSON.parse(line)?.extensionPath);
|
|
208
|
+
runtimeRecordsValid = parsedPaths.every((path) => typeof path === "string");
|
|
209
|
+
runtimeExtensionPaths = parsedPaths.filter((path) => typeof path === "string");
|
|
210
|
+
} catch {}
|
|
211
|
+
const expectedPath = normalizedTargetPath(packedPackagePath);
|
|
212
|
+
return {
|
|
213
|
+
root: "local-resume-evidence",
|
|
214
|
+
fileCount: files.length,
|
|
215
|
+
sessionFiles: files.filter((path) => path.startsWith("sessions/") && path.endsWith(".jsonl")).length,
|
|
216
|
+
debugFiles: files.filter((path) => path.startsWith("debug/")).length,
|
|
217
|
+
runtimeFiles: files.filter((path) => path === "runtime-launches.jsonl").length,
|
|
218
|
+
packedPackagePath,
|
|
219
|
+
runtimeLaunchCount,
|
|
220
|
+
runtimeExtensionPaths,
|
|
221
|
+
packedExtensionPathMatched: (expectedPath.startsWith("/") || /^[a-z]:\//.test(expectedPath))
|
|
222
|
+
&& runtimeLaunchCount > 0
|
|
223
|
+
&& runtimeRecordsValid
|
|
224
|
+
&& runtimeExtensionPaths.every((path) => normalizedTargetPath(path) === expectedPath),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function quotePosix(value) {
|
|
229
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function quotePowerShell(value) {
|
|
233
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildLocalResumeSuiteCommand(
|
|
237
|
+
targetName,
|
|
238
|
+
script = "smoke:local-resume",
|
|
239
|
+
prepDir = ".platform-smoke-runs/local-resume-prep",
|
|
240
|
+
packageName = "pi-cursor-sdk",
|
|
241
|
+
suiteName = "cursor-local-resume-restart",
|
|
242
|
+
) {
|
|
243
|
+
const powershellTarget = platformFor(targetName) === "powershell";
|
|
244
|
+
const evidenceDir = `${prepDir}/${powershellTarget ? "lr" : `local-resume-${suiteName}`}`;
|
|
245
|
+
const packagePath = `${prepDir}/packed-workspace/node_modules/${packageName}`;
|
|
246
|
+
if (powershellTarget) {
|
|
247
|
+
const powershell = `$ErrorActionPreference='Stop';$p=${quotePowerShell(prepDir)};$e=$p+'/lr';$w=$e.Replace('/','\\');$x=$p+${quotePowerShell(`/packed-workspace/node_modules/${packageName}`)};node scripts/platform-smoke/live-suite-runner.mjs --prepare-only --target ${targetName} --package-name ${packageName} --prep-dir $p;if($LASTEXITCODE){exit $LASTEXITCODE};for($i=0;$i -lt 10 -and (Test-Path -LiteralPath $e);$i++){cmd.exe /d /c rd /s /q $w;if(Test-Path -LiteralPath $e){Start-Sleep -Milliseconds 200}};if(Test-Path -LiteralPath $e){throw 'local-resume evidence cleanup failed'};$env:CURSOR_LOCAL_RESUME_SMOKE_EXTENSION_PATH=$x;$env:CURSOR_LOCAL_RESUME_SMOKE_ARTIFACT_DIR=$e;$env:CURSOR_LOCAL_RESUME_SMOKE_KEEP_ARTIFACTS='1';$env:CURSOR_LOCAL_RESUME_SMOKE_EMIT_BUNDLE='1';npm run ${script}`;
|
|
248
|
+
return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${Buffer.from(powershell, "utf16le").toString("base64")}`;
|
|
249
|
+
}
|
|
250
|
+
const prep = `node scripts/platform-smoke/live-suite-runner.mjs --prepare-only --target ${quotePosix(targetName)} --package-name ${quotePosix(packageName)} --prep-dir ${quotePosix(prepDir)}`;
|
|
251
|
+
return `${prep} && rm -rf ${quotePosix(evidenceDir)} && CURSOR_LOCAL_RESUME_SMOKE_EXTENSION_PATH=${quotePosix(packagePath)} CURSOR_LOCAL_RESUME_SMOKE_ARTIFACT_DIR=${quotePosix(evidenceDir)} CURSOR_LOCAL_RESUME_SMOKE_KEEP_ARTIFACTS=1 CURSOR_LOCAL_RESUME_SMOKE_EMIT_BUNDLE=1 npm run ${script}`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const LOCAL_RESUME_SUITES: readonly {
|
|
2
|
+
key: string;
|
|
3
|
+
flag?: string;
|
|
4
|
+
suite: string;
|
|
5
|
+
script: string;
|
|
6
|
+
marker: string;
|
|
7
|
+
stderrPattern: RegExp;
|
|
8
|
+
description: string;
|
|
9
|
+
cursorCalls: number;
|
|
10
|
+
}[];
|
|
11
|
+
export const LOCAL_RESUME_SUITE_NAMES: string[];
|
|
12
|
+
export const LOCAL_RESUME_SUITE_BY_NAME: Map<
|
|
13
|
+
string,
|
|
14
|
+
(typeof LOCAL_RESUME_SUITES)[number]
|
|
15
|
+
>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const LOCAL_RESUME_SUITES = [
|
|
2
|
+
{
|
|
3
|
+
key: "restart",
|
|
4
|
+
suite: "cursor-local-resume-restart",
|
|
5
|
+
script: "smoke:local-resume",
|
|
6
|
+
marker: "local-resume-smoke-ok",
|
|
7
|
+
stderrPattern: /agent-[0-9a-f-]{36}\s+resumed across restart/i,
|
|
8
|
+
description: "Prove guarded local resume across a pi process restart.",
|
|
9
|
+
cursorCalls: 2,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
key: "safety",
|
|
13
|
+
flag: "--safety",
|
|
14
|
+
suite: "cursor-local-resume-safety",
|
|
15
|
+
script: "smoke:local-resume:safety",
|
|
16
|
+
marker: "local-resume-safety-smoke-ok",
|
|
17
|
+
stderrPattern: /rejected for clone and fork-before-future/i,
|
|
18
|
+
description: "Prove local resume fork/clone safety for copied handles and future-marker branches.",
|
|
19
|
+
cursorCalls: 5,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "toolSurface",
|
|
23
|
+
flag: "--tool-surface",
|
|
24
|
+
suite: "cursor-local-resume-tool-surface",
|
|
25
|
+
script: "smoke:local-resume:tool-surface",
|
|
26
|
+
marker: "local-resume-tool-surface-smoke-ok",
|
|
27
|
+
stderrPattern: /rejected after bridge builtin tool surface change/i,
|
|
28
|
+
description: "Prove local resume rejects stale handles after a Pi bridge/tool-surface change.",
|
|
29
|
+
cursorCalls: 3,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "abort",
|
|
33
|
+
flag: "--abort",
|
|
34
|
+
suite: "cursor-local-resume-abort",
|
|
35
|
+
script: "smoke:local-resume:abort",
|
|
36
|
+
marker: "local-resume-abort-smoke-ok",
|
|
37
|
+
stderrPattern: /not reused after aborted bridge turn/i,
|
|
38
|
+
description: "Prove local resume does not persist or reuse an interrupted bridge turn handle.",
|
|
39
|
+
cursorCalls: 3,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "tree",
|
|
43
|
+
flag: "--tree",
|
|
44
|
+
suite: "cursor-local-resume-tree",
|
|
45
|
+
script: "smoke:local-resume:tree",
|
|
46
|
+
marker: "local-resume-tree-smoke-ok",
|
|
47
|
+
stderrPattern: /rejected for tree assistant and resume-entry targets/i,
|
|
48
|
+
description: "Prove local resume tree navigation rejects future-seeing SDK agents.",
|
|
49
|
+
cursorCalls: 4,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "copySwitch",
|
|
53
|
+
flag: "--copy-switch",
|
|
54
|
+
suite: "cursor-local-resume-copy-switch",
|
|
55
|
+
script: "smoke:local-resume:copy-switch",
|
|
56
|
+
marker: "local-resume-copy-switch-smoke-ok",
|
|
57
|
+
stderrPattern: /rejected for copied session switch/i,
|
|
58
|
+
description: "Prove copied session files reject copied local resume handles.",
|
|
59
|
+
cursorCalls: 2,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "fallback",
|
|
63
|
+
flag: "--fallback",
|
|
64
|
+
suite: "cursor-local-resume-fallback",
|
|
65
|
+
script: "smoke:local-resume:fallback",
|
|
66
|
+
marker: "local-resume-fallback-smoke-ok",
|
|
67
|
+
stderrPattern: /missing agent-missing-.* fell back from original/i,
|
|
68
|
+
description: "Prove missing local SDK agent fallback creates a new agent and emits continuity notice.",
|
|
69
|
+
cursorCalls: 2,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "compaction",
|
|
73
|
+
flag: "--compaction",
|
|
74
|
+
suite: "cursor-local-resume-compaction",
|
|
75
|
+
script: "smoke:local-resume:compaction",
|
|
76
|
+
marker: "local-resume-compaction-smoke-ok",
|
|
77
|
+
stderrPattern: /replaced by and resumed post-compaction/i,
|
|
78
|
+
description: "Prove compaction creates a post-compaction local agent and restart resumes that generation.",
|
|
79
|
+
cursorCalls: 5,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "defaultDryRun",
|
|
83
|
+
flag: "--default-dry-run",
|
|
84
|
+
suite: "cursor-local-resume-default-dry-run",
|
|
85
|
+
script: "smoke:local-resume:default-dry-run",
|
|
86
|
+
marker: "local-resume-default-dry-run-smoke-ok",
|
|
87
|
+
stderrPattern: /built-in default resumed .* env opt-out rejected it/i,
|
|
88
|
+
description: "Prove the built-in local resume default resumes, and env opt-out wins.",
|
|
89
|
+
cursorCalls: 3,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "cleanup",
|
|
93
|
+
flag: "--cleanup",
|
|
94
|
+
suite: "cursor-local-resume-cleanup",
|
|
95
|
+
script: "smoke:local-resume:cleanup",
|
|
96
|
+
marker: "local-resume-cleanup-smoke-ok",
|
|
97
|
+
stderrPattern: /cleanup deleted recorded old .* preserved current/i,
|
|
98
|
+
description: "Prove recorded-ID-only local SDK cleanup deletes an old recorded agent and preserves the current one.",
|
|
99
|
+
cursorCalls: 4,
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const LOCAL_RESUME_SUITE_NAMES = LOCAL_RESUME_SUITES.map((suite) => suite.suite);
|
|
104
|
+
export const LOCAL_RESUME_SUITE_BY_NAME = new Map(LOCAL_RESUME_SUITES.map((suite) => [suite.suite, suite]));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LOCAL_RESUME_SUITES } from "./local-resume-suites.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Scenario definitions for all required platform smoke suites.
|
|
3
5
|
*
|
|
@@ -19,6 +21,8 @@ export const SCENARIOS = {
|
|
|
19
21
|
},
|
|
20
22
|
},
|
|
21
23
|
|
|
24
|
+
...Object.fromEntries(LOCAL_RESUME_SUITES.map(({ suite, description, cursorCalls }) => [suite, { description, cursorCalls }])),
|
|
25
|
+
|
|
22
26
|
"cursor-native-visual-matrix": {
|
|
23
27
|
description: "Prove provider reality, native tool replay, card rendering, JSONL correctness.",
|
|
24
28
|
cursorCalls: 1,
|
|
@@ -79,7 +83,12 @@ NATIVE_MATRIX_OK package=<name> grep=<yes/no> find=<yes/no> list=<yes/no> shell=
|
|
|
79
83
|
{ id: "native-shell-failure", toolName: "bash", isError: true, contains: "native shell failure" },
|
|
80
84
|
],
|
|
81
85
|
visualEvidence: [
|
|
82
|
-
{
|
|
86
|
+
{
|
|
87
|
+
id: "native-read-package",
|
|
88
|
+
pattern: "^\\s*read\\s+(?:(?:\\./)?package\\.json|.*[\\\\/]package\\.json)\\s*$",
|
|
89
|
+
wrappedPattern: "^\\s*read\\s+.*[\\\\/]package\\.(?:json|js\\s+on|j\\s*son)\\s*$",
|
|
90
|
+
jsonlResultId: "native-read-package",
|
|
91
|
+
},
|
|
83
92
|
{ id: "native-shell-success", pattern: "^\\s*cursor visual smoke\\s*$", jsonlResultId: "native-shell-output" },
|
|
84
93
|
{ id: "native-write-diff", pattern: "^\\s*\\+.*beta", jsonlResultId: "native-write-diff" },
|
|
85
94
|
{ id: "native-edit-diff", pattern: "^\\s*\\+.*gamma", jsonlResultId: "native-edit-diff" },
|
|
@@ -133,7 +142,12 @@ BRIDGE_MATRIX_OK bash_ok=<yes/no> read_ok=<yes/no> read_missing_error=<yes/no>`,
|
|
|
133
142
|
],
|
|
134
143
|
expectedJSONLResultToolOrder: ["bash", "read", "read"],
|
|
135
144
|
visualEvidence: [
|
|
136
|
-
{
|
|
145
|
+
{
|
|
146
|
+
id: "bridge-read-success",
|
|
147
|
+
pattern: "^\\s*read\\s+(?:\\./package\\.json|.*[\\\\/]package\\.j(?:son|s))\\s*$",
|
|
148
|
+
wrappedPattern: "^\\s*read\\s+.*[\\\\/]package\\.(?:json|js\\s+on|j\\s*son)\\s*$",
|
|
149
|
+
jsonlResultId: "bridge-read-success",
|
|
150
|
+
},
|
|
137
151
|
{ id: "bridge-read-failure", pattern: "^\\s*read \\./definitely-missing-platform-smoke-file\\.txt|ENOENT: no such file", jsonlResultId: "bridge-read-failure" },
|
|
138
152
|
{ id: "bridge-shell-success", pattern: "^\\s*bridge visual smoke\\s*$", jsonlResultId: "bridge-shell-success" },
|
|
139
153
|
],
|