Files
PaperClipAI/tests/runner-e2e/native-active-stop-flow.ts
T
DottaandPaperclip 90a4d86417 fix(runner-e2e): bind active-stop evidence before dispatch
Accept either canonical permission/tool arrival order while binding the exact native-origin and tool-start rows before Stop and through cancellation. Version the pending receipt and suite; retain strict identity, no-effects, unanswered-request, and settlement checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-30 22:14:28 -05:00

201 lines
18 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { expect, type Page } from "@playwright/test";
import { pollUntil, type RunnerApi } from "./api.js";
import { collectRunEvents } from "./run-observations.js";
import { createTaskThroughUi } from "./user-actions.js";
import { createDeniedTargetFixture, exists, observeRunProcesses } from "./copilot-local-fixtures.js";
import { assertCopilotRemoteRetirement, copilotRemoteDeniedSample, prepareCopilotRemoteAction, type CopilotRemoteBootstrap, type CopilotRemoteFixture, type CopilotRemoteSnapshot } from "./copilot-protection-evidence.js";
import { cursorDeniedCommand } from "./cursor-native-evidence.js";
import { assertActiveStopRetirement, readActiveStopCaller, observeActiveStopPending, readActiveStopSettlement, type ActiveStopRemoteObservation, type ActiveStopCaller, type ActiveStopPending, type ActiveStopScope } from "./native-active-stop-evidence.js";
import type { BootstrapReadProof } from "./native-bootstrap-read-proof.js";
import type { LiveFixtureValues } from "./live-fixtures.js";
import type { MatrixExecution } from "./types.js";
type Row = Record<string, any>;
type Check = { id: string; passed: boolean; detail: string };
export interface ActiveStopState { run: Row; issue: Row; events: readonly unknown[] }
/** Retain a real pending API observation before the only mutating request.
* Never answer first, wait for natural completion, or infer order from clocks. */
export async function stopAtPendingPermission(input: {
scope: ActiveStopScope; caller: ActiveStopCaller; bootstrap?: BootstrapReadProof; deadlineAt: number;
load(): Promise<ActiveStopState>;
retain(receipt: ActiveStopPending): Promise<void>;
stop(runId: string, cancellationRequestId: string): Promise<Row>;
}) {
const cancellationRequestId = randomUUID();
const state = await input.load();
const pending = observeActiveStopPending({ ...state, scope: input.scope, caller: input.caller, cancellationRequestId, bootstrap: input.bootstrap });
await input.retain(pending);
const fresh = observeActiveStopPending({ ...await input.load(), scope: input.scope, caller: input.caller, cancellationRequestId, bootstrap: input.bootstrap });
if (fresh.requestRowSha256 !== pending.requestRowSha256 || fresh.permissionRowSha256 !== pending.permissionRowSha256
|| fresh.toolOriginRowSha256 !== pending.toolOriginRowSha256 || fresh.toolStartedRowSha256 !== pending.toolStartedRowSha256
|| Date.now() >= input.deadlineAt) throw new Error("Native active Stop pending boundary changed or expired");
const dispatchMonotonicNs = process.hrtime.bigint().toString();
const stopped = await input.stop(input.scope.runId, cancellationRequestId);
if (stopped.id !== input.scope.runId || stopped.resultJson?.nativeCancellation?.intentId !== `native-cancellation:${cancellationRequestId}`) throw new Error("Native active Stop response has a foreign intent");
const label = "cancelled unanswered native permission";
const final = await pollUntil({ label, deadlineAt: input.deadlineAt, intervalMs: 200, load: async () => {
const value = await input.load();
if (["succeeded", "failed", "timed_out"].includes(value.run.status)) throw new Error(`Stopped waiting for ${label}: unexpected run terminal`);
return value;
}, accept: value => { readActiveStopSettlement({ ...value, pending, dispatchMonotonicNs, bootstrap: input.bootstrap }); return true; } });
return { pending, dispatchMonotonicNs, settlement: readActiveStopSettlement({ ...final, pending, dispatchMonotonicNs, bootstrap: input.bootstrap }) };
}
export async function runNativeActiveStopFlow(input: {
page: Page; api: RunnerApi; fixtures: LiveFixtureValues; execution: MatrixExecution; nonce: string; workspacePath: string; deadlineAt: number;
remoteBootstrap?: CopilotRemoteBootstrap;
registerCleanupAssertion(callback: () => Promise<Check[]>): void;
registerBeforeEnvironmentTeardownAssertion(callback: () => Promise<Check[]>): void;
observe(issue: Row, runs: Row[]): void;
capture(id: string, label: string, file: string): Promise<void>;
evidence(name: string, value: unknown): Promise<void>;
}) {
const { api, page, fixtures, execution, nonce } = input, provider = execution.profile.qualificationCandidate;
if ((provider !== "cursor" && provider !== "copilot") || execution.task.id !== "pending-permission-stop") throw new Error("Unknown native active Stop case");
const remote = execution.environment.id === "daytona";
if ((!remote && execution.environment.id !== "local") || (remote && !input.remoteBootstrap)) throw new Error("Active Stop requires an isolated admitted environment");
const checks: Check[] = []; let issue: Row = {}, runs: Row[] = [], events: Row[] = [];
const check = (id: string, passed: boolean, detail: string) => { checks.push({ id, passed, detail }); expect(passed, detail).toBe(true); };
const name = `active-stop-${nonce}.txt`, local = remote ? undefined : await createDeniedTargetFixture(input.workspacePath, name);
const target = local?.targetRelativePath ?? name;
let command = provider === "cursor" && local ? cursorDeniedCommand(local.targetPath) : undefined;
const observer = remote ? undefined : observeRunProcesses();
let processes: { captured: boolean; live: number[] } = { captured: false, live: [] };
let processIdentity: string | undefined, processError = false;
const remoteObservations: ActiveStopRemoteObservation[] = [];
let retirement: ReturnType<typeof assertActiveStopRetirement> | undefined;
const samples: Array<{ phase: string; absent: boolean }> = [];
let fixture: CopilotRemoteFixture | undefined, baseline: CopilotRemoteSnapshot | undefined, sealed: CopilotRemoteSnapshot | undefined;
let completed: Awaited<ReturnType<typeof stopAtPendingPermission>> | undefined;
const observeProcesses = () => {
const run = runs[0], authority = run?.processPid ? { pid: run.processPid, groupId: run.processGroupId, startedAt: run.processStartedAt, runId: run.id } : undefined;
if (authority) { const key = JSON.stringify(authority); if (processIdentity && processIdentity !== key) processError = true; processIdentity ??= key; }
if (observer) processes = observer.sample(authority);
};
const load = async (): Promise<ActiveStopState> => {
if (issue.id) issue = await api.get<Row>(`/api/issues/${issue.id}`);
const list = await api.get<Row[]>(`/api/companies/${fixtures.company.id}/heartbeat-runs?limit=100`);
runs = await Promise.all(list.map(r => api.get<Row>(`/api/heartbeat-runs/${r.id}`)));
if (runs.length > 1) throw new Error("Native active Stop dispatched an extra run");
events = runs[0] ? await collectRunEvents<Row>((afterSeq, limit) => api.get(`/api/heartbeat-runs/${runs[0]!.id}/events?afterSeq=${afterSeq}&limit=${limit}`)) : [];
observeProcesses(); input.observe(issue, runs); return { run: runs[0] ?? {}, issue, events };
};
const sample = async (phase: string) => {
let value: boolean;
if (remote) {
if (!fixture || !baseline) throw new Error("Missing remote active Stop observer");
if (phase !== "pending") throw new Error("Remote filesystem phase requires a fresh live snapshot");
const snap = await fixture.snapshot(phase);
remoteObservations.push({ phase, source: "live-snapshot", snapshot: snap });
value = !copilotRemoteDeniedSample(snap, baseline, target, "pending").exists;
await input.evidence(`active-stop-${phase}-remote.json`, snap);
} else value = !await exists(local!.targetPath);
samples.push({ phase, absent: value }); check(`no-effect-${phase}`, value, "Exact target remains absent at the causal observation boundary");
};
const scope = (): ActiveStopScope => ({ provider, companyId: fixtures.company.id, issueId: issue.id, runId: runs[0]!.id, target, ...(command ? { commandSha256: command.commandSha256 } : {}) });
const bootstrap = (): BootstrapReadProof | undefined => fixture ? { actionFile: fixture.actionFile, events } : undefined;
const cleanup = async () => {
const receipt: Check[] = [];
try {
await load();
if (remote) {
if (!fixture || !baseline) throw new Error("Remote observer was never armed");
sealed ??= await fixture.finish(); assertCopilotRemoteRetirement(sealed, baseline); processes = sealed.processes;
} else if (processes.captured && processes.live.length) {
await pollUntil({ label: "active Stop provider retirement", deadlineAt: Date.now() + 5000, intervalMs: 100, load: async () => { observeProcesses(); return processes; }, accept: p => p.live.length === 0 });
}
if (!remote) await sample("after-cleanup");
const watch = remote ? sealed!.watcher : local!.watcher.finish();
retirement = assertActiveStopRetirement({ completed: Boolean(completed), identityChanged: processError, processes, watcher: watch,
...(remote ? { environment: "daytona" as const, remote: { scope: scope(), observations: remoteObservations } } : { environment: "local" as const, samples }) });
if (!completed) throw new Error("Missing active Stop settlement");
readActiveStopSettlement({ ...await load(), ...completed, bootstrap: bootstrap() });
receipt.push({ id: "active-stop-retired-no-effects", passed: true, detail: remote ? "Exact run root/descendants retired with continuous no-effects proof through retirement; later UI/API checks do not claim fresh filesystem observation" : "Exact run root/descendants retired; unanswered target remained absent through cleanup" });
return receipt;
} finally {
if (timer) clearInterval(timer);
try {
await input.evidence("native-active-stop-cleanup.json", { processes, processError, samples, remoteObservations, retirement, sealed, watcher: local?.watcher.finish(), checks: receipt });
} finally {
if (remote) await fixture?.close();
}
}
};
const timer = observer ? setInterval(() => { try { observeProcesses(); } catch { processError = true; } }, 100) : undefined;
(remote ? input.registerBeforeEnvironmentTeardownAssertion : input.registerCleanupAssertion)(cleanup);
const prompt = () => provider === "cursor"
? `${execution.task.buildPrompt(nonce)}\nUse this exact native shell command once:\n${command!.command}`
: `${execution.task.buildPrompt(nonce)}\nUse the native edit tool once to create exactly ${target} with content MUST_NOT_EXIST. Do not use a shell command.`;
try {
const caller = readActiveStopCaller(await api.get("/api/health"), await api.get("/api/auth/get-session"));
const agent = await api.get<Row>(`/api/agents/${fixtures.agent.id}`);
const configured = await api.patch<Row>(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxPermissionMode: "approve-reads", ...(provider === "cursor" ? { acpxSessionMode: "agent" } : {}), timeoutSec: 120, lifecycleMode: "per_turn" } });
check("explicit-per-turn-policy", configured.adapterConfig?.acpxPermissionMode === "approve-reads" && configured.adapterConfig?.lifecycleMode === "per_turn" && (provider !== "cursor" || configured.adapterConfig?.acpxSessionMode === "agent"), "Agent mode and per-turn restrictive permission policy selected before startup");
const project = await api.post<Row>(`/api/companies/${fixtures.company.id}/projects`, { name: `Native active Stop ${nonce}`, executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", sharedWorkspaceConcurrency: "serialize", allowIssueOverride: false, environmentId: fixtures.environment.id, workspaceStrategy: { type: "project_primary" } }, workspace: { name: "Primary", sourceType: "local_path", cwd: input.workspacePath, isPrimary: true } });
if (!remote) await sample("before-request");
await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name });
issue = (await pollUntil({ label: "browser-created active Stop task", deadlineAt: input.deadlineAt, load: async () => (await api.get<Row[]>(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(i => i.title === execution.task.buildTitle(nonce)), accept: Boolean }))!;
if (remote) {
await pollUntil({ label: "active Stop remote bootstrap", deadlineAt: input.deadlineAt, load, accept: state => state.run.status === "running" });
const bound = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId: runs[0]!.id, targets: [target], actionPrompt: async value => {
fixture = value; if (provider === "cursor") command = cursorDeniedCommand(join(value.remoteCwd, target));
const prepared = await prepareCopilotRemoteAction({ fixture: value, companyId: fixtures.company.id, environmentId: fixtures.environment.id, runId: runs[0]!.id, target, prompt: prompt() });
baseline = prepared.baseline;
remoteObservations.push({ phase: "before-request", source: "live-snapshot", snapshot: baseline });
await input.evidence("active-stop-before-request-remote.json", baseline); return prepared.prompt;
} });
if (bound !== fixture) throw new Error("Remote active Stop publication identity changed");
}
const label = "unanswered native permission";
await pollUntil({ label, deadlineAt: input.deadlineAt, intervalMs: 200, load: async () => {
const state = await load(); if (["failed", "timed_out", "cancelled", "succeeded"].includes(state.run.status)) throw new Error(`Stopped waiting for ${label}: provider ended without pending callback`); return state;
}, accept: state => { observeActiveStopPending({ ...state, scope: scope(), caller, cancellationRequestId: randomUUID(), bootstrap: bootstrap() }); return true; } });
await page.goto(`/${fixtures.company.issuePrefix}/issues/${issue.identifier ?? issue.id}`);
const card = page.getByTestId("task-chat-runtime-request").filter({ visible: true });
await expect(card).toHaveCount(1); await expect(card.getByRole("button", { name: "Deny", exact: true })).toBeEnabled();
await input.capture("pending-permission", "Native permission left unanswered before Stop", "pending-permission.png"); await sample("pending");
completed = await stopAtPendingPermission({ scope: scope(), caller, bootstrap: bootstrap(), deadlineAt: input.deadlineAt, load,
retain: proof => input.evidence("native-active-stop-pending.json", proof), stop: (runId, cancellationRequestId) => api.post(`/api/heartbeat-runs/${runId}/cancel`, { cancellationRequestId }) });
check("pending-callback-cancelled", true, "Unanswered native permission closed through a cancelled provider turn and the exact caller-owned Stop acknowledgement");
await input.evidence("native-active-stop-settlement.json", completed);
// Only after confirmed cancellation: test the real public stale-response fence.
const response = await api.request.post(`/api/heartbeat-runs/${runs[0]!.id}/runtime-requests/${encodeURIComponent(completed.pending.requestId)}/resolve`, { data: { turnId: completed.pending.turnId, requestKind: "permission_approval", resolution: { action: "accept" } } });
check("stale-answer-refused", response.status() === 409, "Stopped permission rejects a later answer rather than replaying work");
await load(); readActiveStopSettlement({ ...await load(), ...completed, bootstrap: bootstrap() });
if (remote) {
// finish drains the observer's automatic retirement seal. It does not
// extend the remote watch through subsequent host UI/cleanup assertions.
sealed = await fixture!.finish(); assertCopilotRemoteRetirement(sealed, baseline!); processes = sealed.processes;
remoteObservations.push({ phase: "owned-process-retirement", source: "retirement-seal", snapshot: sealed });
await input.evidence("active-stop-owned-process-retirement-remote.json", sealed);
} else await sample("after-stop");
await page.reload();
const finalUiTimeout = () => {
const remaining = input.deadlineAt - Date.now();
if (remaining <= 0) throw new Error("Active Stop final UI deadline elapsed");
return Math.min(30_000, remaining);
};
// Absence of a permission button during React loading is not proof that a
// stopped request is unanswerable. First observe the actual task/run UI.
await expect(page.getByTestId("issue-detail-header").getByRole("button", {
name: "Change status (current: In Progress)", exact: true,
})).toBeVisible({ timeout: finalUiTimeout() });
await expect(page.getByTestId("task-chat-thread").getByTestId("task-chat-collapsible-marker")
.filter({ has: page.getByText("Run cancelled", { exact: true }) }))
.toBeVisible({ timeout: finalUiTimeout() });
await expect(page.getByTestId("task-chat-history-loading"))
.toHaveCount(0, { timeout: finalUiTimeout() });
await expect(card.getByRole("button", { name: "Deny", exact: true }))
.toHaveCount(0, { timeout: finalUiTimeout() });
await load(); readActiveStopSettlement({ ...await load(), ...completed, bootstrap: bootstrap() });
check("one-unfinished-cancelled-run", runs.length === 1 && issue.status === "in_progress" && runs[0]!.status === "cancelled", "No automatic follow-up run or false task completion");
await input.capture("final-state", "Stopped native permission is no longer answerable", "final-state.png");
await input.evidence("api-state.json", { issue, run: runs[0], runs, checks, samples, remoteObservations, runEvents: events, runEventsByRun: [{ runId: runs[0]!.id, events }], activeStop: completed });
return { issue, runs, checks };
} finally {
// Local observation continues through cleanup. Remote cleanup revalidates
// the lifetime seal and fresh API state without claiming later file reads.
await input.evidence("native-active-stop-checks.json", { issue, runs, checks, samples, remoteObservations, completed });
}
}