From 819d16717df1881d6631ac782ae77e881af98f7e Mon Sep 17 00:00:00 2001 From: superwesleyhys-ux <251160695+superwesleyhys-ux@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:14:11 +0800 Subject: [PATCH] test(coordination): carry a recorded decision rejection in the production-scale fixture The shared production-scale coordination fixture only generated approvals, so no provider conformance arm ever had to prove that an explicitly refused decision stays recorded without becoming active authority. Add a bounded rejection band to the checked-in envelope and the shared generator, expose the derived expectations, assert the dimension on every provider arm, and add one independent mutation/negative case. - envelope: rejected_standing_decision_count. The refusal is scoped under a second decision kind so it keeps its own decision identity and cannot merge into the approved scope. - generator: expected_inactive_standing_decision_count, plus a retained-standing count that now includes refusals; expected_user_archive_count is derived from the retained count instead of a fixed offset. - conformance: per-arm inactive_count and per-entry active === (outcome === "approve"), so no arm can present a refusal as an approval. - fixture test: a rejection is a receipt, only an explicit approval activates a scope, and dropping the typed decision_scope leaves no entry at all. Refs GH-C102 (#4541) Signed-off-by: superwesleyhys-ux <251160695+superwesleyhys-ux@users.noreply.github.com> --- .../authority_store_conformance.ts | 11 ++- .../production_scale_coordination_fixture.ts | 40 +++++++++- ...production_scale_rejected_decision.test.ts | 79 +++++++++++++++++++ .../coordination_production_scale_v0.json | 1 + 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/control_plane_ts/production_scale_rejected_decision.test.ts diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts index bac4074891..ea03f13635 100644 --- a/tests/control_plane_ts/authority_store_conformance.ts +++ b/tests/control_plane_ts/authority_store_conformance.ts @@ -959,7 +959,16 @@ export function registerAuthorityStoreConformance( const todos = loaded.head.todos as Record[]; const leases = loaded.head.leases as Record[]; const standing = projectStandingDecisions(todos)!; - assert.equal(standing.active_count, 1); // Four receipts, one scope/owner. + assert.equal(standing.active_count, 1); // Four approvals, one scope/owner. + assert.equal( + standing.inactive_count, + fixture.expected_inactive_standing_decision_count, + "a recorded rejection stays a standing receipt without becoming authority", + ); + for (const entry of standing.entries as JsonObject[]) { + assert.equal(entry.active, entry.outcome === "approve", + "only an explicit approval may activate a standing decision"); + } assert.equal(standing.conflict_count, undefined); assert.equal(todos.length, fixture.expected_initial_todo_count); assert.equal(leases.length, fixture.expected_current_lease_count); diff --git a/tests/control_plane_ts/production_scale_coordination_fixture.ts b/tests/control_plane_ts/production_scale_coordination_fixture.ts index fdd267013f..cbb4dc5aea 100644 --- a/tests/control_plane_ts/production_scale_coordination_fixture.ts +++ b/tests/control_plane_ts/production_scale_coordination_fixture.ts @@ -23,6 +23,7 @@ const envelope = JSON.parse(readFileSync(new URL( current_lease_count: number; retired_lease_count: number; standing_user_decision_count: number; + rejected_standing_decision_count: number; scoped_without_outcome_count: number; linked_decision_count: number; completion_target_index: number; @@ -104,6 +105,13 @@ export interface ProductionScaleCoordinationFixture { readonly expected_agent_archive_count_after_terminals: number; readonly expected_user_archive_count: number; readonly expected_standing_user_decision_count: number; + /** + * Standing receipts whose recorded outcome is not `approve`. + * + * Authority collapses per decision identity, so every rejection sharing one + * scope produces a single inactive entry rather than one per Todo. + */ + readonly expected_inactive_standing_decision_count: number; readonly semantic_cases: Readonly>>; readonly presentation_cases: Readonly>>; readonly update_cases: Readonly>>; @@ -210,6 +218,19 @@ function todoRecords( record.unblocks_todo_id = todoId("agent", envelope.completion_target_index); } } + // An explicit rejection is a recorded decision, not absent authority: the + // same broad goal is refused under a second decision kind. It stays a + // standing receipt while its outcome keeps it inactive, so a provider + // cannot present "no active approval" as "no decision was made". + const rejectedStart = partialEnd + envelope.linked_decision_count; + if (role === "user" && index >= rejectedStart && + index < rejectedStart + envelope.rejected_standing_decision_count) { + record.task_class = "user_gate"; + record.decision_scope = {kind: "write_scope", granularity: "goal", scope_key: goalId}; + record.decision_outcome = "reject"; + record.global_gate = true; + record.goal_bound = true; + } return record; }); } @@ -278,6 +299,14 @@ export function productionScaleCoordinationFixture( todo.task_class === "user_gate" && todo.decision_outcome === "approve" && todo.global_gate === true && todo.goal_bound === true, ).length; + const rejectedStanding = users.filter(todo => + todo.task_class === "user_gate" && todo.decision_outcome === "reject" && + todo.global_gate === true && todo.goal_bound === true, + ); + const expectedInactiveStanding = new Set(rejectedStanding.map(todo => { + const scope = todo.decision_scope as {kind: string; granularity: string; scope_key: string}; + return JSON.stringify([scope.kind, scope.granularity, scope.scope_key, "global"]); + })).size; return { projection: schema === "legacy" ? legacyProjection @@ -292,8 +321,14 @@ export function productionScaleCoordinationFixture( expected_initial_todo_count: todos.length, expected_current_lease_count: leases.length, expected_agent_archive_count_after_terminals: expectedAgentDone + 2 - 5, - expected_user_archive_count: expectedUserDone - 5, - expected_standing_user_decision_count: expectedStanding, + // Archive keeps every standing receipt, approved or rejected, so each + // rejection leaves one fewer movable completed row behind. + expected_user_archive_count: Math.min( + expectedUserDone - expectedStanding - rejectedStanding.length, + expectedUserDone - 5, + ), + expected_standing_user_decision_count: expectedStanding + rejectedStanding.length, + expected_inactive_standing_decision_count: expectedInactiveStanding, semantic_cases: envelope.semantic_cases, presentation_cases: envelope.presentation_cases, update_cases: envelope.update_cases, @@ -307,6 +342,7 @@ export function productionScaleCoordinationFixture( } export const PRODUCTION_SCALE_RETIRED_LEASE_COUNT = envelope.retired_lease_count; +export const PRODUCTION_SCALE_REJECTED_DECISION_COUNT = envelope.rejected_standing_decision_count; function requireSafeCount(value: number, label: string): number { if (!Number.isSafeInteger(value) || value < 1) { diff --git a/tests/control_plane_ts/production_scale_rejected_decision.test.ts b/tests/control_plane_ts/production_scale_rejected_decision.test.ts new file mode 100644 index 0000000000..e61d60f26d --- /dev/null +++ b/tests/control_plane_ts/production_scale_rejected_decision.test.ts @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import { + isStandingDecisionReceipt, + projectStandingDecisions, +} from "../../loopx/control_plane/todos/standing_decision.ts"; +import { + PRODUCTION_SCALE_REJECTED_DECISION_COUNT, + productionScaleCoordinationFixture, +} from "./production_scale_coordination_fixture.ts"; + +function standingEntries(todos: readonly JsonObject[]): JsonObject[] { + const standing = projectStandingDecisions(todos); + return (standing?.entries ?? []) as JsonObject[]; +} + +function rejectedTodos(todos: readonly JsonObject[]): JsonObject[] { + return todos.filter(todo => todo.decision_outcome === "reject"); +} + +test("production-scale rejections are standing receipts that stay inactive", () => { + const fixture = productionScaleCoordinationFixture("fixture-goal"); + const todos = fixture.projection.todos as JsonObject[]; + const rejected = rejectedTodos(todos); + assert.equal(rejected.length, PRODUCTION_SCALE_REJECTED_DECISION_COUNT); + assert.equal(rejected.length > 0, true, "the fixture must carry a recorded rejection"); + for (const todo of rejected) { + assert.equal(isStandingDecisionReceipt(todo), true, + "an explicit rejection is a decision, not an absent one"); + assert.equal(todo.status, "done"); + assert.equal(todo.global_gate, true); + } + const entries = standingEntries(todos); + const rejections = entries.filter(entry => entry.outcome === "reject"); + assert.equal(rejections.length, fixture.expected_inactive_standing_decision_count); + for (const entry of rejections) { + assert.equal(entry.active, false, "a rejection must never read as an approval"); + } +}); + +test("only an explicit approval activates a production-scale decision scope", () => { + const fixture = productionScaleCoordinationFixture("fixture-goal"); + const todos = fixture.projection.todos as JsonObject[]; + const before = standingEntries(todos); + const approved = before.filter(entry => entry.active === true).length; + const approvedTodo = todos + .filter(todo => todo.decision_outcome === "approve" && isStandingDecisionReceipt(todo)) + .sort((left, right) => String(left.completed_at).localeCompare(String(right.completed_at))) + .at(-1)!; + // Mutation: the newest rejection decides the scope once its outcome changes. + const newest = rejectedTodos(todos) + .sort((left, right) => String(left.completed_at).localeCompare(String(right.completed_at))) + .at(-1)!; + const mutated = todos.map(todo => todo === newest + ? {...todo, decision_outcome: "approve"} : todo); + const after = standingEntries(mutated); + assert.equal(after.filter(entry => entry.active === true).length, approved + 1); + assert.equal(after.some(entry => entry.outcome === "reject" && entry.active === true), false); + // The untouched approval is still the same receipt. + assert.equal(after.some(entry => entry.source_todo_id === approvedTodo.todo_id), true); +}); + +test("a rejection without its typed decision scope leaves no standing entry", () => { + const fixture = productionScaleCoordinationFixture("fixture-goal"); + const todos = fixture.projection.todos as JsonObject[]; + const stripped = todos.map(todo => todo.decision_outcome === "reject" + ? {...todo, decision_scope: undefined} : todo); + for (const todo of stripped) { + if (todo.decision_outcome === "reject") { + assert.equal(isStandingDecisionReceipt(todo), false, + "decision identity comes from the typed scope, never from prose"); + } + } + assert.equal(standingEntries(stripped).some(entry => entry.outcome === "reject"), false); + assert.equal(standingEntries(stripped).length, + standingEntries(todos).length - fixture.expected_inactive_standing_decision_count); +}); diff --git a/tests/fixtures/control_plane/coordination_production_scale_v0.json b/tests/fixtures/control_plane/coordination_production_scale_v0.json index 41c4ea668d..9c573ca372 100644 --- a/tests/fixtures/control_plane/coordination_production_scale_v0.json +++ b/tests/fixtures/control_plane/coordination_production_scale_v0.json @@ -16,6 +16,7 @@ "current_lease_count": 64, "retired_lease_count": 160, "standing_user_decision_count": 4, + "rejected_standing_decision_count": 2, "scoped_without_outcome_count": 12, "linked_decision_count": 12, "completion_target_index": 160,