diff --git a/tests/runner-e2e/native-active-stop-evidence.ts b/tests/runner-e2e/native-active-stop-evidence.ts index ad746983064..89e6ba5c3d4 100644 --- a/tests/runner-e2e/native-active-stop-evidence.ts +++ b/tests/runner-e2e/native-active-stop-evidence.ts @@ -6,11 +6,12 @@ import { readCursorToolEvidence } from "./cursor-native-evidence.js"; import { bootstrapReadExecutionId, withoutProvenBootstrapReads, type BootstrapReadProof } from "./native-bootstrap-read-proof.js"; type Row = Record; +export interface ActiveStopCaller { type: "board"; userId: "local-board"; source: "local_implicit" } export type ActiveStopProvider = "cursor" | "copilot"; export interface ActiveStopScope { provider: ActiveStopProvider; companyId: string; issueId: string; runId: string; target: string; commandSha256?: string } export interface ActiveStopPending { schema: "paperclip.e2e.native-active-stop-pending.v1"; - scope: ActiveStopScope; requestId: string; toolCallId: string; nativeSessionId: string; turnId: string; + caller: ActiveStopCaller; scope: ActiveStopScope; requestId: string; toolCallId: string; nativeSessionId: string; turnId: string; normalizedSessionId: string; sourceInstanceId: string; requestSourceSeq: number; permissionSourceSeq: number; requestRowSha256: string; permissionRowSha256: string; cancellationRequestId: string; observedMonotonicNs: string; } @@ -19,6 +20,18 @@ const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 && const uuid = (v: unknown): v is string => typeof v === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u.test(v); const hash = (v: unknown) => `sha256:${createHash("sha256").update(canonicalJson(v)).digest("hex")}`; function fail(condition: unknown, reason: string): asserts condition { if (!condition) throw new Error(`Native active Stop: ${reason}`); } +/** The isolated Product server uses local-trusted board authentication. Bind + * the same public session observed by the API client, never infer a null actor. */ +export function readActiveStopCaller(health: unknown, authentication: unknown): ActiveStopCaller { + const session = rec(rec(authentication).session); + fail(rec(health).deploymentMode === "local_trusted" && session.userId === "local-board" + && session.id === "paperclip:local_implicit:local-board", "expected isolated board caller missing"); + return { type: "board", userId: "local-board", source: "local_implicit" }; +} +function isActiveStopCaller(caller: unknown): caller is ActiveStopCaller { + const value = rec(caller); + return value.type === "board" && value.userId === "local-board" && value.source === "local_implicit"; +} function canonicalRows(events: readonly unknown[], scope: ActiveStopScope) { fail(events.length <= 20_000 && events.length > 0, "bounded durable evidence required"); const rows = events.map(rec).filter(row => rec(row.payload).prpEvent !== undefined); @@ -92,15 +105,15 @@ function origin(events: readonly unknown[], scope: ActiveStopScope, bootstrap?: .every(x => rec(x.event.payload).status !== "completed"), "target operation completed"); return { rows, notice, permission, created: created[0]!, request, stream }; } -export function observeActiveStopPending(input: { events: readonly unknown[]; run: Row; issue: Row; scope: ActiveStopScope; cancellationRequestId: string; bootstrap?: BootstrapReadProof }): ActiveStopPending { +export function observeActiveStopPending(input: { events: readonly unknown[]; run: Row; issue: Row; scope: ActiveStopScope; caller: ActiveStopCaller; cancellationRequestId: string; bootstrap?: BootstrapReadProof }): ActiveStopPending { const { run, issue, scope } = input, proof = origin(input.events, scope, input.bootstrap); - fail(uuid(input.cancellationRequestId) && run.id === scope.runId && run.companyId === scope.companyId && run.nativeIssueId === scope.issueId + fail(isActiveStopCaller(input.caller) && uuid(input.cancellationRequestId) && run.id === scope.runId && run.companyId === scope.companyId && run.nativeIssueId === scope.issueId && run.status === "running" && run.runtimeMode === "native" && issue.id === scope.issueId && issue.companyId === scope.companyId && issue.status === "in_progress" && run.resultJson?.startupCancellation == null && run.resultJson?.nativeCancellation == null, "run is not fresh active work"); fail(!(scope.provider === "cursor" ? readCursorToolEvidence(input.events, scope.runId) : readCopilotToolEvidence(input.events, scope.runId)).some(n => n.stage === "permission_delivered"), "permission already answered"); fail(!proof.rows.some(x => terminals.has(x.event.eventType) || closures.has(x.event.eventType)), "request or provider already settled"); fail(!proof.rows.some(x => x.event.eventType === "tool.execution.completed" && rec(x.event.payload).executionId === bootstrapReadExecutionId(proof.notice.toolCallId)), "operation already ended"); - return { schema: "paperclip.e2e.native-active-stop-pending.v1", scope, requestId: proof.notice.requestId!, toolCallId: proof.notice.toolCallId, + return { schema: "paperclip.e2e.native-active-stop-pending.v1", scope, caller: input.caller, requestId: proof.notice.requestId!, toolCallId: proof.notice.toolCallId, nativeSessionId: proof.notice.sessionId, turnId: proof.notice.turnId, normalizedSessionId: proof.permission.event.normalizedSessionId, sourceInstanceId: proof.permission.event.sourceInstanceId, requestSourceSeq: proof.created.event.sourceSeq, permissionSourceSeq: proof.permission.event.sourceSeq, requestRowSha256: hash(proof.created.row), permissionRowSha256: hash(proof.permission.row), cancellationRequestId: input.cancellationRequestId, @@ -108,7 +121,7 @@ export function observeActiveStopPending(input: { events: readonly unknown[]; ru } export function readActiveStopSettlement(input: { events: readonly unknown[]; run: Row; issue: Row; pending: ActiveStopPending; dispatchMonotonicNs: string; bootstrap?: BootstrapReadProof }) { const before = input.pending, { run, issue } = input; - fail(before?.schema === "paperclip.e2e.native-active-stop-pending.v1" && uuid(before.cancellationRequestId) + fail(before?.schema === "paperclip.e2e.native-active-stop-pending.v1" && isActiveStopCaller(before.caller) && uuid(before.cancellationRequestId) && /^[1-9][0-9]{0,29}$/u.test(before.observedMonotonicNs) && /^[1-9][0-9]{0,29}$/u.test(input.dispatchMonotonicNs) && BigInt(before.observedMonotonicNs) < BigInt(input.dispatchMonotonicNs), "pre-dispatch pending observation missing"); const proof = origin(input.events, before.scope, input.bootstrap); @@ -120,7 +133,7 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru const closed = proof.rows.filter(x => closures.has(x.event.eventType)), terminal = proof.rows.filter(x => terminals.has(x.event.eventType)); fail(closed.length === 1 && terminal.length === 1, "one closed request and one terminal required"); const c = closed[0]!, t = terminal[0]!, cp = rec(c.event.payload), tp = rec(t.event.payload); - fail(c.event.eventType === "runtime_request.cancelled" && cp.requestId === before.requestId && cp.turnId === before.turnId + fail(proof.stream(c.event) && proof.stream(t.event) && c.event.eventType === "runtime_request.cancelled" && cp.requestId === before.requestId && cp.turnId === before.turnId && cp.requestKind === "permission_approval" && cp.requestType === "permission" && cp.reason === "explicit_cancellation" && cp.provider === "acpx" && cp.itemId === proof.request.itemId && cp.replayAllowed === false && t.event.eventType === "turn.cancelled" && tp.status === "cancelled" && tp.provider === "acpx" && tp.providerTurnId === before.turnId && tp.error === null && c.event.sourceSeq > Math.max(before.requestSourceSeq, before.permissionSourceSeq) && t.event.sourceSeq > c.event.sourceSeq, @@ -131,6 +144,7 @@ export function readActiveStopSettlement(input: { events: readonly unknown[]; ru const stop = rec(rec(run.resultJson).nativeCancellation), startup = rec(rec(run.resultJson).startupCancellation); fail(run.id === before.scope.runId && run.companyId === before.scope.companyId && run.nativeIssueId === before.scope.issueId && run.runtimeMode === "native" && run.status === "cancelled" && issue.id === before.scope.issueId && issue.companyId === before.scope.companyId && issue.status === "in_progress" + && rec(startup.requestedBy).type === before.caller.type && rec(startup.requestedBy).userId === before.caller.userId && startup.cancellationRequestId === before.cancellationRequestId && stop.intentId === `native-cancellation:${before.cancellationRequestId}` && stop.schema === "paperclip.native-cancellation.v1" && stop.companyId === before.scope.companyId && stop.runId === run.id && stop.issueId === issue.id && stop.scope === "run" && stop.dispatched === true && stop.dispatchState === "acknowledged" && stop.reasonCode === "cancellation_run_only" diff --git a/tests/runner-e2e/native-active-stop-flow.ts b/tests/runner-e2e/native-active-stop-flow.ts index 07c337aa905..0d6c8d7fdde 100644 --- a/tests/runner-e2e/native-active-stop-flow.ts +++ b/tests/runner-e2e/native-active-stop-flow.ts @@ -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, observeActiveStopPending, readActiveStopSettlement, type ActiveStopPending, type ActiveStopScope } from "./native-active-stop-evidence.js"; +import { assertActiveStopRetirement, readActiveStopCaller, observeActiveStopPending, readActiveStopSettlement, 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"; @@ -17,16 +17,16 @@ export interface ActiveStopState { run: Row; issue: Row; events: readonly unknow /** 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; bootstrap?: BootstrapReadProof; deadlineAt: number; + scope: ActiveStopScope; caller: ActiveStopCaller; bootstrap?: BootstrapReadProof; deadlineAt: number; load(): Promise; retain(receipt: ActiveStopPending): Promise; stop(runId: string, cancellationRequestId: string): Promise; }) { const cancellationRequestId = randomUUID(); const state = await input.load(); - const pending = observeActiveStopPending({ ...state, scope: input.scope, cancellationRequestId, bootstrap: input.bootstrap }); + 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, cancellationRequestId, bootstrap: input.bootstrap }); + 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 || 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); @@ -120,6 +120,7 @@ export async function runNativeActiveStopFlow(input: { ? `${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(`/api/agents/${fixtures.agent.id}`); const configured = await api.patch(`/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"); @@ -140,12 +141,12 @@ export async function runNativeActiveStopFlow(input: { 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(), cancellationRequestId: randomUUID(), bootstrap: bootstrap() }); return true; } }); + }, 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(), bootstrap: bootstrap(), deadlineAt: input.deadlineAt, load, + 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); diff --git a/tests/runner-e2e/native-active-stop.test.ts b/tests/runner-e2e/native-active-stop.test.ts index a320ab95e55..86bdf28bec8 100644 --- a/tests/runner-e2e/native-active-stop.test.ts +++ b/tests/runner-e2e/native-active-stop.test.ts @@ -1,10 +1,11 @@ import { createHash } from "node:crypto"; import { describe, expect, it, vi } from "vitest"; -import { assertActiveStopRetirement, observeActiveStopPending, readActiveStopSettlement, type ActiveStopProvider } from "./native-active-stop-evidence.js"; +import { assertActiveStopRetirement, 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"; +const caller = readActiveStopCaller({ deploymentMode: "local_trusted" }, { session: { userId: "local-board", id: "paperclip:local_implicit:local-board" } }); const cancellationRequestId = "11111111-2222-4333-8444-555555555555"; type Row = Record; function fixture(provider: ActiveStopProvider = "copilot") { @@ -28,11 +29,11 @@ function fixture(provider: ActiveStopProvider = "copilot") { const run: Row = { id: "run", companyId: "company", nativeIssueId: "issue", runtimeMode: "native", status: "running", resultJson: {} }; const issue = { id: "issue", companyId: "company", status: "in_progress" }; const state = () => ({ events, run, issue }); - const pending = () => observeActiveStopPending({ ...state(), scope, cancellationRequestId }); + const pending = () => observeActiveStopPending({ ...state(), scope, caller, cancellationRequestId }); const settle = (requestId = cancellationRequestId) => { events.push(row(5, "runtime_request.cancelled", { provider: "acpx", requestId: "request", turnId: "turn", requestKind: "permission_approval", requestType: "permission", itemId: "tool", reason: "explicit_cancellation", replayAllowed: false }), row(6, "turn.cancelled", { provider: "acpx", providerTurnId: "turn", status: "cancelled", error: null })); - run.status = "cancelled"; run.resultJson = { startupCancellation: { cancellationRequestId: requestId }, nativeCancellation: { + run.status = "cancelled"; run.resultJson = { startupCancellation: { cancellationRequestId: requestId, requestedBy: { type: "board", userId: "local-board" } }, nativeCancellation: { schema: "paperclip.native-cancellation.v1", intentId: `native-cancellation:${requestId}`, companyId: "company", runId: "run", issueId: "issue", scope: "run", dispatched: true, dispatchState: "acknowledged", reasonCode: "cancellation_run_only", effects: ["release_run_resources"], intentAuditId: "audit-intent", acknowledgementAuditId: "audit-ack", } }; @@ -61,9 +62,9 @@ describe("definitely active native permission Stop", () => { f.notice(3, "tool", { toolCallId: "bootstrap", operation: "read", status: "completed", readTargetSha256 }), f.row(4, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", executionId: "bootstrap", transport: "builtin", status: "completed", operation: "read", target: actionFile })); const bootstrap = { actionFile, events: f.events }; - expect(() => observeActiveStopPending({ ...f.state(), scope: f.scope, cancellationRequestId, bootstrap })).not.toThrow(); + expect(() => observeActiveStopPending({ ...f.state(), scope: f.scope, caller, cancellationRequestId, bootstrap })).not.toThrow(); payload(f.events[2]!).details.find((d: Row) => d.name === "readTargetSha256").value = `sha256:${"0".repeat(64)}`; - expect(() => observeActiveStopPending({ ...f.state(), scope: f.scope, cancellationRequestId, bootstrap })).toThrow(/bootstrap/); + expect(() => observeActiveStopPending({ ...f.state(), scope: f.scope, caller, cancellationRequestId, bootstrap })).toThrow(/bootstrap/); }); it("does not compare remote wall clock or transaction timestamps to the operator clock", () => { const f = fixture(); for (const row of f.events) { frame(row).emittedAt = "2099-01-01T00:00:00Z"; row.createdAt = "1900-01-01T00:00:00Z"; } @@ -97,6 +98,11 @@ describe("definitely active native permission Stop", () => { ["replay permitted", ({ f }: ReturnType) => { payload(f.events[4]!).replayAllowed = true; }], ["tampered pending row", ({ f }: ReturnType) => { payload(f.events[3]!).request.itemId = "changed"; }], ["missing pending receipt", (s: ReturnType) => { s.pending.requestRowSha256 = ""; }], + ["missing caller", ({ f }: ReturnType) => { delete f.run.resultJson.startupCancellation.requestedBy; }], + ["null caller", ({ f }: ReturnType) => { f.run.resultJson.startupCancellation.requestedBy.userId = null; }], + ["foreign caller", ({ f }: ReturnType) => { f.run.resultJson.startupCancellation.requestedBy.userId = "another-board-user"; }], + ["agent caller", ({ f }: ReturnType) => { f.run.resultJson.startupCancellation.requestedBy.type = "agent"; }], + ["unbound caller receipt", (s: ReturnType) => { (s.pending as Row).caller = null; }], ["foreign intent", ({ f }: ReturnType) => { f.run.resultJson.nativeCancellation.intentId = "native-cancellation:other"; }], ["foreign ack scope", ({ f }: ReturnType) => { f.run.resultJson.nativeCancellation.companyId = "other"; }], ["undispatched cancellation", ({ f }: ReturnType) => { f.run.resultJson.nativeCancellation.dispatched = false; }], @@ -107,6 +113,21 @@ describe("definitely active native permission Stop", () => { ["native session switched", ({ f }: ReturnType) => { payload(f.events[0]!).provenance.sessionId = "other"; }], ["target operation completed", ({ f }: ReturnType) => { f.events.push(f.row(7, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", transport: "builtin", executionId: "tool", status: "completed" })); }], ] as const)("rejects %s", (_label, mutate) => { const s = settled(); mutate(s); expect(s.read).toThrow(); }); + it.each(["closure", "terminal"] as const)("rejects missing/null envelope turn and foreign null-turn stream on %s", kind => { + for (const turn of [null, undefined]) for (const mismatch of ["none", "session", "source"]) { + const s = settled(), event = frame(s.f.events[kind === "closure" ? 4 : 5]!); + event.turnId = turn; + if (mismatch === "session") event.normalizedSessionId = "foreign-session"; + if (mismatch === "source") { event.sourceInstanceId = "foreign-source"; event.sourceEventId = `foreign-source:run:${event.sourceSeq}`; } + expect(s.read).toThrow(/pending callback/); + } + }); + it.each([ + [{ deploymentMode: "authenticated" }, { session: { userId: "local-board", id: "paperclip:local_implicit:local-board" } }], + [{ deploymentMode: "local_trusted" }, { session: { userId: null, id: "paperclip:local_implicit:local-board" } }], + [{ deploymentMode: "local_trusted" }, { session: { userId: "another", id: "paperclip:local_implicit:another" } }], + [{ deploymentMode: "local_trusted" }, { session: { userId: "local-board", id: "paperclip:session:local-board" } }], + ])("rejects a non-fixture authentication context", (health, session) => expect(() => readActiveStopCaller(health, session)).toThrow()); it("rejects a Stop dispatch not causally after the pending observation", () => { const { f, pending } = settled(); expect(() => readActiveStopSettlement({ ...f.state(), pending, dispatchMonotonicNs: pending.observedMonotonicNs })).toThrow(/pre-dispatch/); @@ -117,7 +138,7 @@ describe("active Stop flow wiring", () => { it("awaits retention and rechecks pending before issuing the single caller UUID request", async () => { const f = fixture(); const order: string[] = []; let retainedId = ""; const stop = vi.fn(async (runId: string, id: string) => { expect(runId).toBe("run"); expect(id).toBe(retainedId); order.push("stop"); return f.settle(id); }); - const result = await stopAtPendingPermission({ scope: f.scope, deadlineAt: Date.now() + 1000, + const result = await stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: Date.now() + 1000, load: async () => { order.push("load"); return f.state(); }, retain: async receipt => { order.push("retain-start"); await Promise.resolve(); retainedId = receipt.cancellationRequestId; order.push("retain-done"); }, stop }); expect(order).toEqual(["load", "retain-start", "retain-done", "load", "stop", "load"]); @@ -125,13 +146,13 @@ describe("active Stop flow wiring", () => { }); it.each(["retention failed", "competing Stop", "answered after capture"])("never sends Stop when %s", async mode => { const f = fixture(), stop = vi.fn(); - await expect(stopAtPendingPermission({ scope: f.scope, deadlineAt: Date.now() + 1000, load: async () => f.state(), stop, + await expect(stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: Date.now() + 1000, load: async () => f.state(), stop, retain: async () => { if (mode === "retention failed") throw Error("disk failure"); if (mode === "competing Stop") f.run.resultJson.startupCancellation = { cancellationRequestId: "foreign" }; else f.events.push(f.row(5, "runtime_request.resolved", {})); }, })).rejects.toThrow(); expect(stop).not.toHaveBeenCalled(); }); it("fails promptly on a definitive provider failure before calling a throwing evidence reader", async () => { const f = fixture(); const started = Date.now(); - await expect(stopAtPendingPermission({ scope: f.scope, deadlineAt: started + 10000, load: async () => f.state(), retain: async () => {}, stop: async (_run, id) => { + await expect(stopAtPendingPermission({ scope: f.scope, caller, deadlineAt: started + 10000, load: async () => f.state(), retain: async () => {}, stop: async (_run, id) => { f.settle(id); f.run.status = "failed"; f.events.length = 0; return f.run; } })).rejects.toThrow(/Stopped waiting.*unexpected run terminal/); expect(Date.now() - started).toBeLessThan(500);