mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Native runs report their task outcome through a structured finish result. > - A Board comment can permit a passive wait for the next response. > - That exception accepted reports that also admitted blocking unfinished work. > - The task then stayed In Progress without a runner, and recovery treated the wait as healthy. > - This pull request rejects that contradiction and uses the existing bounded continuation path. > - Real wait conditions and protection against obsolete requests remain in force. ## Linked Issues or Issue Description **What happened?** An agent answered a status inquiry with `yielded` and `response_wake`. The same result listed blocking remaining work. The server accepted an indefinite wait without a question, approval, dependency, or pause. No further run was queued. **Expected behavior** Unfinished ordinary tasks must continue or have a recorded reason to wait. A status reply alone must not suspend the work. **Steps to reproduce** 1. Add a Board status inquiry to an unfinished assigned task. 2. Submit a successful native result with `yielded`, `response_wake`, and `remainingWork[].blocksCompletion: true`. 3. Leave the task without any real wait condition. 4. Observe that the old policy preserves In Progress with no continuation and suppresses recovery. **Paperclip version or commit** Reproduced in the native status policy at `da887ea3e`. The branch is based on current master. **Deployment mode** Authenticated server with the native Paperclip Runner. Related work: Refs #13338 (native response waits and recovery). Refs #12071 (separate legacy recovery and retry-state work). This change fixes the native unfinished-response-wait exception. ## What Changed - Reject contradictory finish reports while the provider can still correct them. - Route accepted unfinished response waits through the existing one-follow-up continuation budget. Repeated incomplete results create a visible recovery action. - Preserve questions, approvals, dependencies, pauses, conversation lifecycles, and superseded Board requests. - Recheck the current Board source in the decision transaction before queuing repair. - Let normal recovery reconsider old committed waits that report blocking work and still have a current source. - Add policy and database regression tests. Document the rule. ## Verification - `pnpm exec vitest run server/src/services/native-runtime/status-arbiter.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts --no-file-parallelism`: 346 tests passed, no skips. - `pnpm -r typecheck`: passed. - `pnpm build`: passed. - `git diff --check origin/master...HEAD`: passed. - Final head `8f0824d9bb4d631f2347f8afffe5fff6d574cb69`: all CI checks green (54 passed, 2 intentionally skipped), including all general tests, serialized server suites, runner tests, eight E2E shards, and the canary dry run. - Greptile: 5/5 on the final head, with no unresolved review threads. - The broad local `pnpm test:run` encountered an unrelated timing failure in `workspace-runtime.test.ts` (waiting for managed process-tree listeners). That test passed on an isolated rerun. The duplicate broad run was stopped; the complete CI suite passed on the final commit. - The transaction-race regression reproduced an obsolete continuation before the fix. All 12 source-change cases now pass, covering passive waits, corrective continuations, and exhausted-repair decisions. ## Risks - Agents that previously parked unfinished ordinary tasks must now continue or record an actual wait condition. - Old contradictory waits become eligible for normal recovery. Existing ownership, budget, pause, and supersession checks still apply. - The rule uses the structured blocking-work flag. It does not infer omitted work from prose. - No schema, dependency, or UI change. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository inspection, code editing, and test execution. The session does not expose a more specific deployment identifier or context-window size. ## 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 either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
654 lines
21 KiB
TypeScript
654 lines
21 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { NativeEvidenceAssessment } from "./evidence-classifier.js";
|
|
import { arbitrateNativeStatus } from "./status-arbiter.js";
|
|
|
|
function assessment(
|
|
overrides: Partial<NativeEvidenceAssessment> = {},
|
|
): NativeEvidenceAssessment {
|
|
return {
|
|
objectiveClaimSatisfied: true,
|
|
objectiveSatisfied: true,
|
|
allCriteriaSatisfied: true,
|
|
verificationPassed: true,
|
|
hasFailedVerification: false,
|
|
hasBlockingRemainingWork: false,
|
|
reportedDisposition: "done",
|
|
summary: "Complete",
|
|
contractRevisionMatches: true,
|
|
criterionAssessments: [],
|
|
verificationAssessments: [],
|
|
verificationCaveats: [],
|
|
acceptedEvidenceRefs: ["event:2"],
|
|
missingRequirements: [],
|
|
rejectedEvidence: [],
|
|
unverifiableEvidence: [],
|
|
blocker: null,
|
|
continuation: null,
|
|
attentionRequests: [],
|
|
ignoredAttentionRequests: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function arbitrate(
|
|
overrides: Partial<Parameters<typeof arbitrateNativeStatus>[0]> = {},
|
|
) {
|
|
return arbitrateNativeStatus({
|
|
assessment: assessment(),
|
|
terminalState: "succeeded",
|
|
workspaceFinalizeStatus: "succeeded",
|
|
agentId: "agent",
|
|
priorIssueStatus: "in_progress",
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("native status authority", () => {
|
|
it("a reviewer finishes its decision without completing rejected or still-reviewed work", () => {
|
|
for (const priorIssueStatus of ["in_progress", "in_review"] as const) {
|
|
const decision = arbitrate({ priorIssueStatus, nativeReviewOutcome: "resolved" });
|
|
expect(decision).toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus });
|
|
expect(decision.effects).not.toContainEqual(expect.objectContaining({ kind: "enqueue_continuation" }));
|
|
}
|
|
});
|
|
|
|
it("routes an unfinished reviewer action to bounded recovery instead of retrying the worker task", () => {
|
|
const decision = arbitrate({ priorIssueStatus: "in_review", nativeReviewOutcome: "pending" });
|
|
expect(decision).toMatchObject({ statusAction: "preserve", toStatus: "in_review" });
|
|
expect(decision.effects).toContainEqual(expect.objectContaining({
|
|
kind: "record_recovery", cause: "native_review_unresolved", agentId: "agent",
|
|
}));
|
|
expect(decision.effects.some((effect) => ["enqueue_continuation", "schedule_retry"].includes(effect.kind))).toBe(false);
|
|
});
|
|
|
|
it("treats only the authorized Board response_wake as passive and preserves governance", () => {
|
|
const passive = assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Wait for the next request",
|
|
idempotencyKey: "board-wait",
|
|
},
|
|
});
|
|
expect(
|
|
arbitrate({ assessment: passive, boardResponseWaitAuthorized: true }),
|
|
).toMatchObject({
|
|
reasonCode: "board_response_waiting",
|
|
toStatus: "in_progress",
|
|
effects: [],
|
|
});
|
|
expect(arbitrate({ assessment: passive })).toMatchObject({
|
|
reasonCode: "live_continuation_registered",
|
|
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
|
|
});
|
|
expect(
|
|
arbitrate({ assessment: passive, boardResponseWaitOrigin: true }),
|
|
).toMatchObject({
|
|
reasonCode: "board_response_wait_superseded",
|
|
statusAction: "preserve",
|
|
effects: [],
|
|
});
|
|
expect(
|
|
arbitrate({
|
|
assessment: passive,
|
|
boardResponseWaitAuthorized: true,
|
|
governanceGate: { kind: "approval", id: "approval" },
|
|
}),
|
|
).toMatchObject({
|
|
reasonCode: "governed_response_waiting",
|
|
toStatus: "in_review",
|
|
effects: [expect.objectContaining({ kind: "create_interaction" })],
|
|
});
|
|
for (const kind of ["same_agent", "retry", "monitor"] as const) {
|
|
expect(
|
|
arbitrate({
|
|
assessment: {
|
|
...passive,
|
|
continuation: { ...passive.continuation!, kind },
|
|
},
|
|
boardResponseWaitAuthorized: true,
|
|
}),
|
|
).toMatchObject({
|
|
reasonCode: "live_continuation_registered",
|
|
effects: [expect.objectContaining({ continuationKind: kind })],
|
|
});
|
|
}
|
|
});
|
|
it("repairs unfinished response waits once using the server continuation budget", () => {
|
|
const unfinished = assessment({
|
|
reportedDisposition: "yielded", hasBlockingRemainingWork: true,
|
|
continuation: { kind: "response_wake", summary: "Answered status; await next message", idempotencyKey: "model-key" },
|
|
});
|
|
for (const boardResponseWaitAuthorized of [true, false]) {
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitAuthorized })).toMatchObject({
|
|
reasonCode: "completion_evidence_incomplete",
|
|
toStatus: "in_progress",
|
|
effects: [{ kind: "enqueue_continuation", continuationKind: "same_agent", idempotencyKey: "native-completion-incomplete" }],
|
|
});
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitAuthorized, allowIncompleteContinuation: false })).toMatchObject({
|
|
reasonCode: "prior_status_preserved_no_live_path",
|
|
effects: [{ kind: "record_finalization_error", cause: "completion_evidence_incomplete" }],
|
|
});
|
|
}
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitOrigin: true, boardResponseWaitAuthorized: true }))
|
|
.toMatchObject({ reasonCode: "completion_evidence_incomplete" });
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitOrigin: true, boardResponseWaitAuthorized: false }))
|
|
.toMatchObject({ reasonCode: "board_response_wait_superseded", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, hasActivePauseHold: true }))
|
|
.toMatchObject({ reasonCode: "response_wait_pause_preserved", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, hasUnresolvedIssueBlockers: true }))
|
|
.toMatchObject({ toStatus: "blocked", reasonCode: "durable_dependency_blocker_bound", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, governanceGate: { kind: "interaction", id: "question" } }))
|
|
.toMatchObject({ reasonCode: "governed_response_waiting", effects: [{ kind: "create_interaction" }] });
|
|
expect(arbitrate({ assessment: unfinished, externalChatResponseWaitAuthorization: "authorized" }))
|
|
.toMatchObject({ reasonCode: "external_chat_response_waiting", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, externalChatResponseWaitAuthorization: "revoked" }))
|
|
.toMatchObject({ reasonCode: "external_chat_response_wait_authorization_lost", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, isConversation: true, boardResponseWaitAuthorized: true }))
|
|
.toMatchObject({ reasonCode: "board_response_waiting", effects: [] });
|
|
});
|
|
|
|
it("marks done only from successful finalization and complete durable evidence", () => {
|
|
expect(arbitrate()).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "done",
|
|
toStatus: "done",
|
|
reasonCode: "completion_contract_satisfied",
|
|
effects: [{ kind: "release_checkout" }],
|
|
}),
|
|
);
|
|
expect(arbitrate({ completionClaimPolicyAccepted: true })).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
verificationPassed: false,
|
|
missingRequirements: ["test"],
|
|
}),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "in_progress",
|
|
toStatus: "in_progress",
|
|
reasonCode: "completion_evidence_incomplete",
|
|
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
|
|
}),
|
|
);
|
|
const claimOnly = assessment({
|
|
objectiveSatisfied: false,
|
|
allCriteriaSatisfied: false,
|
|
verificationPassed: false,
|
|
criterionAssessments: [
|
|
{
|
|
criterionId: "objective",
|
|
claimStatus: "satisfied",
|
|
outcome: "missing",
|
|
evidenceRefs: [],
|
|
reasonCode: "criterion_evidence_missing",
|
|
},
|
|
],
|
|
verificationAssessments: [
|
|
{
|
|
commandOrCheck: "Answered the question",
|
|
claimStatus: "passed",
|
|
outcome: "unverifiable",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_has_no_durable_reference",
|
|
reportedReasonCode: null,
|
|
detail: null,
|
|
},
|
|
],
|
|
acceptedEvidenceRefs: [],
|
|
missingRequirements: ["objective"],
|
|
});
|
|
expect(arbitrate({ assessment: claimOnly })).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
reasonCode: "completion_evidence_incomplete",
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({ assessment: claimOnly, completionClaimPolicyAccepted: true }),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("creates explicit liveness paths for review, continuation, cancellation, and governance", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({ reportedDisposition: "needs_review" }),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: {
|
|
kind: "retry",
|
|
summary: "Retry the task",
|
|
idempotencyKey: "retry-task",
|
|
},
|
|
}),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
effects: [
|
|
expect.objectContaining({
|
|
kind: "enqueue_continuation",
|
|
continuationKind: "retry",
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
const externalChatWait = assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Wait for the next authorized provider message",
|
|
idempotencyKey: "external-chat-wait",
|
|
},
|
|
});
|
|
expect(
|
|
arbitrate({
|
|
assessment: externalChatWait,
|
|
externalChatResponseWaitAuthorization: "authorized",
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "in_progress",
|
|
toStatus: "in_progress",
|
|
reasonCode: "external_chat_response_waiting",
|
|
effects: [],
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({
|
|
assessment: externalChatWait,
|
|
externalChatResponseWaitAuthorization: "revoked",
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "preserve",
|
|
toStatus: "in_progress",
|
|
reasonCode: "external_chat_response_wait_authorization_lost",
|
|
effects: [],
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({
|
|
assessment: externalChatWait,
|
|
externalChatResponseWaitAuthorization: "not_applicable",
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
reasonCode: "live_continuation_registered",
|
|
effects: [
|
|
expect.objectContaining({
|
|
kind: "enqueue_continuation",
|
|
continuationKind: "response_wake",
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(arbitrate({ terminalState: "cancelled" })).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
effects: [expect.objectContaining({ kind: "release_run_resources" })],
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({ governanceGate: { kind: "interaction", id: "interaction" } }),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "governed_gate_pending",
|
|
effects: [
|
|
{
|
|
kind: "create_interaction",
|
|
gate: { kind: "interaction", id: "interaction" },
|
|
},
|
|
{
|
|
kind: "notify_owner",
|
|
agentId: "agent",
|
|
reason: "governed_gate_pending",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
expect(
|
|
arbitrate({
|
|
governanceGate: { kind: "interaction", id: "interaction" },
|
|
assessment: assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Resume from the response",
|
|
idempotencyKey: "interaction-response:interaction",
|
|
},
|
|
}),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "governed_response_waiting",
|
|
effects: [
|
|
{
|
|
kind: "create_interaction",
|
|
gate: { kind: "interaction", id: "interaction" },
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("accepts low-risk completion claims with unrun verification caveats", () => {
|
|
const withCaveat = assessment({
|
|
verificationPassed: false,
|
|
criterionAssessments: [
|
|
{
|
|
criterionId: "objective",
|
|
claimStatus: "satisfied",
|
|
outcome: "missing",
|
|
evidenceRefs: [],
|
|
reasonCode: "criterion_evidence_missing",
|
|
},
|
|
],
|
|
verificationAssessments: [
|
|
{
|
|
commandOrCheck: "Run npm test",
|
|
claimStatus: "not_run",
|
|
outcome: "missing",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_not_run",
|
|
reportedReasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this environment.",
|
|
},
|
|
],
|
|
verificationCaveats: [
|
|
{
|
|
commandOrCheck: "Run npm test",
|
|
reasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this environment.",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(
|
|
arbitrate({
|
|
assessment: withCaveat,
|
|
completionClaimPolicyAccepted: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps failed verification with the agent and routes explicit attention to its owner", () => {
|
|
const failed = assessment({
|
|
verificationPassed: false,
|
|
hasFailedVerification: true,
|
|
verificationAssessments: [
|
|
{
|
|
commandOrCheck: "Run npm test",
|
|
claimStatus: "failed",
|
|
outcome: "rejected",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_reported_failed",
|
|
reportedReasonCode: null,
|
|
detail: "One test failed.",
|
|
},
|
|
],
|
|
});
|
|
expect(
|
|
arbitrate({
|
|
assessment: failed,
|
|
completionClaimPolicyAccepted: true,
|
|
reviewOwnerUserId: "user-1",
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
reasonCode: "completion_evidence_incomplete",
|
|
effects: [
|
|
expect.objectContaining({
|
|
kind: "enqueue_continuation",
|
|
agentId: "agent",
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
|
|
const withAttention = assessment({
|
|
attentionRequests: [
|
|
{
|
|
kind: "approval",
|
|
summary: "Approve publication",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "approval",
|
|
legacy: false,
|
|
},
|
|
],
|
|
});
|
|
expect(
|
|
arbitrate({
|
|
assessment: withAttention,
|
|
completionClaimPolicyAccepted: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "actionable_attention_pending",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("blocks only for a task-wide blocker with a named owner and action", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
blocker: {
|
|
boardOwned: true,
|
|
scope: "task_wide",
|
|
unblockAction: "Approve access",
|
|
},
|
|
}),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "blocked",
|
|
unblockDescriptor: { owner: "board", action: "Approve access" },
|
|
effects: [
|
|
{ kind: "bind_blocker", owner: "board", action: "Approve access" },
|
|
{
|
|
kind: "notify_owner",
|
|
agentId: "agent",
|
|
reason: "task_wide_blocker_bound",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("waits for an explicit unblock instead of inventing another productive track", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
objectiveClaimSatisfied: false,
|
|
objectiveSatisfied: false,
|
|
allCriteriaSatisfied: false,
|
|
verificationPassed: false,
|
|
hasBlockingRemainingWork: true,
|
|
blocker: {
|
|
boardOwned: false,
|
|
scope: "current_track",
|
|
unblockAction:
|
|
"Grant access to the current Board comment attachment, then explicitly retry.",
|
|
},
|
|
}),
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "blocked",
|
|
toStatus: "blocked",
|
|
policyVersion: "phase6-v7",
|
|
reasonCode: "current_track_blocker_waiting",
|
|
unblockDescriptor: {
|
|
owner: "board",
|
|
action:
|
|
"Grant access to the current Board comment attachment, then explicitly retry.",
|
|
},
|
|
effects: [
|
|
{
|
|
kind: "bind_blocker",
|
|
owner: "board",
|
|
action:
|
|
"Grant access to the current Board comment attachment, then explicitly retry.",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("blocks a current-track result when a durable dependency already gates the issue", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
blocker: {
|
|
boardOwned: false,
|
|
scope: "current_track",
|
|
unblockAction: "Wait for child task DOT-52",
|
|
},
|
|
}),
|
|
hasUnresolvedIssueBlockers: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "blocked",
|
|
toStatus: "blocked",
|
|
reasonCode: "durable_dependency_blocker_bound",
|
|
unblockDescriptor: {
|
|
owner: { agentId: "agent" },
|
|
action: "Wait for child task DOT-52",
|
|
},
|
|
effects: [],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("lets a durable dependency override an optimistic done result", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({ reportedDisposition: "done", blocker: null }),
|
|
completionClaimPolicyAccepted: true,
|
|
hasUnresolvedIssueBlockers: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "blocked",
|
|
toStatus: "blocked",
|
|
reasonCode: "durable_dependency_blocker_bound",
|
|
effects: [],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not create a duplicate review after this run's review was already accepted", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "needs_review",
|
|
attentionRequests: [
|
|
{
|
|
kind: "review",
|
|
summary: "Accept the plan",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "review",
|
|
legacy: false,
|
|
},
|
|
],
|
|
}),
|
|
governanceResolvedForRun: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "in_review",
|
|
reasonCode: "governance_response_continuation_queued",
|
|
effects: [],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not create a duplicate review after this run's planning confirmation was already accepted", () => {
|
|
expect(
|
|
arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
attentionRequests: [
|
|
{
|
|
kind: "approval",
|
|
summary: "Accept the pinned plan revision.",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "approval",
|
|
legacy: false,
|
|
},
|
|
],
|
|
}),
|
|
governanceResolvedForRun: true,
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
statusAction: "in_review",
|
|
reasonCode: "governance_response_continuation_queued",
|
|
effects: [],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves authoritative terminal statuses", () => {
|
|
expect(arbitrate({ priorIssueStatus: "done" })).toEqual(
|
|
expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "terminal_status_preserved",
|
|
effects: [],
|
|
}),
|
|
);
|
|
});
|
|
it("routes each explicit request to its own reviewer", () => {
|
|
const decision = arbitrate({ assessment: assessment({ attentionRequests: [
|
|
{ kind: "approval", summary: "Approve release", ownerClass: "human", targetAgentId: null, sourceIndex: 0, sourceKind: "approval", legacy: false },
|
|
{ kind: "review", summary: "Review code", ownerClass: "agent", targetAgentId: "review-agent", sourceIndex: 1, sourceKind: "review", legacy: false },
|
|
] }), reviewOwnerUserId: "release-owner" });
|
|
expect(decision.effects).toEqual([
|
|
expect.objectContaining({ kind: "bind_reviewer", requestKey: "attention-0", prompt: "Approve release", ownerUserId: "release-owner", ownerAgentId: null }),
|
|
expect.objectContaining({ kind: "bind_reviewer", requestKey: "attention-1", prompt: "Review code", ownerUserId: null, ownerAgentId: "review-agent" }),
|
|
]);
|
|
});
|
|
|
|
});
|