mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
feat(runner): add deterministic Phase 7 control plane
Implement seeded state, typed semantic commands, command claims, deterministic audit/wake/fault/replay behavior, and focused conformance tests. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b8875b2ec4
commit
5cd95a0e9f
4 files changed
+2789
No files matched your search
@@ -197,3 +197,6 @@ function canonicalJson(value: unknown): string {
|
||||
}
|
||||
return JSON.stringify(value) ?? "undefined";
|
||||
}
|
||||
|
||||
export * from "./phase7-control-plane-types.js";
|
||||
export * from "./phase7-mock-control-plane-adapter.js";
|
||||
@@ -0,0 +1,555 @@
|
||||
import type {
|
||||
CompleteControlPlaneRunInput,
|
||||
ControlPlanePort,
|
||||
OpenControlPlaneRunInput,
|
||||
} from "../contracts/control-plane-port.js";
|
||||
import type { NativeRunEvent, NativeRunResult } from "../contracts/types.js";
|
||||
import type { PersistedNativeSession } from "../contracts/native-session-backend.js";
|
||||
import type { PrpEvent } from "../protocol/phase1-contract.js";
|
||||
|
||||
export type Phase7JsonPrimitive = string | number | boolean | null;
|
||||
export type Phase7JsonValue =
|
||||
| Phase7JsonPrimitive
|
||||
| Phase7JsonValue[]
|
||||
| { [key: string]: Phase7JsonValue };
|
||||
|
||||
export type Phase7TaskStatus =
|
||||
| "backlog"
|
||||
| "todo"
|
||||
| "in_progress"
|
||||
| "in_review"
|
||||
| "done"
|
||||
| "blocked"
|
||||
| "cancelled";
|
||||
|
||||
export interface Phase7FixtureCompany {
|
||||
id: string;
|
||||
name: string;
|
||||
issuePrefix: string;
|
||||
status: "active" | "paused";
|
||||
budgetId: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureActor {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
status: "active" | "paused";
|
||||
budgetId: string;
|
||||
capabilityGrants: string[];
|
||||
}
|
||||
|
||||
export interface Phase7FixtureTask {
|
||||
id: string;
|
||||
companyId: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: Phase7TaskStatus;
|
||||
priority: "critical" | "high" | "medium" | "low";
|
||||
workMode: "standard" | "ask" | "planning" | "skill_test";
|
||||
parentId: string | null;
|
||||
assigneeActorId: string | null;
|
||||
checkoutRunId: string | null;
|
||||
executionRunId: string | null;
|
||||
startedAt: string | null;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureComment {
|
||||
id: string;
|
||||
taskId: string;
|
||||
authorActorId: string | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureDocumentRevision {
|
||||
id: string;
|
||||
documentId: string;
|
||||
revision: number;
|
||||
body: string;
|
||||
changeSummary: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureDocument {
|
||||
id: string;
|
||||
taskId: string;
|
||||
key: string;
|
||||
title: string;
|
||||
format: "markdown";
|
||||
latestRevisionId: string;
|
||||
revisions: Phase7FixtureDocumentRevision[];
|
||||
}
|
||||
|
||||
export type Phase7InteractionKind =
|
||||
| "confirmation"
|
||||
| "checkbox"
|
||||
| "questions"
|
||||
| "suggest_tasks"
|
||||
| "item_verdicts";
|
||||
|
||||
export interface Phase7FixtureInteraction {
|
||||
id: string;
|
||||
taskId: string;
|
||||
kind: Phase7InteractionKind;
|
||||
status: "pending" | "answered" | "accepted" | "rejected" | "expired";
|
||||
title: string;
|
||||
prompt: string;
|
||||
payload: Phase7JsonValue;
|
||||
targetRevisionId: string | null;
|
||||
continuationPolicy: "none" | "wake_assignee" | "wake_assignee_on_accept";
|
||||
result: Phase7JsonValue | null;
|
||||
createdAt: string;
|
||||
resolvedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureApprovalComment {
|
||||
id: string;
|
||||
authorActorId: string | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureApproval {
|
||||
id: string;
|
||||
companyId: string;
|
||||
taskIds: string[];
|
||||
type: string;
|
||||
status: "pending" | "approved" | "rejected" | "cancelled";
|
||||
requestedByActorId: string | null;
|
||||
payload: Phase7JsonValue;
|
||||
decisionNote: string | null;
|
||||
comments: Phase7FixtureApprovalComment[];
|
||||
createdAt: string;
|
||||
decidedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureArtifact {
|
||||
id: string;
|
||||
taskId: string;
|
||||
filename: string;
|
||||
contentType: string;
|
||||
byteSize: number;
|
||||
sha256: string;
|
||||
contentRef: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureWorkProduct {
|
||||
id: string;
|
||||
taskId: string;
|
||||
type: "artifact" | "document" | "pull_request" | "preview_url" | "runtime_service" | "commit" | "branch";
|
||||
title: string;
|
||||
metadata: Phase7JsonValue;
|
||||
artifactId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureBlocker {
|
||||
id: string;
|
||||
taskId: string;
|
||||
blockedByTaskId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureWorkspaceService {
|
||||
id: string;
|
||||
taskId: string;
|
||||
name: string;
|
||||
status: "stopped" | "starting" | "running" | "failed";
|
||||
url: string | null;
|
||||
lastTransitionAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureBudget {
|
||||
id: string;
|
||||
scope: "company" | "actor";
|
||||
scopeId: string;
|
||||
limitCents: number;
|
||||
spentCents: number;
|
||||
hardStop: boolean;
|
||||
}
|
||||
|
||||
export type Phase7WakeReason =
|
||||
| "manual"
|
||||
| "issue_commented"
|
||||
| "interaction_resolved"
|
||||
| "approval_resolved"
|
||||
| "blockers_resolved"
|
||||
| "scheduled_retry"
|
||||
| "resume";
|
||||
|
||||
export interface Phase7WakeContext {
|
||||
reason: Phase7WakeReason;
|
||||
payload: Phase7JsonValue;
|
||||
}
|
||||
|
||||
export interface Phase7ScheduledWake {
|
||||
id: string;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
reason: Phase7WakeReason;
|
||||
payload: Phase7JsonValue;
|
||||
dueAt: string;
|
||||
status: "scheduled" | "delivered" | "cancelled";
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureRun {
|
||||
id: string;
|
||||
companyId: string;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
backendKind: OpenControlPlaneRunInput["backendKind"];
|
||||
sourceInstanceId: string;
|
||||
status: "running" | "succeeded" | "failed" | "cancelled" | "budget_stopped";
|
||||
attempt: number;
|
||||
openedAt: string;
|
||||
finishedAt: string | null;
|
||||
wake: Phase7WakeContext;
|
||||
capabilities: string[];
|
||||
events: Array<NativeRunEvent | PrpEvent>;
|
||||
result: NativeRunResult | CompleteControlPlaneRunInput | null;
|
||||
sessionCheckpoint: PersistedNativeSession | null;
|
||||
}
|
||||
|
||||
export interface Phase7AuditRecord {
|
||||
id: string;
|
||||
at: string;
|
||||
runId: string | null;
|
||||
actorId: string | null;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
details: Phase7JsonValue;
|
||||
}
|
||||
|
||||
export interface Phase7DecisionRecord {
|
||||
id: string;
|
||||
at: string;
|
||||
runId: string | null;
|
||||
operation: string;
|
||||
outcome: "allowed" | "denied" | "duplicate" | "reconciled" | "faulted";
|
||||
reason: string;
|
||||
stateRevision: number;
|
||||
entityRefs: string[];
|
||||
}
|
||||
|
||||
export interface Phase7FaultRule {
|
||||
id: string;
|
||||
operation: "open_run" | "append_event" | "apply_command" | "complete_run";
|
||||
commandKind?: Phase7SemanticCommand["kind"];
|
||||
effect: "retryable_error" | "lost_ack";
|
||||
remaining: number;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface Phase7IdempotencyRecord {
|
||||
scope: string;
|
||||
key: string;
|
||||
inputCanonical: string;
|
||||
result: Phase7CommandResult;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureState {
|
||||
schema: "paperclip.phase7.mock-state.v1";
|
||||
revision: number;
|
||||
clock: { epochMs: number; tick: number };
|
||||
counters: Record<string, number>;
|
||||
lifecycle: "stopped" | "running";
|
||||
activeRunId: string | null;
|
||||
company: Phase7FixtureCompany;
|
||||
actors: Phase7FixtureActor[];
|
||||
tasks: Phase7FixtureTask[];
|
||||
comments: Phase7FixtureComment[];
|
||||
documents: Phase7FixtureDocument[];
|
||||
interactions: Phase7FixtureInteraction[];
|
||||
approvals: Phase7FixtureApproval[];
|
||||
artifacts: Phase7FixtureArtifact[];
|
||||
workProducts: Phase7FixtureWorkProduct[];
|
||||
blockers: Phase7FixtureBlocker[];
|
||||
workspaceServices: Phase7FixtureWorkspaceService[];
|
||||
budgets: Phase7FixtureBudget[];
|
||||
runs: Phase7FixtureRun[];
|
||||
wakes: Phase7ScheduledWake[];
|
||||
audit: Phase7AuditRecord[];
|
||||
decisions: Phase7DecisionRecord[];
|
||||
idempotency: Phase7IdempotencyRecord[];
|
||||
faults: Phase7FaultRule[];
|
||||
}
|
||||
|
||||
export interface Phase7RunContext {
|
||||
schema: "paperclip.phase7.run-context.v1";
|
||||
company: Pick<Phase7FixtureCompany, "id" | "name" | "issuePrefix" | "status">;
|
||||
actor: Pick<Phase7FixtureActor, "id" | "name" | "role" | "status" | "capabilityGrants">;
|
||||
activeTask: Phase7FixtureTask;
|
||||
ancestors: Phase7FixtureTask[];
|
||||
wake: Phase7WakeContext;
|
||||
capabilities: string[];
|
||||
budget: { limitCents: number; spentCents: number; remainingCents: number };
|
||||
interactionResults: Array<Pick<Phase7FixtureInteraction, "id" | "kind" | "status" | "result">>;
|
||||
}
|
||||
|
||||
export interface Phase7OpenFixtureRunInput extends OpenControlPlaneRunInput {
|
||||
wake?: Phase7WakeContext;
|
||||
capabilities?: string[];
|
||||
resume?: boolean;
|
||||
}
|
||||
|
||||
interface Phase7BaseCommand {
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export type Phase7SemanticCommand =
|
||||
| (Phase7BaseCommand & { kind: "report_progress"; body: string })
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "write_document";
|
||||
key: string;
|
||||
title: string;
|
||||
body: string;
|
||||
baseRevisionId: string | null;
|
||||
changeSummary?: string | null;
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "request_human_input";
|
||||
interactionKind: Phase7InteractionKind;
|
||||
title: string;
|
||||
prompt: string;
|
||||
payload?: Phase7JsonValue;
|
||||
targetRevisionId?: string | null;
|
||||
continuationPolicy: Phase7FixtureInteraction["continuationPolicy"];
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "resolve_human_input";
|
||||
interactionId: string;
|
||||
outcome: "answered" | "accepted" | "rejected" | "expired";
|
||||
result: Phase7JsonValue;
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "register_deliverable";
|
||||
filename: string;
|
||||
contentType: string;
|
||||
byteSize: number;
|
||||
sha256: string;
|
||||
contentRef: string;
|
||||
title: string;
|
||||
})
|
||||
| (Phase7BaseCommand & { kind: "set_dependencies"; blockedByTaskIds: string[] })
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "create_task";
|
||||
title: string;
|
||||
description?: string | null;
|
||||
assigneeActorId?: string | null;
|
||||
priority?: Phase7FixtureTask["priority"];
|
||||
blockedByTaskIds?: string[];
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "request_approval";
|
||||
approvalType: string;
|
||||
payload: Phase7JsonValue;
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "decide_approval";
|
||||
approvalId: string;
|
||||
decision: "approved" | "rejected" | "cancelled";
|
||||
note: string;
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "comment_on_approval";
|
||||
approvalId: string;
|
||||
body: string;
|
||||
})
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "control_workspace_service";
|
||||
serviceId: string;
|
||||
action: "start" | "stop" | "fail";
|
||||
url?: string | null;
|
||||
})
|
||||
| (Phase7BaseCommand & { kind: "record_budget_usage"; cents: number })
|
||||
| (Phase7BaseCommand & { kind: "finish_task"; summary: string })
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "block_task";
|
||||
reason: string;
|
||||
blockedByTaskIds?: string[];
|
||||
})
|
||||
| (Phase7BaseCommand & { kind: "request_review"; summary: string })
|
||||
| (Phase7BaseCommand & { kind: "release_task"; reason: string })
|
||||
| (Phase7BaseCommand & {
|
||||
kind: "schedule_wake";
|
||||
reason: Phase7WakeReason;
|
||||
payload?: Phase7JsonValue;
|
||||
delayTicks: number;
|
||||
});
|
||||
|
||||
export interface Phase7CommandEnvelope {
|
||||
runId: string;
|
||||
idempotencyKey: string;
|
||||
expectedRevision?: number;
|
||||
command: Phase7SemanticCommand;
|
||||
}
|
||||
|
||||
export interface Phase7CommandResult {
|
||||
commandId: string;
|
||||
commandKind: Phase7SemanticCommand["kind"];
|
||||
disposition: "applied" | "duplicate";
|
||||
stateRevision: number;
|
||||
entityRefs: string[];
|
||||
scheduledWakeIds: string[];
|
||||
}
|
||||
|
||||
export interface Phase7CommandErrorResult {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
export type Phase7CommandOutcome =
|
||||
| { ok: true; result: Phase7CommandResult }
|
||||
| {
|
||||
ok: false;
|
||||
commandKind: Phase7SemanticCommand["kind"];
|
||||
stateRevision: number;
|
||||
error: Phase7CommandErrorResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Claims enforced by the mock command boundary itself. The semantic catalog
|
||||
* may hide ungranted operations earlier, but direct adapter consumers cannot
|
||||
* bypass these checks.
|
||||
*/
|
||||
export const PHASE7_COMMAND_REQUIRED_CLAIMS = {
|
||||
report_progress: [],
|
||||
write_document: [],
|
||||
request_human_input: [],
|
||||
resolve_human_input: ["control_plane:interactions"],
|
||||
register_deliverable: [],
|
||||
set_dependencies: ["dependencies:write"],
|
||||
create_task: ["delegation:tasks:create"],
|
||||
request_approval: ["governance:approvals:request"],
|
||||
decide_approval: ["governance:approvals:decide"],
|
||||
comment_on_approval: ["governance:approvals:comment"],
|
||||
control_workspace_service: ["workspace:control"],
|
||||
record_budget_usage: ["control_plane:budget"],
|
||||
finish_task: [],
|
||||
block_task: [],
|
||||
request_review: [],
|
||||
release_task: ["control_plane:release"],
|
||||
schedule_wake: ["control_plane:wakes"],
|
||||
} as const satisfies Record<Phase7SemanticCommand["kind"], readonly string[]>;
|
||||
|
||||
export interface Phase7MockControlPlanePort extends ControlPlanePort {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
openFixtureRun(input: Phase7OpenFixtureRunInput): Promise<Phase7RunContext>;
|
||||
context(runId: string): Phase7RunContext;
|
||||
applyCommand(envelope: Phase7CommandEnvelope): Promise<Phase7CommandResult>;
|
||||
tryApplyCommand(envelope: Phase7CommandEnvelope): Promise<Phase7CommandOutcome>;
|
||||
snapshot(): Readonly<Phase7FixtureState>;
|
||||
decisionRecords(): readonly Phase7DecisionRecord[];
|
||||
serialize(): string;
|
||||
}
|
||||
|
||||
export interface Phase7FixtureSeed {
|
||||
epochMs?: number;
|
||||
company?: Partial<Phase7FixtureCompany>;
|
||||
actors?: Phase7FixtureActor[];
|
||||
tasks?: Phase7FixtureTask[];
|
||||
comments?: Phase7FixtureComment[];
|
||||
documents?: Phase7FixtureDocument[];
|
||||
interactions?: Phase7FixtureInteraction[];
|
||||
approvals?: Phase7FixtureApproval[];
|
||||
artifacts?: Phase7FixtureArtifact[];
|
||||
workProducts?: Phase7FixtureWorkProduct[];
|
||||
blockers?: Phase7FixtureBlocker[];
|
||||
workspaceServices?: Phase7FixtureWorkspaceService[];
|
||||
budgets?: Phase7FixtureBudget[];
|
||||
faults?: Phase7FaultRule[];
|
||||
}
|
||||
|
||||
export function createPhase7FixtureState(seed: Phase7FixtureSeed = {}): Phase7FixtureState {
|
||||
const company: Phase7FixtureCompany = {
|
||||
id: "company-1",
|
||||
name: "Mock Paperclip Company",
|
||||
issuePrefix: "MCK",
|
||||
status: "active",
|
||||
budgetId: "budget-company-1",
|
||||
...seed.company,
|
||||
};
|
||||
const actors = seed.actors ?? [
|
||||
{
|
||||
id: "actor-1",
|
||||
companyId: company.id,
|
||||
name: "Mock Engineer",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
budgetId: "budget-actor-1",
|
||||
capabilityGrants: [],
|
||||
},
|
||||
];
|
||||
const tasks = seed.tasks ?? [
|
||||
{
|
||||
id: "task-1",
|
||||
companyId: company.id,
|
||||
identifier: `${company.issuePrefix}-1`,
|
||||
title: "Deterministic fixture task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: actors[0]?.id ?? null,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
];
|
||||
const budgets = seed.budgets ?? [
|
||||
{
|
||||
id: company.budgetId,
|
||||
scope: "company",
|
||||
scopeId: company.id,
|
||||
limitCents: 10_000,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
},
|
||||
...actors.map((actor) => ({
|
||||
id: actor.budgetId,
|
||||
scope: "actor" as const,
|
||||
scopeId: actor.id,
|
||||
limitCents: 1_000,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
})),
|
||||
];
|
||||
return {
|
||||
schema: "paperclip.phase7.mock-state.v1",
|
||||
revision: 0,
|
||||
clock: { epochMs: seed.epochMs ?? Date.UTC(2026, 7, 9), tick: 0 },
|
||||
counters: {},
|
||||
lifecycle: "stopped",
|
||||
activeRunId: null,
|
||||
company,
|
||||
actors: structuredClone(actors),
|
||||
tasks: structuredClone(tasks),
|
||||
comments: structuredClone(seed.comments ?? []),
|
||||
documents: structuredClone(seed.documents ?? []),
|
||||
interactions: structuredClone(seed.interactions ?? []),
|
||||
approvals: structuredClone(seed.approvals ?? []),
|
||||
artifacts: structuredClone(seed.artifacts ?? []),
|
||||
workProducts: structuredClone(seed.workProducts ?? []),
|
||||
blockers: structuredClone(seed.blockers ?? []),
|
||||
workspaceServices: structuredClone(seed.workspaceServices ?? []),
|
||||
budgets: structuredClone(budgets),
|
||||
runs: [],
|
||||
wakes: [],
|
||||
audit: [],
|
||||
decisions: [],
|
||||
idempotency: [],
|
||||
faults: structuredClone(seed.faults ?? []),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
CONTROL_PLANE_CONFORMANCE_OPEN,
|
||||
runControlPlanePortConformance,
|
||||
} from "../conformance/control-plane-port.js";
|
||||
import type { Phase7FixtureSeed } from "./phase7-control-plane-types.js";
|
||||
import {
|
||||
Phase7MockControlPlaneAdapter,
|
||||
Phase7MockControlPlaneError,
|
||||
} from "./phase7-mock-control-plane-adapter.js";
|
||||
|
||||
const OPEN = {
|
||||
identity: {
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
companyId: "company-1",
|
||||
issueId: "task-1",
|
||||
agentId: "actor-1",
|
||||
},
|
||||
backendKind: "mock" as const,
|
||||
sourceInstanceId: "fixture-source",
|
||||
};
|
||||
|
||||
function seeded(seed: Phase7FixtureSeed = {}) {
|
||||
return new Phase7MockControlPlaneAdapter({
|
||||
workspaceServices: [
|
||||
{
|
||||
id: "service-1",
|
||||
taskId: "task-1",
|
||||
name: "preview",
|
||||
status: "stopped",
|
||||
url: null,
|
||||
lastTransitionAt: "2026-08-09T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
...seed,
|
||||
});
|
||||
}
|
||||
|
||||
describe("Phase7MockControlPlaneAdapter", () => {
|
||||
it("produces deterministic immutable context and state across every fixture domain", async () => {
|
||||
const runScenario = async () => {
|
||||
const adapter = seeded();
|
||||
await adapter.start();
|
||||
const context = await adapter.openFixtureRun({
|
||||
...OPEN,
|
||||
wake: {
|
||||
reason: "issue_commented",
|
||||
payload: {
|
||||
commentId: "comment-wake",
|
||||
apiKey: "must-not-be-visible",
|
||||
nested: { authorization: "Bearer must-not-be-visible" },
|
||||
},
|
||||
},
|
||||
capabilities: [
|
||||
"control_plane:wakes",
|
||||
"governance:approvals:comment",
|
||||
"governance:approvals:request",
|
||||
"workspace:control",
|
||||
"task:write",
|
||||
"task:write",
|
||||
],
|
||||
});
|
||||
expect(context.capabilities).toEqual([
|
||||
"control_plane:wakes",
|
||||
"governance:approvals:comment",
|
||||
"governance:approvals:request",
|
||||
"task:write",
|
||||
"workspace:control",
|
||||
]);
|
||||
expect(context.wake.payload).toEqual({
|
||||
commentId: "comment-wake",
|
||||
apiKey: "[REDACTED]",
|
||||
nested: { authorization: "[REDACTED]" },
|
||||
});
|
||||
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "progress-1",
|
||||
command: { kind: "report_progress", body: "Fixture work started." },
|
||||
});
|
||||
const documentResult = await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "document-1",
|
||||
command: {
|
||||
kind: "write_document",
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
body: "# Deterministic plan",
|
||||
baseRevisionId: null,
|
||||
},
|
||||
});
|
||||
const revisionId = documentResult.entityRefs
|
||||
.find((ref) => ref.startsWith("revision:"))!
|
||||
.slice("revision:".length);
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "interaction-1",
|
||||
command: {
|
||||
kind: "request_human_input",
|
||||
interactionKind: "confirmation",
|
||||
title: "Confirm plan",
|
||||
prompt: "Accept the deterministic plan?",
|
||||
targetRevisionId: revisionId,
|
||||
continuationPolicy: "wake_assignee",
|
||||
},
|
||||
});
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "artifact-1",
|
||||
command: {
|
||||
kind: "register_deliverable",
|
||||
filename: "report.json",
|
||||
contentType: "application/json",
|
||||
byteSize: 42,
|
||||
sha256: "sha256-fixture",
|
||||
contentRef: "fixture://artifacts/report.json",
|
||||
title: "Fixture report",
|
||||
},
|
||||
});
|
||||
const approvalResult = await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "approval-1",
|
||||
command: {
|
||||
kind: "request_approval",
|
||||
approvalType: "request_board_approval",
|
||||
payload: { summary: "Approve fixture action" },
|
||||
},
|
||||
});
|
||||
const approvalId = approvalResult.entityRefs
|
||||
.find((ref) => ref.startsWith("approval:"))!
|
||||
.slice("approval:".length);
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "approval-comment-1",
|
||||
command: {
|
||||
kind: "comment_on_approval",
|
||||
approvalId,
|
||||
body: "Deterministic approval context.",
|
||||
},
|
||||
});
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "workspace-1",
|
||||
command: {
|
||||
kind: "control_workspace_service",
|
||||
serviceId: "service-1",
|
||||
action: "start",
|
||||
url: "http://fixture.invalid",
|
||||
},
|
||||
});
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "wake-1",
|
||||
command: {
|
||||
kind: "schedule_wake",
|
||||
reason: "scheduled_retry",
|
||||
payload: { attempt: 2 },
|
||||
delayTicks: 3,
|
||||
},
|
||||
});
|
||||
return { adapter, snapshot: adapter.snapshot() };
|
||||
};
|
||||
|
||||
const left = await runScenario();
|
||||
const right = await runScenario();
|
||||
expect(left.adapter.serialize()).toBe(right.adapter.serialize());
|
||||
expect(left.snapshot).toMatchObject({
|
||||
schema: "paperclip.phase7.mock-state.v1",
|
||||
comments: [{ body: "Fixture work started." }],
|
||||
documents: [{ key: "plan", revisions: [{ revision: 1 }] }],
|
||||
interactions: [{ kind: "confirmation", status: "pending" }],
|
||||
approvals: [{
|
||||
type: "request_board_approval",
|
||||
status: "pending",
|
||||
comments: [{ body: "Deterministic approval context." }],
|
||||
}],
|
||||
artifacts: [{ filename: "report.json" }],
|
||||
workProducts: [{ type: "artifact" }],
|
||||
workspaceServices: [{ id: "service-1", status: "running" }],
|
||||
wakes: [{ reason: "scheduled_retry", status: "scheduled" }],
|
||||
});
|
||||
expect(Object.isFrozen(left.snapshot)).toBe(true);
|
||||
expect(Object.isFrozen(left.snapshot.tasks)).toBe(true);
|
||||
expect(left.snapshot.audit.length).toBeGreaterThanOrEqual(7);
|
||||
expect(left.adapter.decisionRecords().every((decision) => Object.isFrozen(decision))).toBe(true);
|
||||
});
|
||||
|
||||
it("commits a lost-ack command once and returns its stored result on retry", async () => {
|
||||
const adapter = seeded({
|
||||
faults: [
|
||||
{
|
||||
id: "lost-progress-ack",
|
||||
operation: "apply_command",
|
||||
commandKind: "report_progress",
|
||||
effect: "lost_ack",
|
||||
remaining: 1,
|
||||
code: "fixture_ack_lost",
|
||||
},
|
||||
],
|
||||
});
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
const envelope = {
|
||||
runId: "run-1",
|
||||
idempotencyKey: "progress-retry",
|
||||
command: { kind: "report_progress" as const, body: "Exactly once." },
|
||||
};
|
||||
|
||||
await expect(adapter.applyCommand(envelope)).rejects.toMatchObject({
|
||||
code: "fixture_ack_lost",
|
||||
retryable: true,
|
||||
});
|
||||
const replayed = await adapter.applyCommand(envelope);
|
||||
expect(replayed.disposition).toBe("duplicate");
|
||||
expect(adapter.snapshot().comments.filter((comment) => comment.body === "Exactly once.")).toHaveLength(1);
|
||||
expect(adapter.decisionRecords().map((decision) => decision.outcome)).toContain("faulted");
|
||||
expect(adapter.decisionRecords().map((decision) => decision.outcome)).toContain("duplicate");
|
||||
|
||||
await expect(
|
||||
adapter.applyCommand({
|
||||
...envelope,
|
||||
command: { kind: "report_progress", body: "Mutated retry." },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "idempotency_conflict" });
|
||||
});
|
||||
|
||||
it("serializes, restores, and replays gapped events without a live service", async () => {
|
||||
const adapter = seeded();
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
const event = (sourceSeq: number) => ({
|
||||
schema: "paperclip.prp.event.v1" as const,
|
||||
sourceEventId: `fixture-source:event:${sourceSeq}`,
|
||||
sourceSeq,
|
||||
sourceInstanceId: "fixture-source",
|
||||
sourceKind: "runner" as const,
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
eventType: "session.started" as const,
|
||||
schemaVersion: 1 as const,
|
||||
priority: 1 as const,
|
||||
emittedAt: `2026-08-09T00:00:0${sourceSeq}.000Z`,
|
||||
payload: {},
|
||||
});
|
||||
expect((await adapter.appendEvent(event(2))).highestContiguousSourceSeq).toBe(0);
|
||||
expect((await adapter.appendEvent(event(1))).highestContiguousSourceSeq).toBe(2);
|
||||
|
||||
const restored = Phase7MockControlPlaneAdapter.restore(adapter.serialize());
|
||||
const replay = await restored.replayEvents({
|
||||
runId: "run-1",
|
||||
sourceInstanceId: "fixture-source",
|
||||
afterSourceSeq: 0,
|
||||
limit: 10,
|
||||
});
|
||||
expect(replay.events.map((entry) => entry.sourceSeq)).toEqual([1, 2]);
|
||||
expect(replay.highestContiguousSourceSeq).toBe(2);
|
||||
expect(restored.context("run-1")).toEqual(adapter.context("run-1"));
|
||||
});
|
||||
|
||||
it("reconciles terminal state, releases locks, and schedules dependent wakes", async () => {
|
||||
const adapter = new Phase7MockControlPlaneAdapter({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
companyId: "company-1",
|
||||
identifier: "MCK-1",
|
||||
title: "Blocking task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: "actor-1",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
companyId: "company-1",
|
||||
identifier: "MCK-2",
|
||||
title: "Dependent task",
|
||||
description: null,
|
||||
status: "blocked",
|
||||
priority: "high",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: "actor-1",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
],
|
||||
blockers: [
|
||||
{
|
||||
id: "blocker-1",
|
||||
taskId: "task-2",
|
||||
blockedByTaskId: "task-1",
|
||||
createdAt: "2026-08-09T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "finish-1",
|
||||
command: { kind: "finish_task", summary: "Blocking work completed." },
|
||||
});
|
||||
|
||||
const terminal = {
|
||||
schema: "paperclip.prp.terminal.v1" as const,
|
||||
turnTerminalState: "completed" as const,
|
||||
runTerminalState: "succeeded" as const,
|
||||
reportedWorkDisposition: "done" as const,
|
||||
};
|
||||
const result = {
|
||||
schema: "paperclip.run_result.v1" as const,
|
||||
reportedWorkDisposition: "done" as const,
|
||||
summary: "Blocking work completed.",
|
||||
completionClaim: {
|
||||
contractRevision: "phase7-v1",
|
||||
objectiveSatisfied: true,
|
||||
criteria: [{ criterionId: "objective", status: "satisfied" as const, evidenceRefs: [] }],
|
||||
remainingWork: [],
|
||||
},
|
||||
evidence: [],
|
||||
verification: [],
|
||||
attentionRequests: [],
|
||||
artifacts: [],
|
||||
};
|
||||
await adapter.appendEvent({
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: "fixture-source:terminal:1",
|
||||
sourceSeq: 1,
|
||||
sourceInstanceId: "fixture-source",
|
||||
sourceKind: "runner",
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
eventType: "run.terminal",
|
||||
schemaVersion: 1,
|
||||
priority: 0,
|
||||
emittedAt: "2026-08-09T00:00:10.000Z",
|
||||
payload: terminal,
|
||||
});
|
||||
await adapter.completeRun({ result, terminal });
|
||||
|
||||
const snapshot = adapter.snapshot();
|
||||
expect(snapshot.tasks.find((task) => task.id === "task-1")).toMatchObject({
|
||||
status: "done",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
});
|
||||
expect(snapshot.tasks.find((task) => task.id === "task-2")?.status).toBe("todo");
|
||||
expect(snapshot.blockers).toEqual([]);
|
||||
expect(snapshot.wakes).toMatchObject([
|
||||
{ taskId: "task-2", reason: "blockers_resolved", status: "scheduled" },
|
||||
]);
|
||||
expect(snapshot.runs[0]?.status).toBe("succeeded");
|
||||
});
|
||||
|
||||
it("enforces budget hard stops and releases run authority", async () => {
|
||||
const adapter = seeded({
|
||||
budgets: [
|
||||
{
|
||||
id: "budget-company-1",
|
||||
scope: "company",
|
||||
scopeId: "company-1",
|
||||
limitCents: 100,
|
||||
spentCents: 90,
|
||||
hardStop: true,
|
||||
},
|
||||
{
|
||||
id: "budget-actor-1",
|
||||
scope: "actor",
|
||||
scopeId: "actor-1",
|
||||
limitCents: 100,
|
||||
spentCents: 90,
|
||||
hardStop: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun({ ...OPEN, capabilities: ["control_plane:budget"] });
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "usage-1",
|
||||
command: { kind: "record_budget_usage", cents: 10 },
|
||||
});
|
||||
const snapshot = adapter.snapshot();
|
||||
expect(snapshot.actors[0]?.status).toBe("paused");
|
||||
expect(snapshot.runs[0]?.status).toBe("budget_stopped");
|
||||
expect(snapshot.tasks[0]).toMatchObject({
|
||||
status: "blocked",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
});
|
||||
await expect(
|
||||
adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "after-budget-stop",
|
||||
command: { kind: "report_progress", body: "Should not be accepted." },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "budget_hard_stop" });
|
||||
});
|
||||
|
||||
it("returns typed denials when a command claim is missing", async () => {
|
||||
const adapter = seeded();
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
|
||||
const outcome = await adapter.tryApplyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "workspace-without-claim",
|
||||
command: {
|
||||
kind: "control_workspace_service",
|
||||
serviceId: "service-1",
|
||||
action: "start",
|
||||
},
|
||||
});
|
||||
|
||||
expect(outcome).toEqual({
|
||||
ok: false,
|
||||
commandKind: "control_workspace_service",
|
||||
stateRevision: 2,
|
||||
error: {
|
||||
code: "operation_not_authorized",
|
||||
message: "the fixture run does not hold the required command claim",
|
||||
retryable: false,
|
||||
},
|
||||
});
|
||||
expect(adapter.snapshot().workspaceServices[0]?.status).toBe("stopped");
|
||||
expect(adapter.decisionRecords().at(-1)).toMatchObject({
|
||||
operation: "control_workspace_service",
|
||||
outcome: "denied",
|
||||
reason: "required_claim_missing",
|
||||
});
|
||||
});
|
||||
|
||||
it("enforces active-task ownership and legal transitions", async () => {
|
||||
const adapter = seeded({
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
companyId: "company-1",
|
||||
identifier: "MCK-1",
|
||||
title: "Active task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "high",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: "actor-1",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
companyId: "company-1",
|
||||
identifier: "MCK-2",
|
||||
title: "Other task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "low",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: "actor-1",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
|
||||
await expect(adapter.tryApplyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "cross-task-write",
|
||||
command: { kind: "report_progress", taskId: "task-2", body: "Not allowed." },
|
||||
})).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "task_scope_violation" },
|
||||
});
|
||||
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "review-once",
|
||||
command: { kind: "request_review", summary: "Ready for review." },
|
||||
});
|
||||
await expect(adapter.tryApplyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "review-twice",
|
||||
command: { kind: "request_review", summary: "Still ready." },
|
||||
})).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "task_transition_illegal" },
|
||||
});
|
||||
expect(adapter.snapshot().comments.map((comment) => comment.body)).toEqual([
|
||||
"Ready for review.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects stale interaction targets and invalid interaction outcomes", async () => {
|
||||
const adapter = seeded();
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun({
|
||||
...OPEN,
|
||||
capabilities: ["control_plane:interactions"],
|
||||
});
|
||||
const first = await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "plan-v1",
|
||||
command: {
|
||||
kind: "write_document",
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
body: "Version one",
|
||||
baseRevisionId: null,
|
||||
},
|
||||
});
|
||||
const firstRevisionId = first.entityRefs.find((ref) => ref.startsWith("revision:"))!.slice(9);
|
||||
const second = await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "plan-v2",
|
||||
command: {
|
||||
kind: "write_document",
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
body: "Version two",
|
||||
baseRevisionId: firstRevisionId,
|
||||
},
|
||||
});
|
||||
const secondRevisionId = second.entityRefs.find((ref) => ref.startsWith("revision:"))!.slice(9);
|
||||
|
||||
await expect(adapter.tryApplyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "stale-confirmation",
|
||||
command: {
|
||||
kind: "request_human_input",
|
||||
interactionKind: "confirmation",
|
||||
title: "Confirm",
|
||||
prompt: "Accept?",
|
||||
targetRevisionId: firstRevisionId,
|
||||
continuationPolicy: "wake_assignee",
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "interaction_target_stale" },
|
||||
});
|
||||
|
||||
const interaction = await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "current-confirmation",
|
||||
command: {
|
||||
kind: "request_human_input",
|
||||
interactionKind: "confirmation",
|
||||
title: "Confirm",
|
||||
prompt: "Accept?",
|
||||
targetRevisionId: secondRevisionId,
|
||||
continuationPolicy: "wake_assignee",
|
||||
},
|
||||
});
|
||||
const interactionId = interaction.entityRefs.find((ref) => ref.startsWith("interaction:"))!.slice(12);
|
||||
await expect(adapter.tryApplyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "invalid-confirmation-answer",
|
||||
command: {
|
||||
kind: "resolve_human_input",
|
||||
interactionId,
|
||||
outcome: "answered",
|
||||
result: { answer: "yes" },
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "interaction_outcome_invalid" },
|
||||
});
|
||||
});
|
||||
|
||||
it("performs complete mock operations without any Paperclip network request", async () => {
|
||||
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
|
||||
new Error("network access is forbidden in the mock control plane"),
|
||||
);
|
||||
try {
|
||||
const adapter = seeded();
|
||||
await adapter.start();
|
||||
await adapter.openFixtureRun(OPEN);
|
||||
await adapter.applyCommand({
|
||||
runId: "run-1",
|
||||
idempotencyKey: "offline-progress",
|
||||
command: { kind: "report_progress", body: "Committed offline." },
|
||||
});
|
||||
const restored = Phase7MockControlPlaneAdapter.restore(adapter.serialize());
|
||||
expect(restored.snapshot().comments).toMatchObject([{ body: "Committed offline." }]);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
fetch.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("implements the existing ControlPlanePort conformance contract", async () => {
|
||||
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
||||
const adapter = new Phase7MockControlPlaneAdapter({
|
||||
company: {
|
||||
id: identity.companyId,
|
||||
name: "Conformance Company",
|
||||
issuePrefix: "CNF",
|
||||
status: "active",
|
||||
budgetId: "budget-company-conformance",
|
||||
},
|
||||
actors: [
|
||||
{
|
||||
id: identity.agentId,
|
||||
companyId: identity.companyId,
|
||||
name: "Conformance Actor",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
budgetId: "budget-actor-conformance",
|
||||
capabilityGrants: [],
|
||||
},
|
||||
],
|
||||
tasks: [
|
||||
{
|
||||
id: identity.issueId,
|
||||
companyId: identity.companyId,
|
||||
identifier: "CNF-1",
|
||||
title: "Conformance task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: identity.agentId,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
],
|
||||
budgets: [
|
||||
{
|
||||
id: "budget-company-conformance",
|
||||
scope: "company",
|
||||
scopeId: identity.companyId,
|
||||
limitCents: 100,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
},
|
||||
{
|
||||
id: "budget-actor-conformance",
|
||||
scope: "actor",
|
||||
scopeId: identity.agentId,
|
||||
limitCents: 100,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(runControlPlanePortConformance({ port: adapter, start: () => adapter.start() }))
|
||||
.resolves.toMatchObject({
|
||||
eventCount: 3,
|
||||
terminalReplayIdempotent: true,
|
||||
openBindingRejected: true,
|
||||
eventIdMutationRejected: true,
|
||||
eventSequenceMutationRejected: true,
|
||||
replayBindingRejected: true,
|
||||
resultMutationRejected: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses stable typed errors for retryable scripted failures", async () => {
|
||||
const adapter = seeded({
|
||||
faults: [
|
||||
{
|
||||
id: "retry-open",
|
||||
operation: "open_run",
|
||||
effect: "retryable_error",
|
||||
remaining: 1,
|
||||
code: "fixture_transient",
|
||||
},
|
||||
],
|
||||
});
|
||||
await adapter.start();
|
||||
const error = await adapter.openFixtureRun(OPEN).catch((caught) => caught);
|
||||
expect(error).toBeInstanceOf(Phase7MockControlPlaneError);
|
||||
expect(error).toMatchObject({ code: "fixture_transient", retryable: true });
|
||||
await expect(adapter.openFixtureRun(OPEN)).resolves.toMatchObject({
|
||||
activeTask: { id: "task-1", status: "in_progress" },
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user