mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
test(runner-e2e): bound remote Stop proof to process lifetime
Distinguish live snapshots from the automatic owned-process retirement seal. Preserve local four-phase observation and separately revalidate remote UI/API cancellation state without claiming later filesystem reads. Version the suite and calibrate stale/replayed/foreign/lost-descendant evidence. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2467e24614
commit
a31fc90b17
6 files changed
+147
-25
No files matched your search
@@ -415,9 +415,18 @@ interrupted or normal terminal evidence fails. Only after this proof does the
|
||||
fixture attempt a stale public answer, requiring HTTP409 and an unanswerable
|
||||
browser card. It retains one cancelled run, issue `in_progress`, exact target
|
||||
absence through continuous observation, and all observed owned descendants
|
||||
retired. Remote proof is sealed before environment teardown; unproven cleanup
|
||||
fails independently. Only fully attested bootstrap reads may precede the one
|
||||
tested operation; alternate operations or attempts are rejected.
|
||||
retired. Local files require four fresh observations through cleanup. Daytona
|
||||
instead retains two live snapshots (baseline and pending) plus one automatic
|
||||
owned-process-retirement seal, with a continuous zero-mutation watcher and the
|
||||
same complete root/descendant journal. Suite version 2 retains this as
|
||||
`paperclip.e2e.native-active-stop-remote-retirement.v1`; it explicitly records
|
||||
`filesystemAfterRetirementObserved:false`. The per-turn observer seals itself
|
||||
when the owned tree retires, before the sandbox is released. Reading that receipt
|
||||
later is not a fresh post-UI or post-cleanup filesystem observation. Relabeled,
|
||||
reused, missing or out-of-order samples fail. The stale-answer and rendered UI
|
||||
checks remain separate, followed by fresh API cancellation/no-extra-run checks
|
||||
both after UI and in cleanup. Unproven cleanup fails independently. Only fully
|
||||
attested bootstrap reads may precede the one tested operation; alternate operations or attempts are rejected.
|
||||
|
||||
The existing `copilot-protection` denial remains distinct: rejecting a permission
|
||||
before Stop does not exercise this pending-callback boundary. This new suite has
|
||||
|
||||
@@ -1447,7 +1447,12 @@ not qualify this case. Native permission cards use runtime requests; ordinary
|
||||
issue-interaction rows are not substituted for their authority. Pending and final
|
||||
screenshots, `native-active-stop-pending.json`, `native-active-stop-settlement.json`
|
||||
and cleanup evidence retain the boundaries. Daytona also requires the exact
|
||||
owned lease and sealed remote observer proof before sandbox deletion.
|
||||
owned lease and sealed remote observer proof before sandbox deletion. Suite
|
||||
version 2 records remote filesystem coverage only through verified retirement
|
||||
of the owned runner/provider tree, with no target or workspace mutations; it
|
||||
does not call a later read of the seal a fresh observation. Local cases retain
|
||||
four filesystem phases through cleanup. Remote UI/stale-answer checks and final
|
||||
API checks are separate from this lifetime-bound filesystem proof.
|
||||
|
||||
This suite adds an active-work cancellation oracle; it does not reinterpret
|
||||
older denial or cancellation results. Current live qualification is pending.
|
||||
|
||||
@@ -1052,7 +1052,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
|
||||
description: "Stop while one exact Cursor or Copilot native permission remains unanswered; require cancelled provider settlement, caller-owned acknowledgement, stale-answer refusal and independent retirement/no effects.",
|
||||
groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => ["cursor", "copilot"].includes(profile.qualificationCandidate ?? "")),
|
||||
environments: runnerEnvironments, tasks: nativeActiveStopTasks, expectedMatrixSize: 4,
|
||||
definitionMetadata: { version: 1, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v1", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "owned-lease-sealed-observer", providerDeath: "not-covered" },
|
||||
definitionMetadata: { version: 2, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v1", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" },
|
||||
},
|
||||
{
|
||||
id: "copilot-protection", label: "Copilot native protection", manualOnly: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { canonicalJson } from "../../packages/shared/src/portability-hash.js";
|
||||
import { hasAcpxNativeOrigin } from "./acpx-native-origin.js";
|
||||
import { assertCopilotRemoteRetirement, copilotRemoteDeniedSample, type CopilotRemoteSnapshot } from "./copilot-protection-evidence.js";
|
||||
import { readCopilotToolEvidence } from "./copilot-evidence.js";
|
||||
import { readCursorToolEvidence } from "./cursor-native-evidence.js";
|
||||
import { bootstrapReadExecutionId, withoutProvenBootstrapReads, type BootstrapReadProof } from "./native-bootstrap-read-proof.js";
|
||||
@@ -158,18 +159,59 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru
|
||||
taskStillInProgress: true, normalCompletionAccepted: false, replayAllowed: false };
|
||||
}
|
||||
|
||||
/** Read-only watcher/process receipts are mandatory through teardown, even if
|
||||
* canonical cancellation already passed. A missing phase is not no-effect proof. */
|
||||
export interface ActiveStopRemoteObservation {
|
||||
phase: "before-request" | "pending" | "owned-process-retirement";
|
||||
source: "live-snapshot" | "retirement-seal";
|
||||
snapshot: CopilotRemoteSnapshot;
|
||||
}
|
||||
|
||||
/** A per-turn Daytona observer seals itself when the owned tree retires. The
|
||||
* host may retrieve that receipt later; retrieval is not a fresh observation. */
|
||||
export function readActiveStopRemoteRetirement(input: {
|
||||
scope: Pick<ActiveStopScope, "companyId" | "runId" | "target">; observations: readonly ActiveStopRemoteObservation[];
|
||||
}) {
|
||||
const observations = input.observations;
|
||||
fail(observations.length === 3
|
||||
&& observations.map(o => `${o.phase}:${o.source}`).join(",") === "before-request:live-snapshot,pending:live-snapshot,owned-process-retirement:retirement-seal",
|
||||
"remote seal cannot stand in for a fresh causal sample");
|
||||
const [baseline, pending, terminal] = observations.map(o => o.snapshot) as [CopilotRemoteSnapshot, CopilotRemoteSnapshot, CopilotRemoteSnapshot];
|
||||
fail(baseline.binding.companyId === input.scope.companyId && baseline.binding.runId === input.scope.runId, "remote observation belongs to another run");
|
||||
for (const snapshot of [baseline, pending, terminal]) {
|
||||
fail(!copilotRemoteDeniedSample(snapshot, baseline, input.scope.target, "pending").exists, "remote target changed");
|
||||
}
|
||||
fail(BigInt(baseline.observedMonotonicNs) < BigInt(pending.observedMonotonicNs)
|
||||
&& BigInt(pending.observedMonotonicNs) < BigInt(terminal.observedMonotonicNs), "remote observation reused or out of order");
|
||||
fail(!baseline.setup.published && pending.setup.published && pending.setup.sha256 === terminal.setup.sha256
|
||||
&& baseline.processes.captured && pending.processes.captured
|
||||
&& baseline.processes.live.includes(baseline.processes.root?.pid ?? -1)
|
||||
&& pending.processes.live.includes(pending.processes.root?.pid ?? -1), "remote pending lifetime unproven");
|
||||
assertCopilotRemoteRetirement(terminal, baseline);
|
||||
assertCopilotRemoteRetirement(terminal, pending);
|
||||
for (const snapshot of [baseline, pending]) {
|
||||
fail(snapshot.processes.journal.every(p => terminal.processes.journal.some(q =>
|
||||
p.pid === q.pid && p.startTicks === q.startTicks && p.bootId === q.bootId)), "remote descendant journal lost");
|
||||
}
|
||||
return { schema: "paperclip.e2e.native-active-stop-remote-retirement.v1" as const,
|
||||
coverage: "continuous-through-owned-process-retirement" as const, filesystemAfterRetirementObserved: false as const,
|
||||
binding: terminal.binding, target: input.scope.target,
|
||||
observations: observations.map(o => ({ phase: o.phase, source: o.source,
|
||||
observedMonotonicNs: o.snapshot.observedMonotonicNs, snapshotSha256: hash(o.snapshot) })) };
|
||||
}
|
||||
|
||||
/** Local files remain accessible through teardown. Remote per-turn files do
|
||||
* not: require the complete lifetime seal, never relabel it as a later sample. */
|
||||
export function assertActiveStopRetirement(input: {
|
||||
completed: boolean; identityChanged: boolean;
|
||||
processes: { captured: boolean; live: readonly number[] };
|
||||
watcher: { complete: boolean; targetMutationCount: number };
|
||||
samples: ReadonlyArray<{ phase: string; absent: boolean }>;
|
||||
}) {
|
||||
} & ({ environment: "local"; samples: ReadonlyArray<{ phase: string; absent: boolean }> }
|
||||
| { environment: "daytona"; remote: Parameters<typeof readActiveStopRemoteRetirement>[0] })) {
|
||||
fail(input.completed && !input.identityChanged && input.processes.captured
|
||||
&& input.processes.live.length === 0 && input.watcher.complete && input.watcher.targetMutationCount === 0,
|
||||
"retirement or continuous no-effect proof is incomplete");
|
||||
fail(input.samples.length === 4 && input.samples.every(s => s.absent === true)
|
||||
if (input.environment === "daytona") return readActiveStopRemoteRetirement(input.remote);
|
||||
fail(input.environment === "local" && input.samples.length === 4 && input.samples.every(s => s.absent === true)
|
||||
&& input.samples.map(s => s.phase).join(",") === "before-request,pending,after-stop,after-cleanup",
|
||||
"causal target observation missing or changed");
|
||||
return { coverage: "local-through-cleanup" as const };
|
||||
}
|
||||
@@ -7,7 +7,7 @@ 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 ActiveStopCaller, type ActiveStopPending, type ActiveStopScope } from "./native-active-stop-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";
|
||||
@@ -60,6 +60,8 @@ export async function runNativeActiveStopFlow(input: {
|
||||
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;
|
||||
@@ -80,7 +82,9 @@ export async function runNativeActiveStopFlow(input: {
|
||||
let value: boolean;
|
||||
if (remote) {
|
||||
if (!fixture || !baseline) throw new Error("Missing remote active Stop observer");
|
||||
const snap = sealed ?? await fixture.snapshot(phase);
|
||||
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);
|
||||
@@ -98,17 +102,18 @@ export async function runNativeActiveStopFlow(input: {
|
||||
} 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 });
|
||||
}
|
||||
await sample("after-cleanup");
|
||||
if (!remote) await sample("after-cleanup");
|
||||
const watch = remote ? sealed!.watcher : local!.watcher.finish();
|
||||
assertActiveStopRetirement({ completed: Boolean(completed), identityChanged: processError, processes, watcher: watch, samples });
|
||||
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: "Exact run root/descendants retired; unanswered target remained absent through continuous observation" });
|
||||
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, sealed, watcher: local?.watcher.finish(), checks: receipt });
|
||||
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();
|
||||
}
|
||||
@@ -133,7 +138,8 @@ export async function runNativeActiveStopFlow(input: {
|
||||
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; samples.push({ phase: "before-request", absent: true });
|
||||
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");
|
||||
@@ -154,8 +160,13 @@ export async function runNativeActiveStopFlow(input: {
|
||||
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) { sealed = await fixture!.finish(); assertCopilotRemoteRetirement(sealed, baseline!); processes = sealed.processes; }
|
||||
await sample("after-stop");
|
||||
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();
|
||||
@@ -174,12 +185,14 @@ export async function runNativeActiveStopFlow(input: {
|
||||
.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, runEvents: events, runEventsByRun: [{ runId: runs[0]!.id, events }], activeStop: completed });
|
||||
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 {
|
||||
// Keep watcher/process observation live through the registered cleanup assertion.
|
||||
await input.evidence("native-active-stop-checks.json", { issue, runs, checks, samples, completed });
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { assertActiveStopRetirement, readActiveStopCaller, observeActiveStopPending, readActiveStopSettlement, type ActiveStopProvider } from "./native-active-stop-evidence.js";
|
||||
import { assertActiveStopRetirement, readActiveStopRemoteRetirement, type ActiveStopRemoteObservation, readActiveStopCaller, observeActiveStopPending, readActiveStopSettlement, type ActiveStopProvider } from "./native-active-stop-evidence.js";
|
||||
import { stopAtPendingPermission } from "./native-active-stop-flow.js";
|
||||
import { runnerMatrix, runnerSuites, suiteDefinitionHash } from "./catalog.js";
|
||||
import { selectRunnerExecutions, parseRunnerSelectors } from "./selectors.js";
|
||||
@@ -166,7 +166,7 @@ describe("explicit active Stop discovery", () => {
|
||||
expect(cells).toHaveLength(4); expect(suite.manualOnly).toBe(true);
|
||||
expect(cells.map(e => `${e.profile.qualificationCandidate}/${e.environment.id}`).sort()).toEqual(["copilot/daytona", "copilot/local", "cursor/daytona", "cursor/local"]);
|
||||
expect(cells.every(e => e.task.expectedRunCount === 1 && e.task.flow === "native_active_stop" && e.task.expectedTerminalState?.run === "cancelled")).toBe(true);
|
||||
expect(suite.definitionMetadata).toMatchObject({ version: 1, normalCompletionAccepted: false, providerDeath: "not-covered" });
|
||||
expect(suite.definitionMetadata).toMatchObject({ version: 2, normalCompletionAccepted: false, providerDeath: "not-covered" });
|
||||
expect(suiteDefinitionHash(suite)).not.toBe(suiteDefinitionHash({ ...suite, definitionMetadata: { ...suite.definitionMetadata, normalCompletionAccepted: true } }));
|
||||
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(e => e.suite.id === suite.id)).toBe(false);
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe("explicit active Stop discovery", () => {
|
||||
|
||||
|
||||
describe("active Stop cleanup remains mandatory", () => {
|
||||
const proof = () => ({ completed: true, identityChanged: false, processes: { captured: true, live: [] as number[] },
|
||||
const proof = () => ({ environment: "local" as const, completed: true, identityChanged: false, processes: { captured: true, live: [] as number[] },
|
||||
watcher: { complete: true, targetMutationCount: 0 }, samples: ["before-request", "pending", "after-stop", "after-cleanup"].map(phase => ({ phase, absent: true })) });
|
||||
it("requires all causal samples plus the continuous watcher and retired owned identities", () => expect(() => assertActiveStopRetirement(proof())).not.toThrow());
|
||||
it.each([
|
||||
@@ -188,3 +188,56 @@ describe("active Stop cleanup remains mandatory", () => {
|
||||
["missing settlement", (p: ReturnType<typeof proof>) => { p.completed = false; }],
|
||||
] as const)("rejects %s even after an apparent cancelled run", (_label, mutate) => { const p = proof(); mutate(p); expect(() => assertActiveStopRetirement(p)).toThrow(); });
|
||||
});
|
||||
|
||||
function remoteRetirementProof() {
|
||||
const root = { pid: 50, ppid: 1, startTicks: "200", bootId: "12345678-1234-1234-1234-123456789abc" };
|
||||
const child = { ...root, pid: 51, ppid: 50, startTicks: "201" };
|
||||
const snapshot = (sequence: number, live: number[]) => ({
|
||||
binding: { companyId: "company", environmentId: "env", runId: "run", leaseId: "lease", sandboxId: "sandbox", image: `image@sha256:${"a".repeat(64)}`, remoteCwd: "/home/daytona/workspace" },
|
||||
observedAtMs: sequence, receivedAtMs: sequence + 1, observedMonotonicNs: String(sequence), complete: true, workspace: {},
|
||||
targets: { "target.txt": { absent: true, sha256: null, complete: true, mutationCount: 0, parent: { dev: "1", ino: "2" } } },
|
||||
watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 },
|
||||
processes: { captured: true, root, journal: [root, child], live },
|
||||
setup: { path: ".action.txt", sha256: sequence === 1 ? null : `sha256:${"b".repeat(64)}`, published: sequence !== 1 }, attached: null,
|
||||
});
|
||||
const observations: ActiveStopRemoteObservation[] = [
|
||||
{ phase: "before-request", source: "live-snapshot", snapshot: snapshot(1, [50, 51]) },
|
||||
{ phase: "pending", source: "live-snapshot", snapshot: snapshot(2, [50, 51]) },
|
||||
{ phase: "owned-process-retirement", source: "retirement-seal", snapshot: snapshot(3, []) },
|
||||
];
|
||||
return { scope: { companyId: "company", runId: "run", target: "target.txt" }, observations };
|
||||
}
|
||||
describe("Daytona active Stop observation lifetime", () => {
|
||||
it("binds one continuous owned-tree retirement seal without claiming a later filesystem observation", () => {
|
||||
const remote = remoteRetirementProof(), terminal = remote.observations[2]!.snapshot;
|
||||
expect(assertActiveStopRetirement({ environment: "daytona", completed: true, identityChanged: false,
|
||||
processes: terminal.processes, watcher: terminal.watcher, remote })).toMatchObject({
|
||||
schema: "paperclip.e2e.native-active-stop-remote-retirement.v1", coverage: "continuous-through-owned-process-retirement",
|
||||
filesystemAfterRetirementObserved: false, observations: [
|
||||
{ phase: "before-request", source: "live-snapshot" }, { phase: "pending", source: "live-snapshot" },
|
||||
{ phase: "owned-process-retirement", source: "retirement-seal" },
|
||||
],
|
||||
});
|
||||
});
|
||||
it.each([
|
||||
["seal relabeled fresh", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.source = "live-snapshot"; }],
|
||||
["seal replayed as pending", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[1]!.snapshot = p.observations[2]!.snapshot; }],
|
||||
["after-UI seal replay", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations.push({ ...p.observations[2]!, phase: "after-cleanup" as any }); }],
|
||||
["missing pending", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations.splice(1, 1); }],
|
||||
["all foreign run snapshots", (p: ReturnType<typeof remoteRetirementProof>) => { for (const o of p.observations) o.snapshot.binding.runId = "other"; }],
|
||||
["all foreign company snapshots", (p: ReturnType<typeof remoteRetirementProof>) => { for (const o of p.observations) o.snapshot.binding.companyId = "other"; }],
|
||||
["foreign lease", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.binding.leaseId = "other"; }],
|
||||
["foreign run", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[1]!.snapshot.binding.runId = "other"; }],
|
||||
["replacement root", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.processes.root = { ...p.observations[2]!.snapshot.processes.root!, startTicks: "300" }; }],
|
||||
["live child", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.processes.live = [51]; }],
|
||||
["lost child journal", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.processes.journal.pop(); }],
|
||||
["unobserved pending root", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[1]!.snapshot.processes.live = []; }],
|
||||
["transient target mutation", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.watcher.targetMutationCount = 1; }],
|
||||
["workspace mutation", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.watcher.workspaceMutationCount = 1; }],
|
||||
["late target present", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.targets["target.txt"]!.absent = false; }],
|
||||
["incomplete watcher", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.watcher.complete = false; }],
|
||||
["changed action", (p: ReturnType<typeof remoteRetirementProof>) => { p.observations[2]!.snapshot.setup.sha256 = `sha256:${"c".repeat(64)}`; }],
|
||||
] as const)("rejects %s", (_label, mutate) => {
|
||||
const proof = remoteRetirementProof(); mutate(proof); expect(() => readActiveStopRemoteRetirement(proof)).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user