Skip to content

Bound deployment spend with three gates - #43

Open
artyomsv wants to merge 24 commits into
masterfrom
feat/fleet-cost-caps
Open

Bound deployment spend with three gates#43
artyomsv wants to merge 24 commits into
masterfrom
feat/fleet-cost-caps

Conversation

@artyomsv

@artyomsv artyomsv commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Bounds what a deployment can spend, refuses pathologically large diffs, and gives a
refused review a terminal state it can be cleared from.

This is Spec A of two. The per-repo admission rate limit is deliberately not here —
it is the only part needing new storage, and the design's final section records what it
must carry.

Why

The ROADMAP recorded this as a deferred, known operator-facing gap. Precisely stated, the
codebase had no admission or spend limiting at all. The bounds that existed were
narrower than they sound:

  • maxTokens per LLM call — bounds one call's output, nothing aggregate
  • SPIRE_REVIEW_MAX_ATTEMPTS — the auto-retry budget, not a spend budget
  • the conversation turn cap — per thread, default 4, and an @-mention removes it entirely
  • the diff token clip — truncates a large diff rather than refusing it

The two findings that reshaped the design

The conversation path was unbounded, and the code already said so

ConversationSaga.planFollowUp emitted a paid call guarded only by isSpendable. Threads
cost nothing to open, the turn cap is per-thread, and CallRefs states outright that an
@-mention removes it. The comment directly above that guard already recorded that this path
had been assumed safe once and was not.

The first draft of this design repeated the assumption. A cap gating /review while leaving
this open would have closed the front door and left the window up.

A refused review was stuck, and after ADR-024 also unarchivable

The pre-spend refusal this design modelled on wrote a note and nothing else — no status
change. AttentionQueries' own javadoc records the consequence: the review "sits in
REVIEWING until REVIEW_STUCK eventually fires"
, and that row blames "a webhook delivery
path or a worker"
— false when the truth is a deliberate policy decision. ADR-024's archive
guard refuses a reviewing row, so it could not even be cleared.

Tolerable for a one-time configuration error. Unacceptable for a cap that refuses by design.

What changed

Three gates, each where its inputs already are:

Gate Location Checks
Post-diff ResultSaga, on DiffFetched diff size
Pre-spend ResultSaga, before GenerateReview spend / usage
Conversation ConversationSaga.planFollowUp spend / usage

The diff check must be on DiffFetched: changedFiles and sizeBytes exist there and
nowhere later, and refusing at that point skips the context fan-out — per-issue API calls,
a 20-second bounded wait and an encrypted blob write — that would otherwise be discarded.

One decision, one implementation. SpendGate is shared by both spend gates and by the
attention row. Two copies of a money check drift, and drift in a money gate is invisible
until it fails to fire.

A terminal refused status, distinct from failed because the archive guard, the
attention queries and the reviews-list filters all key on status — filing a policy decision
as an infrastructure failure would put it in the same bucket as an outage. Same reasoning
that split pr_state out of status.

Both axes. Every cap is money or calls, whichever trips first, because a
money-denominated cap is inert by design on an UNMETERED deployment where every charge is
an asserted zero. An UNKNOWN-priced row's NULL cost is skipped by SUM and caught by the
call count — exactly the accounting hole ADR-023 identified.

Unset means unlimited, and unset is the default. A deployment that configures nothing
behaves exactly as it does today, and an unset cap does not even read the ledger.

Three things worth a reviewer's attention

The spend read deliberately does not filter archived_at. Ten ledger reads beside it do,
and are right to — they answer "what does this review's page show", and a purged review has
no page. This one answers "what has already been spent", and archiving must not refund a
budget.

The terminal status was being silently overwritten. refuse routes through
RecordFailureReviewFailedTerminally, which was projected as status = 'failed' — so
the refusal was relabelled one Kafka round trip later while its note stayed correct. The
aggregate keeps one terminal-failure event, so refused is a read-model refinement the
projection now declines to coarsen.

