test/doc: repeated plugin rollback is a two-state toggle and nothing pins or documents it #130
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#130
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Split out of #78. Verified 2026-09-04. The behaviour is defensible; what is missing is a test and a sentence. Filed rather than fixed inside the residual sweep because it is coverage, not correctness.
What actually happens
crates/indexer/src/promotion.rs:552-565:promoteis documented as "ROLLBACK as well as promotion, because they are THE SAME TRANSACTION WITH THE GENERATIONS SWAPPED", and it moves the incumbent tosuperseded.SUPERSEDED_RETENTION_LIMIT = 1, enforced inside the same transaction (everything insupersededoutsideORDER BY id DESC LIMIT 1is moved todeleting).So: promote 2 →
{1 superseded, 2 active}; rollback →{1 active, 2 superseded}; second rollback → back to{1 superseded, 2 active}. It toggles between exactly two generations and can never reach a third.Why the audit's word "silently" is wrong
cmd_rollback(crates/cli/src/plugin.rs:2624-2704) reads the target before acting and printspackage generation {t}, from its own retained rowsplus a domain naming the generation being replaced.plugin statusprints the whole generation inventory with states androllback: available — n superseded generation(s) retained. And with retention 1 there is nothing older to reach — the older generation is already indeleting— so "roll back further" is not an available semantic. A second rollback is "the same epoch transition in reverse" applied to the new state, which is literally what #78 calls rollback; acceptance 8 says only "rollback works while retention permits".What is missing
1. No test drives a repeated rollback. Every rollback test drives exactly one call:
crates/cli/tests/plugin_activation_cli.rs:704rollback_restores_the_retained_generation:781rollback_with_nothing_retained_refuses_and_exits_non_zero:1450,:1833capability-grant carriage across a rollbackcrates/indexer/tests/generation_promotion.rs:788a_rollback_restores_the_old_projection_with_the_producer_gonecrates/indexer/tests/generation_collect.rs:905,generation_crash_matrix.rs:462(crash during one rollback),bench_promotion_lock.rs:211crates/cli/tests/plugin_duplicate_id_cli.rs:459(disclosure only)So a future change to the target-selection query —
ORDER BY id DESC→ASC, or a retention bump that makes a third generation reachable — is ungraded.2. No prose anywhere says rollback is a two-state toggle rather than a stack pop. An operator reading
rollback: available — 1 superseded generation(s) retainedtwice has no way to learn that the second call returns them to where they started.Triage 2026-09-06 at
f6a878a: CLOSING. Both halves — a test that drives more than one rollback, and operator-facing prose — are in. The interesting part is that this issue's own proposed mutation was run and survived, and that is recorded rather than papered over.Verified against master and by re-running the test myself.
The test
a_second_rollback_swaps_back_rather_than_stepping_further_out—crates/indexer/tests/generation_promotion.rs:877. It drives three rollbacks, compares the projection at every step, and asserts periodicity, which is the property "two-state toggle" actually names.The prose, and it is in the payload rather than only in doc comments
crates/cli/src/activation.rs:1135-1139—plugin statustells the operator outright that "it SWAPS rather than pops, so running it twice returns you here".crates/cli/src/plugin.rs:2732-2737— theplugin rollbacknotes.crates/indexer/src/promotion.rs:553— the API doc.That ordering matters for this repo's own rule that disclosures belong in the payload, not in prose an operator never reads. The first of those three is the one that will actually reach someone mid-incident.
The filing was wrong about the mutation, and the tree says so
This issue proposed grading the behaviour by flipping
ORDER BY id DESC→ASC. That mutation was run and SURVIVED, and it is recorded as a survivor with its reason atgeneration_promotion.rs:851-864: underSUPERSEDED_RETENTION_LIMIT = 1the two queries are identical, so the mutation is structurally ungradable — not a weak test, an impossible one.The killing mutation used instead (rollback refuses a second call) is recorded as RED and isolating. Recording a survivor with its reason, rather than inventing a test that looks like coverage, is the right outcome and is why I am comfortable closing this rather than asking for the originally-specified mutation.
Residual
None. The only thing carried forward is the corrected understanding of what is gradable here, which now lives in the test file.
🤖 Triage lane, 2026-09-06, master
f6a878a