From f67dcc56fd3d5a2d99323aa7049f75b074a41dd4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 3 Oct 2026 12:27:48 -0500 Subject: [PATCH] test(cursor): verify public install and provider-loss settlement Co-Authored-By: Paperclip --- scripts/grok-public-install-sandbox.mjs | 5 +++-- scripts/verify-cursor-npm-install.mjs | 11 +++++++---- tests/runner-e2e/catalog.ts | 2 +- tests/runner-e2e/harness-env.ts | 3 ++- tests/runner-e2e/native-provider-loss-flow.ts | 4 ++-- tests/runner-e2e/native-provider-loss.test.ts | 11 ++++++++++- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/scripts/grok-public-install-sandbox.mjs b/scripts/grok-public-install-sandbox.mjs index c86eaf9f51f..07dc9207841 100644 --- a/scripts/grok-public-install-sandbox.mjs +++ b/scripts/grok-public-install-sandbox.mjs @@ -7,17 +7,18 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [ 'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts', ]; -export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite }) { +export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMb = 256 }) { if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) { throw new Error('Public-install verification requires an unprivileged host user'); } + if (!Number.isSafeInteger(temporarySizeMb) || temporarySizeMb < 256 || temporarySizeMb > 2048) throw new Error('Invalid bounded public-install temporary size'); return [ 'run', '--rm', '--platform', 'linux/amd64', '--user', `${uid}:${gid}`, '--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', '--pids-limit', '256', '--memory', '3g', '--network', download ? 'bridge' : 'none', - '--tmpfs', '/tmp:rw,nosuid,nodev,size=256m,mode=1777', + '--tmpfs', `/tmp:rw,nosuid,nodev,size=${temporarySizeMb}m,mode=1777`, '--env', 'HOME=/tmp', '--env', 'npm_config_cache=/cache', '--env', 'npm_config_nodedir=/usr/local', '--env', 'npm_config_audit=false', '--env', 'npm_config_fund=false', diff --git a/scripts/verify-cursor-npm-install.mjs b/scripts/verify-cursor-npm-install.mjs index f14c0289391..b5456607a50 100644 --- a/scripts/verify-cursor-npm-install.mjs +++ b/scripts/verify-cursor-npm-install.mjs @@ -56,7 +56,8 @@ const sentinelSource = join(root,'lifecycle-sentinel'); mkdirSync(sentinelSource writeFileSync(join(sentinelSource,'package.json'), JSON.stringify({name:'paperclip-verification-lifecycle-sentinel',version:'1.0.0',private:true,scripts:{postinstall:'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'ok\')"'}})); run('npm',['pack','--ignore-scripts','--pack-destination',assets],sentinelSource); for (const file of readdirSync(assets)) chmodSync(join(assets,file),0o644); -const isolated = (command, options={}) => run('docker',grokConsumerDockerArgs({assets,consumer,cache,uid:process.getuid(),gid:process.getgid(),command,...options})); +// Cursor's verified execution snapshot exceeds the smaller Grok fixture tmpfs. +const isolated = (command, options={}) => run('docker',grokConsumerDockerArgs({assets,consumer,cache,uid:process.getuid(),gid:process.getgid(),temporarySizeMb:1024,command,...options})); isolated(['npm','install','--ignore-scripts','--omit=dev',...readdirSync(assets).filter(f=>f.endsWith('.tgz')).map(f=>`/packages/${f}`)],{download:true}); const sentinel = join(consumer,'node_modules/paperclip-verification-lifecycle-sentinel/lifecycle-ran'); assert.equal(existsSync(sentinel),false); @@ -71,11 +72,13 @@ const setupOutput = isolated(['node','node_modules/paperclipai/dist/index.js','r console.log(setupOutput.trim()); assert.match(setupOutput,/Verified Cursor 2026\.09\.26-dd393fe \(linux-x64\), paperclip-cursor-usage-v4/); const probe = `import assert from 'node:assert/strict'; -import { verifyQualifiedAcpxInstallation } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/installation-integrity.js'; +import { verifyAcpxProfileInstallation } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/profile-installation.js'; import { resolveQualifiedAcpxProfile } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js'; const profile = resolveQualifiedAcpxProfile('cursor', 'gpt-5.6-luna[context=272k,reasoning=medium,fast=false]'); -const installation = await verifyQualifiedAcpxInstallation(profile, () => { throw new Error('Cursor must not resolve an npm package'); }); -assert.equal(installation.agentServerPackageJsonPath,null); assert.equal(installation.agentRuntimePackageJsonPath,null); +const installation = await verifyAcpxProfileInstallation(profile); +assert.equal(installation.agentServerPackageJsonPath,'/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/provider-assets/cursor/linux-x64/.paperclip-cursor-closure.json'); +assert.equal(installation.agentRuntimePackageJsonPath,null); +assert.equal(installation.commandDigest,profile.commandDigest); await (await installation.openCommand()).close(); console.log('Installed Cursor closure and command lease verified without credentials or candidate overrides');`; writeFileSync(join(assets,'probe.mjs'),probe,{mode:0o644}); diff --git a/tests/runner-e2e/catalog.ts b/tests/runner-e2e/catalog.ts index 16c98efd89a..31975040020 100644 --- a/tests/runner-e2e/catalog.ts +++ b/tests/runner-e2e/catalog.ts @@ -1112,7 +1112,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [ tasks: [{ id: "pending-permission-provider-loss", label: "Owned runtime loss with pending permission", groups: [], workMode: "standard", flow: "native_provider_loss", expectedRunCount: 1, attemptTimeoutMs: { local: 300_000, daytona: 300_000 }, turnTimeoutMs: 120_000, - expectedTerminalState: { issue: "in_progress", run: "failed" }, + expectedTerminalState: { issue: "blocked", run: "failed" }, buildTitle: nonce => `Native provider loss ${nonce}`, buildVisibleMarker: nonce => `NATIVE-PROVIDER-LOSS-${nonce}`, buildPrompt: () => "Attempt the supplied exact native shell operation once and leave its permission callback unanswered. Do not retry, use another tool or path, answer your own permission, call paperclip_finish, or claim completion. The qualification fixture will deliberately lose the isolated runtime before any approval.", diff --git a/tests/runner-e2e/harness-env.ts b/tests/runner-e2e/harness-env.ts index 94d3873d69b..4e18e3154f1 100644 --- a/tests/runner-e2e/harness-env.ts +++ b/tests/runner-e2e/harness-env.ts @@ -118,7 +118,8 @@ export function buildRunnerE2EProcessEnvironment( const admittedSuite = execution.suite.id === "extended-harnesses" || execution.suite.id === "rich-acp-warm-continuity" || (execution.suite.id === "cursor-native" && agent === "cursor") - || (execution.suite.id === "native-active-stop" && agent === "cursor"); + || (execution.suite.id === "native-active-stop" && agent === "cursor") + || (execution.suite.id === "native-provider-loss" && agent === "cursor"); if (!admittedSuite || !execution.suite.manualOnly) { throw new Error("Candidate qualification requires an explicit provider qualification suite"); } diff --git a/tests/runner-e2e/native-provider-loss-flow.ts b/tests/runner-e2e/native-provider-loss-flow.ts index 0527ed3194e..1652179b2fe 100644 --- a/tests/runner-e2e/native-provider-loss-flow.ts +++ b/tests/runner-e2e/native-provider-loss-flow.ts @@ -20,7 +20,7 @@ export function assertProviderLossSettlement(run: Row, issue: Row, events: reado const fail = (ok: boolean, reason: string) => { if (!ok) throw new Error(`Native provider loss: ${reason}`); }; fail(run.id === pending.scope.runId && run.companyId === pending.scope.companyId && run.nativeIssueId === pending.scope.issueId && run.status === "failed" && run.runtimeMode === "native" && typeof run.error === "string" && run.error.length > 0 - && issue.id === pending.scope.issueId && issue.companyId === pending.scope.companyId && issue.status === "in_progress" + && issue.id === pending.scope.issueId && issue.companyId === pending.scope.companyId && issue.status === "blocked" && run.resultJson?.nativeCancellation == null, "failure must leave the task open without claiming Stop or success"); const frames = events.flatMap(row => row.payload?.prpEvent ? [row.payload.prpEvent] : []); const closures = frames.filter(frame => ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(frame.eventType) @@ -145,7 +145,7 @@ export async function runNativeProviderLossFlow(input: { await expect(page.getByTestId("task-chat-history-loading")).toHaveCount(0); await expect(card.getByRole("button", { name: "Deny", exact: true })).toHaveCount(0); await load(); assertProviderLossSettlement(runs[0]!, issue, events, pending!); - check("one-unfinished-failed-run", runs.length === 1 && issue.status === "in_progress", "No automatic mutation replay or false task completion"); + check("one-unfinished-failed-run", runs.length === 1 && issue.status === "blocked", "A visible blocked task owns the failure; no automatic mutation replay or false completion"); await input.capture("final-state", "Lost runtime fails clearly and leaves the task open", "final-state.png"); await input.evidence("api-state.json", { issue, runs, checks, settlement, runEvents: events, runEventsByRun: [{ runId: runs[0]!.id, events }] }); return { issue, runs, checks }; diff --git a/tests/runner-e2e/native-provider-loss.test.ts b/tests/runner-e2e/native-provider-loss.test.ts index a8768467724..f90660c6f27 100644 --- a/tests/runner-e2e/native-provider-loss.test.ts +++ b/tests/runner-e2e/native-provider-loss.test.ts @@ -2,11 +2,20 @@ import { expect, it } from "vitest"; import { assertProviderLossSettlement } from "./native-provider-loss-flow.js"; import { bootstrapReadExecutionId } from "./native-bootstrap-read-proof.js"; import type { ActiveStopPending } from "./native-active-stop-evidence.js"; +import { buildRunnerE2EProcessEnvironment } from "./harness-env.js"; +import { runnerMatrix } from "./catalog.js"; + +it("admits only explicit Cursor loss cells through the qualification harness", () => { + const execution = runnerMatrix.find(row => row.suite.id === "native-provider-loss" && row.environment.id === "local")!; + expect(JSON.parse(buildRunnerE2EProcessEnvironment({}, [execution]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION!)) + .toEqual([{ agent: "cursor", model: execution.profile.model }]); + expect(() => buildRunnerE2EProcessEnvironment({}, [{ ...execution, profile: { ...execution.profile, qualificationCandidate: "copilot" } }])).toThrow("explicit provider qualification suite"); +}); function fixture() { const pending = { scope: { runId: "run", companyId: "company", issueId: "issue" }, requestId: "permission", turnId: "turn", toolCallId: "mutation" } as ActiveStopPending; const run = { id: "run", companyId: "company", nativeIssueId: "issue", runtimeMode: "native", status: "failed", error: "provider_transport_failed", resultJson: {} }; - const issue = { id: "issue", companyId: "company", status: "in_progress" }; + const issue = { id: "issue", companyId: "company", status: "blocked" }; const events = [{ payload: { prpEvent: { eventType: "runtime_request.expired", payload: { requestId: "permission", turnId: "turn", requestKind: "permission_approval" } } } }]; return { pending, run, issue, events }; }