Thanks to visit codestin.com
Credit goes to github.com

Skip to content

test: property-check write-queue serialization and clamp invariants - #575

Merged
ndycode merged 2 commits into
mainfrom
claude/audit-56-write-queue-property
Jun 11, 2026
Merged

ndycode merged 2 commits into
mainfrom
claude/audit-56-write-queue-property

Conversation

@ndycode

@ndycode ndycode commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Second property suite of this wave (sibling: #574; complements the example-based #567). The settings write queue is the per-path serialization primitive behind every settings write — its Windows-safety claim is precisely a concurrency invariant, which is the kind of thing the repo's test/property/ tradition exists to pin across the whole input space rather than at hand-picked points. This adds test/property/settings-write-queue.property.test.ts (2 fast-check properties).

Invariants pinned

  • Serialization under any schedule: for arbitrary schedules of up to 12 tasks spread over three path keys, with each task independently succeeding, failing once retryably (EBUSY) then succeeding, or failing fatally (ENOSPC):
    • every key's invocations form contiguous groups in submission order — a task's retries can never interleave with another task on the same path;
    • fatal tasks reject with their own error and never block successors on the same key;
    • every task runs at least once, on its own key.
      Each property iteration uses a unique key namespace so the module-level queue map never couples runs.
  • Retry-after clamping: any positive 429 retryAfterMs hint (up to 2×10⁹) produces exactly one sleep of max(10, min(30000, round(hint))) — used verbatim inside the range, clamped at the 10ms floor and the 30s ceiling outside it.

Validation

  • vitest run test/property/settings-write-queue.property.test.ts — 2/2 passing (default fast-check run counts)
  • npm run typecheck — clean
  • npx eslint test/property/settings-write-queue.property.test.ts --max-warnings=0 — clean

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB


Generated by Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

adds test/property/settings-write-queue.property.test.ts with two fast-check properties pinning the write-queue's concurrency and rate-limit invariants. both previously-flagged gaps — windows-relevant errno codes and retry-budget exhaustion — are addressed.

  • serialization property: covers all four task behaviors (ok, flaky, fatal, exhausted) and all five windows-relevant errno codes (EBUSY, EPERM, EACCES, EAGAIN, ENOTEMPTY); checks contiguous per-key grouping, correct outcomes, exhausted-task retry count, and "every task ran at least once on its own key."
  • clamp property: verifies that any positive 429 retryAfterMs hint (1..2×10⁹) produces exactly one sleep of max(10, min(30000, round(hint))); uses a unique key per iteration to avoid coupling runs through the module-level queue map.

Confidence Score: 5/5

test-only addition with no changes to production code; safe to merge

the new test file exercises all retryable windows errno codes, all four task behaviors including retry-budget exhaustion, per-key serialization contiguity, and the 429 clamp formula — the two previously flagged gaps are both addressed and the implementation logic is sound

no files require special attention

Important Files Changed

Filename Overview
test/property/settings-write-queue.property.test.ts adds two fast-check properties covering queue serialization (all 4 behaviors incl. exhausted, all 5 windows-relevant errno codes) and 429 clamp; logic is sound, minor nits around a magic retry-count and integer-only rounding coverage

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[fc.asyncProperty: arbSchedule] --> B[schedule.map: submit tasks]
    B --> C{spec.behavior}
    C -->|ok| D[return result-taskIndex]
    C -->|flaky| E{flakyFailed?}
    E -->|no| F[throw retryableCode\nadd to flakyFailed]
    E -->|yes| D
    C -->|fatal| G[throw ENOSPC\nnon-retryable]
    C -->|exhausted| H[throw retryableCode\nevery attempt]
    F -->|retry| E
    H -->|retry x4| I[budget exhausted\nthrow lastError]
    G --> J[reject immediately\ndon't block successors]
    D --> K[outcome: ok]
    I --> L[outcome: error]
    J --> L
    K --> M[Assert: contiguous groups\nper key in submission order]
    L --> M
    M --> N[Assert: exhausted tasks\ninvoked exactly SETTINGS_WRITE_MAX_ATTEMPTS times]
    N --> O[Assert: every task ran\nat least once on its own key]
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
test/property/settings-write-queue.property.test.ts:3
the exhausted-invocation count is hardcoded to `4`, but the source exports `SETTINGS_WRITE_MAX_ATTEMPTS`. if that constant is bumped (e.g. to 5) the assertion silently checks the wrong number and could pass or fail incorrectly depending on the new value — import the constant so the check stays in sync automatically.

```suggestion
import { withQueuedRetry, SETTINGS_WRITE_MAX_ATTEMPTS } from "../../lib/codex-manager/settings-write-queue.js";
```

### Issue 2 of 3
test/property/settings-write-queue.property.test.ts:88-96
**magic retry-count decoupled from source constant** — the `4` here shadows `SETTINGS_WRITE_MAX_ATTEMPTS = 4` from the source module. if that constant is bumped to 5 the assertion would still check 4 and silently pass with a wrong expectation. import and use `SETTINGS_WRITE_MAX_ATTEMPTS` directly so the check stays tied to the real budget.

### Issue 3 of 3
test/property/settings-write-queue.property.test.ts:126
**rounding branch never exercised**`fc.integer` always produces whole numbers, so `Math.round(retryAfterMs)` is a no-op on every iteration and the rounding code in `resolveRetryDelayMs` is never actually tested. switching to `fc.float({ min: 1, max: 2_000_000_000, noNaN: true })` would exercise the round-up/round-down paths at the clamp boundaries (e.g. `29999.6` rounds to `30000`, `10.4` rounds to `10`).

Reviews (2): Last reviewed commit: "test: draw transient codes from the full..." | Re-trigger Greptile

fast-check properties over withQueuedRetry, complementing the
example-based suite:

- for ANY schedule of ok/flaky/fatal tasks across three keys, every
  key's invocations form contiguous groups in submission order
  (retries never interleave with another task), fatal tasks reject
  without blocking successors, and every task runs on its own key
- any positive 429 retry-after hint is clamped into the 10ms..30s
  range and used verbatim within it

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ndycode, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 40 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db1d5ff7-473c-4ae1-943b-6d796ffabd34

📥 Commits

Reviewing files that changed from the base of the PR and between b566656 and 3e85766.

📒 Files selected for processing (1)
  • test/property/settings-write-queue.property.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/audit-56-write-queue-property
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-56-write-queue-property

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread test/property/settings-write-queue.property.test.ts Outdated
Comment thread test/property/settings-write-queue.property.test.ts
…ustion

From review: flaky and the new exhausted behavior throw any of the
five retryable Windows codes (EPERM/EACCES dominate in practice, not
just EBUSY), and the exhausted variant pins that a task burning the
whole four-attempt budget rejects with the retryable error while its
key's successors still run.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@ndycode
ndycode merged commit f471bcb into main Jun 11, 2026
2 checks passed
ndycode added a commit that referenced this pull request Jun 19, 2026
…racle (#593)

* test: property-check the auth rate limiter against a sliding-window oracle

Three fast-check properties over the real module under fake timers
(maxAttempts 3 / 1s window so sequences cross the expiry boundary):

- model-based: for any record/reset/advance interleaving across three
  accounts with decorated id spellings (case/whitespace variants),
  getAttemptsRemaining and canAttemptAuth match a trivial
  timestamps-in-window oracle keyed by canonical id - pinning both the
  sliding window and the trim+lowercase bucket mapping at once
- checkAuthRateLimit throws AuthRateLimitError exactly when blocked,
  carrying the canonical accountId, zero attemptsRemaining, and a
  resetAfterMs that agrees with the live getTimeUntilReset
- over-recording past maxAttempts can never wedge a bucket: any burst
  unblocks fully after one quiet window, with getTimeUntilReset
  bounded by windowMs throughout

Config and registry are module state, so each property restores the
documented defaults and clears buckets, and ids are namespaced per
iteration. Companion to #574/#575/#579/#592; same conventions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: exhaustive event narrowing and constant-derived gaps length

Greptile flagged the bare else branches (a future event kind would
silently model as a reset) and the literal 11 coupled to
MAX_ATTEMPTS * 4 - 1.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
…contracts (#594)

* test: property-check SessionAffinityStore TTL, eviction, and reindex contracts

Five fast-check properties over the real store using its injectable
now parameters (1s TTL floor so sequences cross expiry often):

- model-based TTL/upsert equivalence: for any remember/update/forget/
  advance interleaving through whitespace-decorated key spellings,
  getPreferredAccountIndex and getLastResponseId match a trivial
  TTL map (remember preserves the continuation id, response-id writes
  refresh expiry and never create entries)
- capacity: size() never exceeds maxEntries, and LRU eviction can
  never evict the entry just written
- write-version conflicts: a stale version loses to a live entry on
  both the index and response-id channels, but may rebind once the
  entry expires
- forgetAccount + reindexAfterRemoval mirror an account-array splice,
  with both return counts pinned against the model
- prune removes exactly the expired entries; lazily-reaped sessions
  (touched while expired) correctly do not count as prunable

The prune model initially missed that updateLastResponseId deletes an
expired entry outright; fast-check found the 8-event counterexample
and the model now mirrors the lazy reap.

Companion to #574/#575/#579/#592/#593; same conventions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: fix remember model after lazy reap; cover clearAll; decorate prune keys

Greptile P1: the model carried a responseId across expiry, but the
assertion block's reads lazily reap expired entries from the store, so
a remember after expiry finds no existing entry and the id is gone -
the model now inherits the id only from a live entry (verified at
FAST_CHECK_NUM_RUNS=1000, where the original 4-event counterexample
sequence reproduces without the fix).

P2s: a sixth property pins clearAll (#474 invalidation) - size drops
to zero, every read goes null, and the store stays usable - and the
prune property now routes remember/forget/updateLastResponseId
through decorated key spellings like the model property does.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
…595)

* test: property-check table formatter alignment under unicode content

Four fast-check properties over buildTable/buildTableRow with cell
content mixing ASCII, CJK, and emoji so widths are exercised in
display columns rather than UTF-16 units (ui-02):

- every line of any table (header, separator, all rows) has exactly
  the layout's display width, for any column set, alignment mix,
  missing cells, or extra cells beyond the column count
- content that fits is preserved verbatim with padding on the
  declared side
- overflowing content truncates to a prefix of the original plus an
  ellipsis, never exceeding the column width even when a wide glyph
  cannot fill the final column
- zero-width columns render empty and never leak an ellipsis that
  would desync the row from the header layout

Companion to the property suites in #574/#575/#592-#594.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: cover right-aligned overflow truncation too

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: validate overflow prefix on the left channel; generate default align

Greptile P1: stripping leading spaces on a right-aligned row conflated
alignment padding with spaces that belong to the truncated content
(latent spurious failure under a different fast-check seed - e.g.
' a<CJK>' at width 3 right-aligned). Prefix fidelity now checks the
left-aligned rendering of the same value, where the cell starts at
column 0, while the right-aligned row keeps its width and trailing-
ellipsis assertions. Verified at FAST_CHECK_NUM_RUNS=1000.

Greptile P2: arbColumn and the fit property now generate undefined
align too, exercising formatCell's default-left branch.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
)

* test: property-check plugin health classification and report shape

Three fast-check properties over getAccountHealth/formatHealthReport:

- counts and status derive exactly from the per-account classification
  for any pool (timestamps straddling now, undefined fallbacks, health
  0..100): healthy iff not rate-limited, not cooling, health >= 50,
  circuit closed; status partitions empty->healthy, none->unhealthy,
  some->degraded, all->healthy
- an open circuit disqualifies an otherwise perfect account: tripping
  the account:<index> fallback breaker for any subset is reflected in
  circuitState, subtracted from healthyAccountCount, and drives the
  status partition
- the formatted report names every account with its health percentage
  and exactly the flags its classification implies, and the summary
  lines appear iff their counts are nonzero

Companion to the property suites in #574/#575/#592-#595.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: exercise half-open disqualification and email-aliased circuits

Greptile flagged two coverage gaps: the half-open state was unreachable
without advancing the clock, and accounts sharing an email silently
shared a breaker without an explicit contract. Property 2 now probes
open -> half-open under fake timers (both states disqualify, and the
report renders the precise circuit flag), pool emails are unique by
construction, and a dedicated property pins shared-email accounts to
one breaker while distinct emails stay isolated - derived through the
same getAccountIdentityKey the SUT uses.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
* test: property-check model-map resolution invariants

Six fast-check properties over the model resolution pipeline
(resolveNormalizedModel / getModelProfile / getNormalizedModel),
generating known aliases, synthesized GPT-5 spellings (minors 0-9,
mini/nano/pro/codex variants, -/./space separators, suffixes),
provider prefixes, random casing, and raw garbage:

- closed world: every resolution lands on a MODEL_PROFILES key, so
  getModelProfile's DEFAULT_MODEL fallback is pure defence
- idempotence: normalized outputs are fixpoints
- provider prefixes and casing never change the resolution
- codex dominance: unmapped ids mentioning codex resolve to
  CURRENT_CODEX_MODEL, never a general model
- the inverse: unmapped general GPT-5 spellings stay codex-free and
  in the gpt-5 family (no silent codex routing for general requests)
- every explicit MODEL_MAP alias resolves to its mapped target under
  any prefix/casing spelling

Companion to the property suites in #574/#575/#592-#596.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: combine prefix and casing mutation in the invariance property

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
…598)

* test: property-check display-width clustering and parseKey totality

Six fast-check properties over the ui-02 measurement layer and the
stdin key parser:

- displayWidth is total, integer, non-negative, and bounded by two
  columns per code point over an adversarial alphabet (ZWJ, VS-16,
  keycap, combining marks, skin tones, regional indicators, CJK,
  emoji)
- plain alphabets (no joiners/modifiers) sum per-character widths
  exactly and concatenate additively - the table formatter's standing
  assumption
- truncateToWidth returns a self-consistent prefix (its reported
  width IS displayWidth of the kept text), stays in budget, stops
  only when the remaining gap is 0 or 1 columns (clusters max out at
  2), and is idempotent
- truncation prefixes grow monotonically with the width budget
- parseKey is total over arbitrary byte buffers and only returns
  known KeyAction values
- the full recognized-sequence table is pinned, and everything else
  maps to null

Companion to the property suites in #574/#575/#592-#597.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: make the 2-column cluster granularity self-enforcing

One extra budget column can admit at most two more columns of content,
which is externally equivalent to 'no cluster wider than 2' - the
assumption the truncation maximality bound relies on.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
)

* test: property-check forecast recommendation and summary contracts

Seven fast-check properties over recommendForecastAccount,
summarizeForecast, and buildForecastExplanation with generated result
pools (0-8 accounts, full availability/risk/wait/flag space):

- a recommendation always points at a recommendable result (not
  disabled, hard-failed, exhausted, or unavailable); null only when
  no such candidate exists
- a ready candidate always wins with the minimal risk score among
  ready candidates, and the reason says so
- with no ready candidate, the shortest delayed wait wins
- an empty candidate pool names the actual blocker class: 'blocked or
  exhausted' guidance iff some account is blocked/exhausted rather
  than disabled/hard-failed (the #exhausted-flag regression guard)
- the recommendation is invariant under input order
- the summary partitions availability exactly (ready + delayed +
  unavailable === total) and counts high-risk rows
- the explanation mirrors inputs in order and marks selected on
  exactly the recommended index

Companion to the property suites in #574/#575/#592-#598.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: keep forecast generator in the reachable domain; shuffle permutations

Greptile flagged that availability and exhausted were drawn
independently (evaluateForecastAccount only emits exhausted accounts
as delayed) and that order-invariance only probed reversal. The
generator now derives the pairing, and the invariance property checks
an arbitrary generated permutation plus the reversal. Validated at
FAST_CHECK_NUM_RUNS=1000.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
#600)

* test: property-check context-overflow classification and synthetic SSE

Four fast-check properties over the prompt-too-long recovery path:

- the seven documented overflow phrases classify at any casing and
  position, but only behind the 400 status gate (phrase list pinned
  verbatim so dropping one fails here)
- noise-only and empty bodies never classify at any status
- the synthetic response round-trips: 200 with the synthetic headers,
  parseable Responses-API SSE (created first, completed last, model
  echoed, /compact notice in the terminal output payload), and is
  never re-classified as overflow - the recovery path cannot recurse
  on its own output (recovery-01 dialect guard)
- handleContextOverflow intercepts exactly the classifier-positive
  400s, and a declined response's body stays readable (clone-read)

Companion to the property suites in #574/#575/#592-#599.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: await the async round-trip property so its assertions gate the test

Greptile P1: the it callback was synchronous, so the asyncProperty's
promise was dropped and vitest greened the test before any assertion
ran. Same pattern as the fourth test now.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
…#601)

* test: property-check env boolean parsing and policy key/tag contracts

Four fast-check properties over the shared env parser and the account
policy identity helpers:

- parseBooleanEnv: the six documented literals parse under any casing
  and whitespace padding; every other string (and undefined) returns
  undefined, never a boolean - the contract that unified three
  divergent local copies
- getAccountPolicyKey: always a sha256: handle that never leaks the
  raw identity into the policy file key; accountId wins over email;
  email matches case-insensitively; no identity degrades to the
  shared unknown bucket
- normalizeAccountPolicyTag: outputs are idempotent fixpoints in the
  [a-z0-9._-]{1,64} language, and null occurs exactly for inputs that
  trim to nothing (disallowed runs are dashed, not dropped)

Validated at FAST_CHECK_NUM_RUNS=1000. Companion to the property
suites in #574/#575/#592-#600.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: pin both directions of the whitespace-only-iff-null tag contract

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: generate the whitespace-only accountId fall-through

A defined but whitespace-only accountId defers to the email identity
(the accountId?.trim() || chain); the existing branch assertions cover
it once generated.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
ndycode added a commit that referenced this pull request Jun 19, 2026
* test: property-check the circuit breaker's availability contract

Five fast-check properties over the real CircuitBreaker under fake
timers (small windows so sequences cross every boundary):

- isAvailable/getTimeUntilAvailable agree with the very next
  canExecute outcome after ANY event sequence - the polling view
  callers route on is a faithful prediction, never stale
- an open circuit rejects strictly before resetTimeoutMs and admits
  the probe exactly at it, from any prior history (fresh forced open)
- opens exactly at failureThreshold inside one window, never below
- failures spaced beyond failureWindowMs never accumulate
- the half-open slot admits exactly one probe; success closes with a
  clean failure count, failure reopens for a full timeout

Companion to the property suites in #574/#575/#579; same conventions.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

* test: pin getTimeUntilReset alongside the injectable-now twin

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB

---------

Co-authored-by: Claude <[email protected]>
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