The UI had no refused status, and rendered one as a success. ReviewStatus is a
compile-time union over runtime JSON, so tsc had nothing to check and no test constructed a
refused row — progress() fell through to its default branch and produced five done
segments with a green "done" label. Fixed in e336ca1, with a test that builds a refused row
and asserts badge, progress chain and chip membership. The class of gap is recorded as debt:
any new backend status degrades silently into whatever the default branch does — and the review
below found three more instances of it on the detail page.

Not built

The admission rate limit (Spec B), per-actor limits, per-repo spend caps — which need
provider_type on llm_charge, the existing debt entry's own recommendation — queuing, and
cost estimation before a call. That last one would need a token estimate the system cannot
validate, which is the fabricated-number problem ADR-023 exists to prevent; these caps refuse
on measured history and measured input only.

Verification

Final counts are below, after the review fixes. Ten tasks, each with a test written and confirmed failing before its implementation. Two
tests were rewritten after proving order-dependent — the module shares one Postgres across
~76 suites and several write real charges into the same rolling window a cap reads, so the
cap tests measure a live baseline and set limits as baseline + headroom rather than using
literals.

Details in docs/DECISIONS.md (ADR-025), docs/SMOKE-TEST.md, and
docs/superpowers/specs/2026-08-09-fleet-cost-caps-design.md.


Reviewed on four lenses; every finding fixed

Security, code-quality, rules and QA. Rules came back clean. The other three found 13 issues,
all fixed in this PR.

The one that mattered most was not in the diff. chargeGeneratedCalls sat inside
ifCurrentRun, so when an author pushed a new commit while the LLM call was in flight, the whole
handler was dropped and the charge was never written. That line was correct when written —
dropping a stale run's outcome is right, and the charge merely shared the block. This PR promotes
the ledger from a report into an enforcement input, which turned an under-reported cost card into
a cap-evasion primitive: push-then-push in a loop buys uncounted paid calls, unbounded, rather than
the "in-flight × per-review cost" overshoot the design claims. Fixed in 467a8ae, with the charge
recorded before the staleness guard and the guard applied to everything downstream.

A refusal could still trigger spending. onReviewFailed bypassed the new terminal-status guard.
The review found it could relabel a refusal failed; the fix found the retryable branch was worse —
it wrote status='reviewing' and a fresh retry_at, so the sweeper would re-run the pipeline and
spend again on a review refused for exceeding a spend cap. Both closed in 8bfb2f2.

A refused review told the operator its diff was clean. refused reached the list surfaces but
not the detail page, which rendered ✓ clean — No issues found in this diff. for a review nothing
had looked at — and the actionable note was displayed nowhere in the UI. This branch's own runbook
asserts that note is visible, which is useful evidence Mode M had never had a live pass. Fixed in
ff53185, fcf89ee and cf33792, the last because Re-run was hidden on a refused review while the
backend permitted it — the refusal advises raising the cap and then offered no way to act on it.

