Skip to content

test(coordination): carry a recorded decision rejection in the production-scale fixture - #4549

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
superwesleyhys-ux:gh-c102-rejected-standing-decision
Sep 16, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
superwesleyhys-ux:gh-c102-rejected-standing-decision

Conversation

@superwesleyhys-ux

@superwesleyhys-ux superwesleyhys-ux commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Refs GH-C102 — contributor task board row: "Extend the shared production-scale coordination fixture with one accepted RFC invariant or reproduced public regression that its current envelope does not cover. Update the checked-in envelope, shared generator, and an independent negative or mutation-style assertion; run the same dimension through every affected provider conformance arm."

Claimed in #4541.

Invariant

projectStandingDecisions keeps an explicit reject / cancel as a
standing_decision_receipt_v0 entry with active: false, counts it under
inactive_count, retains it during archive exactly like an approval, and never
lets it raise active_count: activation is decision_outcome === "approve" and
nothing else. tests/control_plane_ts/standing_decision.test.ts covers the pure
projection, but the shared fixture every provider arm reduces only ever
generated approvals, so no arm had to prove the refusal half.

What changed

File Change
tests/fixtures/control_plane/coordination_production_scale_v0.json new rejected_standing_decision_count: 2 band
tests/control_plane_ts/production_scale_coordination_fixture.ts generate the band, export PRODUCTION_SCALE_REJECTED_DECISION_COUNT, expose expected_inactive_standing_decision_count, derive expected_user_archive_count from the retained count
tests/control_plane_ts/authority_store_conformance.ts per-arm inactive_count and per-entry active === (outcome === "approve")
tests/control_plane_ts/production_scale_rejected_decision.test.ts new: receipt / mutation / negative cases

Design choices worth reviewing:

  • A second decision kind, not a second scope_key. The rejection band uses
    kind: "write_scope" on the same granularity: "goal" and scope_key, so
    authority identity ([kind, granularity, scope_key, global]) keeps the refusal
    in its own group. Reusing the approved kind with a different key would have
    been ambiguous about which dimension separates them.
  • expected_user_archive_count is derived, not relaxed. Archive retains every
    standing receipt, so two refusals leave two fewer movable rows: the value is
    now min(done - retained, done - 5) instead of done - 5. The dimension still
    moves 186 rows; it is not loosened to make an arm pass.
  • Two Todos, one receipt. Both refusals share one identity, so the projection
    collapses them to a single inactive entry via the existing chronology rule.
    expected_inactive_standing_decision_count counts identities, and the focused
    test pins that collapse.

Non-goals: no standing-decision predicate change, no projection semantics change,
no production code, no provider configuration change.

Validation

  • node --test tests/control_plane_ts/production_scale_rejected_decision.test.ts
    (3 tests: receipt shape, approval-only activation mutation, typed-scope removal)
  • node --test tests/control_plane_ts/authority_store.test.ts → 99/99 (file arm,
    includes the new inactive_count assertions)
  • node --test tests/control_plane_ts/sqlite_authority_store.test.ts → 112/112
    (SQLite arm)
  • npm run typecheck:control-plane → clean
  • python3 examples/control_plane/todo-archive-completed-smoke.py,
    python3 examples/control_plane/todo-standing-decision-authority-smoke.py
    → ok (archive retention contract unchanged in behavior)
  • python3 -m pytest -q --basetemp=<fresh tmp> tests/control_plane/test_todo_semantic_kernel.py tests/control_plane/test_coordination_runtime_shadow_adapter.py → 21 passed

Two environment notes, both pre-existing and unrelated to this change:

  1. The SQLite and NoKV arms need the qualified Node 22.22.3 runtime (SQLite
    3.51.3+ WAL-reset fix). The default runtime here was 22.22.2, which fails
    those arms at baseline; they pass on 22.22.3.
  2. Full-suite runs under concurrent load report failures in the real-SIGKILL,
    concurrency, bootstrap and rollback families only — none of them touch
    standing decisions, archiving, or the production-scale fixture, and all pass
    when the affected files run on their own. pytest also needs --basetemp
    here because the default tmp_path root cannot be created in this sandbox.
    CI remains authoritative for the NoKV and PostgreSQL arms.

