mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
## Thinking Path > - Paperclip manages work done by AI agents. > - Native execution reports work completion through a saved result. > - Some native providers can build that result from their final message. > - This path can bypass the finish tool's publication check. > - A task can then be marked done without the document the user requested. > - This change checks publication again at finalization and status commit. > - Missing output uses the existing bounded repair path with specific feedback. ## Linked Issues or Issue Description **What happened?** A native provider returned a final message about a local Markdown briefing. It had not published a task document or a verified attachment. Its synthetic result claimed success. The server accepted the claim and marked the task done. A real PostgreSQL regression reproduced this on public master. **Expected behavior** A requested task document must have valid saved publication evidence before the task becomes done. The check must apply even when the provider does not call the finish tool. A plain answer still needs no document. An explicit document-free fallback requires the actual human rejection for the current unchanged request. **Steps to reproduce** 1. Request a Markdown briefing document on a task. 2. Complete a native turn with a synthetic `provider:opencode:agent-message` result and no published output. 3. Admit the result through the normal control-plane port and finalize it. 4. The old code marks the task done. The repaired code keeps it open and queues one repair with publication instructions. **Paperclip version or commit** Reproduced from public master `7d1e9b3477f12780e987c32fb8191d476c210fda`. Related: #18661 added the finish-tool publication guard. This change applies that guard to finalization. #18180 covers a different fallback problem: no final text. This change preserves native provider fallback generation and its saved claims. ## What Changed - Run the existing deliverable guard before accepting a native done decision. Keep the original provider result unchanged. - Route missing publication through the existing single incomplete-result continuation. Include the publication feedback in the wake. A repeated incomplete result uses the existing named recovery action. - Inspect publication through a typed safe projection of the accepted ledger. Keep whole-protocol validation at result admission. An authoritative done decision must pass publication checks even when its accepted ledger is partial. - Recheck publication under the status-commit transaction. Hold only matching proof rows with non-waiting share locks. Changed or locked proof uses the existing bounded status replanning path. - Permit an explicit document-free decline fallback only for the saved human rejection selected by the current continuation. Require the same original request, current saved description, source revision and source contract. Model prose or an unrelated old decision is insufficient. - Add real database controls for publication, plain answers, authenticated decline, stale or wrong decisions, bounded recovery, and deletion between assessment and commit. ## Verification - The first regression failed before the repair: the synthetic result marked an unpublished task done. The retained real PostgreSQL run took 21.846 seconds, including setup and cleanup. - Two description-edit regressions also failed before their repair: a description change after admission or at the commit boundary still let the old decline waive the newly required document. Both now keep the task open. The red run took 25.738 seconds. - The commit-race regression also failed before its repair: removing publication after assessment still allowed done. That run took 17.448 seconds. - All thirteen focused real database controls pass after the repair. They exercise the normal control-plane port, finalizer, semantic publication tools, saved human rejection and status transaction. Owned database processes, data and output storage are removed after each run. - All 469 tests across the seven relevant and affected suites pass on head `f8870469f9879a53551f0faaff0fe5b1234ea303`, based on master `c7390bef20`. The six-suite run passed 426 tests in 97.319 seconds including owned setup and cleanup. The unchanged reassessment-worker suite passed 43 tests in 48.901 seconds. All required current-head checks pass in [CI run 41937653209](https://github.com/paperclipai/paperclip/actions/runs/41937653209), including aggregate verify, typechecking, build, general and serialized tests, native Runner tests and all eight browser shards. Fresh Greptile review is 5/5 on this exact head, with no open findings. - Local scoped server typechecking reported no errors in the four changed files and 11 errors in reused dependency declarations and exports. That setup failure remains retained. Fresh current-head CI passes the full Typecheck + Release Registry gate and Build. Local typecheck is not claimed as passing. - CI on the previous head found one shared status-projection regression across Studio, routine, status-authority and worker controls. Those phases use opaque accepted ledgers. The correction inspects only publication references, preserves the accepted marker and bindings, and forces the publication check for an authoritative done decision. A new real commit control verifies that an empty accepted ledger cannot close a requested-document task. All original affected assertions remain unchanged. The retained failed run is [41934314437](https://github.com/paperclipai/paperclip/actions/runs/41934314437). - No provider call, paid campaign, staging deployment or eval regrade is part of this PR. No model, timeout, assertion, workflow, dependency or lockfile change. Focused suites: ```sh ./node_modules/.bin/vitest run --config "$PWD/server/vitest.config.ts" --root server \ src/services/native-runtime/paperclip-control-plane-port.test.ts \ src/services/native-runtime/native-completion-feedback.test.ts \ src/services/native-runtime/native-deliverable-feedback.test.ts \ src/services/native-runtime/native-skill-test-status.test.ts \ src/services/native-runtime/native-routine-status.test.ts \ src/__tests__/native-status-arbiter-corpus.test.ts \ src/__tests__/native-status-reassessment-work.test.ts ``` ## Risks A native result that previously closed a task without its requested output now leaves it open. The existing repair path can run once and then records a named recovery action. Database contention does not grant completion. The existing first-task decision gate still runs first. The conditional decline check is deliberately conservative; unmatched wording or missing authoritative source data does not waive publication. This change closes a false completion path. It does not establish why a provider omitted publication, prove a live provider repair, or qualify a full evaluation plan. ## Model Used OpenAI Codex, based on GPT-6, assisted with diagnosis, code and controls. The exact serving model identifier and context-window size are not exposed in this session. Capabilities used: repository inspection, code editing, local code execution and GitHub tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal issue identity - [x] I have run relevant tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have documented the behavior and limits in this PR; no operator command or public API changed - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open findings - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
3184 lines
178 KiB
TypeScript
3184 lines
178 KiB
TypeScript
import { nativeResponsePresentationWork } from "./native-response-presentation-work.js";
|
|
import { resolveExternalTestDatabaseUrl } from "../../__tests__/helpers/external-test-database.js";
|
|
import { plugins } from "@paperclipai/db";
|
|
import { readTaskPanelActivations } from "../plugin-task-panels.js";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { and, asc, eq, sql } from "drizzle-orm";
|
|
import { createHash, generateKeyPairSync, randomUUID } from "node:crypto";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { renderPaperclipWakePrompt } from "@paperclipai/adapter-utils/server-utils";
|
|
import { ONBOARDING_FIRST_TASK_ORIGIN_KIND } from "@paperclipai/shared";
|
|
import { readCompletedAssistantMessageCandidate, resolveHeartbeatRunResponse, selectHeartbeatRunFinalAgentMessage } from "../heartbeat-run-summary.js";
|
|
import {
|
|
activityLog,
|
|
agentWakeupRequests,
|
|
agents,
|
|
assets,
|
|
authUsers,
|
|
companies,
|
|
companyMemberships,
|
|
completionContracts,
|
|
createDb,
|
|
heartbeatRunEvents,
|
|
heartbeatRuns,
|
|
issueComments,
|
|
issueRecoveryActions,
|
|
issueRelations,
|
|
issueThreadInteractions,
|
|
issueWorkProducts,
|
|
issues,
|
|
nativeRunFinalizations,
|
|
nativeRunResults,
|
|
statusDecisionEffects,
|
|
statusDecisions,
|
|
workAssessments,
|
|
} from "@paperclipai/db";
|
|
import {
|
|
executeNativeSession,
|
|
parseNativeExecutionInput,
|
|
type NativeSessionBackend,
|
|
type PrpEvent,
|
|
type PrpStructuredRunResult,
|
|
type PrpTerminalState,
|
|
} from "../../vendor/paperclip-runner/index.js";
|
|
import {
|
|
CONTROL_PLANE_CONFORMANCE_RESULT,
|
|
CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
CONTROL_PLANE_CONFORMANCE_OPEN,
|
|
runControlPlanePortConformance,
|
|
} from "../../vendor/paperclip-runner/testing.js";
|
|
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
|
import { startRecoveryPostgres } from "../../__tests__/helpers/recovery-postgres.js";
|
|
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
|
|
import { nativeSha256 } from "./canonical.js";
|
|
import { NativeRunCoordinatorStore } from "./native-run-coordinator-store.js";
|
|
import { finalizeNativeRun, repairCommittedNativeChatResponse } from "./native-run-finalizer.js";
|
|
import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js";
|
|
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
|
|
import { documentService } from "../documents.js";
|
|
import { claimNativeReviewExecutionLock, getNativeReviewAssignment } from "./native-review-participant.js";
|
|
import { claimQueuedNativeReviewRun } from "./native-review-dispatch.js";
|
|
import { stageNativeRunnerWakeAttachments } from "./native-runner-file-handoff.js";
|
|
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
|
import { nativeCompletionFeedback } from "./native-completion-feedback.js";
|
|
import { ensureNativeCompletionContract, nativeCompletionSource } from "./completion-contracts.js";
|
|
import { commitNativeStatusDecision } from "./status-decision-committer.js";
|
|
import { NATIVE_STATUS_ARBITER_POLICY_VERSION } from "./status-arbiter.js";
|
|
import { createStorageService } from "../../storage/service.js";
|
|
import { createLocalDiskStorageProvider } from "../../storage/local-disk-provider.js";
|
|
import { buildOnboardingFirstTaskOpeningQuestion, ONBOARDING_FIRST_TASK_OPENING_QUESTION_ID } from "../onboarding-first-task-assets.js";
|
|
import { assertCurrentWakeCommentsRead } from "./current-wake-comments.js";
|
|
import { buildExecutionContinuation } from "../execution-continuation.js";
|
|
import { childReviewOutcomes } from "./child-review-outcomes.js";
|
|
import { createPostgresRunDispatchAdapter } from "../../modules/run-dispatch/adapters/postgres.js";
|
|
import { createGovernedWaitEventObservation, materializeRuntimeQuestionFallback, nativeGovernedWaitResult } from "./native-session-executor.js";
|
|
import {
|
|
subscribeAllCompanyLiveEvents,
|
|
subscribeCompanyLiveEvents,
|
|
} from "../live-events.js";
|
|
|
|
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_CONTROL_PLANE_TEST_DATABASE_URL");
|
|
|
|
describe("PaperclipControlPlanePort conformance", () => {
|
|
let temporary: Awaited<ReturnType<typeof startRecoveryPostgres>> | null = null;
|
|
let outputStorageRoot: string;
|
|
let db: ReturnType<typeof createDb>;
|
|
const contractId = "00000000-0000-4000-8000-000000000004";
|
|
const contractSha = "phase6-conformance-contract";
|
|
const taskIssueId = "00000000-0000-4000-8000-000000000008";
|
|
const taskContractId = "00000000-0000-4000-8000-000000000009";
|
|
const taskRunId = "00000000-0000-4000-8000-000000000010";
|
|
const taskSessionId = "00000000-0000-4000-8000-000000000018";
|
|
const taskWorkProductId = "00000000-0000-4000-8000-000000000017";
|
|
const workspaceFailureIssueId = "00000000-0000-4000-8000-000000000011";
|
|
const workspaceFailureContractId = "00000000-0000-4000-8000-000000000012";
|
|
const workspaceFailureRunId = "00000000-0000-4000-8000-000000000013";
|
|
const governanceIssueId = "00000000-0000-4000-8000-000000000014";
|
|
const governanceContractId = "00000000-0000-4000-8000-000000000015";
|
|
const governanceRunId = "00000000-0000-4000-8000-000000000016";
|
|
const conformanceRunnerId = "00000000-0000-4000-8000-000000000005";
|
|
const taskRunnerId = "00000000-0000-4000-8000-000000000019";
|
|
const workspaceFailureRunnerId = "00000000-0000-4000-8000-000000000020";
|
|
const governanceRunnerId = "00000000-0000-4000-8000-000000000021";
|
|
|
|
beforeAll(async () => {
|
|
outputStorageRoot = await mkdtemp(join(tmpdir(), "paperclip-native-port-output-"));
|
|
if (external) db = createDb(external);
|
|
else {
|
|
temporary = await startRecoveryPostgres("paperclip-native-port-");
|
|
db = createDb(temporary.connectionString);
|
|
}
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
await db.insert(companies).values({ id: identity.companyId, name: "Phase 6", issuePrefix: "P6C" });
|
|
await db.insert(agents).values({
|
|
id: identity.agentId,
|
|
companyId: identity.companyId,
|
|
name: "Native conformance",
|
|
adapterType: "codex_local",
|
|
status: "running",
|
|
});
|
|
await db.insert(issues).values({
|
|
id: identity.issueId,
|
|
companyId: identity.companyId,
|
|
title: "Native conformance",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: contractId,
|
|
companyId: identity.companyId,
|
|
issueId: identity.issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Conformance",
|
|
criteria: [{ id: "objective", requirement: "Complete conformance" }],
|
|
},
|
|
canonicalSha256: contractSha,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: conformanceRunnerId,
|
|
completionContractId: contractId,
|
|
completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
await db.insert(issues).values({
|
|
id: taskIssueId,
|
|
companyId: identity.companyId,
|
|
title: "Complete one native task",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: taskContractId,
|
|
companyId: identity.companyId,
|
|
issueId: taskIssueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Complete one native task",
|
|
criteria: [{ id: "objective", requirement: "Complete the task" }],
|
|
},
|
|
canonicalSha256: "phase6-task-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: taskRunId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
runtimeModeResolverVersion: "phase6-v1",
|
|
runtimeModeReason: "eligible_opt_in",
|
|
nativeIssueId: taskIssueId,
|
|
nativeSessionId: taskSessionId,
|
|
runnerInstanceId: taskRunnerId,
|
|
completionContractId: taskContractId,
|
|
completionContractSha256: "phase6-task-contract",
|
|
contextSnapshot: { issueId: taskIssueId },
|
|
});
|
|
await db.insert(issueWorkProducts).values({
|
|
id: taskWorkProductId,
|
|
companyId: identity.companyId,
|
|
issueId: taskIssueId,
|
|
type: "artifact",
|
|
provider: "paperclip",
|
|
title: "Verified native task result",
|
|
status: "ready_for_review",
|
|
reviewState: "approved",
|
|
createdByRunId: taskRunId,
|
|
});
|
|
await db.insert(issues).values({
|
|
id: workspaceFailureIssueId,
|
|
companyId: identity.companyId,
|
|
title: "Preserve status after workspace failure",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: workspaceFailureContractId,
|
|
companyId: identity.companyId,
|
|
issueId: workspaceFailureIssueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Preserve status after workspace failure",
|
|
criteria: [{ id: "objective", requirement: "Preserve the result" }],
|
|
},
|
|
canonicalSha256: "phase6-workspace-failure-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: workspaceFailureRunId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: workspaceFailureIssueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: workspaceFailureRunnerId,
|
|
completionContractId: workspaceFailureContractId,
|
|
completionContractSha256: "phase6-workspace-failure-contract",
|
|
contextSnapshot: { issueId: workspaceFailureIssueId },
|
|
});
|
|
await db.insert(issues).values({
|
|
id: governanceIssueId,
|
|
companyId: identity.companyId,
|
|
title: "Respect pending governance",
|
|
status: "in_review",
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: governanceContractId,
|
|
companyId: identity.companyId,
|
|
issueId: governanceIssueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Respect pending governance",
|
|
criteria: [{ id: "objective", requirement: "Respect pending governance" }],
|
|
},
|
|
canonicalSha256: "phase6-governance-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: governanceRunId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: governanceIssueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: governanceRunnerId,
|
|
completionContractId: governanceContractId,
|
|
completionContractSha256: "phase6-governance-contract",
|
|
contextSnapshot: { issueId: governanceIssueId },
|
|
});
|
|
await db.insert(issueThreadInteractions).values({
|
|
companyId: identity.companyId,
|
|
issueId: governanceIssueId,
|
|
kind: "request_confirmation",
|
|
status: "pending",
|
|
payload: { version: 1, prompt: "Approve completion?" },
|
|
});
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
if (temporary) {
|
|
await db.delete(activityLog);
|
|
await db.update(heartbeatRuns).set({ wakeupRequestId: null });
|
|
await db.delete(agentWakeupRequests);
|
|
await db.delete(statusDecisionEffects);
|
|
await db.delete(nativeRunFinalizations);
|
|
await db.delete(statusDecisions);
|
|
await db.delete(workAssessments);
|
|
await db.delete(nativeRunResults);
|
|
await db.delete(issueThreadInteractions);
|
|
await db.delete(issueWorkProducts);
|
|
await db.delete(issueRelations);
|
|
await db.delete(heartbeatRunEvents);
|
|
await db.delete(heartbeatRuns);
|
|
await db.delete(completionContracts);
|
|
await db.delete(issues);
|
|
await db.delete(assets);
|
|
await db.delete(agents);
|
|
await db.delete(companyMemberships);
|
|
await db.delete(companies);
|
|
await db.delete(authUsers);
|
|
await temporary.cleanup();
|
|
}
|
|
await rm(outputStorageRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it.each(["settles", "checkpoint timeout", "unscoped terminal"] as const)("retains a durably recorded governed terminal while its postcommit observer is pending: %s", async mode => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, issueId: randomUUID(), runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
const fixtureContractId = randomUUID();
|
|
await db.insert(issues).values({ id: identity.issueId, companyId: identity.companyId, title: "Human-only waiting checkpoint",
|
|
status: "in_progress", assigneeAgentId: identity.agentId });
|
|
const [baseContract] = await db.select().from(completionContracts).where(eq(completionContracts.id, contractId));
|
|
await db.insert(completionContracts).values({ ...baseContract!, id: fixtureContractId, issueId: identity.issueId });
|
|
await db.insert(heartbeatRuns).values({ id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId, nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: runnerId, completionContractId: fixtureContractId, completionContractSha256: contractSha });
|
|
const interaction = await issueThreadInteractionService(db).create({ id: identity.issueId, companyId: identity.companyId }, {
|
|
kind: "ask_user_questions", title: "Choose the format", resolverPolicy: "human_only", continuationPolicy: "none",
|
|
sourceRunId: identity.runId, payload: { version: 1, title: "Choose the format", questions: [{ id: "format",
|
|
prompt: "Which format?", required: true, selectionMode: "single", options: [{ id: "onsite", label: "On site" }] }] },
|
|
}, { agentId: identity.agentId, runId: identity.runId });
|
|
let releaseObserver!: () => void;
|
|
const observerGate = new Promise<void>(resolve => { releaseObserver = resolve; });
|
|
let terminalWasRecorded = false;
|
|
let terminalRecorded!: () => void;
|
|
const recorded = new Promise<void>(resolve => { terminalRecorded = resolve; });
|
|
let observerFinished!: () => void;
|
|
const observerDone = new Promise<void>(resolve => { observerFinished = resolve; });
|
|
const observation = createGovernedWaitEventObservation(async () => {
|
|
const [pending] = await db.select().from(issueThreadInteractions).where(and(
|
|
eq(issueThreadInteractions.id, interaction.id), eq(issueThreadInteractions.companyId, identity.companyId),
|
|
eq(issueThreadInteractions.sourceRunId, identity.runId), eq(issueThreadInteractions.status, "pending"),
|
|
));
|
|
return pending?.status === "pending" && pending.sourceRunId === identity.runId
|
|
? nativeGovernedWaitResult({ interaction: pending, completionContract: contract }) : null;
|
|
});
|
|
let triggerConsumed = false;
|
|
const port = new PaperclipControlPlanePort(db, { ...identity,
|
|
completionContractId: fixtureContractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: "park-control" }, {
|
|
async onCommittedEvent(event) {
|
|
await observation.observe(event, event.eventType === "item.completed");
|
|
if (event.eventType === "turn.interrupted") { terminalWasRecorded = true; terminalRecorded(); try { await observerGate; } finally { observerFinished(); } }
|
|
},
|
|
});
|
|
const contract = { revision: "phase6-v1", objective: "Conformance", criteria: [{ id: "objective", requirement: "Complete conformance" }] };
|
|
const yielded = nativeGovernedWaitResult({ interaction: { id: interaction.id,
|
|
title: interaction.title ?? null, summary: interaction.summary ?? null }, completionContract: contract });
|
|
const event = (sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent => ({
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:${sourceSeq}`, sourceInstanceId: runnerId, sourceSeq,
|
|
sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId, ...(mode === "unscoped terminal" ? {} : { turnId: "park-turn" }),
|
|
eventType, schemaVersion: 1, priority: 0, emittedAt: new Date().toISOString(), payload,
|
|
});
|
|
let starts = 0, terminalEmitted = false;
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "mock", name: "park-observer", version: "1",
|
|
capabilities: { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true } }; },
|
|
async openSession() { return { identity: () => identity,
|
|
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async startTurn() { starts++; return { turnId: "park-turn" }; },
|
|
cancel() { return { cleanup: Promise.resolve() }; },
|
|
async *events() { yield event(1, "item.completed", { kind: "dynamicToolCall" }); terminalEmitted = true; yield event(2, "turn.interrupted", { status: "interrupted", error: null }); },
|
|
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
activeTurnId: terminalEmitted ? null : "park-turn", cursor: terminalEmitted ? "2" : "1" }; },
|
|
async result() { return null; }, async close() {},
|
|
}; },
|
|
};
|
|
const options = { backend, controlPlane: port, timeoutMs: 2_000,
|
|
...(mode === "checkpoint timeout" ? { checkpointTimeoutMs: 100 } : {}),
|
|
runnerInstanceId: runnerId, controlPlaneInstanceId: "park-control",
|
|
input: parseNativeExecutionInput({ schema: "paperclip.native-execution-input.v1", binding: {
|
|
companyId: identity.companyId, runId: identity.runId, issueId: identity.issueId, agentId: identity.agentId, executionWorkspaceId: "park-workspace" },
|
|
task: { identifier: "PARK-1", title: "Wait for a human", description: null, prompt: "Wait for a human", workMode: "standard" },
|
|
workspace: { cwd: process.cwd(), repoUrl: null, repoRef: null, branchName: null },
|
|
session: { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1 },
|
|
provider: { kind: "codex", model: null }, interactionResponses: [], credentialBindings: [],
|
|
completionContract: { id: fixtureContractId, sha256: contractSha, schemaVersion: "paperclip.completion-contract.v1", contract } }),
|
|
resolveGovernedWait: ({ event }: { event: PrpEvent }) => {
|
|
const result = observation.consume(event);
|
|
triggerConsumed ||= result !== null;
|
|
return result;
|
|
},
|
|
};
|
|
if (mode === "unscoped terminal") releaseObserver();
|
|
const execution = executeNativeSession(options);
|
|
const outcome = execution.then(value => ({ value }), error => ({ error }));
|
|
try {
|
|
await Promise.race([recorded, outcome.then(result => { if ("error" in result) throw result.error; throw new Error("run settled before recording terminal"); })]);
|
|
const rows = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId));
|
|
expect(rows.some(row => row.eventType === "turn.interrupted")).toBe(true);
|
|
expect(triggerConsumed).toBe(true);
|
|
if (mode !== "unscoped terminal") {
|
|
await new Promise(resolve => setTimeout(resolve, 700));
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).toEqual([]);
|
|
releaseObserver();
|
|
await observerDone;
|
|
if (mode === "settles") expect(await outcome).not.toHaveProperty("error");
|
|
else {
|
|
expect(await outcome).toMatchObject({ error: { message: "native session checkpoint timed out after 100ms" } });
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).toEqual([]);
|
|
const restored = new PaperclipControlPlanePort(db, { ...identity, completionContractId: fixtureContractId,
|
|
completionContractSha256: contractSha, sourceInstanceId: runnerId, controlPlaneSourceInstanceId: "park-control" });
|
|
const checkpoint = await restored.loadSessionCheckpoint();
|
|
expect(checkpoint?.governedWait?.sourceEvent).toMatchObject({ sourceSeq: 1, turnId: "park-turn" });
|
|
const session = await backend.openSession({ identity });
|
|
const recovering = { ...backend, async recoverSession() { return { recovered: true as const, session }; } };
|
|
await expect(executeNativeSession({ ...options, checkpointTimeoutMs: 1_000, controlPlane: restored,
|
|
backend: recovering, persistedSession: checkpoint })).resolves.toMatchObject({ result: yielded });
|
|
}
|
|
} else {
|
|
expect(await outcome).not.toHaveProperty("error");
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).toHaveLength(1);
|
|
}
|
|
expect(starts).toBe(1);
|
|
expect((await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, interaction.id)))[0]).toMatchObject({
|
|
status: "pending", result: null, effectiveResolverPolicy: "human_only", continuationPolicy: "none", sourceRunId: identity.runId,
|
|
});
|
|
} finally { releaseObserver(); await outcome; if (terminalWasRecorded) await observerDone; }
|
|
});
|
|
|
|
async function terminalObserverFixture(options: {
|
|
onCommittedEvent?: (event: PrpEvent) => Promise<void>;
|
|
onDuplicateEvent?: (event: PrpEvent) => Promise<void>;
|
|
assertControllerActive?: () => void;
|
|
} = {}) {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({ id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId, nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: runnerId, completionContractId: contractId, completionContractSha256: contractSha });
|
|
const binding = { ...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: "observer-control" };
|
|
const port = new PaperclipControlPlanePort(db, binding, options);
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const event: PrpEvent = { schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceInstanceId: runnerId,
|
|
sourceSeq: 1, sourceKind: "runner", runId: identity.runId, normalizedSessionId: identity.sessionId,
|
|
turnId: "observer-turn", eventType: "turn.interrupted", schemaVersion: 1, priority: 0,
|
|
emittedAt: new Date().toISOString(), payload: { status: "interrupted", error: null } };
|
|
const snapshot = { backendKind: "mock" as const, sessionId: identity.sessionId, identity };
|
|
const result = { result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL };
|
|
return { identity, runnerId, binding, port, event, snapshot, result };
|
|
}
|
|
|
|
it.each(["default", "deferred"] as const)("keeps %s terminal observation owned before checkpoint and result acceptance", async mode => {
|
|
let release!: () => void;
|
|
const gate = new Promise<void>(resolve => { release = resolve; });
|
|
let observed = 0;
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { observed++; await gate; } });
|
|
const signal = new AbortController().signal;
|
|
let appendSettled = false;
|
|
const append = f.port.appendEvent(f.event, { signal, ...(mode === "deferred" ? { deferPostCommitObservers: true } : {}) })
|
|
.then(receipt => { appendSettled = true; return receipt; });
|
|
let checkpointSettled = false, resultSettled = false;
|
|
try {
|
|
if (mode === "deferred") {
|
|
expect(await append).toMatchObject({ disposition: "committed" });
|
|
const checkpoint = f.port.checkpointSession(f.snapshot, { signal }).then(() => { checkpointSettled = true; });
|
|
const result = f.port.completeRun(f.result, { signal }).then(() => { resultSettled = true; });
|
|
await new Promise(resolve => setTimeout(resolve, 30));
|
|
expect(checkpointSettled).toBe(false);
|
|
expect(resultSettled).toBe(false);
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.identity.runId)))[0]!.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
release(); await Promise.all([checkpoint, result]);
|
|
} else {
|
|
await new Promise(resolve => setTimeout(resolve, 30));
|
|
expect(appendSettled).toBe(false);
|
|
}
|
|
} finally { release(); await append; }
|
|
expect(observed).toBe(1);
|
|
});
|
|
|
|
it("returns the default committed receipt when cancellation arrives during its postcommit observer", async () => {
|
|
let release!: () => void, observed!: () => void;
|
|
const gate = new Promise<void>(resolve => { release = resolve; });
|
|
const started = new Promise<void>(resolve => { observed = resolve; });
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { observed(); await gate; } });
|
|
const controller = new AbortController();
|
|
const outcome = f.port.appendEvent(f.event, { signal: controller.signal }).then(receipt => ({ receipt }), error => ({ error }));
|
|
try {
|
|
await started;
|
|
expect(await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.identity.runId))).toHaveLength(1);
|
|
const cancellation = new Error("cancelled after event transaction committed");
|
|
controller.abort(cancellation);
|
|
release();
|
|
expect(await outcome).toMatchObject({ receipt: { disposition: "committed" } });
|
|
await expect(f.port.checkpointSession(f.snapshot, { signal: controller.signal })).rejects.toBe(cancellation);
|
|
await expect(f.port.completeRun(f.result, { signal: controller.signal })).rejects.toBe(cancellation);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
} finally { release(); await outcome; }
|
|
});
|
|
|
|
it("retains the original deferred observer rejection for checkpoint and result acceptance", async () => {
|
|
let reject!: (error: Error) => void;
|
|
const gate = new Promise<void>((_resolve, no) => { reject = no; });
|
|
const original = new Error("accounting persistence rejected");
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { await gate; } });
|
|
const signal = new AbortController().signal;
|
|
await f.port.appendEvent(f.event, { signal, deferPostCommitObservers: true });
|
|
const checkpoint = f.port.checkpointSession(f.snapshot, { signal }).catch(error => error);
|
|
const result = f.port.completeRun(f.result, { signal }).catch(error => error);
|
|
reject(original);
|
|
expect(await checkpoint).toBe(original); expect(await result).toBe(original);
|
|
await expect(f.port.completeRun(f.result)).rejects.toBe(original);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.identity.runId)))[0]!.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
});
|
|
|
|
it.each(["abort", "detach"] as const)("revokes a checkpoint and result waiting for deferred observation on %s", async reason => {
|
|
let release!: () => void;
|
|
const gate = new Promise<void>(resolve => { release = resolve; });
|
|
let detached = false;
|
|
const original = new Error("controller authority revoked");
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { await gate; },
|
|
assertControllerActive() { if (detached) throw original; } });
|
|
const controller = new AbortController();
|
|
await f.port.appendEvent(f.event, { signal: controller.signal, deferPostCommitObservers: true });
|
|
const checkpoint = f.port.checkpointSession(f.snapshot, { signal: controller.signal }).catch(error => error);
|
|
const result = f.port.completeRun(f.result, { signal: controller.signal }).catch(error => error);
|
|
if (reason === "abort") controller.abort(original); else detached = true;
|
|
release();
|
|
expect(await checkpoint).toBe(original); expect(await result).toBe(original);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.identity.runId)))[0]!.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
});
|
|
|
|
it("keeps a later observer error visible after an aborted checkpoint stops waiting", async () => {
|
|
let reject!: (error: Error) => void;
|
|
const gate = new Promise<void>((_resolve, no) => { reject = no; });
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { await gate; } });
|
|
const controller = new AbortController();
|
|
await f.port.appendEvent(f.event, { signal: controller.signal, deferPostCommitObservers: true });
|
|
const checkpoint = f.port.checkpointSession(f.snapshot, { signal: controller.signal }).catch(error => error);
|
|
const stop = new Error("checkpoint authority revoked");
|
|
controller.abort(stop);
|
|
expect(await checkpoint).toBe(stop);
|
|
const original = new Error("retained accounting failure after revocation");
|
|
reject(original);
|
|
await expect(f.port.completeRun(f.result)).rejects.toBe(original);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.identity.runId)))[0]!.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
});
|
|
|
|
it("joins the exact deferred observer on duplicate append and replays it after a new port opens", async () => {
|
|
let release!: () => void;
|
|
const gate = new Promise<void>(resolve => { release = resolve; });
|
|
let observed = 0, replayed = 0;
|
|
const f = await terminalObserverFixture({ async onCommittedEvent() { observed++; await gate; },
|
|
async onDuplicateEvent() { throw new Error("must join the original owned observation"); } });
|
|
const signal = new AbortController().signal;
|
|
await f.port.appendEvent(f.event, { signal, deferPostCommitObservers: true });
|
|
let duplicateSettled = false;
|
|
const duplicate = f.port.appendEvent(f.event, { signal }).then(value => { duplicateSettled = true; return value; });
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 30)); expect(duplicateSettled).toBe(false);
|
|
release(); expect(await duplicate).toMatchObject({ disposition: "duplicate" });
|
|
expect(observed).toBe(1);
|
|
const restored = new PaperclipControlPlanePort(db, f.binding, { async onDuplicateEvent() { replayed++; } });
|
|
await restored.openRun({ identity: f.identity, backendKind: "mock", sourceInstanceId: f.runnerId });
|
|
await expect(restored.appendEvent(f.event)).resolves.toMatchObject({ disposition: "duplicate" });
|
|
expect(replayed).toBe(1);
|
|
expect(await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.identity.runId))).toHaveLength(1);
|
|
} finally { release(); await duplicate; }
|
|
});
|
|
|
|
it.each(["company", "agent", "run", "session", "source", "failed", "unproven", "nonterminal", "unscoped"] as const)("does not grant deferred terminal authority to %s evidence", async fault => {
|
|
const f = await terminalObserverFixture();
|
|
const event = { ...f.event,
|
|
...(fault === "run" ? { runId: randomUUID() } : {}),
|
|
...(fault === "session" ? { normalizedSessionId: randomUUID() } : {}),
|
|
...(fault === "source" ? { sourceInstanceId: randomUUID() } : {}),
|
|
...(fault === "failed" ? { eventType: "turn.failed" as const } : {}),
|
|
...(fault === "unproven" ? { payload: { providerTerminalObserved: false } } : {}),
|
|
...(fault === "nonterminal" ? { eventType: "item.completed" as const } : {}),
|
|
...(fault === "unscoped" ? { turnId: undefined } : {}),
|
|
};
|
|
const port = fault === "company" || fault === "agent" ? new PaperclipControlPlanePort(db, { ...f.binding,
|
|
...(fault === "company" ? { companyId: randomUUID() } : { agentId: randomUUID() }) }) : f.port;
|
|
await expect(port.appendEvent(event, { signal: new AbortController().signal, deferPostCommitObservers: true })).rejects.toThrow();
|
|
expect(await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.identity.runId))).toEqual([]);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.identity.runId))).toEqual([]);
|
|
});
|
|
|
|
it("publishes native recovery metadata in the existing snapshot transaction and fences stale generations", async () => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
const owner = { ...identity, authorityRunId: identity.runId, bootId: randomUUID(), generation: 1, leaseOwner: "snapshot-owner" };
|
|
await db.insert(heartbeatRuns).values({ id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId, nativeSessionId: identity.sessionId,
|
|
runnerInstanceId: runnerId, completionContractId: contractId, completionContractSha256: contractSha,
|
|
runnerProfileJson: { unrelated: "preserve" } });
|
|
await db.insert(nativeRunFinalizations).values({ runId: identity.runId, companyId: identity.companyId, issueId: identity.issueId,
|
|
phase: "observed",
|
|
controllerBootId: owner.bootId, controllerGeneration: owner.generation, leaseOwner: owner.leaseOwner,
|
|
leaseExpiresAt: new Date(Date.now() + 60_000) });
|
|
const port = new PaperclipControlPlanePort(db, { ...identity, completionContractId: contractId,
|
|
completionContractSha256: contractSha, sourceInstanceId: runnerId, controlPlaneSourceInstanceId: "snapshot-control" }, {
|
|
nativeRecovery: { owner, compatibility: { configDigest: `sha256:${"a".repeat(64)}`, processConfigDigest: null } },
|
|
});
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const snapshot = { backendKind: "mock" as const, sessionId: identity.sessionId, identity };
|
|
const statements: string[] = [];
|
|
const previousDebug = db.$client.options.debug;
|
|
db.$client.options.debug = (_connection, statement) => { statements.push(statement); };
|
|
try { await port.checkpointSession(snapshot); }
|
|
finally { db.$client.options.debug = previousDebug; }
|
|
expect(statements.filter(statement => /^begin\b/i.test(statement.trim()))).toHaveLength(1);
|
|
expect(statements.filter(statement => /^update\s+"heartbeat_runs"/i.test(statement.trim()))).toHaveLength(1);
|
|
expect(statements.filter(statement => /^(?:with|select)\b/i.test(statement.trim()))).toHaveLength(1);
|
|
const [saved] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
expect(saved.runnerProfileJson).toMatchObject({ unrelated: "preserve", sessionCheckpoint: snapshot,
|
|
nativeControllerRecovery: { schema: "paperclip.native-controller-recovery.v1", snapshotSha256: nativeSha256(saved.runnerProfileJson?.sessionCheckpoint),
|
|
checkpoint: null, processConfigDigest: null } });
|
|
// Missing PRP is an explicit incomplete publication, never fabricated authority.
|
|
await db.update(nativeRunFinalizations).set({ controllerGeneration: 2 }).where(eq(nativeRunFinalizations.runId, identity.runId));
|
|
await expect(port.checkpointSession({ ...snapshot, providerSessionId: "stale" })).rejects.toThrow("checkpoint_owner_lost");
|
|
await expect(port.completeRun({ result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL }))
|
|
.rejects.toThrow("checkpoint_owner_lost");
|
|
const [unchanged] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
expect(unchanged.runnerProfileJson).toEqual(saved.runnerProfileJson);
|
|
// A valid coordinator lock does not certify a lease that expires while the
|
|
// subsequent run-row lock waits. Recheck time at the actual write boundary.
|
|
await db.update(nativeRunFinalizations).set({ controllerGeneration: 1, leaseExpiresAt: new Date(Date.now() + 150) })
|
|
.where(eq(nativeRunFinalizations.runId, identity.runId));
|
|
let locked!: () => void, release!: () => void;
|
|
const acquired = new Promise<void>(resolve => { locked = resolve; });
|
|
const held = new Promise<void>(resolve => { release = resolve; });
|
|
const blocker = db.transaction(async tx => {
|
|
await tx.select({ id: heartbeatRuns.id }).from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId)).for("update");
|
|
locked();
|
|
await held;
|
|
});
|
|
await acquired;
|
|
const waiting = port.checkpointSession(snapshot);
|
|
const rejected = expect(waiting).rejects.toThrow("checkpoint_owner_lost");
|
|
try { await new Promise(resolve => setTimeout(resolve, 200)); }
|
|
finally { release(); await blocker; }
|
|
await rejected;
|
|
});
|
|
|
|
it("permits only read-only admission while an existing startup drains", async () => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
});
|
|
let awaitingStartup = true;
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `draining-${identity.runId}`,
|
|
}, {
|
|
assertControllerActive: () => { throw new Error("controller_detached"); },
|
|
assertRunAdmissionActive: () => { if (!awaitingStartup) throw new Error("controller_detached"); },
|
|
});
|
|
const open = { identity, backendKind: "mock" as const, sourceInstanceId: runnerId };
|
|
await expect(port.openRun({ ...open, sourceInstanceId: randomUUID() })).rejects.toThrow("native_open_run_binding_mismatch");
|
|
await db.update(heartbeatRuns).set({ runnerInstanceId: randomUUID() }).where(eq(heartbeatRuns.id, identity.runId));
|
|
await expect(port.openRun(open)).rejects.toThrow("native_open_run_not_authorized");
|
|
await db.update(heartbeatRuns).set({ runnerInstanceId: runnerId }).where(eq(heartbeatRuns.id, identity.runId));
|
|
await expect(port.openRun(open)).resolves.toBeUndefined();
|
|
await expect(port.checkpointSession({ backendKind: "mock", sessionId: identity.sessionId, identity })).rejects.toThrow("controller_detached");
|
|
await expect(port.completeRun({ result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL })).rejects.toThrow("controller_detached");
|
|
awaitingStartup = false;
|
|
await expect(port.openRun(open)).rejects.toThrow("controller_detached");
|
|
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
expect(run.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["checkpoint", "detach"], ["result", "detach"], ["event", "detach"],
|
|
["checkpoint", "abort"], ["result", "abort"], ["event", "abort"],
|
|
["deferred event", "detach"], ["deferred event", "abort"],
|
|
] as const)("revokes an in-flight %s on %s", async (operation, reason) => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
const binding = { ...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `detached-${identity.runId}` };
|
|
let detached = false;
|
|
const controller = new AbortController();
|
|
const port = new PaperclipControlPlanePort(db, binding, {
|
|
assertControllerActive: () => { if (detached) throw new Error("controller_detached"); },
|
|
});
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const mutate = () => operation === "event" || operation === "deferred event" ? port.appendEvent({
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceSeq: 1,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, eventType: "turn.interrupted", schemaVersion: 1,
|
|
turnId: "revoked-turn",
|
|
priority: 0, emittedAt: new Date().toISOString(), payload: { reason: "governed_wait" },
|
|
}, { signal: controller.signal, ...(operation === "deferred event" ? { deferPostCommitObservers: true } : {}) }) : operation === "checkpoint"
|
|
? port.checkpointSession({ backendKind: "mock", sessionId: identity.sessionId, identity }, { signal: controller.signal })
|
|
: port.completeRun({ result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL }, { signal: controller.signal });
|
|
let pending: Promise<unknown> | undefined;
|
|
await db.transaction(async tx => {
|
|
await tx.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId)).for("update");
|
|
const [backend] = await tx.execute(sql`select pg_backend_pid() as pid`) as unknown as Array<{ pid: number }>;
|
|
pending = mutate().then(() => null, error => error);
|
|
let waiting = false;
|
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
const [state] = await db.execute(sql`select exists (
|
|
select 1 from pg_stat_activity where ${backend.pid} = any(pg_blocking_pids(pid))
|
|
) as waiting`) as unknown as Array<{ waiting: boolean }>;
|
|
if (state.waiting) { waiting = true; break; }
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
expect(waiting).toBe(true);
|
|
if (reason === "detach") detached = true;
|
|
else controller.abort(new Error("controller_detached"));
|
|
});
|
|
expect(await pending).toMatchObject({ message: "controller_detached" });
|
|
await expect(mutate()).rejects.toThrow("controller_detached");
|
|
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
expect(run.runnerProfileJson?.sessionCheckpoint).toBeUndefined();
|
|
expect(run.nextEventSeq).toBe(1);
|
|
expect(await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId))).toEqual([]);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).toEqual([]);
|
|
expect(await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, identity.runId))).toEqual([]);
|
|
// A new controller's independently authorized port can still settle it.
|
|
const replacement = new PaperclipControlPlanePort(db, binding);
|
|
await replacement.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
await expect(replacement.completeRun({ result: CONTROL_PLANE_CONFORMANCE_RESULT,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL })).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("persists panel activation under the authenticated run and recovers it across source replay", async () => {
|
|
await db.insert(plugins).values({ pluginKey: "example.live-view", packageName: "example-live-view", version: "1.0.0", status: "ready",
|
|
manifestJson: { ui: { slots: [{ type: "detailTab", id: "screen", entityTypes: ["agent"], taskSidePanel: { availabilityPath: "/agents/:entityId/ready", activation: { namespace: "example.runtime", activityKey: "desktop.used" } } }] } } as never });
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
const binding = { ...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `panel-${identity.runId}` };
|
|
const event: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceSeq: 1,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, eventType: "runtime.activity.recorded", schemaVersion: 1,
|
|
priority: 1, emittedAt: new Date().toISOString(),
|
|
payload: { namespace: "example.runtime", activityKey: "desktop.used" },
|
|
};
|
|
const port = new PaperclipControlPlanePort(db, binding);
|
|
const replayBinding = structuredClone(binding);
|
|
binding.runId = randomUUID();
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const notifications: string[] = [];
|
|
const unsubscribe = subscribeCompanyLiveEvents(identity.companyId, event => {
|
|
if (event.type === "heartbeat.run.event" && event.payload.runId === identity.runId) notifications.push(String(event.payload.eventType));
|
|
});
|
|
try { await port.appendEvent(event); } finally { unsubscribe(); }
|
|
expect(notifications).toEqual(["runtime.activity.recorded"]);
|
|
expect(await readTaskPanelActivations(db, identity.companyId, identity.runId)).toEqual([{ pluginKey: "example.live-view", slotId: "screen" }]);
|
|
const restarted = new PaperclipControlPlanePort(db, replayBinding);
|
|
expect((await restarted.appendEvent(event)).disposition).toBe("duplicate");
|
|
const activations = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId));
|
|
expect(activations.filter(row => row.eventType === "runtime.activity.recorded")).toHaveLength(1);
|
|
expect(await readTaskPanelActivations(db, identity.companyId, CONTROL_PLANE_CONFORMANCE_OPEN.identity.runId)).toEqual([]);
|
|
expect(await readTaskPanelActivations(db, randomUUID(), identity.runId)).toEqual([]);
|
|
});
|
|
|
|
it("omits API credentials from persisted tool events and checkpoints without changing wire results", async () => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
const observed: PrpEvent[] = [];
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `api-${identity.runId}`,
|
|
}, { onCommittedEvent: async event => { observed.push(event); } });
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const result = { operationId: "call_api", apiOperationId: "POST /api/companies/{companyId}/exports", callId: "private-export",
|
|
status: 200, ok: true, data: { credentials: "private-history-fixture-value" } };
|
|
const wireResult = { success: true, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
|
|
const event: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceSeq: 1,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, eventType: "item.completed", schemaVersion: 1,
|
|
priority: 0, emittedAt: new Date().toISOString(),
|
|
payload: { kind: "dynamicToolCall", item: { type: "tool_result", id: "private-export", tool_use_id: "private-export", result: wireResult } },
|
|
};
|
|
await port.appendEvent(event);
|
|
expect((await port.appendEvent(event)).disposition).toBe("duplicate");
|
|
expect(port.normalizeReplayPayload(event.payload)).toEqual(observed[0]?.payload);
|
|
expect(port.normalizeReplayPayload(observed[0]!.payload)).toEqual(observed[0]?.payload);
|
|
await expect(port.appendEvent({ ...event, payload: { ...event.payload, item: {
|
|
...(event.payload.item as object), result: { success: true, contentItems: [{ type: "inputText",
|
|
text: JSON.stringify({ ...result, data: { credentials: "different-history-fixture-value" } }),
|
|
}] },
|
|
} } })).rejects.toThrow();
|
|
await port.checkpointSession({ backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
semanticResult: { ...CONTROL_PLANE_CONFORMANCE_RESULT, summary: JSON.stringify(result) },
|
|
});
|
|
const persisted = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId));
|
|
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
for (const saved of [persisted, observed, run.runnerProfileJson]) {
|
|
expect(JSON.stringify(saved)).not.toContain("private-history-fixture-value");
|
|
expect(JSON.stringify(saved)).toContain("apiPayloadRedacted");
|
|
}
|
|
expect(result.data.credentials).toBe("private-history-fixture-value");
|
|
});
|
|
|
|
it.each(["call_api", "create_case"])("retains bounded %s schema-denial guidance through persistence and replay", async operationId => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
const observed: PrpEvent[] = [];
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `api-${identity.runId}`,
|
|
}, { onCommittedEvent: async event => { observed.push(event); } });
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
const payload = {
|
|
provider: "codex", code: "semantic_tool_denied", operationId, callId: "schema-denied",
|
|
message: `provider arguments for ${operationId} failed JSON Schema validation`, paperclipExecuted: false,
|
|
inputSchemaGuidance: "Invalid tool arguments. Check these input schema locations: /required (missing title).",
|
|
};
|
|
const event: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceSeq: 1,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, eventType: "harness.diagnostic", schemaVersion: 1,
|
|
priority: 0, emittedAt: new Date().toISOString(), payload,
|
|
debug: { operationId, data: { value: "private-diagnostic-debug" } },
|
|
};
|
|
await port.appendEvent(event);
|
|
expect(observed[0]?.payload).toEqual(payload);
|
|
expect(observed[0]?.debug).toMatchObject({ apiPayloadRedacted: true });
|
|
expect(JSON.stringify(observed[0])).not.toContain("private-diagnostic-debug");
|
|
expect((await port.appendEvent(event)).disposition).toBe("duplicate");
|
|
expect(port.normalizeReplayPayload(payload)).toEqual(port.normalizeReplayPayload(observed[0]!.payload));
|
|
const saved = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId));
|
|
expect(saved).toHaveLength(1);
|
|
expect((saved[0]!.payload?.prpEvent as unknown as PrpEvent).payload).toEqual(payload);
|
|
expect(JSON.stringify(saved)).not.toContain("private-diagnostic-debug");
|
|
await expect(port.appendEvent({ ...event, payload: {
|
|
...payload, inputSchemaGuidance: "Invalid tool arguments. Check these input schema locations: /properties/title/type.",
|
|
} })).rejects.toThrow();
|
|
});
|
|
|
|
it("redacts identity private material before persisting native events and checkpoints", async () => {
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, runId: randomUUID(), sessionId: randomUUID() };
|
|
const runnerId = randomUUID();
|
|
const privateKeyPem = generateKeyPairSync("ed25519").privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
const keyBody = privateKeyPem.split("\n")[1];
|
|
await db.insert(heartbeatRuns).values({
|
|
id: identity.runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: runnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
contextSnapshot: { issueId: identity.issueId },
|
|
});
|
|
const observed: PrpEvent[] = [];
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
...identity, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: runnerId, controlPlaneSourceInstanceId: `identity-${identity.runId}`,
|
|
}, { privateKeyPem, onCommittedEvent: async event => { observed.push(event); } });
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerId });
|
|
await port.appendEvent({
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:1`, sourceSeq: 1,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, eventType: "tool.execution.started", schemaVersion: 1,
|
|
priority: 0, emittedAt: new Date().toISOString(), payload: {
|
|
schema: "paperclip.tool.execution.v1", executionId: "identity-output", transport: "builtin",
|
|
operation: "execute", name: "diagnostic", target: null, namespace: null, readOnly: true,
|
|
status: "running", durationMs: null, exitCode: null, progress: null,
|
|
output: privateKeyPem, outputBytes: Buffer.byteLength(privateKeyPem), outputTruncated: false,
|
|
outputDigest: `sha256:${"a".repeat(64)}`,
|
|
},
|
|
});
|
|
await port.checkpointSession({ backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
semanticResult: { ...CONTROL_PLANE_CONFORMANCE_RESULT, summary: privateKeyPem },
|
|
});
|
|
let sourceSeq = 2;
|
|
for (const [itemId, chunks] of [
|
|
["halves", [keyBody.slice(0, 31), keyBody.slice(31)]],
|
|
["bytes", [...keyBody]],
|
|
] as const) {
|
|
for (const [index, chunk] of chunks.entries()) {
|
|
const event: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:${sourceSeq}`, sourceSeq: sourceSeq++,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, turnId: "identity-turn", itemId,
|
|
eventType: "item.delta", schemaVersion: 1, priority: 1, emittedAt: new Date().toISOString(),
|
|
payload: { itemId, kind: "commandExecution", text: chunk, update: { delta: chunk } },
|
|
};
|
|
await port.appendEvent(event);
|
|
expect((await port.appendEvent(event)).disposition).toBe("duplicate");
|
|
if (index === 0) await port.appendEvent({
|
|
...event, sourceEventId: `${runnerId}:${sourceSeq}`, sourceSeq: sourceSeq++,
|
|
payload: { itemId: "another-provider-item", kind: "commandExecution", text: "Unrelated output\n", update: { delta: "Unrelated output\n" } },
|
|
});
|
|
}
|
|
}
|
|
await port.appendEvent({
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:${sourceSeq}`, sourceSeq: sourceSeq++,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, turnId: "tail-turn", itemId: "reasoning-item",
|
|
eventType: "item.delta", schemaVersion: 1, priority: 1, emittedAt: new Date().toISOString(),
|
|
payload: { kind: "reasoning", text: "Thinking-" },
|
|
});
|
|
const terminal: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1", sourceEventId: `${runnerId}:${sourceSeq}`, sourceSeq: sourceSeq++,
|
|
sourceInstanceId: runnerId, sourceKind: "runner", runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId, turnId: "tail-turn",
|
|
eventType: "turn.completed", schemaVersion: 1, priority: 0, emittedAt: new Date().toISOString(),
|
|
payload: { status: "completed" },
|
|
};
|
|
await port.appendEvent(terminal);
|
|
expect((await port.appendEvent(terminal)).disposition).toBe("duplicate");
|
|
expect(observed.at(-1)?.payload.outputTails).toEqual([{ itemId: "reasoning-item", payload: { kind: "reasoning", text: "-" } }]);
|
|
const persistedEvents = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, identity.runId));
|
|
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
for (const value of [persistedEvents, observed, run.runnerProfileJson]) {
|
|
expect(JSON.stringify(value)).not.toContain(keyBody);
|
|
expect(JSON.stringify(value)).toContain("***REDACTED***");
|
|
}
|
|
for (const itemId of ["halves", "bytes"]) {
|
|
const deltas = observed.filter(event => event.payload.itemId === itemId).map(event => event.payload);
|
|
expect(deltas.map(delta => delta.text).join("")).toBe("***REDACTED***");
|
|
expect(deltas.map(delta => (delta.update as { delta: string }).delta).join("")).toBe("***REDACTED***");
|
|
}
|
|
});
|
|
|
|
it("runs the unchanged package conformance suite against Paperclip persistence", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const committedEventIds: string[] = [];
|
|
const duplicateEventIds: string[] = [];
|
|
const port = new PaperclipControlPlanePort(
|
|
db,
|
|
{
|
|
companyId: identity.companyId,
|
|
issueId: identity.issueId,
|
|
runId: identity.runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: contractId,
|
|
completionContractSha256: contractSha,
|
|
sourceInstanceId: conformanceRunnerId,
|
|
controlPlaneSourceInstanceId: "control-conformance",
|
|
},
|
|
{
|
|
onCommittedEvent: async (event) => {
|
|
committedEventIds.push(event.sourceEventId);
|
|
},
|
|
onDuplicateEvent: async (event) => {
|
|
duplicateEventIds.push(event.sourceEventId);
|
|
},
|
|
},
|
|
);
|
|
await expect(runControlPlanePortConformance({ port })).resolves.toEqual({
|
|
eventCount: 3,
|
|
highestContiguousSourceSeq: 3,
|
|
duplicateDisposition: "duplicate",
|
|
terminalReplayIdempotent: true,
|
|
openBindingRejected: true,
|
|
eventIdMutationRejected: true,
|
|
eventSequenceMutationRejected: true,
|
|
replayBindingRejected: true,
|
|
resultMutationRejected: true,
|
|
});
|
|
expect(committedEventIds).toEqual([
|
|
"00000000-0000-4000-8000-000000000005:event:1",
|
|
"00000000-0000-4000-8000-000000000005:event:3",
|
|
"00000000-0000-4000-8000-000000000005:event:2",
|
|
]);
|
|
expect(duplicateEventIds).toEqual([
|
|
"00000000-0000-4000-8000-000000000005:event:2",
|
|
]);
|
|
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).resolves.toHaveLength(1);
|
|
await finalizeNativeRun({
|
|
db,
|
|
runId: identity.runId,
|
|
workspaceFinalizeStatus: "succeeded",
|
|
});
|
|
await expect(db.select().from(nativeRunFinalizations)
|
|
.where(eq(nativeRunFinalizations.runId, identity.runId))).resolves.toEqual([
|
|
expect.objectContaining({ phase: "committed" }),
|
|
]);
|
|
// Completion and restart reconciliation must retain the contract even if
|
|
// the originating heartbeat never returns from its adapter completion.
|
|
const [completedRun] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, identity.runId));
|
|
expect(completedRun.resultJson?.nativeTerminal).toEqual(CONTROL_PLANE_CONFORMANCE_TERMINAL);
|
|
await expect(db.select().from(issues).where(eq(issues.id, identity.issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_progress", statusVersion: 1 }),
|
|
]);
|
|
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, identity.runId))).resolves.toEqual([
|
|
expect.objectContaining({ phase: "committed" }),
|
|
]);
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, identity.issueId))).resolves.toEqual([
|
|
expect.objectContaining({ toStatus: "in_progress", reasonCode: "completion_evidence_incomplete", applicationState: "applied" }),
|
|
]);
|
|
await expect(db.select().from(activityLog).where(eq(activityLog.entityId, identity.issueId))).resolves.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ action: "issue.updated" })]),
|
|
);
|
|
});
|
|
|
|
it("streams committed native rows and deltas after visibility without repeating receipts", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = "40000000-0000-4000-8000-000000000042";
|
|
const runId = "41000000-0000-4000-8000-000000000042";
|
|
const sessionId = "42000000-0000-4000-8000-000000000042";
|
|
const runnerInstanceId = "43000000-0000-4000-8000-000000000042";
|
|
const localContractId = "44000000-0000-4000-8000-000000000042";
|
|
const localContractSha = "safe-progress-signal-contract";
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "Signal committed safe progress",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Signal committed safe progress",
|
|
criteria: [{ id: "objective", requirement: "Signal progress" }],
|
|
},
|
|
canonicalSha256: localContractSha,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: localContractSha,
|
|
contextSnapshot: { issueId },
|
|
});
|
|
const observerDb = createDb(external ?? temporary!.connectionString);
|
|
const observed: Array<Record<string, unknown>> = [];
|
|
const committedCallbacks: string[] = [];
|
|
let visibilityCheck: Promise<Array<{ eventType: string }>> | null = null;
|
|
const unsubscribe = subscribeAllCompanyLiveEvents((event) => {
|
|
if (
|
|
event.companyId !== identity.companyId ||
|
|
event.type !== "heartbeat.run.event" ||
|
|
event.payload.runId !== runId
|
|
) return;
|
|
observed.push(event.payload);
|
|
visibilityCheck = observerDb
|
|
.select({ eventType: heartbeatRunEvents.eventType })
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, runId));
|
|
});
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: localContractSha,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: `safe-progress-control-${runId}`,
|
|
}, {
|
|
onCommittedEvent: async (event) => {
|
|
committedCallbacks.push(event.sourceEventId);
|
|
},
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId, runId, sessionId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
const progressEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "safe-progress-signal:1",
|
|
sourceSeq: 1,
|
|
sourceInstanceId: runnerInstanceId,
|
|
sourceKind: "runner",
|
|
runId,
|
|
normalizedSessionId: sessionId,
|
|
turnId: "safe-progress-signal-turn",
|
|
eventType: "tool.execution.started",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-09-08T10:30:00.000Z",
|
|
payload: {
|
|
schema: "paperclip.tool.execution.v1",
|
|
executionId: "safe-progress-execution",
|
|
transport: "builtin",
|
|
operation: "execute",
|
|
name: "must-not-cross-live-signal",
|
|
target: null,
|
|
namespace: null,
|
|
readOnly: false,
|
|
status: "running",
|
|
durationMs: null,
|
|
exitCode: null,
|
|
progress: null,
|
|
output: "must-not-cross-live-signal",
|
|
outputBytes: 26,
|
|
outputTruncated: false,
|
|
outputDigest: `sha256:${"a".repeat(64)}`,
|
|
},
|
|
};
|
|
try {
|
|
await expect(port.appendEvent(progressEvent)).resolves.toMatchObject({
|
|
disposition: "committed",
|
|
});
|
|
await expect(visibilityCheck).resolves.toEqual([
|
|
{ eventType: "tool.execution.started" },
|
|
]);
|
|
expect(observed).toEqual([{
|
|
runId,
|
|
agentId: identity.agentId,
|
|
issueId,
|
|
seq: 1,
|
|
eventType: "tool.execution.started",
|
|
stream: "system",
|
|
level: "info",
|
|
payload: { prpEvent: progressEvent },
|
|
}]);
|
|
|
|
await expect(port.appendEvent(progressEvent)).resolves.toMatchObject({
|
|
disposition: "duplicate",
|
|
});
|
|
expect(observed).toHaveLength(1);
|
|
|
|
const unsubscribeThrowingListener = subscribeCompanyLiveEvents(
|
|
identity.companyId,
|
|
() => {
|
|
throw new Error("simulated_live_listener_failure");
|
|
},
|
|
);
|
|
try {
|
|
await expect(port.appendEvent({
|
|
...progressEvent,
|
|
sourceEventId: "safe-progress-signal:2",
|
|
sourceSeq: 2,
|
|
})).resolves.toMatchObject({ disposition: "committed" });
|
|
} finally {
|
|
unsubscribeThrowingListener();
|
|
}
|
|
expect(committedCallbacks).toEqual([
|
|
"safe-progress-signal:1",
|
|
"safe-progress-signal:2",
|
|
]);
|
|
expect(observed).toHaveLength(1);
|
|
|
|
await expect(port.appendEvent({
|
|
...progressEvent,
|
|
sourceEventId: "safe-progress-signal:3",
|
|
sourceSeq: 3,
|
|
eventType: "item.delta",
|
|
itemId: "streaming-message",
|
|
payload: { kind: "agentMessage", text: "still writing" },
|
|
})).resolves.toMatchObject({ disposition: "committed" });
|
|
expect(observed).toHaveLength(2);
|
|
expect(observed[1]).toMatchObject({ seq: 3, eventType: "item.delta", payload: {
|
|
prpEvent: { eventType: "item.delta", payload: { text: "still writing" } },
|
|
} });
|
|
expect(committedCallbacks).toEqual([
|
|
"safe-progress-signal:1",
|
|
"safe-progress-signal:2",
|
|
"safe-progress-signal:3",
|
|
]);
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|
|
|
|
it("recovers a runtime question when the event commits before its callback", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = "40000000-0000-4000-8000-000000000041";
|
|
const runId = "41000000-0000-4000-8000-000000000041";
|
|
const sessionId = "42000000-0000-4000-8000-000000000041";
|
|
const runnerInstanceId = "43000000-0000-4000-8000-000000000041";
|
|
const localContractId = "44000000-0000-4000-8000-000000000041";
|
|
const contractSha256 = "runtime-question-recovery-contract";
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "Recover a committed runtime question",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v1",
|
|
objective: "Recover a committed runtime question",
|
|
criteria: [{ id: "objective", requirement: "Recover the question" }],
|
|
},
|
|
canonicalSha256: contractSha256,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: contractSha256,
|
|
contextSnapshot: { issueId },
|
|
});
|
|
|
|
const binding = {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: contractSha256,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: "runtime-question-recovery-control",
|
|
};
|
|
const questionEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "runtime-question-recovery:1",
|
|
sourceSeq: 1,
|
|
sourceInstanceId: runnerInstanceId,
|
|
sourceKind: "runner",
|
|
runId,
|
|
normalizedSessionId: sessionId,
|
|
turnId: "runtime-question-recovery-turn",
|
|
eventType: "runtime_request.expired",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T03:00:00.000Z",
|
|
payload: {
|
|
requestId: "runtime-question-recovery-request",
|
|
requestKind: "runtime",
|
|
requestType: "input",
|
|
reason: "provider_process_lost",
|
|
replayAllowed: false,
|
|
request: {
|
|
schema: "paperclip.runtime_request.v2",
|
|
requestKind: "runtime",
|
|
requestId: "runtime-question-recovery-request",
|
|
type: "input",
|
|
status: "pending",
|
|
prompt: "Choose a recovery option",
|
|
turnId: "runtime-question-recovery-turn",
|
|
itemId: "runtime-question-recovery-item",
|
|
input: {
|
|
schema: "paperclip.question_set.v1",
|
|
title: "Choose a recovery option",
|
|
questions: [
|
|
{
|
|
id: "recovery-option",
|
|
prompt: "Which option should recovery use?",
|
|
required: true,
|
|
answerMode: "single_select",
|
|
options: [
|
|
{ id: "safe", label: "Safe recovery" },
|
|
{ id: "fast", label: "Fast recovery" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const port = new PaperclipControlPlanePort(db, binding, {
|
|
onCommittedEvent: async () => {
|
|
throw new Error("simulated_post_commit_crash");
|
|
},
|
|
onDuplicateEvent: async (event) => {
|
|
await materializeRuntimeQuestionFallback({ db, binding, event });
|
|
},
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId, runId, sessionId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
|
|
await expect(port.appendEvent(questionEvent)).rejects.toThrow(
|
|
"simulated_post_commit_crash",
|
|
);
|
|
await expect(
|
|
db
|
|
.select()
|
|
.from(issueThreadInteractions)
|
|
.where(eq(issueThreadInteractions.issueId, issueId)),
|
|
).resolves.toEqual([]);
|
|
|
|
await expect(port.appendEvent(questionEvent)).resolves.toMatchObject({
|
|
disposition: "duplicate",
|
|
});
|
|
await expect(
|
|
db
|
|
.select()
|
|
.from(issueThreadInteractions)
|
|
.where(eq(issueThreadInteractions.issueId, issueId)),
|
|
).resolves.toEqual([
|
|
expect.objectContaining({
|
|
kind: "ask_user_questions",
|
|
status: "pending",
|
|
idempotencyKey: `runtime-input-durable:v1:${runId}:runtime-question-recovery-request`,
|
|
sourceRunId: runId,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("persists a delayed final answer and replay before resolving the selected task response", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const sessionId = taskSessionId;
|
|
const evidenceRef = `work_product:${taskWorkProductId}`;
|
|
const taskResult = structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT);
|
|
taskResult.completionClaim.contractRevision = "phase6-v1";
|
|
taskResult.completionClaim.criteria[0]!.evidenceRefs = [evidenceRef];
|
|
taskResult.evidence = [{ kind: "work_product", ref: evidenceRef }];
|
|
taskResult.verification[0]!.artifactRef = evidenceRef;
|
|
const event = (sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent => ({
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: `phase6-task:${sourceSeq}`,
|
|
sourceSeq,
|
|
sourceInstanceId: taskRunnerId,
|
|
sourceKind: "runner",
|
|
runId: taskRunId,
|
|
normalizedSessionId: sessionId,
|
|
turnId: "turn-phase6-paperclip-task",
|
|
eventType,
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: `2026-08-09T02:59:0${sourceSeq}.000Z`,
|
|
payload,
|
|
});
|
|
const finalText = "The launch has two stages. Source: https://example.invalid/launch/STREAM-42";
|
|
const finalAnswer = event(2, "item.completed", { kind: "agentMessage", channel: "final", text: finalText });
|
|
const events = [
|
|
event(1, "run.result.proposed", taskResult),
|
|
finalAnswer,
|
|
finalAnswer, // Exact replay must not create another stored answer.
|
|
event(3, "turn.completed", {}),
|
|
];
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "phase6-scripted",
|
|
version: "1",
|
|
capabilities: { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true },
|
|
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
|
};
|
|
},
|
|
async openSession(input) {
|
|
return {
|
|
identity: () => input.identity,
|
|
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async *events() {
|
|
yield events[0]!;
|
|
await new Promise((resolve) => setTimeout(resolve, 6_000));
|
|
yield* events.slice(1);
|
|
},
|
|
async startTurn() { return { turnId: "turn-phase6-paperclip-task" }; },
|
|
cancel() { throw new Error("A completion report must not interrupt the provider"); },
|
|
async result() { return { result: taskResult, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, turnId: "turn-phase6-paperclip-task" }; },
|
|
async snapshot() { return { backendKind: "mock", sessionId, identity: input.identity, providerSessionId: "provider-phase6-paperclip-task" }; },
|
|
async close() {},
|
|
};
|
|
},
|
|
};
|
|
const execution = parseNativeExecutionInput({
|
|
schema: "paperclip.native-execution-input.v4",
|
|
executionMode: "default",
|
|
planningContext: null,
|
|
binding: {
|
|
companyId: identity.companyId,
|
|
runId: taskRunId,
|
|
issueId: taskIssueId,
|
|
agentId: identity.agentId,
|
|
executionWorkspaceId: "workspace-phase6",
|
|
},
|
|
task: {
|
|
identifier: "P6C-2",
|
|
title: "Complete one native task",
|
|
description: null,
|
|
prompt: "# P6C-2: Complete one native task",
|
|
workMode: "standard",
|
|
},
|
|
workspace: {
|
|
cwd: process.cwd(),
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
session: {
|
|
normalizedSessionId: sessionId,
|
|
driverKind: "codex_app_server",
|
|
protocolVersion: 1,
|
|
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
|
},
|
|
provider: { kind: "codex", model: null, approvalPolicy: "never" },
|
|
completionContract: {
|
|
id: taskContractId,
|
|
sha256: "phase6-task-contract",
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
contract: { revision: "phase6-v1", objective: "Complete one native task", criteria: [{ id: "objective", requirement: "Complete the task" }] },
|
|
},
|
|
interactionResponses: [],
|
|
credentialBindings: [],
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
let resultCommitMarkers = 0;
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId: taskIssueId,
|
|
runId: taskRunId,
|
|
agentId: identity.agentId,
|
|
sessionId: taskSessionId,
|
|
completionContractId: taskContractId,
|
|
completionContractSha256: "phase6-task-contract",
|
|
sourceInstanceId: taskRunnerId,
|
|
controlPlaneSourceInstanceId: "phase6-control-plane",
|
|
}, {onResultCommitted:async()=>{resultCommitMarkers++;}});
|
|
const completed = await executeNativeSession({
|
|
input: execution,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: taskRunnerId,
|
|
controlPlaneInstanceId: "phase6-control-plane",
|
|
});
|
|
expect(completed.terminal.runTerminalState).toBe("succeeded");
|
|
const stored = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, taskRunId)).orderBy(asc(heartbeatRunEvents.seq));
|
|
const answers = stored.filter((row) => row.eventType === "item.completed");
|
|
expect(answers).toHaveLength(1);
|
|
const finalAgentMessage = selectHeartbeatRunFinalAgentMessage({
|
|
candidates: answers.flatMap((row) => {
|
|
const candidate = readCompletedAssistantMessageCandidate({ seq: row.seq, prpEvent: row.payload?.prpEvent });
|
|
return candidate ? [candidate] : [];
|
|
}),
|
|
});
|
|
const response = resolveHeartbeatRunResponse({
|
|
resultJson: { nativeResult: taskResult }, finalAgentMessage,
|
|
});
|
|
expect(resolveHeartbeatRunResponse({ resultJson: { nativeResult: taskResult } }).text).toBe(taskResult.summary);
|
|
expect(response.text).toBe(finalText);
|
|
expect(response.decision.chosenSource).toBe("final_agent_message");
|
|
expect(stored.findIndex((row) => row.eventType === "run.result.accepted"))
|
|
.toBeGreaterThan(stored.findIndex((row) => row.eventType === "item.completed"));
|
|
// A workspace recovery owner can commit before the live heartbeat reaches
|
|
// presentation. The file-preparation comment must not hide the real reply.
|
|
const [workspaceNotice] = await db.insert(issueComments).values({
|
|
companyId: identity.companyId, issueId: taskIssueId,
|
|
authorAgentId: identity.agentId, authorType: "agent", createdByRunId: taskRunId,
|
|
body: "## Workspace Ready",
|
|
presentation: { kind: "system_notice", tone: "info", title: "Workspace ready", density: "compact", detailsDefaultOpen: false },
|
|
}).returning();
|
|
const [prepared] = await db.insert(issueComments).values({
|
|
companyId: identity.companyId, issueId: taskIssueId,
|
|
authorAgentId: identity.agentId, authorType: "agent", createdByRunId: taskRunId,
|
|
body: "Prepared Continuity file for this response.",
|
|
}).returning();
|
|
const receipt = { semanticToolReceipts: { file: {
|
|
operationId: "register_deliverable", result: {
|
|
commandId: "deliverable-prepared:attachment-1", disposition: "applied",
|
|
attachmentId: "attachment-1", entityRefs: ["attachment-1", prepared!.id],
|
|
},
|
|
} } };
|
|
await db.update(heartbeatRuns).set({
|
|
contextSnapshot: { skipIssueComment: true },
|
|
resultJson: sql`coalesce(${heartbeatRuns.resultJson}, '{}'::jsonb) || ${JSON.stringify(receipt)}::jsonb`,
|
|
}).where(eq(heartbeatRuns.id, taskRunId));
|
|
await finalizeNativeRun({ db, runId: taskRunId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
const repair = () => repairCommittedNativeChatResponse(db, {
|
|
companyId: identity.companyId, issueId: taskIssueId, runId: taskRunId,
|
|
});
|
|
expect(await repair()).toBe(false);
|
|
await db.update(heartbeatRuns).set({ contextSnapshot: { externalChatContinuation: true } })
|
|
.where(eq(heartbeatRuns.id, taskRunId));
|
|
expect(await repair()).toBe(false);
|
|
await db.update(heartbeatRuns).set({ contextSnapshot: {} }).where(eq(heartbeatRuns.id, taskRunId));
|
|
const work = nativeResponsePresentationWork(db);
|
|
const [entry] = await work.load({ companyId: identity.companyId, runId: taskRunId });
|
|
expect(entry?.id).toBe(taskRunId);
|
|
expect(await work.check(entry)).toBeNull();
|
|
expect(await repair()).toBe(false);
|
|
expect(await db.select({ body: issueComments.body }).from(issueComments)
|
|
.where(eq(issueComments.createdByRunId, taskRunId)).orderBy(asc(issueComments.createdAt)))
|
|
.toEqual([{ body: workspaceNotice!.body }, { body: prepared!.body }, { body: finalText }]);
|
|
await expect(port.completeRun({
|
|
result: taskResult,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
turnId: "turn-phase6-paperclip-task",
|
|
callerResultId: `phase6-scripted-runner:${taskRunId}:result`,
|
|
callerDedupeKey: `${taskRunId}:phase6-task-contract`,
|
|
})).resolves.toBeUndefined();
|
|
await expect(port.completeRun({
|
|
result: { ...structuredClone(taskResult), summary: "Conflicting changed result bytes" },
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
turnId: "turn-phase6-paperclip-task",
|
|
callerResultId: `phase6-scripted-runner:${taskRunId}:result`,
|
|
callerDedupeKey: `${taskRunId}:phase6-task-contract`,
|
|
})).rejects.toThrow("structured_result_replay_conflict");
|
|
expect(resultCommitMarkers).toBe(1);
|
|
await Promise.all([
|
|
finalizeNativeRun({ db, runId: taskRunId, workspaceFinalizeStatus: "succeeded" }),
|
|
finalizeNativeRun({ db, runId: taskRunId, workspaceFinalizeStatus: "succeeded" }),
|
|
]);
|
|
await expect(db.select().from(issues).where(eq(issues.id, taskIssueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "done", statusVersion: 1 }),
|
|
]);
|
|
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, taskRunId))).resolves.toHaveLength(1);
|
|
await expect(db.select().from(workAssessments).where(eq(workAssessments.runId, taskRunId))).resolves.toHaveLength(1);
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, taskIssueId))).resolves.toHaveLength(1);
|
|
});
|
|
|
|
it("fails closed when the bound company does not own the run", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: "00000000-0000-4000-8000-000000000099",
|
|
issueId: identity.issueId,
|
|
runId: identity.runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: contractId,
|
|
completionContractSha256: contractSha,
|
|
sourceInstanceId: conformanceRunnerId,
|
|
controlPlaneSourceInstanceId: "control-conformance",
|
|
});
|
|
await expect(port.openRun(CONTROL_PLANE_CONFORMANCE_OPEN)).rejects.toThrow("binding_mismatch");
|
|
});
|
|
|
|
it("revalidates every persisted native binding before open and completion", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = "00000000-0000-4000-8000-000000000091";
|
|
const localContractId = "00000000-0000-4000-8000-000000000092";
|
|
const runId = "00000000-0000-4000-8000-000000000093";
|
|
const sessionId = "00000000-0000-4000-8000-000000000094";
|
|
const runnerInstanceId = "00000000-0000-4000-8000-000000000095";
|
|
const contractSha256 = "strict-native-binding-contract";
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "Strict native binding",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "strict-binding-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "strict-binding-v1",
|
|
objective: "Enforce the persisted binding",
|
|
criteria: [{ id: "objective", requirement: "Reject every mismatch" }],
|
|
},
|
|
canonicalSha256: contractSha256,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: contractSha256,
|
|
contextSnapshot: { issueId },
|
|
});
|
|
const createPort = () => new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: contractSha256,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: "strict-binding-control",
|
|
});
|
|
const open = {
|
|
identity: { ...identity, issueId, runId, sessionId },
|
|
backendKind: "mock" as const,
|
|
sourceInstanceId: runnerInstanceId,
|
|
};
|
|
const mutations = [
|
|
{ invalid: { nativeIssueId: taskIssueId }, restore: { nativeIssueId: issueId } },
|
|
{ invalid: { nativeSessionId: taskSessionId }, restore: { nativeSessionId: sessionId } },
|
|
{ invalid: { runnerInstanceId: taskRunnerId }, restore: { runnerInstanceId } },
|
|
{ invalid: { completionContractId: taskContractId }, restore: { completionContractId: localContractId } },
|
|
{ invalid: { completionContractSha256: "wrong-contract" }, restore: { completionContractSha256: contractSha256 } },
|
|
] satisfies Array<{
|
|
invalid: Partial<typeof heartbeatRuns.$inferInsert>;
|
|
restore: Partial<typeof heartbeatRuns.$inferInsert>;
|
|
}>;
|
|
for (const mutation of mutations) {
|
|
// Verify service-level binding checks even for corrupted historical rows.
|
|
await db.transaction(async (tx) => {
|
|
await tx.execute(sql`set local session_replication_role = replica`);
|
|
await tx.update(heartbeatRuns).set(mutation.invalid).where(eq(heartbeatRuns.id, runId));
|
|
});
|
|
await expect(createPort().openRun(open)).rejects.toThrow("native_open_run_not_authorized");
|
|
await db.transaction(async (tx) => {
|
|
await tx.execute(sql`set local session_replication_role = replica`);
|
|
await tx.update(heartbeatRuns).set(mutation.restore).where(eq(heartbeatRuns.id, runId));
|
|
});
|
|
}
|
|
|
|
const openedPort = createPort();
|
|
await openedPort.openRun(open);
|
|
await db.update(heartbeatRuns).set({ runnerInstanceId: taskRunnerId }).where(eq(heartbeatRuns.id, runId));
|
|
await expect(openedPort.completeRun({
|
|
result: CONTROL_PLANE_CONFORMANCE_RESULT,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "strict-native-binding-result",
|
|
})).rejects.toThrow("native_result_binding_mismatch");
|
|
await db.update(heartbeatRuns).set({ runnerInstanceId }).where(eq(heartbeatRuns.id, runId));
|
|
});
|
|
|
|
it.each(["port", "coordinator"])("does not deadlock %s result persistence against a task mutation that updates its run", async (kind) => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const runId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: conformanceRunnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
});
|
|
let resultCommitted = false;
|
|
const port = kind === "port" ? new PaperclipControlPlanePort(db, {
|
|
...identity, runId, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: conformanceRunnerId, controlPlaneSourceInstanceId: "result-lock-order",
|
|
}, {onResultCommitted:async()=>{
|
|
const rows=await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId,runId));
|
|
expect(rows).toHaveLength(1);resultCommitted=true;
|
|
}}) : new NativeRunCoordinatorStore(db, {
|
|
...identity, runId, completionContractId: contractId, completionContractSha256: contractSha,
|
|
normalizedSessionId: identity.sessionId, runnerSourceInstanceId: conformanceRunnerId,
|
|
completionContractRevision: "standalone-v1", completionContractCriterionIds: ["objective"],
|
|
});
|
|
let completion: Promise<unknown> | undefined;
|
|
try {
|
|
await db.transaction(async (tx) => {
|
|
await tx.select().from(issues).where(eq(issues.id, identity.issueId)).for("update");
|
|
const [backend] = await tx.execute(sql`select pg_backend_pid() as pid`) as unknown as Array<{ pid: number }>;
|
|
completion = port.completeRun({
|
|
result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "result-lock-order",
|
|
}).then(() => null, (error: unknown) => error);
|
|
let waiting = false;
|
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
const [state] = await db.execute(sql`select exists (
|
|
select 1 from pg_stat_activity where ${backend.pid} = any(pg_blocking_pids(pid))
|
|
) as waiting`) as unknown as Array<{ waiting: boolean }>;
|
|
if (state.waiting) { waiting = true; break; }
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
expect(waiting).toBe(true);
|
|
expect(resultCommitted).toBe(false);
|
|
await tx.execute(sql`set local lock_timeout = '1s'`);
|
|
await tx.update(heartbeatRuns).set({ updatedAt: new Date() }).where(eq(heartbeatRuns.id, runId));
|
|
});
|
|
} finally {
|
|
expect(await completion).toBeNull();
|
|
expect(resultCommitted).toBe(kind === "port");
|
|
}
|
|
});
|
|
|
|
it.each(["port", "coordinator"])("clears a resumed session's recovery state when %s persists its result", async (kind) => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const runId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId, companyId: identity.companyId, agentId: identity.agentId,
|
|
status: "running", runtimeMode: "native", nativeIssueId: identity.issueId,
|
|
nativeSessionId: identity.sessionId, runnerInstanceId: conformanceRunnerId,
|
|
completionContractId: contractId, completionContractSha256: contractSha,
|
|
});
|
|
await db.insert(nativeRunFinalizations).values({
|
|
runId, companyId: identity.companyId, issueId: identity.issueId,
|
|
phase: "observed", recoveryState: "resuming_session", recoveryRequestId: "restart-request",
|
|
});
|
|
const port = kind === "port" ? new PaperclipControlPlanePort(db, {
|
|
...identity, runId, completionContractId: contractId, completionContractSha256: contractSha,
|
|
sourceInstanceId: conformanceRunnerId, controlPlaneSourceInstanceId: "resumed-result",
|
|
}) : new NativeRunCoordinatorStore(db, {
|
|
...identity, runId, completionContractId: contractId, completionContractSha256: contractSha,
|
|
normalizedSessionId: identity.sessionId, runnerSourceInstanceId: conformanceRunnerId,
|
|
completionContractRevision: "standalone-v1", completionContractCriterionIds: ["objective"],
|
|
});
|
|
await port.completeRun({
|
|
result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "resumed-result",
|
|
});
|
|
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, runId))).resolves.toEqual([
|
|
expect.objectContaining({ phase: "workspace_finalizing", recoveryState: null, recoveryRequestId: null }),
|
|
]);
|
|
});
|
|
|
|
it("does not let native completion bypass a pending issue interaction", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId: governanceIssueId,
|
|
runId: governanceRunId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: governanceContractId,
|
|
completionContractSha256: "phase6-governance-contract",
|
|
sourceInstanceId: governanceRunnerId,
|
|
controlPlaneSourceInstanceId: "control-phase6-governance",
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, runId: governanceRunId, issueId: governanceIssueId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: governanceRunnerId,
|
|
});
|
|
await port.completeRun({
|
|
result: CONTROL_PLANE_CONFORMANCE_RESULT,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "phase6-governance-result",
|
|
});
|
|
await finalizeNativeRun({ db, runId: governanceRunId, workspaceFinalizeStatus: "succeeded" });
|
|
await expect(db.select().from(issues).where(eq(issues.id, governanceIssueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_review" }),
|
|
]);
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, governanceIssueId))).resolves.toEqual([
|
|
expect.objectContaining({ toStatus: "in_review", reasonCode: "governed_gate_pending" }),
|
|
]);
|
|
});
|
|
|
|
it("P6-23 materializes native review through the authorized interaction service", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = "30000000-0000-4000-8000-000000000024";
|
|
const localContractId = "31000000-0000-4000-8000-000000000024";
|
|
const runId = "32000000-0000-4000-8000-000000000024";
|
|
const runnerInstanceId = "33000000-0000-4000-8000-000000000024";
|
|
const reviewerAgentId = "34000000-0000-4000-8000-000000000024";
|
|
await db.insert(authUsers).values({
|
|
id: "reviewer-24", name: "Human reviewer", email: "reviewer-24@example.test",
|
|
createdAt: new Date(), updatedAt: new Date(),
|
|
});
|
|
await db.insert(companyMemberships).values({
|
|
companyId: identity.companyId, principalType: "user", principalId: "reviewer-24",
|
|
status: "active", membershipRole: "member",
|
|
});
|
|
await db.insert(agents).values({
|
|
id: reviewerAgentId, companyId: identity.companyId, name: "Review lead",
|
|
adapterType: "codex_local", status: "idle",
|
|
});
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "Native review interaction",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
responsibleUserId: "reviewer-24",
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: { revision: "phase6-v1", objective: "Review", criteria: [{ id: "objective", requirement: "Review" }] },
|
|
canonicalSha256: "native-review-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "native-review-contract",
|
|
contextSnapshot: { issueId },
|
|
});
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "native-review-contract",
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: "native-review-control",
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId, runId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
const result = { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), completionClaim: { ...CONTROL_PLANE_CONFORMANCE_RESULT.completionClaim, contractRevision: "phase6-v1" }, reportedWorkDisposition: "needs_review" as const, attentionRequests: [{ kind: "approval" as const, summary: "Approve publication", ownerClass: "human" as const }, { kind: "review" as const, summary: "Review release notes", ownerClass: "agent" as const, targetAgentId: reviewerAgentId }] };
|
|
await expect(nativeCompletionFeedback(db, runId, { ...result, attentionRequests: [] }))
|
|
.rejects.toThrow("needs_review requires");
|
|
await expect(nativeCompletionFeedback(db, runId, {
|
|
...result, attentionRequests: [{ kind: "review", summary: "Review work", ownerClass: "agent", targetAgentId: identity.agentId }],
|
|
})).rejects.toThrow("cannot review its own");
|
|
await expect(nativeCompletionFeedback(db, runId, {
|
|
...result, attentionRequests: [{ kind: "review", summary: "Review work", ownerClass: "agent", targetAgentId: "99999999-9999-4999-8999-999999999999" }],
|
|
})).rejects.toThrow("not available in this company");
|
|
await expect(nativeCompletionFeedback(db, runId, {
|
|
...result,
|
|
completionClaim: { ...result.completionClaim, contractRevision: "stale-first-turn" },
|
|
})).rejects.toThrow(/contractRevision.*phase6-v1/);
|
|
await expect(nativeCompletionFeedback(db, runId, result)).resolves.toContain("Completion report accepted");
|
|
await port.completeRun({
|
|
result,
|
|
terminal: { ...CONTROL_PLANE_CONFORMANCE_TERMINAL, reportedWorkDisposition: "needs_review" },
|
|
callerResultId: "native-review-result",
|
|
});
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
// Reconciliation must reuse the review card and its durable wake together.
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
const reviewWakes = await db.select().from(agentWakeupRequests)
|
|
.where(eq(agentWakeupRequests.agentId, reviewerAgentId));
|
|
expect(reviewWakes).toHaveLength(1);
|
|
const agentReview = (await db.select().from(issueThreadInteractions)
|
|
.where(eq(issueThreadInteractions.issueId, issueId)))
|
|
.find((entry) => entry.addresseeAgentId === reviewerAgentId)!;
|
|
expect(reviewWakes[0]).toMatchObject({
|
|
companyId: identity.companyId, status: "queued", reason: "native_completion_review",
|
|
payload: { issueId, nativeReviewInteractionId: agentReview.id },
|
|
});
|
|
await expect(getNativeReviewAssignment(db, {
|
|
companyId: identity.companyId, issueId, agentId: reviewerAgentId,
|
|
contextSnapshot: reviewWakes[0]!.payload,
|
|
})).resolves.toMatchObject({ interaction: { id: agentReview.id } });
|
|
await expect(getNativeReviewAssignment(db, {
|
|
companyId: identity.companyId, issueId, agentId: identity.agentId,
|
|
contextSnapshot: reviewWakes[0]!.payload,
|
|
})).resolves.toBeNull();
|
|
const reviewRunId = "35000000-0000-4000-8000-000000000024";
|
|
const nativeReview = {
|
|
nativeReviewInteractionId: agentReview.id,
|
|
nativeReviewDecisionId: (agentReview.payload as { target: { revisionId: string } }).target.revisionId,
|
|
};
|
|
await db.update(agentWakeupRequests).set({ runId: reviewRunId }).where(eq(agentWakeupRequests.id, reviewWakes[0]!.id));
|
|
const [reviewRun] = await db.insert(heartbeatRuns).values({
|
|
id: reviewRunId, companyId: identity.companyId, agentId: reviewerAgentId,
|
|
status: "queued", runtimeMode: "native", nativeIssueId: issueId,
|
|
wakeupRequestId: reviewWakes[0]!.id,
|
|
contextSnapshot: { issueId, wakeReason: "native_completion_review", ...nativeReview },
|
|
}).returning();
|
|
await expect(createPostgresRunDispatchAdapter(db).cancelStaleQueuedRun({
|
|
companyId: identity.companyId, runId: reviewRunId, expectedStatus: "queued", now: new Date(),
|
|
})).resolves.toMatchObject({ outcome: "not_stale" });
|
|
await db.insert(heartbeatRuns).values({
|
|
id: "35000000-0000-4000-8000-000000000025", companyId: identity.companyId,
|
|
agentId: reviewerAgentId, status: "running", runtimeMode: "native", nativeIssueId: issueId,
|
|
});
|
|
await db.update(issues).set({ executionRunId: "35000000-0000-4000-8000-000000000025" }).where(eq(issues.id, issueId));
|
|
await expect(claimQueuedNativeReviewRun(db, {
|
|
run: reviewRun!, agentNameKey: "reviewer", claimedAt: new Date(), claimValues: {},
|
|
})).resolves.toBeNull();
|
|
await expect(db.select({ status: heartbeatRuns.status, executionRunId: issues.executionRunId })
|
|
.from(heartbeatRuns).innerJoin(issues, eq(issues.id, issueId)).where(eq(heartbeatRuns.id, reviewRunId)))
|
|
.resolves.toMatchObject([{ status: "queued", executionRunId: "35000000-0000-4000-8000-000000000025" }]);
|
|
await expect(db.select({ status: agentWakeupRequests.status })
|
|
.from(agentWakeupRequests).where(eq(agentWakeupRequests.id, reviewWakes[0]!.id)))
|
|
.resolves.toEqual([{ status: "queued" }]);
|
|
await db.update(issues).set({ executionRunId: null }).where(eq(issues.id, issueId));
|
|
await expect(claimQueuedNativeReviewRun(db, {
|
|
run: reviewRun!, agentNameKey: "reviewer", claimedAt: new Date(), claimValues: {},
|
|
})).resolves.toMatchObject({ id: reviewRunId, status: "running" });
|
|
await expect(db.select({ status: agentWakeupRequests.status, executionRunId: issues.executionRunId })
|
|
.from(agentWakeupRequests).innerJoin(issues, eq(issues.id, issueId)).where(eq(agentWakeupRequests.id, reviewWakes[0]!.id)))
|
|
.resolves.toMatchObject([{ status: "claimed", executionRunId: reviewRunId }]);
|
|
await expect(claimQueuedNativeReviewRun(db, {
|
|
run: reviewRun!, agentNameKey: "reviewer", claimedAt: new Date(), claimValues: {},
|
|
})).resolves.toBeNull();
|
|
await expect(claimNativeReviewExecutionLock(db, {
|
|
companyId: identity.companyId, issueId, agentId: identity.agentId, runId: reviewRunId,
|
|
contextSnapshot: nativeReview, agentNameKey: "worker", claimedAt: new Date(),
|
|
})).resolves.toBe(false);
|
|
await expect(claimNativeReviewExecutionLock(db, {
|
|
companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId,
|
|
contextSnapshot: nativeReview, agentNameKey: "reviewer", claimedAt: new Date(),
|
|
})).resolves.toBe(true);
|
|
await expect(stageNativeRunnerWakeAttachments({
|
|
db,
|
|
binding: {
|
|
companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId,
|
|
workspaceRoot: "/tmp/review-handoff", executionTargetKind: "remote",
|
|
},
|
|
})).resolves.toMatchObject({ attachments: [] });
|
|
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, nativeReviewInteractionId: agentReview.id } }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
await expect(stageNativeRunnerWakeAttachments({
|
|
db,
|
|
binding: { companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId, workspaceRoot: "/tmp/review-handoff", executionTargetKind: "remote" },
|
|
})).rejects.toThrow("not_authorized");
|
|
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, wakeReason: "native_completion_review", ...nativeReview } }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
await db.update(issueThreadInteractions).set({ status: "accepted" }).where(eq(issueThreadInteractions.id, agentReview.id));
|
|
await expect(stageNativeRunnerWakeAttachments({
|
|
db,
|
|
binding: { companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId, workspaceRoot: "/tmp/review-handoff", executionTargetKind: "remote" },
|
|
})).rejects.toThrow("not_authorized");
|
|
await db.update(issueThreadInteractions).set({ status: "pending" }).where(eq(issueThreadInteractions.id, agentReview.id));
|
|
await expect(createPostgresRunDispatchAdapter(db).cancelStaleQueuedRun({
|
|
companyId: identity.companyId, runId: reviewRunId, expectedStatus: "queued", now: new Date(),
|
|
})).resolves.toMatchObject({ outcome: "lost_race" });
|
|
await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
const reviewBinding = {
|
|
companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId,
|
|
};
|
|
await expect(assertCurrentWakeCommentsRead(db, reviewBinding, null)).resolves.toBeUndefined();
|
|
await db.update(issues).set({ executionRunId: null }).where(eq(issues.id, issueId));
|
|
await expect(assertCurrentWakeCommentsRead(db, reviewBinding, null))
|
|
.rejects.toThrow("native_current_wake_comments_binding_changed");
|
|
await db.update(issues).set({ executionRunId: reviewRunId }).where(eq(issues.id, issueId));
|
|
const reviewAuthority = new PaperclipRunnerToolAuthority(db, {
|
|
companyId: identity.companyId, issueId, agentId: reviewerAgentId, runId: reviewRunId, nativeReview,
|
|
});
|
|
expect(reviewAuthority.definitions().map((tool) => tool.name)).toEqual([
|
|
"get_task_context", "get_task_history", "list_documents", "read_document", "list_document_revisions", "resolve_review",
|
|
]);
|
|
await expect(reviewAuthority.execute({ tool: "get_task_context", callId: "review-read", arguments: {} }))
|
|
.resolves.toMatchObject({ assignedReview: { id: agentReview.id }, activeTask: { assigneeAgentId: identity.agentId } });
|
|
await expect(reviewAuthority.execute({ tool: "set_dependencies", callId: "review-mutate", arguments: {} }))
|
|
.rejects.toThrow("may only inspect");
|
|
await expect(nativeCompletionFeedback(db, reviewRunId, CONTROL_PLANE_CONFORMANCE_RESULT))
|
|
.rejects.toThrow("Resolve your assigned review");
|
|
await expect(reviewAuthority.execute({ tool: "resolve_review", callId: "review-missing-reason", arguments: { decision: "reject" } }))
|
|
.rejects.toThrow("Explain the changes");
|
|
await expect(reviewAuthority.execute({ tool: "resolve_review", callId: "review-other-target", arguments: { decision: "accept", issueId: taskIssueId } }))
|
|
.rejects.toThrow("Choose accept or reject");
|
|
await db.update(issues).set({ statusVersion: 2 }).where(eq(issues.id, issueId));
|
|
await expect(reviewAuthority.execute({ tool: "get_task_context", callId: "review-stale", arguments: {} }))
|
|
.rejects.toThrow("no longer available");
|
|
await expect(issueThreadInteractionService(db).acceptInteraction(
|
|
{ id: issueId, companyId: identity.companyId, projectId: null, goalId: null }, agentReview.id, {},
|
|
{ agentId: reviewerAgentId, runId: reviewRunId },
|
|
)).rejects.toThrow("no longer current");
|
|
await db.update(issues).set({ statusVersion: 1 }).where(eq(issues.id, issueId));
|
|
|
|
const reviewIssue = { id: issueId, companyId: identity.companyId, projectId: null, goalId: null, status: "in_review" as const };
|
|
const rejectUnboundReview = async (runId?: string) => issueThreadInteractionService(db).acceptInteraction(
|
|
reviewIssue, agentReview.id, {}, { agentId: reviewerAgentId, ...(runId ? { runId } : {}) },
|
|
);
|
|
await expect(rejectUnboundReview("35000000-0000-4000-8000-000000000025"))
|
|
.rejects.toThrow("no longer current");
|
|
await expect(db.select({ status: issueThreadInteractions.status })
|
|
.from(issueThreadInteractions).where(eq(issueThreadInteractions.id, agentReview.id)))
|
|
.resolves.toEqual([{ status: "pending" }]);
|
|
await expect(rejectUnboundReview()).rejects.toThrow("valid authenticated agent run");
|
|
await expect(db.select({ status: issueThreadInteractions.status })
|
|
.from(issueThreadInteractions).where(eq(issueThreadInteractions.id, agentReview.id)))
|
|
.resolves.toEqual([{ status: "pending" }]);
|
|
await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
await expect(rejectUnboundReview(reviewRunId)).rejects.toThrow("no longer current");
|
|
await expect(db.select({ status: issueThreadInteractions.status })
|
|
.from(issueThreadInteractions).where(eq(issueThreadInteractions.id, agentReview.id)))
|
|
.resolves.toEqual([{ status: "pending" }]);
|
|
await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_review", statusVersion: 1, assigneeAgentId: identity.agentId }),
|
|
]);
|
|
const [reviewInteraction] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId));
|
|
expect(reviewInteraction).toMatchObject({
|
|
kind: "request_confirmation",
|
|
status: "pending",
|
|
sourceRunId: runId,
|
|
addresseeUserId: "reviewer-24",
|
|
effectiveResolverPolicy: "human_only",
|
|
});
|
|
await expect(db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, issueId))).resolves.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ effectKind: "bind_reviewer", targetType: "issue_thread_interaction" })]),
|
|
);
|
|
|
|
await issueThreadInteractionService(db).acceptInteraction(
|
|
{ id: issueId, companyId: identity.companyId, projectId: null, goalId: null, status: "in_review" },
|
|
reviewInteraction!.id,
|
|
{},
|
|
{ userId: "reviewer-24" },
|
|
);
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_review" }),
|
|
]);
|
|
const remaining = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId));
|
|
expect(remaining).toHaveLength(2);
|
|
const secondReview = remaining.find((entry) => entry.status === "pending")!;
|
|
await issueThreadInteractionService(db).acceptInteraction(
|
|
{ id: issueId, companyId: identity.companyId, projectId: null, goalId: null, status: "in_review" },
|
|
secondReview.id, {}, { agentId: reviewerAgentId, runId: reviewRunId },
|
|
);
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "done" }),
|
|
]);
|
|
await expect(nativeCompletionFeedback(db, reviewRunId, CONTROL_PLANE_CONFORMANCE_RESULT))
|
|
.resolves.toContain("recorded review decision controls task completion");
|
|
// The real executor checks this fence after the reviewer has accepted and
|
|
// finished. Acceptance releases the lock but keeps the worker assignee.
|
|
await expect(db.select({ executionRunId: issues.executionRunId }).from(issues)
|
|
.where(eq(issues.id, issueId))).resolves.toEqual([{ executionRunId: null }]);
|
|
await expect(assertCurrentWakeCommentsRead(db, reviewBinding, null)).resolves.toBeUndefined();
|
|
await db.update(issueThreadInteractions).set({ resolvedByRunId: null })
|
|
.where(eq(issueThreadInteractions.id, agentReview.id));
|
|
await expect(assertCurrentWakeCommentsRead(db, reviewBinding, null))
|
|
.rejects.toThrow("native_current_wake_comments_binding_changed");
|
|
await db.update(issueThreadInteractions).set({ resolvedByRunId: reviewRunId })
|
|
.where(eq(issueThreadInteractions.id, agentReview.id));
|
|
|
|
await expect(reviewAuthority.execute({ tool: "resolve_review", callId: "review-replay", arguments: { decision: "accept" } }))
|
|
.resolves.toMatchObject({ interactionId: agentReview.id, status: "accepted", deduplicated: true });
|
|
// A separate parent session must see the decision this review run recorded.
|
|
const parentId = "36000000-0000-4000-8000-000000000024";
|
|
const parentRunId = "37000000-0000-4000-8000-000000000024";
|
|
await db.insert(issues).values({ id: parentId, companyId: identity.companyId,
|
|
title: "Resume after review", status: "in_progress", assigneeAgentId: reviewerAgentId });
|
|
await db.insert(heartbeatRuns).values({ id: parentRunId, companyId: identity.companyId,
|
|
agentId: reviewerAgentId, nativeIssueId: parentId, status: "running", runtimeMode: "native" });
|
|
await db.update(issues).set({ executionRunId: parentRunId }).where(eq(issues.id, parentId));
|
|
await db.update(issues).set({ parentId }).where(eq(issues.id, issueId));
|
|
const parentAuthority = new PaperclipRunnerToolAuthority(db, {
|
|
companyId: identity.companyId, issueId: parentId, agentId: reviewerAgentId, runId: parentRunId,
|
|
});
|
|
const expectedChildReview = {
|
|
id: agentReview.id, kind: "native_completion_review", status: "accepted",
|
|
result: { childTaskId: issueId, childTaskStatus: "done", reviewerAgentId,
|
|
reviewerRunId: reviewRunId, sourceRunId: runId },
|
|
};
|
|
await expect(parentAuthority.execute({ tool: "get_task_context", callId: "review-parent-context", arguments: {} }))
|
|
.resolves.toMatchObject({ childReviewOutcomes: expect.arrayContaining([expect.objectContaining({
|
|
...expectedChildReview, result: expect.objectContaining(expectedChildReview.result),
|
|
})]) });
|
|
const answerId = "39000000-0000-4000-8000-000000000024";
|
|
const answers = [{ questionId: "delivery", optionIds: ["zip"], otherText: null }];
|
|
await db.insert(issueThreadInteractions).values({
|
|
id: answerId, companyId: identity.companyId, issueId: parentId,
|
|
kind: "ask_user_questions", status: "answered", resolvedByUserId: "local-board", resolvedAt: new Date(),
|
|
payload: { version: 1, questions: [{ id: "delivery", prompt: "Which delivery format?", selectionMode: "single",
|
|
options: [{ id: "zip", label: "ZIP" }, { id: "source", label: "Source files" }] }] },
|
|
result: { version: 1, answers, summaryMarkdown: "Generated summary is not human authority" },
|
|
});
|
|
const continuation = await buildExecutionContinuation({ db, companyId: identity.companyId,
|
|
issueId: parentId, agentId: reviewerAgentId, runId: parentRunId,
|
|
context: { wakeReason: "issue_blockers_resolved" }, summary: null, exposeLowTrustRaw: false });
|
|
expect(continuation.humanResponses).toEqual([expect.objectContaining({
|
|
id: answerId, resolvedByUserId: "local-board", result: { answers },
|
|
})]);
|
|
expect(continuation.interactionOutcomes).toContainEqual(expect.objectContaining({
|
|
...expectedChildReview, result: expect.objectContaining(expectedChildReview.result),
|
|
}));
|
|
for (const resumedSession of [false, true]) {
|
|
const prompt = renderPaperclipWakePrompt({ executionContinuation: continuation }, { resumedSession });
|
|
const [request, evidence] = prompt.split("### Untrusted continuation evidence");
|
|
expect(request).toContain(answerId);
|
|
expect(request).not.toContain(agentReview.id);
|
|
expect(request).not.toContain("Generated summary");
|
|
expect(evidence).toContain(agentReview.id);
|
|
}
|
|
await db.update(issues).set({ parentId: null }).where(eq(issues.id, issueId));
|
|
await expect(parentAuthority.execute({ tool: "get_task_context", callId: "review-unrelated-context", arguments: {} }))
|
|
.resolves.toMatchObject({ childReviewOutcomes: [] });
|
|
await db.update(issues).set({ parentId }).where(eq(issues.id, issueId));
|
|
const otherCompanyId = "38000000-0000-4000-8000-000000000024";
|
|
await db.insert(companies).values({ id: otherCompanyId, name: "Unrelated company", issuePrefix: "P6R" });
|
|
await expect(childReviewOutcomes(db, otherCompanyId, parentId)).resolves.toEqual([]);
|
|
await db.update(issueThreadInteractions).set({ status: "pending" }).where(eq(issueThreadInteractions.id, agentReview.id));
|
|
expect(await childReviewOutcomes(db, identity.companyId, parentId))
|
|
.not.toContainEqual(expect.objectContaining({ id: agentReview.id }));
|
|
await db.update(issueThreadInteractions).set({ status: "accepted" }).where(eq(issueThreadInteractions.id, agentReview.id));
|
|
|
|
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
|
const newRevision = "review-round-two";
|
|
const reviews = [];
|
|
for (const key of ["one", "two"]) {
|
|
reviews.push(await issueThreadInteractionService(db).create(
|
|
{ id: issueId, companyId: identity.companyId },
|
|
{ kind: "request_confirmation", title: `Review ${key}`, addresseeUserId: "reviewer-24",
|
|
resolverPolicy: "human_only", continuationPolicy: "wake_assignee", sourceRunId: runId,
|
|
payload: { version: 1, prompt: `Approve ${key}`, acceptLabel: "Approve", rejectLabel: "Decline", allowDeclineReason: true,
|
|
target: { type: "custom", key: "native_completion_review", revisionId: newRevision } } },
|
|
{ systemId: "test-multiple-reviewers", runId },
|
|
));
|
|
}
|
|
await issueThreadInteractionService(db).rejectInteraction(
|
|
{ id: issueId, companyId: identity.companyId }, reviews[0]!.id,
|
|
{ reason: "Needs another change" }, { userId: "reviewer-24" },
|
|
);
|
|
// Even if another actor puts the task back in review, a declined decision
|
|
// in the same review round must not be erased by another reviewer's approval.
|
|
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
|
await issueThreadInteractionService(db).acceptInteraction(
|
|
{ id: issueId, companyId: identity.companyId, projectId: null, goalId: null, status: "in_review" },
|
|
reviews[1]!.id, {}, { userId: "reviewer-24" },
|
|
);
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("in_review");
|
|
await db.update(heartbeatRuns).set({ wakeupRequestId: null }).where(eq(heartbeatRuns.id, reviewRunId));
|
|
|
|
});
|
|
|
|
const allowedFirstTaskCompletionScenarios = [
|
|
"ordinary task", "accepted proposal", "accepted conversational proposal", "accepted current plan", "acknowledged rejection", "acknowledged plan rejection",
|
|
] as const;
|
|
const refusedFirstTaskCompletionScenarios = [
|
|
"opening answer only", "agent-resolved without human comment", "quarantined human comment", "newest proposal pending", "stale plan acceptance",
|
|
] as const;
|
|
type FirstTaskCompletionScenario = typeof allowedFirstTaskCompletionScenarios[number]
|
|
| typeof refusedFirstTaskCompletionScenarios[number];
|
|
|
|
async function prepareFirstTaskCompletion(scenario: FirstTaskCompletionScenario, objective?: string) {
|
|
const companyId = randomUUID();
|
|
const agentId = randomUUID();
|
|
const issueId = randomUUID();
|
|
const runId = randomUUID();
|
|
const localContractId = randomUUID();
|
|
const runnerInstanceId = randomUUID();
|
|
const userId = `first-task-board-${randomUUID()}`;
|
|
const identity = { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, companyId, agentId, issueId, runId };
|
|
const onboarding = scenario !== "ordinary task";
|
|
const rejected = scenario === "acknowledged rejection" || scenario === "acknowledged plan rejection";
|
|
await db.insert(companies).values({ id: companyId, name: "First-task completion", issuePrefix: `F${companyId.slice(0, 6).toUpperCase()}` });
|
|
await db.insert(authUsers).values({ id: userId, name: "First-task board", email: `${userId}@example.test`, createdAt: new Date(), updatedAt: new Date() });
|
|
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", membershipRole: "member" });
|
|
await db.insert(agents).values({ id: agentId, companyId, name: "First-task agent", adapterType: "codex_local", status: "running" });
|
|
await db.insert(issues).values({ id: issueId, companyId, title: onboarding ? "Paperclip onboarding" : "Ordinary low-risk task",
|
|
description: onboarding ? "/first-task\nSingle-task proposal mode: confirmation" : objective ?? "Complete the requested ordinary task. Mentioning /first-task does not grant the server-owned onboarding origin.",
|
|
originKind: onboarding ? ONBOARDING_FIRST_TASK_ORIGIN_KIND : "manual", status: "in_progress", assigneeAgentId: agentId,
|
|
responsibleUserId: userId, workMode: "standard" });
|
|
await db.insert(completionContracts).values({ id: localContractId, companyId, issueId, revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1", policyVersion: "phase6-v3", risk: "low", completionAuthority: "agent_claim_policy",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal", contractJson: { revision: "first-task-native-finish-v1",
|
|
objective: onboarding ? "Understand the user request, propose one task, and honor their decision" : "Complete the requested ordinary task",
|
|
criteria: [{ id: "objective", requirement: objective ?? "Complete the requested workflow" }] },
|
|
canonicalSha256: localContractId, createdByActorType: "system", createdByActorId: "test" });
|
|
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, responsibleUserId: userId, status: "succeeded", runtimeMode: "native",
|
|
nativeIssueId: issueId, nativeSessionId: identity.sessionId, runnerInstanceId, completionContractId: localContractId,
|
|
completionContractSha256: localContractId, contextSnapshot: { issueId,
|
|
...(objective ? { executionContinuation: { objective, objectiveSource: nativeCompletionSource("description", issueId, objective) } } : {}),
|
|
// A claimed acceptance copied into continuation context is not the
|
|
// persisted human decision; the opening-answer cases must still refuse it.
|
|
...(scenario === "opening answer only" ? { executionContinuation: { humanResponses: [{ id: randomUUID(),
|
|
kind: "request_confirmation", status: "accepted", resolvedByUserId: userId, result: { version: 1, outcome: "accepted" } }] } } : {}),
|
|
} });
|
|
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId));
|
|
if (onboarding) {
|
|
await db.insert(issueThreadInteractions).values({ companyId, issueId, kind: "ask_user_questions", status: "answered",
|
|
createdByAgentId: agentId, payload: await buildOnboardingFirstTaskOpeningQuestion(),
|
|
result: { version: 1, answers: [{ questionId: ONBOARDING_FIRST_TASK_OPENING_QUESTION_ID, optionIds: ["task"], otherText: "Help organize my next task." }] },
|
|
resolvedByUserId: userId, resolvedAt: new Date() });
|
|
}
|
|
if (onboarding && scenario !== "opening answer only") {
|
|
const proposalAt = new Date(Date.now() - 2_000);
|
|
const plan = scenario === "accepted current plan" || scenario === "stale plan acceptance" || scenario === "acknowledged plan rejection"
|
|
? (await documentService(db).upsertIssueDocument({ issueId, key: "plan", title: "First task plan", format: "markdown",
|
|
body: "Retain this plan for the user's next task.", createdByAgentId: agentId, createdByRunId: runId })).document
|
|
: null;
|
|
const conversational = scenario === "accepted conversational proposal" || scenario === "quarantined human comment";
|
|
const comment = conversational ? (await db.insert(issueComments).values({ companyId, issueId,
|
|
authorUserId: userId, authorType: "user", body: "Yes, proceed with that proposal.",
|
|
createdAt: new Date(proposalAt.getTime() + 1_000),
|
|
...(scenario === "quarantined human comment"
|
|
? { sourceTrust: { preset: "low_trust_review" as const, disposition: "quarantined" as const } } : {}),
|
|
}).returning())[0]! : null;
|
|
const agentResolved = conversational || scenario === "agent-resolved without human comment";
|
|
const outcome = rejected ? "rejected" as const : "accepted" as const;
|
|
// Persisted agent interpretations need a real, trusted human reply. The
|
|
// missing/quarantined variants simulate an untrusted stored result.
|
|
await db.insert(issueThreadInteractions).values({ companyId, issueId,
|
|
kind: plan ? "request_checkbox_confirmation" : "request_confirmation", status: outcome,
|
|
createdByAgentId: agentId, sourceRunId: runId, createdAt: proposalAt,
|
|
payload: plan ? { version: 1, prompt: "May I retain this plan?",
|
|
options: [{ id: "retain-plan", label: "Retain the plan" }], defaultSelectedOptionIds: ["retain-plan"], minSelected: 1,
|
|
target: { type: "issue_document", key: "plan", issueId, documentId: plan.id, revisionId: plan.latestRevisionId! },
|
|
} : { version: 1, prompt: "May I create one task to organize your agenda?" },
|
|
result: { version: 1, outcome, ...(plan && outcome === "accepted" ? { selectedOptionIds: ["retain-plan"] } : {}),
|
|
...(comment ? { commentId: comment.id } : {}),
|
|
...(outcome === "rejected" ? { reason: "Do not do this work." } : {}) },
|
|
resolvedByUserId: agentResolved ? null : userId,
|
|
resolvedByAgentId: agentResolved ? agentId : null, resolvedByRunId: agentResolved ? runId : null,
|
|
resolvedAt: new Date(),
|
|
});
|
|
if (scenario === "newest proposal pending") {
|
|
await db.insert(issueThreadInteractions).values({ companyId, issueId,
|
|
kind: "request_checkbox_confirmation", status: "pending", createdByAgentId: agentId, sourceRunId: runId,
|
|
createdAt: new Date(proposalAt.getTime() + 1_000),
|
|
payload: { version: 1, prompt: "Accept the revised task scope?", options: [{ id: "revised", label: "Use revised scope" }],
|
|
defaultSelectedOptionIds: ["revised"], minSelected: 1 },
|
|
});
|
|
}
|
|
if (scenario === "stale plan acceptance" || scenario === "acknowledged plan rejection") {
|
|
await documentService(db).upsertIssueDocument({ issueId, key: "plan", format: "markdown",
|
|
baseRevisionId: plan!.latestRevisionId, body: "A revised scope that has no persisted human decision.",
|
|
createdByAgentId: agentId, createdByRunId: runId });
|
|
}
|
|
}
|
|
const port = new PaperclipControlPlanePort(db, { companyId, issueId, runId, agentId, sessionId: identity.sessionId,
|
|
completionContractId: localContractId, completionContractSha256: localContractId,
|
|
sourceInstanceId: runnerInstanceId, controlPlaneSourceInstanceId: "first-task-native-finish-control" });
|
|
await port.openRun({ identity, backendKind: "mock", sourceInstanceId: runnerInstanceId });
|
|
const result: PrpStructuredRunResult = { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
summary: rejected
|
|
? "You declined the proposal. I will stop here and have not created or performed the proposed task."
|
|
: "The requested workflow is complete.",
|
|
completionClaim: { contractRevision: "first-task-native-finish-v1", objectiveSatisfied: true,
|
|
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }], remainingWork: [] },
|
|
evidence: [], verification: [{ commandOrCheck: "Check the current task state", status: "passed" }], attentionRequests: [] };
|
|
return { companyId, agentId, issueId, runId, port, result };
|
|
}
|
|
|
|
it("keeps synthetic provider completion open until the requested task document is published", async () => {
|
|
const { companyId, agentId, issueId, runId, port, result } = await prepareFirstTaskCompletion("ordinary task");
|
|
const objective = "Use the connected page service to find recent pages and create a short Markdown briefing document on this task. Put the returned page titles and service receipt code in the document. When complete, give me the document link.";
|
|
await db.update(issues).set({ title: "Create the service briefing", description: objective }).where(eq(issues.id, issueId));
|
|
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } }).where(eq(heartbeatRuns.id, runId));
|
|
const synthetic: PrpStructuredRunResult = { ...result, summary: "Created briefing.md with the returned page titles and service receipt.",
|
|
evidence: [{ ref: "provider:opencode:agent-message" }], artifacts: [] };
|
|
await port.completeRun({ result: synthetic, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL });
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]).toMatchObject({ status: "in_progress" });
|
|
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId)))
|
|
.not.toContainEqual(expect.objectContaining({ toStatus: "done" }));
|
|
const wakes = await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId)));
|
|
expect(wakes).toContainEqual(expect.objectContaining({ payload: expect.objectContaining({
|
|
continuationIdempotencyKey: "native-completion-incomplete", continuationSummary: expect.stringContaining("write_document"),
|
|
}) }));
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
expect(await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))))
|
|
.toHaveLength(wakes.length);
|
|
const followUpId = randomUUID(), runnerInstanceId = randomUUID();
|
|
const [source] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
|
const wake = wakes.find(row => row.payload?.continuationIdempotencyKey === "native-completion-incomplete")!;
|
|
await db.insert(heartbeatRuns).values({ id: followUpId, companyId, agentId, status: "succeeded", runtimeMode: "native",
|
|
nativeIssueId: issueId, nativeSessionId: source!.nativeSessionId, runnerInstanceId,
|
|
completionContractId: source!.completionContractId, completionContractSha256: source!.completionContractSha256,
|
|
wakeupRequestId: wake.id, contextSnapshot: source!.contextSnapshot });
|
|
await db.update(agentWakeupRequests).set({ runId: followUpId }).where(eq(agentWakeupRequests.id, wake.id));
|
|
await db.update(issues).set({ executionRunId: followUpId }).where(eq(issues.id, issueId));
|
|
const followUp = new PaperclipControlPlanePort(db, { companyId, issueId, agentId, runId: followUpId,
|
|
sessionId: source!.nativeSessionId!, sourceInstanceId: runnerInstanceId,
|
|
completionContractId: source!.completionContractId!, completionContractSha256: source!.completionContractSha256!,
|
|
controlPlaneSourceInstanceId: "output-follow-up-control" });
|
|
await followUp.openRun({ identity: { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, companyId, issueId, agentId, runId: followUpId },
|
|
backendKind: "mock", sourceInstanceId: runnerInstanceId });
|
|
await followUp.completeRun({ result: synthetic, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL });
|
|
await finalizeNativeRun({ db, runId: followUpId, workspaceFinalizeStatus: "succeeded" });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("in_progress");
|
|
expect(await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))))
|
|
.toHaveLength(wakes.length);
|
|
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, issueId)))
|
|
.toContainEqual(expect.objectContaining({ ownerAgentId: agentId, cause: "completion_evidence_incomplete", nextAction: expect.stringContaining("write_document") }));
|
|
expect((await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, runId)))[0]!.resultJson)
|
|
.toMatchObject({ result: { reportedWorkDisposition: "done", evidence: synthetic.evidence, artifacts: [] } });
|
|
});
|
|
|
|
it.each(["saved document", "registered attachment", "plain Ask"] as const)(
|
|
"accepts native completion with %s", async (delivery) => {
|
|
const objective = delivery === "plain Ask" ? "What is two plus two?" : "Create a Markdown briefing document on this task.";
|
|
const { companyId, agentId, issueId, runId, port, result } = await prepareFirstTaskCompletion("ordinary task", objective);
|
|
await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, runId));
|
|
const report: PrpStructuredRunResult = { ...result, evidence: [{ ref: "provider:opencode:agent-message" }], artifacts: [] };
|
|
if (delivery === "saved document") {
|
|
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
|
await authority.execute({ tool: "write_document", callId: "briefing", arguments: {
|
|
idempotencyKey: "briefing", key: "briefing", title: "Briefing", body: "Verified service titles and receipt.", baseRevisionId: null,
|
|
} });
|
|
} else if (delivery === "registered attachment") {
|
|
const bytes = Buffer.from("# Briefing\nVerified service titles and receipt.\n");
|
|
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId,
|
|
workspaceRoot: "/workspace/output-control", executionTargetKind: "remote", readRemoteTaskFile: async () => bytes,
|
|
storage: createStorageService(createLocalDiskStorageProvider(outputStorageRoot)) });
|
|
const receipt = await authority.execute({ tool: "register_deliverable", callId: "briefing", arguments: {
|
|
idempotencyKey: "briefing", filename: "briefing.md", contentType: "text/markdown", contentRef: "briefing.md",
|
|
byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex"), title: "Briefing",
|
|
} }) as { entityRefs: string[] };
|
|
report.artifacts = [{ kind: "file", ref: `deliverable:${receipt.entityRefs[0]}` }];
|
|
}
|
|
await port.completeRun({ result: report, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL });
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("done");
|
|
});
|
|
|
|
it.each(["current human rejection", "model-only decline", "different objective revision", "different trigger",
|
|
"changed description after admission", "changed description at commit"] as const)(
|
|
"binds an explicit declined-document fallback to %s", async (scenario) => {
|
|
const objective = "Use the connected page service to find recent pages and create a short Markdown briefing document on this task. Put the returned page titles and service receipt code in the document. When complete, give me the document link. If I decline the action, do not try again or request another connection. Instead, give me a brief explanation that you could not retrieve the data. That explanation is the complete allowed fallback; no briefing is required after a decline.";
|
|
const source = await prepareFirstTaskCompletion("ordinary task", objective);
|
|
const { companyId, agentId, issueId } = source;
|
|
const [issue] = await db.select().from(issues).where(eq(issues.id, issueId));
|
|
const interactionId = randomUUID();
|
|
await db.insert(issueThreadInteractions).values({ id: interactionId, companyId, issueId, sourceRunId: source.runId,
|
|
createdByAgentId: agentId, kind: "request_confirmation", status: "pending", requestedResolverPolicy: "human_only",
|
|
effectiveResolverPolicy: "human_only", continuationPolicy: "wake_assignee", payload: { version: 1, prompt: "Approve service read",
|
|
toolAction: { version: 1, actionRequestId: randomUUID(), invocationId: randomUUID(), toolName: "pages.read",
|
|
toolDisplayName: "Read pages", connectionId: null, applicationId: null, appDisplayName: null, risk: "read",
|
|
previewMarkdown: "Read pages", argumentsSummaryJson: "{}", argumentsHash: "test-hash", expiresAt: "2099-01-01T00:00:00Z" } } });
|
|
if (scenario !== "model-only decline") {
|
|
await issueThreadInteractionService(db).rejectInteraction({ id: issueId, companyId }, interactionId,
|
|
{ reason: "Do not call the service." }, { userId: issue!.responsibleUserId! });
|
|
}
|
|
const contract = await ensureNativeCompletionContract({ db, companyId, issue: issue!, actorId: agentId, humanResponseId: interactionId });
|
|
const runId = randomUUID(), runnerInstanceId = randomUUID();
|
|
const continuedObjective = scenario === "different objective revision" ? `${objective} Still create the document even if I decline.` : objective;
|
|
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "succeeded", runtimeMode: "native", nativeIssueId: issueId,
|
|
nativeSessionId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.sessionId, runnerInstanceId, completionContractId: contract.row.id,
|
|
completionContractSha256: contract.row.canonicalSha256, contextSnapshot: { issueId, executionContinuation: {
|
|
objective: continuedObjective, objectiveSource: nativeCompletionSource("description", issueId, continuedObjective),
|
|
trigger: { interactionId: scenario === "different trigger" ? randomUUID() : interactionId, sourceRunId: source.runId },
|
|
} } });
|
|
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId));
|
|
const port = new PaperclipControlPlanePort(db, { companyId, issueId, agentId, runId,
|
|
sessionId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.sessionId, sourceInstanceId: runnerInstanceId,
|
|
completionContractId: contract.row.id, completionContractSha256: contract.row.canonicalSha256,
|
|
controlPlaneSourceInstanceId: "declined-output-control" });
|
|
await port.openRun({ identity: { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, companyId, issueId, agentId, runId },
|
|
backendKind: "mock", sourceInstanceId: runnerInstanceId });
|
|
const report: PrpStructuredRunResult = { ...source.result, summary: "The service action was declined, so I could not retrieve the data.",
|
|
evidence: [{ ref: "provider:opencode:agent-message" }], completionClaim: { ...source.result.completionClaim,
|
|
contractRevision: contract.contract.revision, criteria: contract.contract.criteria.map(entry => ({ criterionId: entry.id, status: "satisfied", evidenceRefs: [] })) } };
|
|
await port.completeRun({ result: report, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL });
|
|
const changedDescription = "Create the Markdown briefing document on this task. The prior decline does not waive the required document.";
|
|
if (scenario === "changed description after admission") {
|
|
await db.update(issues).set({ description: changedDescription }).where(eq(issues.id, issueId));
|
|
}
|
|
const trigger = `decline_revision_race_${randomUUID().replaceAll("-", "")}`;
|
|
if (scenario === "changed description at commit") {
|
|
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
|
|
begin
|
|
if NEW.issue_id = '${issueId}'::uuid and NEW.run_id = '${runId}'::uuid then
|
|
update issues set description = '${changedDescription}' where id = '${issueId}'::uuid and company_id = '${companyId}'::uuid;
|
|
end if;
|
|
return NEW;
|
|
end $$`));
|
|
await db.execute(sql.raw(`create trigger ${trigger} before insert on work_assessments
|
|
for each row execute function ${trigger}()`));
|
|
}
|
|
try {
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status)
|
|
.toBe(scenario === "current human rejection" ? "done" : scenario === "model-only decline" ? "in_review" : "in_progress");
|
|
} finally {
|
|
if (scenario === "changed description at commit") {
|
|
await db.execute(sql.raw(`drop trigger if exists ${trigger} on work_assessments`));
|
|
await db.execute(sql.raw(`drop function if exists ${trigger}()`));
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rechecks publication removed between assessment and native done commit", async () => {
|
|
const objective = "Create a Markdown briefing document on this task.";
|
|
const { companyId, agentId, issueId, runId, port, result } = await prepareFirstTaskCompletion("ordinary task", objective);
|
|
await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, runId));
|
|
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
|
await authority.execute({ tool: "write_document", callId: "briefing", arguments: {
|
|
idempotencyKey: "briefing", key: "briefing", title: "Briefing", body: "Verified service titles and receipt.", baseRevisionId: null,
|
|
} });
|
|
const report = { ...result, evidence: [{ ref: "provider:opencode:agent-message" }], artifacts: [] };
|
|
await port.completeRun({ result: report, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL });
|
|
const trigger = `publication_race_${randomUUID().replaceAll("-", "")}`;
|
|
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
|
|
begin
|
|
if NEW.issue_id = '${issueId}'::uuid and NEW.run_id = '${runId}'::uuid then
|
|
delete from issue_documents where issue_id = '${issueId}'::uuid and company_id = '${companyId}'::uuid;
|
|
end if;
|
|
return NEW;
|
|
end $$`));
|
|
try {
|
|
await db.execute(sql.raw(`create trigger ${trigger} before insert on work_assessments
|
|
for each row execute function ${trigger}()`));
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("in_progress");
|
|
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId)))
|
|
.not.toContainEqual(expect.objectContaining({ toStatus: "done" }));
|
|
expect(await db.select().from(agentWakeupRequests).where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))))
|
|
.toContainEqual(expect.objectContaining({ payload: expect.objectContaining({
|
|
continuationIdempotencyKey: "native-completion-incomplete", continuationSummary: expect.stringContaining("write_document"),
|
|
}) }));
|
|
} finally {
|
|
await db.execute(sql.raw(`drop trigger if exists ${trigger} on work_assessments`));
|
|
await db.execute(sql.raw(`drop function if exists ${trigger}()`));
|
|
}
|
|
});
|
|
|
|
it.each([false, true])("checks requested publication for an authoritative done decision with an opaque accepted ledger (%s)", async requested => {
|
|
const objective = requested ? "Create a Markdown briefing document on this task." : "What is two plus two?";
|
|
const { companyId, agentId, issueId, runId } = await prepareFirstTaskCompletion("ordinary task", objective);
|
|
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
|
const [issue] = await db.select().from(issues).where(eq(issues.id, issueId));
|
|
const [saved] = await db.insert(nativeRunResults).values({ companyId, issueId, runId,
|
|
completionContractId: run!.completionContractId!, serverFingerprint: randomUUID(), schemaStatus: "accepted",
|
|
resultJson: {}, canonicalSha256: randomUUID() }).returning();
|
|
const [assessment] = await db.insert(workAssessments).values({ companyId, issueId, runId,
|
|
contractId: run!.completionContractId!, resultId: saved!.id, triggerKind: "native_result", triggerActorCompanyId: companyId,
|
|
priorIssueStatus: issue!.status, priorStatusVersion: issue!.statusVersion, policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
|
|
assessmentJson: {}, inputDigest: randomUUID() }).returning();
|
|
await db.insert(nativeRunFinalizations).values({ runId, companyId, issueId, phase: "arbitrating", resultId: saved!.id });
|
|
const commit = commitNativeStatusDecision({ db, companyId, issueId, runId, assessmentId: assessment!.id,
|
|
priorStatus: issue!.status as "in_progress", priorStatusVersion: issue!.statusVersion, priorDecisionId: issue!.lastStatusDecisionId,
|
|
decision: { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "done", toStatus: "done",
|
|
reasonCode: "completion_contract_satisfied", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] } });
|
|
if (requested) await expect(commit).rejects.toThrow("native_status_race");
|
|
else await expect(commit).resolves.toMatchObject({ issue: { status: "done" } });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe(requested ? "in_progress" : "done");
|
|
expect((await db.select().from(nativeRunResults).where(eq(nativeRunResults.id, saved!.id)))[0]!.resultJson).toEqual({});
|
|
});
|
|
|
|
it.each(refusedFirstTaskCompletionScenarios)(
|
|
"refuses native completion feedback without a trusted current first-task decision (%s)", async (scenario) => {
|
|
const { issueId, runId, result } = await prepareFirstTaskCompletion(scenario);
|
|
await expect(nativeCompletionFeedback(db, runId, result))
|
|
.rejects.toThrow("First-task proposal requires a persisted human decision");
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("in_progress");
|
|
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId))).toEqual([]);
|
|
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, runId))).toEqual([]);
|
|
});
|
|
|
|
it.each([...allowedFirstTaskCompletionScenarios, ...refusedFirstTaskCompletionScenarios])(
|
|
"honors the first-task human decision during native completion (%s)", async (scenario) => {
|
|
const { issueId, runId, port, result } = await prepareFirstTaskCompletion(scenario);
|
|
const allowed = allowedFirstTaskCompletionScenarios.some(candidate => candidate === scenario);
|
|
if (allowed) await expect(nativeCompletionFeedback(db, runId, result)).resolves.toContain("Completion report accepted");
|
|
// Invalid results bypass only early feedback to exercise the backstop for a
|
|
// report already committed by a disconnected or older controller.
|
|
await port.completeRun({ result, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, callerResultId: "first-task-native-finish-result" });
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
|
|
const [completedIssue] = await db.select().from(issues).where(eq(issues.id, issueId));
|
|
const decisions = await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId));
|
|
if (!allowed) {
|
|
const expectedReason = scenario === "newest proposal pending" ? "governed_gate_pending" : "onboarding_first_task_decision_required";
|
|
expect(completedIssue).toMatchObject({ status: "in_review" });
|
|
expect(decisions).toContainEqual(expect.objectContaining({ toStatus: "in_review", reasonCode: expectedReason }));
|
|
expect(decisions).not.toContainEqual(expect.objectContaining({ toStatus: "done" }));
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)))[0]!.resultJson)
|
|
.toMatchObject({ finalizationReasonCode: expectedReason });
|
|
} else {
|
|
expect(completedIssue).toMatchObject({ status: "done" });
|
|
expect(decisions).toContainEqual(expect.objectContaining({ toStatus: "done", reasonCode: "completion_claim_policy_accepted" }));
|
|
}
|
|
});
|
|
|
|
it("rechecks the first-task human decision atomically before native status commit", async () => {
|
|
const { companyId, agentId, issueId, runId, port, result } = await prepareFirstTaskCompletion("accepted proposal");
|
|
await expect(nativeCompletionFeedback(db, runId, result)).resolves.toContain("Completion report accepted");
|
|
await port.completeRun({ result, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, callerResultId: "first-task-race-result" });
|
|
expect(await db.select().from(workAssessments).where(eq(workAssessments.runId, runId))).toEqual([]);
|
|
|
|
const interactionId = randomUUID();
|
|
const trigger = `first_task_race_${randomUUID().replace(/-/g, "")}`;
|
|
// Assessment persistence follows the policy read and precedes status commit.
|
|
// Insert a newer proposal at that real database boundary, exactly once.
|
|
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
|
|
begin
|
|
if NEW.issue_id = '${issueId}'::uuid and NEW.run_id = '${runId}'::uuid then
|
|
insert into issue_thread_interactions
|
|
(id, company_id, issue_id, kind, status, created_by_agent_id, source_run_id, payload, created_at)
|
|
values
|
|
('${interactionId}'::uuid, '${companyId}'::uuid, '${issueId}'::uuid,
|
|
'request_confirmation', 'pending', '${agentId}'::uuid, '${runId}'::uuid,
|
|
'{"version":1,"prompt":"Accept the revised task scope?"}'::jsonb, clock_timestamp())
|
|
on conflict (id) do nothing;
|
|
end if;
|
|
return NEW;
|
|
end $$`));
|
|
try {
|
|
await db.execute(sql.raw(`create trigger ${trigger} before insert on work_assessments
|
|
for each row execute function ${trigger}()`));
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
|
|
expect((await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, interactionId)))[0])
|
|
.toMatchObject({ status: "pending", issueId });
|
|
expect((await db.select().from(issues).where(eq(issues.id, issueId)))[0]!.status).toBe("in_review");
|
|
const decisions = await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId));
|
|
expect(decisions).toEqual([expect.objectContaining({ toStatus: "in_review", reasonCode: "governed_gate_pending" })]);
|
|
expect(decisions).not.toContainEqual(expect.objectContaining({ toStatus: "done" }));
|
|
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)))[0])
|
|
.toMatchObject({ nativePhase: "committed", resultJson: { finalizationReasonCode: "governed_gate_pending" } });
|
|
} finally {
|
|
await db.execute(sql.raw(`drop trigger if exists ${trigger} on work_assessments`));
|
|
await db.execute(sql.raw(`drop function if exists ${trigger}()`));
|
|
}
|
|
});
|
|
|
|
it.each(["none", "queued", "claimed", "deferred_issue_execution", "coalesced", "current_run", "current_intent"])(
|
|
"preserves completion caveats and pending child delivery (%s)", async (delivery) => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = randomUUID();
|
|
const localContractId = randomUUID();
|
|
const runId = randomUUID();
|
|
const runnerInstanceId = randomUUID();
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "DOT-29 completion caveat",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v3",
|
|
risk: "low",
|
|
completionAuthority: "agent_claim_policy",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "phase6-v3",
|
|
objective: "Complete the low-risk task",
|
|
criteria: [{ id: "objective", requirement: "Complete the requested work" }],
|
|
},
|
|
canonicalSha256: "dot-29-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "succeeded",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "dot-29-contract",
|
|
contextSnapshot: { issueId },
|
|
});
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "dot-29-contract",
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: "dot-29-control",
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId, runId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
const result: PrpStructuredRunResult = {
|
|
...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
summary: "The requested work is complete; local Node verification could not run.",
|
|
completionClaim: {
|
|
contractRevision: "phase6-v3",
|
|
objectiveSatisfied: true,
|
|
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }],
|
|
remainingWork: [],
|
|
},
|
|
evidence: [],
|
|
verification: [{ commandOrCheck: "Run npm test", status: "not_run" }],
|
|
attentionRequests: [{
|
|
kind: "environment_constraint",
|
|
summary: "Node and npm are unavailable in this sandbox.",
|
|
}],
|
|
};
|
|
await port.completeRun({
|
|
result,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "dot-29-result",
|
|
});
|
|
const pendingDelivery = ["queued", "claimed", "deferred_issue_execution"].includes(delivery);
|
|
if (delivery !== "none") {
|
|
const intentId = randomUUID();
|
|
await db.insert(agentWakeupRequests).values({
|
|
id: intentId, companyId: identity.companyId, agentId: identity.agentId,
|
|
source: "automation", triggerDetail: "system", reason: delivery === "deferred_issue_execution" ? "issue_execution_deferred" : "issue_children_completed",
|
|
requestedByActorType: "system", requestedByActorId: delivery === "deferred_issue_execution"
|
|
? `native-status-wake-dispatch:${randomUUID()}` : "native-status-committer",
|
|
status: delivery.startsWith("current_") ? "claimed" : delivery,
|
|
runId: delivery === "current_run" ? runId : null,
|
|
payload: { issueId, _paperclipWakeContext: { wakeReason: "issue_children_completed", nativeChildCompletionDecisionId: randomUUID() } },
|
|
});
|
|
if (delivery === "current_intent") await db.update(heartbeatRuns)
|
|
.set({ contextSnapshot: { issueId, nativeStatusWakeIntentId: intentId } })
|
|
.where(eq(heartbeatRuns.id, runId));
|
|
}
|
|
const expectedStatus = pendingDelivery ? "in_progress" : "done";
|
|
const expectedReason = pendingDelivery ? "native_child_completion_pending" : "completion_claim_policy_accepted";
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
|
|
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: expectedStatus, statusVersion: 1 }),
|
|
]);
|
|
await expect(db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId))).resolves.toEqual([]);
|
|
await expect(db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, identity.companyId)))
|
|
.resolves.not.toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ payload: expect.objectContaining({ issueId, sourceRunId: runId }) }),
|
|
]));
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ toStatus: expectedStatus, reasonCode: expectedReason }),
|
|
]);
|
|
const [persistedRun] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
|
|
expect(persistedRun).toMatchObject({ status: "succeeded", nativePhase: "committed" });
|
|
expect(persistedRun?.resultJson).toMatchObject({
|
|
finalizationReasonCode: expectedReason,
|
|
verificationCaveats: [{
|
|
commandOrCheck: "Run npm test",
|
|
reasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this sandbox.",
|
|
}],
|
|
ignoredAttentionRequests: [expect.objectContaining({
|
|
sourceKind: "environment_constraint",
|
|
disposition: "verification_caveat",
|
|
})],
|
|
});
|
|
});
|
|
|
|
it("serializes a concurrent child notification with the parent's completion check", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
async function preparedRun(parentId: string | null) {
|
|
const agentId = randomUUID();
|
|
await db.insert(agents).values({ id: agentId, companyId: identity.companyId,
|
|
name: parentId ? "Child worker" : "Parent lead", adapterType: "codex_local", status: "running" });
|
|
const issueId = randomUUID();
|
|
const runId = randomUUID();
|
|
const localContractId = randomUUID();
|
|
const runnerInstanceId = randomUUID();
|
|
await db.insert(issues).values({ id: issueId, companyId: identity.companyId, parentId,
|
|
title: parentId ? "Revised child" : "Deliver revised child result", status: "in_progress",
|
|
assigneeAgentId: agentId, workMode: "standard" });
|
|
await db.insert(completionContracts).values({ id: localContractId, companyId: identity.companyId,
|
|
issueId, revision: 1, schemaVersion: "paperclip.completion-contract.v1", policyVersion: "phase6-v3",
|
|
risk: "low", completionAuthority: "agent_claim_policy", incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: { revision: "handoff-race-v1", objective: "Deliver the requested result",
|
|
criteria: [{ id: "objective", requirement: "Complete the requested work" }] },
|
|
canonicalSha256: localContractId, createdByActorType: "system", createdByActorId: "test" });
|
|
await db.insert(heartbeatRuns).values({ id: runId, companyId: identity.companyId, agentId,
|
|
status: "succeeded", runtimeMode: "native", nativeIssueId: issueId, nativeSessionId: identity.sessionId,
|
|
runnerInstanceId, completionContractId: localContractId, completionContractSha256: localContractId,
|
|
contextSnapshot: { issueId } });
|
|
const port = new PaperclipControlPlanePort(db, { ...identity, agentId, issueId, runId,
|
|
completionContractId: localContractId, completionContractSha256: localContractId,
|
|
sourceInstanceId: runnerInstanceId, controlPlaneSourceInstanceId: "handoff-race" });
|
|
await port.openRun({ identity: { ...identity, agentId, issueId, runId }, backendKind: "mock", sourceInstanceId: runnerInstanceId });
|
|
await port.completeRun({ result: { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
summary: parentId ? "Revised result is ready." : "The result is delivered.",
|
|
completionClaim: { contractRevision: "handoff-race-v1", objectiveSatisfied: true,
|
|
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }], remainingWork: [] },
|
|
evidence: [], verification: [{ commandOrCheck: "node --test", status: "passed" }], attentionRequests: [],
|
|
}, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, callerResultId: "handoff-race-result" });
|
|
return { issueId, runId };
|
|
}
|
|
const parent = await preparedRun(null);
|
|
const child = await preparedRun(parent.issueId);
|
|
const suffix = randomUUID().replace(/-/g, "");
|
|
const trigger = `handoff_barrier_${suffix}`;
|
|
const lockKey = Number.parseInt(suffix.slice(0, 7), 16);
|
|
// A test-only database barrier pauses the real child transaction after it
|
|
// reads the parent and immediately before it inserts the completion wake.
|
|
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
|
|
begin
|
|
if NEW.requested_by_actor_id = 'native-status-committer'
|
|
and NEW.payload->>'issueId' = '${parent.issueId}' then
|
|
perform pg_advisory_xact_lock(${lockKey}, 1);
|
|
end if;
|
|
return NEW;
|
|
end $$`));
|
|
await db.execute(sql.raw(`create trigger ${trigger} before insert on agent_wakeup_requests
|
|
for each row execute function ${trigger}()`));
|
|
let childCompletion: Promise<unknown> | undefined;
|
|
let parentCompletion: Promise<unknown> | undefined;
|
|
let parentSettled = false;
|
|
async function waitUntil(check: () => Promise<boolean>) {
|
|
for (let attempt = 0; attempt < 500; attempt++) {
|
|
if (await check()) return true;
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
return false;
|
|
}
|
|
try {
|
|
await db.transaction(async (tx) => {
|
|
await tx.execute(sql`select pg_advisory_xact_lock(${lockKey}::integer, 1)`);
|
|
const [controller] = await tx.execute(sql`select pg_backend_pid() as pid`) as unknown as Array<{ pid: number }>;
|
|
childCompletion = finalizeNativeRun({ db, runId: child.runId, workspaceFinalizeStatus: "succeeded" })
|
|
.then(() => null, (error: unknown) => error);
|
|
let childPid: number | null = null;
|
|
expect(await waitUntil(async () => {
|
|
const [blocked] = await db.execute(sql`select pid from pg_stat_activity
|
|
where ${controller.pid} = any(pg_blocking_pids(pid)) limit 1`) as unknown as Array<{ pid: number }>;
|
|
childPid = blocked?.pid ?? null;
|
|
return childPid !== null;
|
|
})).toBe(true);
|
|
parentCompletion = finalizeNativeRun({ db, runId: parent.runId, workspaceFinalizeStatus: "succeeded" })
|
|
.then(() => { parentSettled = true; return null; }, (error: unknown) => { parentSettled = true; return error; });
|
|
let parentWaiting = false;
|
|
expect(await waitUntil(async () => {
|
|
const [state] = await db.execute(sql`select exists (select 1 from pg_stat_activity
|
|
where ${childPid}::integer = any(pg_blocking_pids(pid))) as waiting`) as unknown as Array<{ waiting: boolean }>;
|
|
parentWaiting = state.waiting;
|
|
return parentWaiting || parentSettled;
|
|
})).toBe(true);
|
|
expect(parentSettled).toBe(false);
|
|
expect(parentWaiting).toBe(true);
|
|
});
|
|
expect(await childCompletion).toBeNull();
|
|
expect(await parentCompletion).toBeNull();
|
|
expect((await db.select().from(issues).where(eq(issues.id, child.issueId)))[0]!.status).toBe("done");
|
|
expect((await db.select().from(issues).where(eq(issues.id, parent.issueId)))[0]!.status).toBe("in_progress");
|
|
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.issueId, parent.issueId)))
|
|
.toEqual([expect.objectContaining({ reasonCode: "native_child_completion_pending" })]);
|
|
const wakes = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, identity.companyId));
|
|
expect(wakes.filter(wake => wake.payload?.issueId === parent.issueId)).toHaveLength(1);
|
|
} finally {
|
|
await Promise.all([childCompletion, parentCompletion]);
|
|
await db.execute(sql.raw(`drop trigger ${trigger} on agent_wakeup_requests`));
|
|
await db.execute(sql.raw(`drop function ${trigger}()`));
|
|
}
|
|
}, 30_000);
|
|
|
|
it.each(["child_dependency", "child"].flatMap((relationship) =>
|
|
["manual", "task_watchdog", "task_watchdog_product_bug"].map((originKind) => ({ relationship, originKind })),
|
|
))("preserves completion wake identity across reopen ($relationship, $originKind)", async ({ relationship, originKind }) => {
|
|
const hasDependency = relationship !== "child";
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const parentIssueId = randomUUID();
|
|
const childIssueId = randomUUID();
|
|
const localContractId = randomUUID();
|
|
await db.insert(issues).values([
|
|
{
|
|
id: parentIssueId,
|
|
companyId: identity.companyId,
|
|
title: "Accepted plan parent",
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "planning",
|
|
},
|
|
{
|
|
id: childIssueId,
|
|
companyId: identity.companyId,
|
|
parentId: parentIssueId,
|
|
title: "Implement the accepted plan",
|
|
originKind,
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
},
|
|
]);
|
|
if (hasDependency) await db.insert(issueRelations).values({
|
|
companyId: identity.companyId,
|
|
issueId: childIssueId,
|
|
relatedIssueId: parentIssueId,
|
|
type: "blocks",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: localContractId,
|
|
companyId: identity.companyId,
|
|
issueId: childIssueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v3",
|
|
risk: "low",
|
|
completionAuthority: "agent_claim_policy",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: {
|
|
revision: "child-wake-v1",
|
|
objective: "Implement the accepted plan",
|
|
criteria: [{ id: "objective", requirement: "Implement and test" }],
|
|
},
|
|
canonicalSha256: "child-wake-contract",
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
for (const revision of [1, 2]) {
|
|
const runId = randomUUID();
|
|
const runnerInstanceId = randomUUID();
|
|
if (revision === 2) {
|
|
// The original completion wake was already consumed. Feedback reopens
|
|
// the same child while its parent still needs the revised result.
|
|
await db.update(agentWakeupRequests).set({ status: "completed" })
|
|
.where(sql`${agentWakeupRequests.payload}->>'issueId' = ${parentIssueId}`);
|
|
await db.update(issues).set({ status: "blocked" }).where(eq(issues.id, parentIssueId));
|
|
await db.update(issues).set({ status: "in_progress", statusVersion: 2 })
|
|
.where(eq(issues.id, childIssueId));
|
|
}
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: childIssueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "child-wake-contract",
|
|
contextSnapshot: { issueId: childIssueId },
|
|
});
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId: childIssueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: localContractId,
|
|
completionContractSha256: "child-wake-contract",
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: "child-wake-control",
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId: childIssueId, runId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
const result: PrpStructuredRunResult = {
|
|
...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
summary: `Implemented revision ${revision} and passed 44/44 tests.`,
|
|
completionClaim: {
|
|
contractRevision: "child-wake-v1",
|
|
objectiveSatisfied: true,
|
|
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }],
|
|
remainingWork: [],
|
|
},
|
|
verification: [{ commandOrCheck: "node --test", status: "passed" }],
|
|
attentionRequests: [],
|
|
};
|
|
await port.completeRun({
|
|
result,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "child-wake-result",
|
|
});
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
const [completedChild] = await db.select().from(issues).where(eq(issues.id, childIssueId));
|
|
expect(completedChild).toMatchObject({ status: "done", statusVersion: revision === 1 ? 1 : 3 });
|
|
|
|
const parentWakes = await db.select().from(agentWakeupRequests)
|
|
.where(eq(agentWakeupRequests.companyId, identity.companyId))
|
|
.then((rows) => rows.filter((row) => row.payload?.issueId === parentIssueId));
|
|
const wakeCount = originKind === "task_watchdog" ? 1 : revision;
|
|
expect(parentWakes).toHaveLength(wakeCount);
|
|
const latestWake = parentWakes.find((wake) => wake.status === "queued");
|
|
if (originKind === "task_watchdog" && revision === 2) {
|
|
expect(latestWake).toBeUndefined();
|
|
} else expect(latestWake).toMatchObject({
|
|
reason: "issue_children_completed",
|
|
payload: {
|
|
issueId: parentIssueId,
|
|
completedChildIssueId: childIssueId,
|
|
childIssueIds: [childIssueId],
|
|
childIssueSummaries: [{
|
|
id: childIssueId,
|
|
title: "Implement the accepted plan",
|
|
status: "done",
|
|
summary: `Implemented revision ${revision} and passed 44/44 tests.`,
|
|
}],
|
|
childIssueSummaryTruncated: false,
|
|
_paperclipWakeContext: {
|
|
wakeReason: "issue_children_completed",
|
|
childIssueSummaries: [{
|
|
id: childIssueId,
|
|
summary: `Implemented revision ${revision} and passed 44/44 tests.`,
|
|
}],
|
|
},
|
|
},
|
|
});
|
|
if (latestWake) {
|
|
const delivery = latestWake.payload?._paperclipWakeContext as Record<string, unknown>;
|
|
expect(delivery.nativeChildCompletionDecisionId).toBe(
|
|
originKind === "task_watchdog" ? undefined : completedChild!.lastStatusDecisionId,
|
|
);
|
|
}
|
|
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded" });
|
|
const replayWakes = await db.select().from(agentWakeupRequests)
|
|
.where(eq(agentWakeupRequests.companyId, identity.companyId))
|
|
.then((rows) => rows.filter((row) => row.payload?.issueId === parentIssueId));
|
|
expect(replayWakes).toHaveLength(wakeCount);
|
|
expect(new Set(replayWakes.map((wake) => wake.idempotencyKey)).size).toBe(wakeCount);
|
|
}
|
|
});
|
|
|
|
it("returns a rejected native completion review to the original agent with the reviewer reason", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const issueId = "30000000-0000-4000-8000-000000000027";
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: "Rejected native completion review",
|
|
status: "in_review",
|
|
assigneeAgentId: identity.agentId,
|
|
responsibleUserId: "reviewer-25",
|
|
workMode: "standard",
|
|
});
|
|
const [interaction] = await db.insert(issueThreadInteractions).values({
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
kind: "request_confirmation",
|
|
status: "pending",
|
|
continuationPolicy: "wake_assignee",
|
|
requestedResolverPolicy: "human_only",
|
|
effectiveResolverPolicy: "human_only",
|
|
resolverPolicyProvenance: "explicit",
|
|
effectiveResolverPolicySource: "requested",
|
|
sourceRunId: taskRunId,
|
|
addresseeUserId: "reviewer-25",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Approve completion?",
|
|
rejectRequiresReason: true,
|
|
target: { type: "custom", key: "native_completion_review", revisionId: "decision-25" },
|
|
},
|
|
}).returning();
|
|
|
|
const rejected = await issueThreadInteractionService(db).rejectInteraction(
|
|
{ id: issueId, companyId: identity.companyId, status: "in_review" },
|
|
interaction!.id,
|
|
{ reason: "Add the missing external verification." },
|
|
{ userId: "reviewer-25" },
|
|
);
|
|
|
|
expect(rejected).toMatchObject({
|
|
status: "rejected",
|
|
result: { outcome: "rejected", reason: "Add the missing external verification." },
|
|
});
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "todo", assigneeAgentId: identity.agentId }),
|
|
]);
|
|
});
|
|
|
|
it("preserves the result and issue status when workspace finalization fails", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId: workspaceFailureIssueId,
|
|
runId: workspaceFailureRunId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: workspaceFailureContractId,
|
|
completionContractSha256: "phase6-workspace-failure-contract",
|
|
sourceInstanceId: workspaceFailureRunnerId,
|
|
controlPlaneSourceInstanceId: "control-phase6-workspace-failure",
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, runId: workspaceFailureRunId, issueId: workspaceFailureIssueId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: workspaceFailureRunnerId,
|
|
});
|
|
await port.completeRun({
|
|
result: CONTROL_PLANE_CONFORMANCE_RESULT,
|
|
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
callerResultId: "phase6-workspace-failure-result",
|
|
});
|
|
await finalizeNativeRun({
|
|
db,
|
|
runId: workspaceFailureRunId,
|
|
workspaceFinalizeStatus: "failed",
|
|
projectRunStatus: true,
|
|
});
|
|
await expect(db.select().from(issues).where(eq(issues.id, workspaceFailureIssueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
|
|
]);
|
|
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, workspaceFailureRunId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "succeeded", nativePhase: "retryable_failure" }),
|
|
]);
|
|
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, workspaceFailureRunId))).resolves.toHaveLength(1);
|
|
});
|
|
|
|
it("LIVE-01..06 rolls back status, decision, and liveness rows at every materialization failpoint", async () => {
|
|
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
|
|
const cases: Array<{
|
|
suffix: number;
|
|
failpoint: "governance_materialization" | "interaction_materialization" | "continuation_materialization" | "blocker_materialization" | "recovery_materialization" | "status_projection";
|
|
result: PrpStructuredRunResult;
|
|
terminalState?: "succeeded" | "failed" | "cancelled";
|
|
executionState?: Record<string, unknown>;
|
|
}> = [
|
|
{
|
|
suffix: 20,
|
|
failpoint: "interaction_materialization",
|
|
result: { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), reportedWorkDisposition: "needs_review", attentionRequests: [{ kind: "approval", summary: "Approve publication", ownerClass: "human" }] },
|
|
},
|
|
{
|
|
suffix: 21,
|
|
failpoint: "continuation_materialization",
|
|
result: {
|
|
...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
reportedWorkDisposition: "yielded",
|
|
continuation: { kind: "same_agent", summary: "Continue after recovery", idempotencyKey: "live-continue" },
|
|
},
|
|
},
|
|
{
|
|
suffix: 22,
|
|
failpoint: "blocker_materialization",
|
|
result: {
|
|
...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
reportedWorkDisposition: "blocked",
|
|
blocker: {
|
|
reasonCode: "access",
|
|
owner: { kind: "user", name: "Board operator" },
|
|
unblockAction: "Approve access",
|
|
scope: "task_wide",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
suffix: 23,
|
|
failpoint: "status_projection",
|
|
result: { ...structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT), reportedWorkDisposition: "needs_review" },
|
|
},
|
|
{
|
|
suffix: 25,
|
|
failpoint: "recovery_materialization",
|
|
terminalState: "cancelled",
|
|
result: structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
},
|
|
{
|
|
suffix: 26,
|
|
failpoint: "governance_materialization",
|
|
executionState: { status: "pending", stage: "approval" },
|
|
result: structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT),
|
|
},
|
|
];
|
|
for (const entry of cases) {
|
|
const value = String(entry.suffix).padStart(12, "0");
|
|
const issueId = `30000000-0000-4000-8000-${value}`;
|
|
const contractId = `31000000-0000-4000-8000-${value}`;
|
|
const runId = `32000000-0000-4000-8000-${value}`;
|
|
const runnerInstanceId = `33000000-0000-4000-8000-${value}`;
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId: identity.companyId,
|
|
title: `Atomic liveness ${entry.failpoint}`,
|
|
status: "in_progress",
|
|
assigneeAgentId: identity.agentId,
|
|
workMode: "standard",
|
|
executionState: entry.executionState,
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: contractId,
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "phase6-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: { revision: "phase6-v1", objective: "Atomic liveness", criteria: [{ id: "objective", requirement: "Stay atomic" }] },
|
|
canonicalSha256: `contract-${entry.suffix}`,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values({
|
|
id: runId,
|
|
companyId: identity.companyId,
|
|
agentId: identity.agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: identity.sessionId,
|
|
runnerInstanceId,
|
|
completionContractId: contractId,
|
|
completionContractSha256: `contract-${entry.suffix}`,
|
|
contextSnapshot: { issueId },
|
|
});
|
|
const terminal: PrpTerminalState = {
|
|
...CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
|
runTerminalState: entry.terminalState ?? "succeeded",
|
|
reportedWorkDisposition: entry.result.reportedWorkDisposition,
|
|
};
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId: identity.companyId,
|
|
issueId,
|
|
runId,
|
|
agentId: identity.agentId,
|
|
sessionId: identity.sessionId,
|
|
completionContractId: contractId,
|
|
completionContractSha256: `contract-${entry.suffix}`,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: `atomic-control-${entry.suffix}`,
|
|
});
|
|
await port.openRun({
|
|
identity: { ...identity, issueId, runId },
|
|
backendKind: "mock",
|
|
sourceInstanceId: runnerInstanceId,
|
|
});
|
|
await port.completeRun({ result: entry.result, terminal, callerResultId: `atomic-result-${entry.suffix}` });
|
|
await expect(finalizeNativeRun({
|
|
db,
|
|
runId,
|
|
workspaceFinalizeStatus: "succeeded",
|
|
failpoint: entry.failpoint,
|
|
})).resolves.toEqual(expect.objectContaining({ phase: "retryable_failure" }));
|
|
await expect(db.select().from(issues).where(eq(issues.id, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
|
|
]);
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, issueId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(agentWakeupRequests)
|
|
.where(eq(agentWakeupRequests.companyId, identity.companyId))).resolves.not.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ payload: expect.objectContaining({ issueId }) })]),
|
|
);
|
|
await expect(db.select().from(issueRecoveryActions)
|
|
.where(eq(issueRecoveryActions.sourceIssueId, issueId))).resolves.toEqual([
|
|
expect.objectContaining({ status: "active", cause: "side_effect_planning_failed" }),
|
|
]);
|
|
}
|
|
});
|
|
});
|