Also fixed: the configured cap no longer appears on viewer-readable surfaces (ADR-022's third rule);
a failed ledger read is now visibly CAP_UNENFORCEABLE rather than silently indistinguishable from
zero spend; the attention row says "No capacity returns before X" instead of naming an instant that
is only a lower bound, and ADR-025 is corrected where it stated the exact claim as fact; a stored
0 reads as unset rather than refusing every review forever; windowMinutes is bounded on write
and read; a refused follow-up now leaves a durable record; and /api/settings/caps joins the auth
list whose purpose is catching exactly that omission.

The money axis had no end-to-end test. Every cap test used UNMETERED charges costing zero, so
all of them tripped the call axis — the feature's headline capability, refusing on money, was never
exercised. SpendGateTest closes it.

Verification

1283 Java tests across 167 suites; 338 spire-ui vitest tests across 46 files;
tsc --noEmit silent. Forced reruns, not cached passes.

Two items deliberately left, both recorded: refuseForTest's rationale is stale now that both gates
exist, and a refused follow-up is still silent to the person who asked — a NotifyTurnCap-shaped
notice is a design decision rather than a defect fix, and belongs with the rate-limit spec.

artyomsv added 14 commits August 9, 2026 23:14
There is no rate limiting of any kind in the codebase today. The only
bounds that exist are a per-call token cap, the auto-retry budget, the
conversation turn cap and the diff clip -- none of which bound how much
a repository can spend or how often it can be reviewed.

Places each check where its inputs already are: rate at admission, size
on DiffFetched, spend before the LLM call. Records why folding the size
check into the pre-spend gate was rejected -- the diff stats exist on
DiffFetched and nowhere later, so checking them at the spend gate would
need either new columns or a change to a Kafka wire type, and would run
the context fan-out before discarding it.

Keys the rate counter by provider as well as repo, which the ledger
debt entry explicitly predicted this feature would otherwise inherit,
and gives every cap a call-count axis beside the money one, since a
money-only cap is inert by design on an unmetered deployment.

Draft, pending review.
Two findings reshaped it, both verified against the code.

The conversation path was outside every gate. planFollowUp emits a
paid call guarded only by isSpendable, threads are free to open, the
turn cap is per-thread and an @-mention removes it entirely -- the
codebase says so in as many words. A spend cap that skips this path
leaves the abuse case it exists for half-open, gating /review while
comment-driven spend runs free. The first draft repeated an assumption
the code comment above that guard already records as falsified once.

A refused review had no lifecycle. The pre-spend refusal it models on
writes a note and nothing else, so the review sits in reviewing until
the stuck-review row fires blaming a webhook or a worker -- and since
archiving refuses a running review, it cannot even be cleared. That is
tolerable for a one-time configuration error and fatal for a cap that
refuses by design. Refusals now reach a terminal refused status,
distinct from failed so a policy decision is not filed as an outage.

Also: names the two axes, since a review's calls and a repo's
admissions are different units; records that the spend read must not
filter archived charges, or archiving becomes a budget reset; states
the cap as soft with bounded overshoot rather than implying exactness;
corrects the claim that a counter is the only pre-hoc source; and
settles the window question, since a rolling window's capacity-return
instant is computable and so strictly more precise.

Splits the admission rate limit into its own spec at the state seam --
everything here needs no new storage.
Ten tasks from the approved design, each ending in an independently
testable deliverable.

Front-loads the verified signatures, since the previous plan told an
implementer to paste code calling five methods that did not exist.

Orders the refusal vocabulary and the terminal status before any gate,
because all three gates depend on both and a gate without a terminal
state leaves the review stuck and unarchivable.

Records the two tests most likely to be dropped: that an unset cap is
a no-op, which is what stops an upgrade silently changing a running
deployment, and that an at-mention does not bypass the spend cap,
which is the unbounded case the conversation gate exists for.
The pre-spend refusal this copies writes a note and nothing else, so
the review sits in reviewing until the stuck-review row fires blaming
a webhook or a worker -- and since archiving refuses a running review,
it cannot be cleared at all. Tolerable for a one-time configuration
error, unacceptable for a cap that refuses by design.

Uses refused rather than failed because the archive guard, the
attention queries and the reviews list all key on status, so filing a
policy decision as an infrastructure failure puts it in the same
bucket as a genuine outage.

The aggregate has one terminal-failure event, so refused is a
read-model refinement of ReviewFailedTerminally. Projecting that event
unconditionally relabelled the refusal as failed one broker round trip
after the saga wrote it, and invisibly: the note stayed right while
the badge, the list and the attention row all read the status. The
projection no longer coarsens a status the saga made more specific.
Every cap (changed files, diff bytes, spend, calls) has no default,
so a deployment that configures nothing behaves exactly as it does
today -- a non-null default would silently change every running
deployment's behaviour on upgrade. An unparseable stored value is
also treated as unset rather than zero, so a typo in Settings cannot
refuse every review.

The rolling window is the one exception and always has an effective
value (a day), since a window with no length is not a lesser cap but
a meaningless one.

CapSettingsResource writes an empty string to clear a limit back to
unset, because AppSettingRepository has no delete -- CapPolicy already
parses blank the same as absent, so the two meet without a new method.
Deliberately does not filter archived charges. Ten ledger reads beside
this one do, and copying them would make archiving a review refund its
budget -- an operator tidying the list would silently buy more spend.

The discriminating guard is a charge with archived_at stamped, not an
archived review: archiving stamps review_status alone and never the
ledger, so an archived review's charges stay unmarked and a copied
filter leaves that test green. Both cases are pinned; the stamped one
is what fails when the filter comes back.

Counts calls as well as money, because a money-only cap is inert on an
unmetered deployment where every charge is an asserted zero, and an
unpriceable call carries a null cost that SUM skips while the call
axis counts it anyway.

A read failure reports no usage and logs at ERROR. A cap that refuses
every review because its own query failed is an outage wearing the
face of policy.
Adds a Limits section beside Code review and Conversation with the
five fleet-wide caps: max changed files, max diff bytes, spend cap,
call cap, and the rolling window in minutes. Every field is optional
and a blank input sends null, never 0 -- exactly the coercion that
caused ADR-023's "unknown became zero" bug, and here 0 would mean
"cap of zero", refusing every review, instead of "no cap".

Kept as raw input strings rather than numbers so a cleared field
stays blank instead of becoming 0, mirroring the model rate fields'
existing pattern. A typed 0 (or any non-positive or fractional value)
is rejected before any of the three settings groups are saved, since
there is one Save button for the whole page.
Adds the pre-spend gate beside ADR-023's priceability check at
ContextAssembled: deployment-wide spend and call count over the
policy's rolling window, refused through the same terminal path as
an unpriceable model. Both axes are checked because a money-only cap
is inert by design on an UNMETERED deployment -- every charge there
is a legitimate zero, so summed cost never approaches a positive
limit; the call count is what actually bounds an unpriced fleet.

Skips the ledger read entirely when neither limit is configured, so
an unset cap stays a true no-op rather than a query that always
answers allow.

Also fixes a pre-existing hand-built ResultSaga fixture that never
set a cap policy, which the new gate turned into a null pointer.
A reply in a live thread emitted a paid AnswerFollowUp guarded only by
whether the model could be priced. Threads cost nothing to open, the
turn cap is per-thread, and an @-mention removes that cap entirely, so
the only genuinely unbounded spend path in the system was the one the
review gates did not cover.

The gate sits after the mention override rather than beside it: the
override must keep bypassing the turn cap and must not also bypass the
spend cap. The free turn-cap notice stays outside the gate because it
buys nothing, and silencing it would restore the failure it exists to
fix.

A refusal here does not move the review. The review may have completed,
and refusing one reply is not a retraction of that outcome, so this
records the decision on the timeline and leaves status and note alone.

Both paid-call sites now share one SpendGate rather than each holding a
copy of the comparison. Two copies of a money gate are free to drift,
and drift in a money gate is invisible until it fails to fire.
CAP_REACHED fires from the exact question SpendGate already asks
before every paid call, so this row and the two enforcement sites
cannot drift on what "over the cap" means. Unlike the two ledger-wide
cost rows, it carries no acknowledgement: it describes usage right
now rather than a past event, so it clears on its own once the
charges that tripped it age out of the rolling window or the operator
raises the limit.

Names the instant capacity returns as the oldest in-window charge plus
the window length, the concrete advantage a rolling window has over a
fixed bucket -- backed by a new SpendWindow.oldestChargeAt, deliberately
carrying no archived_at filter for the same reason since has none.
The orchestrator now sets a review's status to 'refused' when a spend
or diff-size cap declines to run it, but nothing carried that value
into the dashboard. ReviewStatus is a compile-time union and the status
arrives as runtime JSON, so the type checker had nothing to check and
every test stayed green while the screen was wrong.

STATUS_LABEL is a Record keyed by that union, so it answered undefined
and the badge rendered blank. Worse, miniPipeline tested each known
status in turn and fell through to its terminal case, drawing a review
the deployment declined to spend on as five green segments under
"done". A refusal shown as a success is worse than one shown as
nothing, because silence at least looks like nothing happened.
matchesChip matched no chip, so the row was reachable only under All.

Adds the union member, a Refused label, its own miniPipeline branch and
a warn-coloured pill -- not crit, which would read as an outage, and
not muted, which would read as nothing to do.

Files the row under Needs attention rather than Closed. Closed holds
cancelled and superseded, which mean nothing to do, while a refusal
always leaves a decision: raise the cap, wait for the window, or split
the pull request. What settles it is the diff-size gate, whose refusals
raise no attention row at all, since CAP_REACHED is derived from the
spend and call caps only -- under Closed those reviews would have had
no surface anywhere. The three places that say "needs attention" now
share one predicate so they cannot disagree.

The test renders an actual refused row rather than probing the helpers,
because the badge alone would still let the green "done" through.
ADR-025 records the three gates and why each sits where it does, the
new terminal refused status, the dual money-and-call axis, the single
SpendGate shared by both paid-call sites, and that unset means
unlimited.

Several entries are corrections of earlier reasoning rather than new
decisions, and those matter more. The conversation path was the
unbounded one and the codebase already said so, so a cap gating only
the review path would have closed the front door and left the window
up. A refused review needed a terminal state because the refusal it
copied left one stuck in reviewing until the stuck-review row fired
blaming a webhook, and since archiving refuses a running review it
could not be cleared at all. And refused is a read-model refinement of
the aggregate's one terminal-failure event, which the projection had
been coarsening back to failed one round trip later.

Also records that the spend read must not filter archived charges
while the ten reads beside it must, that the cap is soft with overshoot
bounded by in-flight reviews, that the window rolls so the instant
capacity returns can be named, why oldestChargeAt lives on SpendWindow
rather than in the attention queries, and why the cap row is blocking
rather than a warning -- severity describes impact, not fault.

Records the seam that let a new backend status reach the dashboard as
a success, since that is a fact about the boundary rather than about
this feature, and files the class under techdebt/spire-ui.

Corrects the roadmap and the security doc, which both still told an
operator there was no ceiling on spend, and marks the per-repo
admission rate limit as the one part still deferred.

Adds a runbook mode covering a tripped call cap, a refused review being
archivable, the attention row appearing and clearing itself, the
diff-size gate, and an at-mention failing to buy a way around the cap.

Widens the god-class debt entry to the three classes now past the
guideline, with measured counts and a separate framing for each. The
result saga went 517 to 625 and back to 598 when the spend comparison
was extracted, so its gates want their own collaborator and the
direction is set. The attention queries went 377 to 432 under a
different cause: one private method per condition is that file's own
convention, followed correctly, and it simply has no term that stops
growing.
@artyomsv
artyomsv force-pushed the feat/fleet-cost-caps branch from 8614739 to 42d8ef1 Compare August 9, 2026 23:51
A cap-refused review has no findings and no reconciliation, so the
findings card fell past the failed/cancelled branch into the empty
state and told the operator "clean - no issues found in this diff".
Nothing had reviewed that diff.

`r.note` is rendered in exactly one place in the UI, inside that same
branch, so the actionable refusal text - raise the limit in Settings,
or split the pull request - was written, stored, sent over the wire
and displayed nowhere. That contradicts SMOKE-TEST Mode M-1, which
asserts the note is visible.

The status check becomes a lookup keyed by status, so `refused` gets
its own heading ("Why it was refused" - nothing stalled, nothing was
stopped) and a status missing from the map is the absence of an
explanation card rather than a fall-through into the findings
branches. A blank note now renders a placeholder: an empty card is
the same false reassurance.

findCell joins failed/cancelled in showing a placeholder, since a `0`
was a findings count for a diff no model ever saw.

The existing refused tests covered only the list row; these render
the detail page and assert the "clean" claim is absent, not merely
that the new heading appears.
The ReviewGenerated handler charged inside ifCurrentRun, so a commit
pushed (or the PR closed) while the LLM call was in flight dropped the
handler whole and the charge with it. The worker's PR-head re-check
runs before the call, so nothing about the stale run un-spends the
money.

That was an under-reported cost card until the ledger became an
enforcement input. Now it is a cap-evasion primitive: push-then-push
in a loop buys uncounted paid calls without bound, and one
GenerateReview stands for up to two of them.

Charge first and unconditionally -- the shape FollowUpGenerated has
always had -- and keep the staleness guard on everything downstream,
extracted to onReviewGenerated. Redelivery is still a no-op without a
new mechanism: recordCharges is ON CONFLICT (call_ref, token_type) DO
NOTHING and reviewSlot keys on the commit, so a superseded run's ref
cannot collide with the new run's.
The status guard added with the refused lifecycle lives in
projectTerminalFailure and is reached only from DomainEventSink.
ResultSaga.onReviewFailed is the other writer of a terminal status and
went straight through updateStatus with no precondition -- and
ReviewFailed is the one result the saga does not wrap in its stale-run
guard, so the aggregate already being terminal did not stop it either.

A replayed ReviewFailed (cs.results is at-least-once, and DLQ replay is
deliberate) therefore relabelled the review failed, overwrote the note
that is the operator's only explanation, and populated error_detail --
the field ADR-025 leaves unset for a policy decision so the detail page
does not show an infrastructure fault.

Status, stage, note and error now land in one guarded statement, so the
guard cannot hold for the badge while the rest is overwritten anyway.

scheduleRetry gets the same predicate, for the worse half of the same
defect: a replayed retryable failure dragged a refused review back to
reviewing with a fresh due time, and the sweeper would then re-run the
pipeline and spend again on a review that was refused on purpose.
`rerunnable` listed completed and failed, so the button did not render
for a refused review - while `ReviewRerunService.rerun()` gates on
nothing but `archived`, so the operation was permitted all along.

That worked directly against the operator. The recovery path for a cap
refusal is raise the limit, then re-run, and the refusal note on the
same page says exactly that. With no button, the only way to act on
the advice was to push a commit to a pull request that may be
perfectly finished.

The boolean becomes a total map over `ReviewStatus`, each answer
carrying its reason. It cannot catch a status the backend has and
`api.ts` does not - an unknown one hides the button, which is the
conservative direction when pressing it spends money - but it fails
the build the moment the union grows, so the next status is a decision
rather than whichever way a `||` chain happened to fall. Only
`refused` changes; the other six are pinned as they already behaved.

The confirmation gains a line for a refusal: no model call was made,
so the wording must not imply a previous result is being replaced, and
what the operator needs is that pressing it before raising the limit
changes nothing. Added beside the existing note rather than replacing
it - an earlier round may have posted comments before a later one was
refused.
CapRefusal put the configured cap into detail() and note(), which reach
three surfaces a spire-viewer reads: the review timeline, the review's
note and the CAP_REACHED attention row. ADR-022's third rule makes
configuration admin-only including its reads, and the precedent is in
this repository -- ADR-023's review round dropped per-token rates from
ReviewDetail for exactly this.

The measured figure is the review's own context and stays. The limit
moves to logDetail(), a suffix on detail() rather than a second wording
so the two can never describe one refusal differently.

Money now formats with Locale.ROOT: a de-DE default rendered "$0,01"
into operator-facing text.
Three ways a refusal was quieter or louder than the truth.

SpendWindow.since answered Usage(0, 0) on a failed ledger read, which
is indistinguishable from "nothing has been spent" -- so the gate saw
"allowed" and the attention panel, whose whole contract is that every
row is a condition true right now, reported health while no paid call
was being refused. The plausible trigger is not a total outage but
connection-pool exhaustion under load, i.e. the exact burst the cap
exists to bound. The read answers Optional.empty(), SpendGate.Decision
carries ledgerUnreadable beside the verdict so all three sites still
share one comparison, and CAP_UNENFORCEABLE states the condition.
Failing open is unchanged and deliberate.

"Capacity returns at X" was a lower bound presented as an exact
instant: ageing the oldest charge out restores enough capacity only
when the overshoot is no larger than that charge. With a call cap of
100 and 300 calls in the window it names an instant that changes
nothing. Reworded to "No capacity returns before X"; ADR-025 stated
the exact claim as fact and is corrected.

A refused follow-up wrote only to the timeline -- an in-memory list
capped at 500 entries deployment-wide and lost on restart -- so the
only durable trace of an unanswered reply was the deployment-wide
CAP_REACHED row, which never says which thread. It now appends
FollowUpRefused to the review's event log under the conversation root.
Still no setNote: "Not reviewed" is false on a reviewed PR.

SpendGate's javadoc now states why the spend gates refuse on >= while
the diff gate refuses on >, and SpendGateTest pins both boundaries --
the money axis was reachable by no test at any level.
The "unset never becomes zero" invariant was enforced only at the REST
boundary. CapPolicy reads app_setting directly and returned
OptionalLong.of(0) for a stored "0", after which spent >= 0 is true for
every review, forever, on every gate -- a deployment-wide review outage
from one row. Not reachable through the application today, which is the
argument this repository has already lost once: ADR-023 held the
conversation path safe by construction because the registry forbids an
unpriceable model, and V30 then created rateless models directly in
SQL. A non-positive stored value now reads as unset, alongside the
unparseable one it already treated that way; the REST rejection stays,
so an operator still gets a 400 rather than a silent no-op.

An out-of-range windowMinutes was accepted with a 200 and then threw on
every use. Instant.now().minus(window) throws beyond the instant range,
and it throws inside AttentionQueries.collect (outside its SQLException
catch, so the whole panel goes dark) and inside both spend gates, which
dead-letter. GET /api/settings/caps builds the same Duration, so it
500s too and the value cannot be cleared through the product. Bounded
at a year at the REST boundary, and read as unset above it.

Only the window is bounded above: the four caps are compared, never
added to an instant, so a huge one is an unreachable limit.
/api/settings/caps was missing from OperatorAuthTest's
CONFIGURATION_READS, whose javadoc says a new screen absent from the
list is meant to be a failing test rather than an oversight. The role
was already right; only the assertion was missing.

diffSizeDecision's maxDiffBytes branch was exercised by nothing -- both
fake policies returned empty for it and every case drove
maxChangedFiles -- so the copy-paste that compares the file count
against the byte limit would have shipped green. Verified by making
exactly that mutation: two tests fail.

Neither comparison's boundary was pinned either. The diff gate refuses
on > and the spend gate on >=; both readings are defensible and the
difference was indistinguishable from the drift SpendGate exists to
prevent. Now stated in both javadocs and asserted on both axes.

SpendCapGateTest's refusal case asserted the status and the note but
never that GenerateReview is not emitted, which is the assertion that
matters on the most expensive of the three gates -- a regression that
refuses and spends anyway passed it. The pattern was already in the two
sibling suites.
computeStages had no refused case, so it fell to default and drew all
six stepper nodes grey. A review refused at the pre-spend gate had
fetched its diff and assembled its context; the detail page said
neither happened. cancelled and superseded already mark their completed
steps done, so the omission was inconsistent within the one switch.

Grouped with cancelled/superseded rather than failed: the work that ran
really ran, and no step is marked failed because nothing failed -- the
same split ADR-025 makes for the status itself.
Three gaps the QA lens found, all of the shape where the control is
present, looks tested, and the path that matters has never run.

The money axis was never driven true by a real charge anywhere. Every
integration test that trips a cap uses UNMETERED lines whose cost is an
asserted zero, so SUM(cost_millicents) was never compared against a
limit it could cross -- the primary axis of the feature had no
end-to-end coverage. SpendCapGateTest now records a real metered charge
that crosses a configured spend cap, with the call cap left unset so
the refusal can only be the money one, and asserts the note names spend
and not calls. It also asserts the fixture really recorded money first,
so it cannot pass on ambient spend from a sibling suite.

Which axis is reported when both are over was whichever if was written
first. Money wins, and now says why: it is the axis the operator
configured in the unit they care about, while the call count is the
proxy that bounds an UNMETERED fleet -- reporting the proxy sends them
to raise the wrong limit. Pinned from both sides, so it is ordering
rather than precedence-by-accident.

oldestChargeAt had no direct test; the attention test only asserted its
message contained the wording, never that the instant was right. Now
anchored: the window starts exactly on one seeded charge, which makes
it the minimum whatever else is in the shared database. The first draft
of that test was vacuous -- one distinct timestamp in scope makes MIN
and MAX the same row, and it survived that mutation when it ran first
in the class. A second, newer charge is now seeded so "earliest" means
something; both tests fail on MIN -> MAX.
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.

1 participant