…tion-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 (loopx-project#4541)

Signed-off-by: superwesleyhys-ux <251160695+superwesleyhys-ux@users.noreply.github.com>
@superwesleyhys-ux
superwesleyhys-ux force-pushed the gh-c102-rejected-standing-decision branch from 44e89d8 to 819d167 Compare September 16, 2026 13:06

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

这个 PR 补的是共享 production-scale coordination fixture 里缺的那一半:它以前只生成 approve,于是没有任何 provider arm 需要证明"明确拒绝"这一半。判据本身不是"没有生效的批准",而是"有被记录的拒绝"——如果 provider 把"没有 active 批准"读成"没有任何决定",所有 arm 都照样绿。判据 active = (decision_outcome === "approve") 之前在 standing_decision.test.ts(纯投影单测)里覆盖过,但生成器、envelope 和 conformance harness 这条真实链路上没有拒绝样本。

改动思路

作者没有新造一个 fixture,也没有把断言堆成一个新 smoke,而是扩展现有那条链路:envelope 加一个 rejected_standing_decision_count: 2 band,生成器在同一个 granularity: "goal" 上换一个 kind: "write_scope" 造两条 reject(保持 decision identity 分到独立的组,不与 approve 抢同一个 identity),conformance harness 从"只看 active_count"升级为"看 inactive_count + 逐条 active 与 outcome 一致",再加一个独立模块放 mutation 与负例。三个设计选择我都核对过:

  • 拒绝与批准是两个 dimension,不是两个 key:kind 不同 → authority identity([kind, granularity, scope_key, global])不同 → 拒绝不会把已有批准挤成 inactive。这正是我担心的方向,代码里 candidate() 用 scope.kind/granularity/scope_key 组成 identity,两条拒绝共享同一 identity,所以投影只出一条 inactive 条目。
  • archive 期望是推导出来的,不是放宽:expected_user_archive_count 变成 min(done - standing - rejected, done - 5),只因为"被留下的 standing receipt 多了两条"而下降两条;断言它的 arm 没有改断言本身,仍然通过。
  • 负例是真的负例:mutation case 把最新一条 reject 改成 approve,要求 activation 恰好 +1 且没有任何 reject 仍为 active;另一个 case 去掉 typed decision_scope,要求它不再算 standing receipt——这条正好防住"从 prose 推断身份"。

具体改动

  • tests/fixtures/control_plane/coordination_production_scale_v0.json(+1):新增 rejected_standing_decision_count: 2。
  • tests/control_plane_ts/production_scale_coordination_fixture.ts(+40/-2):生成拒绝 band、导出 PRODUCTION_SCALE_REJECTED_DECISION_COUNT、导出 expected_inactive_standing_decision_count(按 identity 去重计),并把 expected_user_archive_count 改成推导式。
  • tests/control_plane_ts/authority_store_conformance.ts(+11/-1):每个 arm 断言 inactive_count,并对所有条目断言 entry.active === (entry.outcome === "approve")。
  • tests/control_plane_ts/production_scale_rejected_decision.test.ts(新增 79 行):正向(拒绝是 receipt、status done、global_gate)、mutation(reject→approve)、负例(缺 typed scope 不构成 receipt)三类。

关键代码讲解

  1. production_scale_coordination_fixture.ts 的 rejected band:复用 partialEnd + linked_decision_count 之后的 user 行,只加 task_class/decision_scope/decision_outcome/global_gate/goal_bound,与 approve band 的唯一语义差别是 kind 和 outcome——这是"同一判据、第二种结果"的最小构造。
  2. expected_inactive_standing_decision_count:用 new Set(JSON.stringify([kind, granularity, scope_key, "global"])) 按 identity 计数,而不是按行数——所以两条拒绝只贡献 1,和投影的"一个 identity 一条 entry"对齐。
  3. expected_user_archive_count = Math.min(expectedUserDone - expectedStanding - rejectedStanding.length, expectedUserDone - 5):说明保留数变成 max(standing + rejected, 5),即拒绝和批准一样进入"不可搬动"集合;min 保证期望只会变小,不会凭空放宽。
  4. authority_store_conformance.ts 的逐条断言 entry.active === (entry.outcome === "approve"):把"只有显式批准才是权威"变成每个 provider arm 都必须过的判据,而不是只看总数。
  5. production_scale_rejected_decision.test.ts 的 mutation case:把最新 reject 改成 approve 后要求 active 恰好 +1、且没有任何 reject 仍为 active——如果投影是按"最近一条 receipt 决定整组"实现的,这条会精确抓住。

对主干的风险

运行时风险为零:4 个文件全在 tests/,没有触碰 loopx/control_plane/todos/standing_decision.ts 或任何 provider 代码,所以不存在产品行为变化。真正需要审的是"这条测试会不会把旧期望悄悄放宽/写空",我的核对结论是没有:

  • expected_user_archive_count 的下降幅度正好等于两条被保留的拒绝行,且所有断言它的 arm 断言本身未改;fixture 自带的 drift 自检(requireSafeCount 与构造期断言)仍然 fail-closed。
  • 新增断言不是"复述实现":inactive_count 与逐条 active 是判据,mutation/负例给出独立反例。

证据缺口(环境,不是本 PR):SQLite 与 PostgreSQL arm 在本机跑不了——本机是 Node 25.5.0 / SQLite 3.51.2,所有 SQLite arm 在任何 fixture 断言之前就报 SQLite authority runtime is not qualified (… require the WAL-reset fix …, Use the qualified Node 22.22.3 runtime)。也就是说这一半我只在 file / NoKV / JSONL 三个 arm 上验证过;如果你希望 sqlite/postgres 也覆盖到拒绝维度,需要在资格运行时上补跑,这属于环境问题。

我的整体评价

APPROVE。 这是一个"小而准"的测试增强:不新增 fixture、不改产品代码,把一条已经存在的判据(拒绝是决定、且永不激活)第一次推进到所有 provider 共享的 conformance 链路里,并配了 mutation 与"缺 typed scope 不成 receipt"两个独立反例;archive 期望的变化是推导出来的、只降两条,属于"跟着事实走"而不是放宽。证据:git merge-tree 对 main 干净;新模块 + fixture 自检 6 passed;file/NoKV/JSONL arm 301 passed;同一 envelope 的两个 Python 消费者 21 passed;SQLite/PostgreSQL arm 因本机运行时未达资格未执行(原因与判据无关)。

English verdict: APPROVE — at head 819d167 this is a test-only change (4 files under tests/, +128/-3, no production code) that finally makes every shared provider arm prove the refusal half of the standing-decision invariant: a recorded reject stays a standing receipt with active: false, counts under inactive_count, survives archive, and can never raise active_count. I verified the design choices that could have weakened coverage: the rejection band uses a second decision kind so identity grouping cannot deactivate an existing approval, expected_user_archive_count is now a derivation that moves by exactly the two retained rows rather than a relaxed constant, and the new module adds an independent mutation case (newest reject -> approve must raise activation by exactly one) plus a negative case (a rejection without its typed decision scope is not a standing receipt). Evidence: merge-tree against origin/main is clean; the new module and fixture self-test pass (6), the file/NoKV/JSONL authority-store arms pass (301), and the two Python consumers of the same envelope pass (21). The SQLite and PostgreSQL arms were not executed here because this host's Node 25.5.0 / SQLite 3.51.2 is rejected by the runtime qualification check before any fixture assertion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants