pi-cursor-sdk 0.1.39 → 0.1.41
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 +13 -12
- package/docs/cursor-dogfood-checklist.md +7 -1
- package/docs/cursor-live-smoke-checklist.md +13 -13
- package/docs/cursor-model-ux-spec.md +8 -8
- package/docs/cursor-native-tool-replay.md +4 -4
- package/docs/cursor-native-tool-visual-audit.md +5 -5
- package/docs/cursor-testing-lessons.md +5 -5
- package/docs/cursor-tool-surfaces.md +4 -0
- package/docs/platform-smoke.md +22 -7
- package/package.json +8 -5
- package/platform-smoke.config.mjs +5 -0
- package/scripts/debug-provider-events.mjs +1 -0
- package/scripts/isolated-cursor-smoke.sh +7 -7
- 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 +225 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/doctor.mjs +53 -8
- package/scripts/platform-smoke/live-suite-runner.mjs +7 -6
- package/scripts/platform-smoke/platform-build-windows.ps1 +2 -2
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/platform-smoke.mjs +75 -6
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +1 -1
- package/scripts/visual-tui-smoke-self-test.mjs +229 -0
- package/scripts/visual-tui-smoke.mjs +46 -179
- package/shared/cursor-setting-sources.d.mts +1 -0
- package/shared/cursor-setting-sources.mjs +2 -1
- package/src/context.ts +25 -10
- package/src/cursor-active-tools.ts +7 -0
- package/src/cursor-native-tool-display-registration.ts +31 -21
- 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-sdk-event-debug.ts +3 -1
- package/src/cursor-setting-sources.ts +2 -0
- package/src/cursor-skill-tool.ts +2 -1
- package/src/cursor-state.ts +2 -1
- package/src/cursor-tool-manifest.ts +2 -1
- package/src/cursor-usage-accounting.ts +5 -4
|
@@ -603,10 +603,10 @@ async function main() {
|
|
|
603
603
|
const npmInstallPacked = runLogged(logDir, "workspace-npm-install-packed", commandName("npm"), ["install", "--no-save", tarballPath], { cwd: workspaceDir, timeout: 180_000 });
|
|
604
604
|
requireOk(npmInstallPacked, "workspace npm install packed tarball");
|
|
605
605
|
}
|
|
606
|
-
const install = runLogged(logDir, "pi-install", piCli, ["install", "-l", installPath], { cwd: workspaceDir, env: piEnv, timeout: 120_000 });
|
|
607
|
-
requireOk(install, "pi install packed package directory");
|
|
608
|
-
const list = runLogged(logDir, "pi-list", piCli, ["list"], { cwd: workspaceDir, env: piEnv, timeout: 60_000 });
|
|
609
|
-
requireOk(list, "pi list");
|
|
606
|
+
const install = runLogged(logDir, "pi-install", piCli, ["install", "--approve", "-l", installPath], { cwd: workspaceDir, env: piEnv, timeout: 120_000 });
|
|
607
|
+
requireOk(install, "pi install --approve packed package directory");
|
|
608
|
+
const list = runLogged(logDir, "pi-list", piCli, ["list", "--approve"], { cwd: workspaceDir, env: piEnv, timeout: 60_000 });
|
|
609
|
+
requireOk(list, "pi list --approve");
|
|
610
610
|
|
|
611
611
|
const suiteEnv = {
|
|
612
612
|
...process.env,
|
|
@@ -620,16 +620,17 @@ async function main() {
|
|
|
620
620
|
if (args.suite === "cursor-abort-cleanup") writeProcessSnapshot(logDir, "process-before", platform);
|
|
621
621
|
const prompt = renderPrompt(scenario, platform);
|
|
622
622
|
writeFileSync(join(artifactDir, "prompt.txt"), prompt);
|
|
623
|
+
const piArgs = ["--approve", "--cursor-no-fast", "--cursor-mode", "agent", "--model", args.model, "--session-dir", sessionDir, "--session-id", `platform-${args.suite}-${Date.now()}`];
|
|
623
624
|
writeFileSync(join(artifactDir, "pi-command.json"), JSON.stringify({
|
|
624
625
|
piCli,
|
|
625
|
-
args:
|
|
626
|
+
args: piArgs,
|
|
626
627
|
cwd: workspaceDir,
|
|
627
628
|
env: Object.fromEntries(Object.entries(suiteEnv).filter(([key]) => key.startsWith("PI_CURSOR_") || key === "PI_CODING_AGENT_DIR" || key === "TERM")),
|
|
628
629
|
}, null, 2));
|
|
629
630
|
const ptyResult = await runPtyPi({
|
|
630
631
|
artifactDir,
|
|
631
632
|
piCli,
|
|
632
|
-
piArgs
|
|
633
|
+
piArgs,
|
|
633
634
|
env: suiteEnv,
|
|
634
635
|
cwd: workspaceDir,
|
|
635
636
|
sessionDir,
|
|
@@ -137,7 +137,7 @@ if ($PackTarball -and $PiCli -and (Test-Path -LiteralPath $TarballPath)) {
|
|
|
137
137
|
if ($PACKED_NODE_INSTALL_EXIT -eq 0) {
|
|
138
138
|
$PreviousPiOffline = $env:PI_OFFLINE
|
|
139
139
|
$env:PI_OFFLINE = "1"
|
|
140
|
-
& $PiCli install -l (Join-Path ".\node_modules" $PackageName) 1> $PiInstallOut 2> $PiInstallErr
|
|
140
|
+
& $PiCli install --approve -l (Join-Path ".\node_modules" $PackageName) 1> $PiInstallOut 2> $PiInstallErr
|
|
141
141
|
$PI_INSTALL_EXIT = Exit-CodeFromLastCommand
|
|
142
142
|
if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
|
|
143
143
|
} else {
|
|
@@ -163,7 +163,7 @@ if ($PiCli) {
|
|
|
163
163
|
Push-Location $PiProject
|
|
164
164
|
$PreviousPiOffline = $env:PI_OFFLINE
|
|
165
165
|
$env:PI_OFFLINE = "1"
|
|
166
|
-
& $PiCli list 1> $PiListOut 2> $PiListErr
|
|
166
|
+
& $PiCli list --approve 1> $PiListOut 2> $PiListErr
|
|
167
167
|
$PI_LIST_EXIT = Exit-CodeFromLastCommand
|
|
168
168
|
if ($null -eq $PreviousPiOffline) { Remove-Item Env:\PI_OFFLINE -ErrorAction SilentlyContinue } else { $env:PI_OFFLINE = $PreviousPiOffline }
|
|
169
169
|
Pop-Location
|
|
@@ -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
|
],
|
|
@@ -469,13 +469,13 @@ export function buildPlatformBuildCommand(targetName, packageName = "pi-cursor-s
|
|
|
469
469
|
lines.push('echo "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"');
|
|
470
470
|
lines.push(...posixSection("PACKED_NODE_INSTALL_STDOUT", 'cat "$PACK_DIR/packed-node-install.stdout.txt" 2>/dev/null || true'));
|
|
471
471
|
lines.push(...posixSection("PACKED_NODE_INSTALL_STDERR", 'cat "$PACK_DIR/packed-node-install.stderr.txt" 2>/dev/null || true'));
|
|
472
|
-
lines.push(`if [ "$PACKED_NODE_INSTALL_EXIT" -eq 0 ] && [ -n "$PI_CLI" ]; then (cd "$PI_PROJECT" && PI_OFFLINE=1 "$PI_CLI" install -l ./node_modules/${packageName} >"$PACK_DIR/pi-install.stdout.txt" 2>"$PACK_DIR/pi-install.stderr.txt"); PI_INSTALL_EXIT=$?; else echo "packed npm install failed or missing pi cli" >"$PACK_DIR/pi-install.stderr.txt"; PI_INSTALL_EXIT=1; fi`);
|
|
472
|
+
lines.push(`if [ "$PACKED_NODE_INSTALL_EXIT" -eq 0 ] && [ -n "$PI_CLI" ]; then (cd "$PI_PROJECT" && PI_OFFLINE=1 "$PI_CLI" install --approve -l ./node_modules/${packageName} >"$PACK_DIR/pi-install.stdout.txt" 2>"$PACK_DIR/pi-install.stderr.txt"); PI_INSTALL_EXIT=$?; else echo "packed npm install failed or missing pi cli" >"$PACK_DIR/pi-install.stderr.txt"; PI_INSTALL_EXIT=1; fi`);
|
|
473
473
|
lines.push('echo "PLATFORM_PI_INSTALL_EXIT=$PI_INSTALL_EXIT"');
|
|
474
474
|
lines.push(...posixSection("PI_INSTALL_STDOUT", 'cat "$PACK_DIR/pi-install.stdout.txt" 2>/dev/null || true'));
|
|
475
475
|
lines.push(...posixSection("PI_INSTALL_STDERR", 'cat "$PACK_DIR/pi-install.stderr.txt" 2>/dev/null || true'));
|
|
476
476
|
lines.push("");
|
|
477
477
|
lines.push('echo "=== pi list ==="');
|
|
478
|
-
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');
|
|
478
|
+
lines.push('if [ -n "$PI_CLI" ]; then (cd "$PI_PROJECT" && PI_OFFLINE=1 "$PI_CLI" list --approve >"$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');
|
|
479
479
|
lines.push('echo "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"');
|
|
480
480
|
lines.push(...posixSection("PI_LIST_STDOUT", 'cat "$PACK_DIR/pi-list.stdout.txt" 2>/dev/null || true'));
|
|
481
481
|
lines.push(...posixSection("PI_LIST_STDERR", 'cat "$PACK_DIR/pi-list.stderr.txt" 2>/dev/null || true'));
|
|
@@ -3,7 +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
|
+
|
|
8
|
+
import { platformSmokeSuiteEvidence, prunePlatformSmokeArtifacts, redactSecrets, writeLatestPlatformSmokeIndex } from "./platform-smoke/artifacts.mjs";
|
|
7
9
|
|
|
8
10
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -102,6 +104,53 @@ function validateSelections(targets, suites) {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
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
|
+
|
|
105
154
|
// ── commands ───────────────────────────────────────────────────────────────
|
|
106
155
|
async function runDoctor() {
|
|
107
156
|
try {
|
|
@@ -123,8 +172,9 @@ async function runSuite(targetName, suiteName) {
|
|
|
123
172
|
const result = await runTargetSuite(config, targetName, suiteName);
|
|
124
173
|
return result;
|
|
125
174
|
} catch (err) {
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
const message = redactSecrets(err.message);
|
|
176
|
+
console.error(`suite ${suiteName} on ${targetName} exception:`, message);
|
|
177
|
+
return { ok: false, error: message };
|
|
128
178
|
}
|
|
129
179
|
}
|
|
130
180
|
|
|
@@ -133,8 +183,9 @@ async function runTarget(targetName, suites) {
|
|
|
133
183
|
const { runTargetSuites } = await import("./platform-smoke/targets.mjs");
|
|
134
184
|
return await runTargetSuites(config, targetName, suites);
|
|
135
185
|
} catch (err) {
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
const message = redactSecrets(err.message);
|
|
187
|
+
console.error(`target ${targetName} exception:`, message);
|
|
188
|
+
return { ok: false, error: message };
|
|
138
189
|
}
|
|
139
190
|
}
|
|
140
191
|
|
|
@@ -172,6 +223,12 @@ async function main() {
|
|
|
172
223
|
process.exit(2);
|
|
173
224
|
}
|
|
174
225
|
|
|
226
|
+
const pruneResult = prunePlatformSmokeArtifacts(config.artifactRoot, config.artifactRetention);
|
|
227
|
+
if (pruneResult.removed.length > 0) {
|
|
228
|
+
console.log(`Pruned ${pruneResult.removed.length} old platform smoke artifact run(s) from ${pruneResult.root}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const startedAt = new Date().toISOString();
|
|
175
232
|
const targetRuns = targets.map(async (targetName) => {
|
|
176
233
|
console.log(`\n=== Target: ${targetName} ===`);
|
|
177
234
|
const result = args.suite
|
|
@@ -180,9 +237,21 @@ async function main() {
|
|
|
180
237
|
return { targetName, result };
|
|
181
238
|
});
|
|
182
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}`);
|
|
183
251
|
const anyFailed = results.some(({ result }) => !result.ok);
|
|
184
252
|
if (anyFailed) {
|
|
185
|
-
|
|
253
|
+
printFailureEvidence(results, config.artifactRoot);
|
|
254
|
+
console.log("\nOne or more suites failed.");
|
|
186
255
|
process.exit(1);
|
|
187
256
|
}
|
|
188
257
|
return;
|
|
@@ -165,7 +165,7 @@ function buildPiRpcEnv(baseEnv = process.env, nodePath = process.execPath) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function runPiRpcSmoke(sessionDir, piBin) {
|
|
168
|
-
const args = ["-e", root, "--cursor-no-fast", "--model", "cursor/composer-2-5", "--mode", "rpc", "--session-dir", sessionDir];
|
|
168
|
+
const args = ["--approve", "-e", root, "--cursor-no-fast", "--model", "cursor/composer-2-5", "--mode", "rpc", "--session-dir", sessionDir];
|
|
169
169
|
const env = buildPiRpcEnv();
|
|
170
170
|
|
|
171
171
|
const child = spawn(piBin, args, { cwd: root, env, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
@@ -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
|
+
|