Stop a slow host turning a login-guard test into a segfault (closes #186) #188
Reference in New Issue
Block a user
Delete Branch "issue-186-loginguard-test-flake"
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?
Closes #186.
The two defects
1. A failed non-fatal assertion became a segfault.
acquirereturns(nil, false)on every refusal path,TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueingcheckedokwithassert.True, and the next line called the nilrelease. Onetiming miss therefore killed the whole
internal/middlewaretest binaryrather than one test. That assertion, and every other one in the repo
whose value is dereferenced afterwards, is now
require.2.
acquireitself could shed a request with slots standing free.This is the part that is not confined to the test.
acquireselectedover a slot send and an already-armed wait timer; Go picks among ready
cases uniformly at random, so a goroutine descheduled for longer than
the wait had a coin-flip chance of being refused even though a slot was
available — under load, which is exactly when shedding a login is least
defensible.
How the wall-clock dependency was removed rather than widened
acquirenow takes a free slot in a non-blocking preamble, before anytimer is armed — the same shape
lifecycle.waitDoneuses to settle itsown both-ready race for #134.
The test's third acquire is then correct by construction and the 10 ms
wait is irrelevant to it.
The preamble does not let a late arrival barge past a queued waiter.
A waiter can only be parked on a full buffer, and a receive refills
that buffer from the head of the send queue under the channel lock, so
the buffer never appears non-full while anyone is parked. I did not take
that on trust: a scratch harness parked 8 senders on a
cap(1)channel,waited until they were certainly parked, then did one receive
immediately followed by one non-blocking send, 2000 times. 0 barges in
2000 attempts. (My first version of that harness reported 11740 barges
— it was racing the waiters' startup, not testing parked waiters. A
request that has not parked yet can be beaten to a free slot, but that
was equally true of the blocking send this replaces, where arrival order
is likewise decided by who reaches the channel first.)
Placing the preamble ahead of the queue admission also stops a request
that never waits from occupying a waiter's place. The memory accounting
in the
passwordVerifyMaxWaiterscomment is unchanged: 16 parkedwaiters plus 2 in slots.
Cancellation semantics changed, and the doc was wrong about it. The
preamble never consults
ctx, so a request whose context is alreadycancelled is now granted a free slot every time, where the old select
refused it about half the time.
acquire's doc still claimed acancelled request was refused. The behaviour is kept and the doc is
corrected, not the other way round: it matches
lifecycle.waitDone,the caller abandons the work on its own
ctxand the slot comesstraight back, and refusing would mean shedding a request with capacity
standing free — the very thing defect 2 is about.
ctxis stillhonoured once a request has to wait, which is what
TestLoginGuard_AcquireHonoursCancellationpins.TestLoginGuard_FreeSlotBeatsAnExpiredWaitpins the preamble, modelledon
TestWaitDone_DrainedBeforeExpiredContext: 1000 passes with a waitthat has already elapsed on arrival, which is the worst case
scheduling can produce. It waits on nothing. Without the preamble it
fails within a few passes — measured on the reverted-preamble
mutation: pass 2. (An earlier revision of this body said "pass 1";
that was wrong.) Relatedly,
racePasses's comment no longer claimsdetection probability
1 - 2^-N: a pass is only a coin flip once thezero-duration timer has already fired, so the real per-pass probability
is below 1/2 and that bound was optimistic. What the comment now states
is only that the passes are independent.
The concurrency-bound test keeps both bounds
TestLoginGuard_SemaphoreBoundsConcurrentVerificationsused a 10 mssleep to make two workers overlap and asserted the observed maximum was
exactly 2. A sleep only makes overlap likely; on a host that
deschedules a goroutine for longer than the sleep the workers serialise
and the maximum comes back as 1.
Holders now rendezvous, so the overlap is a fact — but the barrier
does not open at the
concurrency-th holder. Opening it there fixesthe lower bound at the cost of the upper one, which is what this
test exists to enforce: holders would leave the instant the count
reached
concurrency, so an over-admitting guard's extra workers wouldarrive after the first holders had already decremented, and
highestwould report
concurrencyhowever many were really let in. A firstrevision of this PR did exactly that and detection of a broken bound
fell to roughly a quarter of runs.
The barrier now opens once every worker's acquire has returned and
any slot it won has been counted: a
sync.WaitGroupofworkers, withDone()called on the refusal path immediately, and on the success pathdirectly after the holder has recorded itself in
highest. Recordingbefore signalling is deliberate and is slightly stronger than "
Done()immediately after the call returns" — it makes it impossible for the
barrier to open while an admitted worker is still on its way to being
counted. Under a correct guard the refused workers return within the
guard's own wait and nothing depends on how long that takes; under a
broken guard every admitted worker is inside simultaneously and
highestis the true maximum. No sleep, and no wall-clock margin wasreintroduced: the only duration left in the test is the 5 s
time.AfterFuncdeadlock guard, which no assertion depends on and whichis reachable only by a worker that never returns from
acquireat all.Mutation evidence,
newLoginGuard'sslots: make(chan struct{}, concurrency)changed toconcurrency * 6, gated throughmake test(
GOFLAGS=-count=1, so no run served a cached result):GOMAXPROCS1 / 2 / 4 / 812 of 12 on the reworked test, and the control run confirms the harness
reproduces the weakness rather than flattering the fix. The failure
reports
expected: 2, actual: 12— the true maximum, not a truncatedobservation. The unmutated tree passes at
GOMAXPROCS1, 2 and default,in 2.01 s each, which is the refused workers' wait and not a margin.
The sibling sweep
Every
*_test.goin the repo was swept for the same shape — a non-fatalassertion on a value's validity that a later line dereferences, indexes
or calls. One sibling, in another package:
internal/database/webhook_db_manager_test.gochecked a slice lengthwith
assert.Lenand indexed it on the next line, so the veryregression it guards (deleting one webhook's DB destroying another's
rows) would have surfaced as an index-out-of-range panic through
internal/databaseinstead of a failing test. Nowrequire.Len.Also checked, and clean: no testify call anywhere in the repo is made
from a goroutine, an
Eventuallycondition, or a spawned cleanup, whereFailNowwould not stop the test.Wall-clock survey of the rest of the suite — the deliverable
Every
time.Sleep/time.After/Eventually/WithTimeout/sub-second constant in the suite was classified. Three classes:
(A) correctness depends on the margin, so a slow host can red
correct code; (B) patience budget only, so a slow host makes it
slower and only broken code fails it; (C) no real dependency
(injected clock, guaranteed-to-expire deadline, or a one-directional
margin).
The two tightest (A)-class risks in the whole repo were in this same
file, in
TestLoginGuard_ShedsPastTheQueueCap, so they are fixed hererather than left for the next red night:
assert.Less(got.elapsed, 100ms)— bounded the latency of a goroutinehand-off, not the guard. A single 100 ms stall reds correct code. It
is removed: shedding is told from queueing by the queue depth,
which is a state fact and was already asserted.
probeWait = 200msfeeding arequire.NotNil— the probe had to becreated, scheduled, shed and delivered within 200 ms. It is now a
5 s patience budget against a queue wait of a minute; only a guard
that actually queues can exhaust it. Mutation preserved: reverting the
queue admission to a blocking send still fails the test (in 5.03 s,
vs 0.20 s before).
fillQueue'srequire.Eventuallybudget went 1 s to 5 s for the samereason — 1 s is the same order as the stalls this suite must survive.
probeQueueCapno longer returns*bool; it returns two bools, whichis the surrounding idiom. (Unnamed, because
nonamedreturnsrejectsnamed bool results.)
Remaining (A)-class risk in the repo — one, not touched here:
internal/delivery/target_database_test.go:203-211— two back-to-backwrites must both land inside the 2 s reopen-debounce window, or
correct code reports 2 reopens and the test fails. Now filed as
#190; not fixed here, as it
is out of this issue's scope.
(B)-class, all with budgets that only broken code can exhaust:
retention_lifecycle_test.go:143,172(5 s),:193,262(10 s hung-stopguards);
archive_sweeper_test.go:274(5 s);engine_integration_test.go:516(5 s),:650,1019(2 s),:741(5 s),:1262(1 s, but the channel is pre-filled before the select);engine_lifecycle_test.go:76,215(10 s);engine_test.go:232(2 s),:983(2 s).(C)-class, no real dependency:
cmd/webhooker/main_test.go:65andinternal/server/shutdown_test.go:35are pure-function table inputs, noclock read;
lifecycle_test.go:18,102,retention_lifecycle_test.go:35,245andengine_lifecycle_test.go:36,59are deadlines that are guaranteed toexpire (the thing they wait on never completes), so slowness cannot flip
the assertion;
circuit_breaker_test.go:104,135,161,281sleep 60 ms pasta 50 ms cooldown, one-directional — oversleeping only makes the
assertion more true;
internal/delivery/engine_integration_test.go:1094is a 2 s server-side sleep against a 1 s client timeout, one-directional
in the same way;
retention_lifecycle_test.go:19,archive_sweeper_test.go:257,912are sweep cadences.Worth naming even though none of them can red CI: five remaining
sleep-for-goroutine-ordering sites, which are the shape that caused this
issue, but which all fail green rather than red — a slow host makes
them under-observe rather than mis-assert.
engine_lifecycle_test.go:29,117(250 ms, "sleep so the doomed pool hasexited before Notify"),
engine_test.go:777(100 ms handler sleep toforce worker overlap, asserting only an upper bound),
engine_test.go:1023(50 ms, soExportScheduleRetry's goroutineattempts the overflow send before the channel is drained),
engine_lifecycle_test.go:230andretention_lifecycle_test.go:209(sleep, then assert nothing happened). The rendezvous in this PR is the
pattern they could follow. Not changed here: they are other packages,
and a test that is too weak is a different problem from one that reds
next.Verification
Iteration evidence. No make target runs one package N times, and
re-running
script/testserves(cached)results, so I compiled thepackage test binary with the same flags
script/testuses(
go test -c -race, run with-test.timeout 30s) and looped that.Disclosure: that
go test -cis the one raw toolchain invocation inthis work; every gate below is a make target or
script/entrypoint.The mutation runs above used
make testwithGOFLAGS=-count=1, whichis the make target with the test cache defeated by environment rather
than by a raw invocation. The binary was built after the last source
edit and lists the new tests.
On the code as it stood at the first revision, 428 iterations, 0
failures:
GOMAXPROCS=1GOMAXPROCS=2, 8 roundsHost load average was 15-22 on 48 cores throughout (this box runs many
sessions), plus 16 spin loops in an earlier round; all spinners were
killed and verified gone. Disclosure: that loop was not re-run after
the rework. It is not the evidence for anything — see the control
below — and the rework's own evidence is the 12-run mutation table.
Honest control, and the reason the iteration count is not the
evidence: the unfixed HEAD binary also survived 228 of the same
stress iterations (100 at
GOMAXPROCS=1under 16 spinners, 128 16-wayparallel) with 0 failures and 0 panics. My loop cannot reproduce the CI
flake on this host, which matches the reviewer of
#180 being unable to. So the
iteration count only shows no regression; what actually justifies the
fix is the by-construction argument and the forced-miss demonstration.
Forced-miss demonstration. Forcing the acquire to miss
deterministically (a stand-in for the scheduling delay CI hit: something
else takes the freed slot before the third acquire runs), in throwaway
copies — unfixed code reproduces run 232 exactly, including the SIGSEGV
that killed every other test in the package; the fixed test against
unfixed production code gives 0 panics, 2 clean failures, and the rest
of the package still runs.
Gate.
make checkexits 0. Disclosure: its hostgo testlineswere served
(cached), because the mutation loops above had already runthe same tree — so the uncached test evidence is the Docker gate below,
not
make check. The authoritative run is the Docker gatewith the cache defeated,
docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain ., exit 0:golangci-lint:v2.12.2container ran 47.5 s andreported
0 issues.;make fmt-checkran.zero
(cached)lines and zeroFAILlines in the whole build log.TestLoginGuard_FreeSlotBeatsAnExpiredWait,TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueing,TestLoginGuard_SemaphoreBoundsConcurrentVerifications,TestLoginGuard_ShedsPastTheQueueCapandTestWebhookDBManager_MultipleWebhooksall appear as--- PASS.CACHEDlayers are the two pinned base-image resolves and sixdeterministic stage-2 packaging steps. No lint or test layer is among
them.
This gate was run on the final head, after the rework commit. CI on that
head is green: run 239, "Successful in 2m50s".
The tagged image was removed,
docker ps -ais empty, and no prune ofany kind was run.
TODO.mdis untouched by this commit.clawbot referenced this pull request2026-08-18 04:10:16 +02:00
Review: FAIL —
needs-reworkIndependent review of
b8940c0againstnext(b573959). One blocking finding: the de-flake ofTestLoginGuard_SemaphoreBoundsConcurrentVerificationstraded away the bound it exists to enforce.BLOCKING 1 — the rendezvous makes the concurrency-bound test detect its own regression ~23% of the time (was 100%)
internal/middleware/loginguard_test.go:252-311.The rendezvous fixes the lower bound (at least
concurrencyholders overlap: correct, and animprovement). It destroys the upper bound, which is what the test actually asserts. Holders leave the
instant the
concurrency-th arrives, sohighestis sampled over a window that collapses to nothing:under a guard that admits more than
concurrency, the extra goroutines reachinside++after thebarrier has opened and after the first two have already decremented, and
highestcomes back as exactlyconcurrency. The old 10 ms hold kept every admitted worker inside simultaneously, sohighestreportedthe true maximum.
Measured, one production mutation only —
newLoginGuard'sslots: make(chan struct{}, concurrency)changed to
concurrency*6, i.e. the bound simply does not hold — gated throughmake test:b8940c0GOMAXPROCSdefault x9, and 1, 2, 4, 8)b573959GOMAXPROCS=1)So the dedicated bound test went from deterministic detection to a coin flip, and it is now itself flaky
in the fail-green direction — the same class #186 exists to
remove, inverted. Its doc comment (
loginguard_test.go:220-225, and the assertion message at:309)still says "no more than N verifications may run at once"; that is no longer what the test checks.
Stated for fairness: that mutation is still caught by
TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueingandTestLoginGuard_AcquireHonoursCancellation,so slot capacity is not wholly unguarded. What is now unguarded is a defect that admits an extra request
without changing the buffer size — a double grant, an early release, a preamble that hands out a slot it
did not take.
Acceptable: keep the rendezvous, but do not open the barrier at
concurrency. Open it once every worker'sacquire attempt has returned, success or refusal — e.g. a
sync.WaitGroupofworkers,Done()called immediately after
AcquireForTestreturns in every path,Wait()ed by one goroutine that thencloses
overlapped. Correct guard: the 10 refused workers return withinguardWait(2 s) and the barrieropens; broken guard: every admitted worker is inside at once and
highestreports the truth. Still nosleep, still deterministic, detection back to 100%. The existing 5 s
time.AfterFuncstays as thedeadlock guard.
MINOR 2 —
acquire's doc comment is no longer true of the codeinternal/middleware/loginguard.go:172-178: "It reports false ... or when the request was cancelledfirst." The preamble does not consult
ctx, so a request whose context is already cancelled is nowgranted a slot whenever one is free, where the old select refused it about half the time. The behaviour is
defensible (the caller owns cancellation, and
BeginPasswordVerification's own doc does not promise it),the sentence is not. Either drop the clause or add
ctx.Err()to the preamble.MINOR 3 — two wall-clock sites missing from the survey (a #186 deliverable)
internal/delivery/engine_test.go:1023— 50 ms sleep soExportScheduleRetry's goroutine attempts theoverflow send before the channel is drained. Same fail-green shape as the four sites the PR does name,
and belongs in that list; a slow host makes it under-observe, never red.
internal/delivery/engine_integration_test.go:1094— 2 s server-side sleep against a 1 s clienttimeout. (C), one-directional, harmless, but unclassified.
Spot-checked and correctly classified:
circuit_breaker_test.go:104(60 ms past a 50 ms cooldown,one-directional),
engine_integration_test.go:1262(channel pre-filled before the select),archive_sweeper_test.go:912(sweep cadence), andtarget_database_test.go:203-211— genuinely (A): astall over 2 s between the two writes flips
Reopens()from 1 to 2 and reds correct code. Agree it shouldbe filed. "Not yet observed" is exactly what was true of this issue's defect until run 232.
Nits
probeQueueCapreturning*bool(loginguard_test.go:585) is not the surrounding idiom; two namedbools, or the retained struct, read better.
the reverted-preamble mutation: pass 2. The test's own doc comment ("fails within a few passes") is
the accurate wording. Relatedly
racePasses's comment claims detection probability1 - 2^-N: a pass isonly a coin flip once the zero-duration timer has fired, so the real per-pass probability is below 1/2
and the stated bound is optimistic.
Verified and correct
chansend/chanrecvrather than from the harness: asender parks only when
qcount == dataqsizandrecvqis empty; a receive with a non-emptysendqgoes through
recv(), which copies the parked sender's value straight back into the buffer slot it justvacated, so
qcountstays atdataqsizfor as long as any sender is parked. The preamble'snon-blocking send therefore fails — on the lock-free
full(c)fast path, or under the lock — whenever awaiter is parked. The not-yet-parked case is a real loss of arrival order, but it was equally lost under
the blocking send, so the change does not introduce it.
the blocking select, so the 16-plus-2 arithmetic in the
passwordVerifyMaxWaiterscomment still boundsretained memory; moving the preamble ahead of queue admission removes the old case where up to 16
never-waiting requests could occupy waiter places and shed a legitimate one.
alone fails
TestLoginGuard_FreeSlotBeatsAnExpiredWait. Reverting queue admission to a blocking sendfails
TestLoginGuard_ShedsPastTheQueueCapin 5.03 s, so the 200 ms to 5 s change and the removal ofassert.Less(elapsed, 100ms)did not cost that test its regression.fillQueue's 1 s to 5 s change ispatience-only.
asserttorequiresweep re-run independently across every*_test.go: one sibling, the onefound (
internal/database/webhook_db_manager_test.go:342). Theassert.True(t, ok)sites insession_test.goare followed by value comparisons, not dereferences. No testify call is made from agoroutine, an
Eventuallycondition, or a spawned cleanup.next, one commit, title ends(closes #186), merges clean against currentnext,TODO.mduntouched, no attribution trailers or vendor references anywhere, inclusive terminology clean.
go test -c -racewas used for iteration only; every gate result cited is a maketarget or the Docker build. The honest control (228 green iterations on the unfixed binary) is correct
and is why the iteration count carries nothing — the by-construction argument and the forced-miss
demonstration do carry the production fix, and I agree with both.
Gate evidence (my own fresh clone, after
make bootstrap)b8940c0:success, run 235, "Successful in 2m46s".make check: exit 0. Lint in the pinned container,0 issues.after 47.6 s;go test13oklines,zero
(cached).docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .: exit 0.golangci-lint run47.6 s andmake fmt-checkboth ran uncached;[builder 9/11] RUN make testran53.8 s; zero
(cached)lines in the whole log; 11--- PASS: TestLoginGuard*lines. The 8CACHEDlayers are base-image resolves, the
go.mod/go.sumcopy andgo mod download.docker ps -aempty, no prune of any kind run.b8940c0424tof932a86e8dReworked, head
f932a86. One commit still; body updated.BLOCKING 1 — upper bound restored. The barrier no longer opens at the
concurrency-th holder. A secondsync.WaitGroupofworkersgates it:Done()on the refusal path immediately, and on the success path directly after the holder has recorded itself inhighest. Recording before signalling is one notch stronger than the proposed "Done()immediately after the call returns" — it makes it impossible for the barrier to open while an admitted worker is still on its way to being counted. The rendezvous and the 5 s deadlock guard are kept; no sleep and no wall-clock margin was reintroduced.concurrency*6mutation throughmake test,GOFLAGS=-count=1so nothing was served cached:f932a86f932a86,GOMAXPROCS1 / 2 / 4 / 812/12. The control confirms the harness reproduces the weakness rather than flattering the fix. Failure reports
expected: 2, actual: 12— the true maximum. Unmutated tree passes atGOMAXPROCS1, 2 and default, 2.01 s each, which is the refused workers' wait.MINOR 2 — doc, not behaviour. The doc is corrected and the behaviour kept: a free slot is granted without consulting
ctx,ctxis honoured only once the request has to wait. Argued in the body rather than changed silently.MINOR 3.
engine_test.go:1023added to the fail-green list (now five sites);engine_integration_test.go:1094added as (C).target_database_test.go:203-211now points at #190.Nits.
probeQueueCapreturns two bools — unnamed, sincenonamedreturnsrejects named bool results. "pass 1" corrected to pass 2 in both body and commit message.racePasses's1 - 2^-Nclaim dropped; the comment now claims only independence.Untouched, as reviewed: barge argument, waiter accounting, both mutation checks, the
assert-to-requiresweep, and the honest 228-iteration control (which stays disclosed, and was not re-run after the rework — disclosed as such).Gates on
f932a86.make checkexit 0 — its hostgo testlines were(cached)from the mutation loops, so the uncached evidence is the Docker gate:docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .exit 0, lint 47.5 s0 issues.,make fmt-checkran, tests 53.7 s over 13 packages, zero(cached)and zeroFAILlines, 8CACHEDlayers all base-image resolves and stage-2 packaging. CI run 239 success in 2m50s. Image removed,docker ps -aempty, no prune of any kind. Rebased onnextat9313b0f;TODO.mduntouched.Review: PASS
Independent re-review of
f932a86againstnext(9313b0f). Round-1 BLOCKING 1 is genuinelyfixed. I reproduced the central claim and probed it harder than the author did; no new defect,
no subtler recurrence of the removed-teeth failure.
The claim everything turns on — reproduced
concurrency*6mutation onnewLoginGuard'sslots, throughmake testwithGOFLAGS=-count=1:f932a86, defaultf932a86,GOMAXPROCS1 / 2 / 4 / 812/12, matching the author's table, and failing for the right reason —
loginguard_test.go:333,expected: 2, actual: 12. Twelve is every worker, i.e. the truemaximum, not a window-truncated observation.
Stronger probe, not run by the author:
concurrency+1, one extra slot with no buffer-sizetell — the exact defect class round 1 said had gone unguarded (double grant, early release,
preamble handing out a slot it did not take). Fails 6/6, 2.00 s each. The upper bound has
real teeth, not merely teeth against a gross mutation.
The author's ordering argument is correct on its merits, not a rationalisation. Under round
1's literal wording (
Done()immediately afterAcquireForTestreturns) the barrier can openwhile an admitted worker sits between its return and its
inside++; another holder is thenreleased from
<-overlapped, decrements, andhighestunder-reports. Recording beforesignalling closes that: every admitted worker's
inside++happens-before itsDone(), nodecrement precedes the barrier, so
highestequals the number admitted exactly — both bounds,deterministically. The reviewer was improved on.
No margin reintroduced, no red-direction flake
Unmutated
f932a86: 11/11 clean — 3 default, 4 atGOMAXPROCS1/2/4/8, 4 atGOMAXPROCS=2under 24 spinners — 2.00-2.01 s each, which is
guardWait, not a margin. Only duration left inthe test is the 5 s
time.AfterFunc; I checked the failure mode rather than the comment: if itfired early under a correct guard the holders release and late workers acquire, but with
concurrency = 2higheststill cannot exceed 2, so the assertion holds either way. It cannotred correct code and cannot green a broken guard.
Round-1 mutations re-verified against the changed test file
Preamble revert still fails
TestLoginGuard_FreeSlotBeatsAnExpiredWait3/3. Queue admissionback to a blocking send still fails
TestLoginGuard_ShedsPastTheQueueCapin 5.02 s (authorreported 5.03 s).
Also verified
acquire's doc now matches the code exactly, including the queue-full / timeout / cancelled-while-waiting split and the explicit ctx paragraph; the preamble is unchanged and still does not
consult
ctx. Survey is complete — I enumerated everytime.Sleep/time.After/Eventually/WithTimeoutin the suite independently and every one is classified; spot-checksof
engine_test.go:1023(fail-green: a short sleep makes the overflow send un-attempted, so itunder-observes),
engine_integration_test.go:1094(C, one-directional) andtarget_database_test.go:203-211(A, correctly filed as#190) are right.
nonamedreturnsis enabled(
default: all, not disabled), so the unnamed two-boolprobeQueueCapsignature is forced, asclaimed.
assert-to-requiresweep re-run: one sibling, the one fixed. CIsuccessonf932a86(run 239, 2m50s), basenext, one commit, title ends(closes #186), fast-forwardsonto
next,TODO.mduntouched, no attribution trailers or vendor references, inclusiveterminology clean. Every other figure in the body checks out against
f932a86.Non-blocking observations
the first failing pass at 0, 0, 1 across three runs (
passis 0-indexed); round 1 measured2. The body's own general claim, "fails within a few passes", is the accurate one and is
present. No change wanted — flagged so a third reviewer does not "correct" the number again.
rendezvousDeadlock(5 s) is safe partly because it exceeds the test'sguardWait(2 s); itscomment justifies it only against scheduling delay. Benign per the analysis above, but the
coupling is undocumented.
(this PR correctly says five, after adding
engine_test.go:1023) and listsinternal/delivery/retention_lifecycle_test.go:209, which lives ininternal/database/.Disclosures
b8940c0is unreachable (force-pushed; absent from git and from the API), so "behaviourunchanged from the previous head" rests on comparing the current preamble against round 1's own
quotation of it, not on a direct diff.
go test, used solely to isolate the mutation failure text thatmake test's-vinterleaving obscured. Every gate and every count above is a make target or the Docker build.
GOFLAGS=-count=1onmake testis inside the make-targets-only rule in my judgement: thetarget still runs and
script/test's-raceand-timeout 30sare preserved; only the testcache is defeated.
unfixed binary is still stated in the body.
Gate evidence (my own fresh
/tmpclone, aftermake bootstrap)docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .— exit 0.[lint 9/9] golangci-lint runran 50.9 s,0 issues.;[lint 7/9] make fmt-checkran;tests produced 13
oklines with real durations, zero(cached), zeroFAIL, and--- PASSfor all fourTestLoginGuard_*under review plusTestWebhookDBManager_MultipleWebhooks. The 8CACHEDlayers are the two pinned base-imageresolves (#7, #8) and six stage-2 packaging steps (#28-#33) — no lint or test layer among them.
Image removed,
docker ps -aempty, all load spinners confirmed killed, no prune of any kind.