voyage/tests/lib
Kjell Tore Guttormsen 156539204a fix(research-loop-cap): claim each turn slot with O_EXCL so the bound survives concurrency
countTurns-then-appendFileSync is read-then-write. N callers that all
observe used == budget-1 all decide to grant, and the bound is exceeded
by N-1. The comment above allowTurn asserted "Append-only: never
read-modify-write" and named the concurrent case - Phase 4.5/5 may spawn
several agents in a single message - as the reason it had to be. The
decision path was exactly what the comment denied, so the concurrency
claim had nothing under it.

Each grant now creates <data root>/trekresearch-loop-claims/<runId>-<slot>.claim
with flag 'wx' (O_CREAT|O_EXCL) before appending. The kernel picks the
winner per slot, slot numbers are bounded by the budget, and each can be
created exactly once - so total grants for a run cannot exceed the budget
however many callers arrive together. The ledger count now only says
where to start looking for a free slot.

Two of the three tests are deterministic and do not race anything: they
assert the invariant directly by pre-creating claims, including the state
a mid-append competitor leaves behind (ledger 7, slots 1-8 claimed, budget
8 -> deny). That matters because the third test - six real concurrent shim
processes at the boundary - passed even BEFORE the fix, since process
startup jitter serialised them. A race test that passes by luck is not
evidence, so it ships as a real-world regression guard next to the two
that are.

Stated rather than left to be discovered: claim files are empty, at most
budget per run, and never cleaned - the same standing as the ledger, which
also grows for the life of the data root. Reusing a runId across runs, or
two runIds colliding after filename sanitisation, both deny a turn, which
is the safe direction for a budget control.

Review finding 3994491ef1fdba6e0e3645b5b713cbdbdeb2b328 (MINOR).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuGhWAbWyRFBFeemfhxoVv
2026-08-12 22:52:21 +02:00
..
agent-frontmatter.test.mjs test(agents): pin examples-in-body invariant (RED before M4 relocation) 2026-06-29 10:11:53 +02:00
arg-parser.test.mjs feat(voyage): S11 — NW2 part B, integrate opt-in --workflow flag 2026-06-18 17:22:09 +02:00
atomic-write.test.mjs refactor(ultraplan-local): extract atomicWriteJson to lib/util 2026-05-01 20:21:15 +02:00
autonomy-gate.test.mjs feat(ultraplan-local): add autonomy-gate state machine + manifest schema extensions for skip_commit_check + memory_write 2026-05-04 06:28:47 +02:00
bash-normalize.test.mjs feat(ultraplan-local): Spor 1 wave 1 — lib/parsers + 66 tests grønn 2026-05-01 05:35:28 +02:00
cache-analyzer.test.mjs test(observability): close 2 MAJOR test gaps from SKAL-2 review 2026-06-26 17:14:03 +02:00
cleanup.test.mjs feat(voyage)!: rename produced_by field values + validator comments [skip-docs] 2026-05-05 14:42:21 +02:00
coordinator-contract.test.mjs feat(eval): add review-coordinator contract reference impl + deterministic test 2026-06-26 11:54:48 +02:00
doc-consistency.test.mjs fix(research-loop-cap): resolve the data root in code so the loop can run 2026-08-12 22:26:46 +02:00
fidelity-diff.test.mjs feat(voyage): S10 — NW2 part A (Workflow port + fidelity harness + smoke) 2026-06-18 14:40:19 +02:00
finding-id.test.mjs feat(ultraplan-local): add lib/parsers/finding-id.mjs (stable SHA1) 2026-05-01 13:28:05 +02:00
findings-schema.test.mjs feat(voyage): S9 — NW1 reviewer-output schema contract (TDD, ungated) 2026-06-18 14:06:07 +02:00
frontmatter.test.mjs feat(voyage)!: bulk content rewrite ultra -> voyage/trek prose [skip-docs] 2026-05-05 15:08:20 +02:00
gates-flag-coverage.test.mjs feat(voyage)!: rename commands, templates, fixtures for v4.0.0 [skip-docs] 2026-05-05 14:13:44 +02:00
gold-corpus.test.mjs feat(eval): add gold.json golden corpus + loader/validator test 2026-06-26 11:51:46 +02:00
gold-eval.test.mjs feat(eval): SKAL-1·4b offline gold-scored output eval 2026-06-30 09:00:33 +02:00
gold-scorer.test.mjs feat(eval): SKAL-1·4b offline gold-scored output eval 2026-06-30 09:00:33 +02:00
jaccard.test.mjs feat(ultraplan-local): add lib/parsers/jaccard.mjs 2026-05-01 13:28:44 +02:00
main-merge-gate.test.mjs feat(voyage)!: rename commands, templates, fixtures for v4.0.0 [skip-docs] 2026-05-05 14:13:44 +02:00
manifest-schema-extensions.test.mjs feat(voyage): add OPTIONAL_STRING_KEYS path to manifest-yaml — profile_used additive 2026-05-09 09:23:32 +02:00
manifest-yaml.test.mjs feat(graceful-handoff): 2.0 — migrate to skills/ with disable-model-invocation [skip-docs] 2026-05-01 05:45:26 +02:00
phase-signal-resolver.test.mjs test(validators): cover fable accept + unknown-model reject in both gate layers 2026-07-02 16:56:26 +02:00
plan-review-dedup.test.mjs fix(voyage): S23 — make /trekplan Phase 9 dedup executable (defect #1) 2026-06-19 21:12:36 +02:00
plan-schema.test.mjs fix(voyage): S26 — parse plan_version prose form (S22 defect #2/#3) 2026-06-19 21:59:31 +02:00
profile-application.test.mjs test(profiles): pin fable profile resolution end-to-end 2026-07-02 17:00:43 +02:00
profile-flag-coverage.test.mjs feat(voyage): document --profile flag in all 6 commands — SC #4 + arv-policy 2026-05-09 09:38:36 +02:00
profile-resolver.test.mjs test(profiles): pin fable profile resolution end-to-end 2026-07-02 17:00:43 +02:00
profile-stats-fields.test.mjs feat(trekresearch): emit and export the five STORM measurement fields 2026-08-09 15:01:15 +02:00
project-discovery.test.mjs feat(voyage)!: bulk content rewrite ultra -> voyage/trek prose [skip-docs] 2026-05-05 15:08:20 +02:00
research-loop-cap.test.mjs fix(research-loop-cap): claim each turn slot with O_EXCL so the bound survives concurrency 2026-08-12 22:52:21 +02:00
review-determinism.test.mjs feat(voyage)!: rename commands, templates, fixtures for v4.0.0 [skip-docs] 2026-05-05 14:13:44 +02:00
rule-catalogue.test.mjs feat(ultraplan-local): add lib/review/rule-catalogue.mjs (12 rule keys) 2026-05-01 13:27:29 +02:00
source-findings.test.mjs feat(voyage)!: bulk content rewrite ultra -> voyage/trek prose [skip-docs] 2026-05-05 15:08:20 +02:00
stats-event-emit.test.mjs feat(voyage)!: rename stats filenames, settings keys, hook prefixes [skip-docs] 2026-05-05 14:49:03 +02:00
synthesis-digest-schema.test.mjs feat(voyage): S12 — NW3 synthesis-agent built + measured → declined per measurement [skip-docs] 2026-06-18 17:58:39 +02:00
test-census.test.mjs feat(eval): SKAL-1·4b offline gold-scored output eval 2026-06-30 09:00:33 +02:00
token-usage.test.mjs feat(stats): add claude-fable-5 to PRICE_TABLE 2026-07-02 17:15:36 +02:00