Eight tests that pass for the wrong reason (and three self-referential assertions) #98
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
[claude 2026-08-23] Found by mutation-auditing comments across six packages (branch
comment-audit). These tests pass when the behaviour their name promises is removed. Each was verified by applying the mutation and observing a PASS.TestRedeemWritesAuditRowAtomicallytx.Commit()TestBumpCredGenerationAuditsAtomicallyTestOpenTCPBridgesBytesst.SetDeadline— the set or the clearTestShutdownFallsBackToSigtermWhenVfkitRefusesnilwithout signalling anythingNoErrorTestUploadUserCAStoresCanonicalLinereq.PublicKeyverbatimTestSnapshotHubPerTenantPayloadssubscribe()snapshot, never the fan-out it namesTestApplyReportSkipsUnchangedLeasesnetTracker.writeThroughnetIPTrack.lastdirectly, bypassing the dedupTestServeNotEnrolledStateDirthat is not the store's, so the lock never guards what it protectsTestRemoveHostRefusesWhileVMsRemainif falseon the in-transaction count checkassert.Error— the FK constraint errors anyway, so the alarm rings for the wrong reasonSelf-referential assertions
Three constants are asserted in terms of themselves, so they pin the shape and not the number:
maxKeyCommentLen—len(comment) <= maxKeyCommentLenpasses at 128 or at 4096.enrollRefillEveryandmaxLimiterEntriesinratelimit_test.go— same pattern.Why this matters beyond the tests
Two of these (
…Atomically) are the only evidence that a security action cannot happen unrecorded. The names promise more than the assertions deliver, which is worse than no test: a reader greps for the guarantee, finds a test named for it, and stops looking.Migrated from git-collab issue
d30269d26462ba7edbd6ce46cb1783707327c346.Original author: a73x dev@a73x.sh.
Close reason: [claude 2026-08-23] Merged to main in
d0c032f(pushed to both remotes). Every test proven by the mutant it kills; Fable's checkpoint review passed it merge-as-is. make ci green, boot gate PASS (full smoke incl. UDP, cold_start=20s).[claude 2026-08-23] Design review of all twelve findings. Every mutation was re-applied on a clean tree (main,
c694896— identical tocomment-auditmodulo comments), the named test run, the result observed, and the tree restored (git diff --quietconfirmed after each). Repair designs only — no repairs implemented.Verdict table
TestRedeemWritesAuditRowAtomicallytx.Commit()(plains.db.Exec): PASSTestBumpCredGenerationAuditsAtomicallyTestSnapshotHubPerTenantPayloadsTestSnapshotHub*test pass. Tenant isolation on the recompute path is pinned by nothing.TestServeNotEnrolledt.TempDir()A,Config.StateDirist.TempDir()B); additionally, deletingstatelock.Acquirefromserveentirely: PASSTestRemoveHostRefusesWhileVMsRemainif false &&on the drain count: PASS — the refusal comes from thevms.host_id REFERENCES hostsFK (foreign_keys(1)in the DSN), not from the check the test namesTestShutdownFallsBackToSigtermWhenVfkitRefusessigtermbody replaced withreturn nil: PASSTestUploadUserCAStoresCanonicalLineline := req.PublicKey(verbatim store): PASS — the test posts a line that is already canonicalTestApplyReportSkipsUnchangedLeasesif t.last[hostID] == value { return nil }deleted fromnetTracker.writeThrough: PASS — the test inspectsnetIPTrack.last, which holds the same value with or without the skipTestOpenTCPBridgesBytesSetDeadlinedeleted in turn (the 10s set, then the clear): PASS both times. The name is honest — bytes-bridging is genuinely proven; the lie is the doc comment's deadline-clear claim.maxKeyCommentLenself-referenceenrollRefillEveryself-referencemaxLimiterEntriesself-referenceRepairs, ordered by risk (worst outcome first)
1.
TestSnapshotHubPerTenantPayloads— cross-tenant leak, nothing catches itRisk: a regression in the recompute fan-out sends tenant A's hosts/VMs/metrics to tenant B's SSE connection. Multi-tenant confidentiality break, and the whole hub suite stays green (verified). Worst outcome on the list.
Repair: stronger assertion in the existing test (extend, don't restructure — the initial-snapshot half is correct and should stay). The test already builds tenant-echoing payloads; it just never drives a recompute:
newNotifier()handle instead of discarding it.buildversioned (payload-for-<tenant>-v<n>), so the second delivery is distinguishable from the first.notif.notify(), then assertchAreceivespayload-for-alpha-v2andchBpayload-for-beta-v2—assert.Equalon exact bytes already forbids receiving the other tenant's payload, which is precisely what kills the broadcast mutant I ran.No seam needed;
notifieris already injectable.2–3.
TestRedeemWritesAuditRowAtomically/TestBumpCredGenerationAuditsAtomically— security actions unrecordedRisk: the only evidence that an enrollment or a credential revoke cannot commit without its audit row. If the write drifted out of the transaction, hosts could enroll unrecorded — invisible until an incident needs the log.
The seam question. Proving "inside the transaction" needs a fault injected between the audit write and the commit, and the store has no Go-level seam for that (
*sql.DBis concrete, no exec hook). But it doesn't need one:store_test.gois an internal test package with white-box access tos.db, and SQLite gives fault injection for free:I validated this end-to-end with a throwaway test (deleted after): with the trigger installed,
RedeemEnrollmentTokenerrors, zero host rows exist, and the token is not consumed — a second redeem afterDROP TRIGGERsucceeds. Same shape forBumpCredGeneration: errors, andcred_generationstays 1. I then re-applied the move-after-commit mutation with this test in place: FAIL ("no host may exist without its audit row") — the design kills the mutant. No harness change required.Repair: new tests alongside, existing ones renamed. The existing tests are good success-path tests (row content, and the valuable
raw token must never reach the audit logassertion) — keep them asTestRedeemWritesAuditRow/TestBumpCredGenerationAudits. The atomicity claim moves to new fault-injection tests that own the name:TestRedeemRefusesToEnrollUnaudited/TestBumpCredGenerationRefusesUnaudited(or keep the…Atomicallynames on the new tests). Each: install trigger → call → require error → assert no host / generation unchanged → drop trigger → assert the operation still works (token unconsumed / bump lands), proving rollback was complete, not just refusal.4.
TestServeNotEnrolled— two agents on one identityRisk:
serverunning without the state lock (or acquiring it after touching state) allows a second agent on the same StateDir: two hypervisor managers driving the same VM records and disks. DeletingAcquireentirely passes today's suite.Repair: restructure + one new test.
state.OpenandConfig.StateDir— the test currently constructs a pairing production (RunCLI) can never produce.TestServeRefusesASecondAgentOnTheSameState:statelock.Acquire(dir)in the test first, thenserve(st, Config{StateDir: dir})on that same dir must error with "already running" — and not "not enrolled", which pins the ordering promise inserve's own comment (lock before anything else).assert.ErrorContains(t, err, "already running")kills both the delete-Acquire mutant and any reorder past the identity check.statelockhas its own tests; what's unpinned is thatservecalls it, on the right dir, first.5.
TestRemoveHostRefusesWhileVMsRemain— alarm rings for the wrong reasonRisk: latent rather than live — the FK currently refuses too. But the FK is one schema edit away from not being the same guarantee (an
ON DELETE CASCADEadded for another reason would turn "refuse" into "silently delete the host and its VM rows"), and the curated error ("still has N VM(s); not drained") is part of the API surface the console shows an operator.Repair: stronger assertion, two sites.
assert.ErrorContains(t, err, "not drained")in this test and in both refusal branches ofTestRemoveHostDrainSequence(same weakness, found in passing — it uses bareassert.Errortwice). One-line changes; kills theif falsemutant because the FK's error text ("FOREIGN KEY constraint failed") doesn't match.6.
TestShutdownFallsBackToSigtermWhenVfkitRefuses— guest never stopsRisk: an unhealthy vfkit means Shutdown quietly does nothing; the VM runs until Destroy. Operational, single-host blast radius.
Repair: stronger assertion via the existing seam.
p.signalis already an injectable func field used by five sibling tests. Split the test's two claims:pidfile.Ownedaccepts a missing boot-ID line, verified), assert exactly oneSIGTERMto that pid after the 500 response.return syscall.ESRCH, assertNoError— this keeps the current test's actual claim under an honest name (…TreatsAGoneProcessAsStopped). Today's pid2147483647reaches the realsyscall.Killand gets ESRCH, so the tolerance path is exercised — but only NoError is observed, which is why the no-op mutant survives.7.
TestUploadUserCAStoresCanonicalLine— CA lookup by line breaksRisk: verbatim storage makes
TenantForUserCA(exact line match) miss whenever the presented form differs from the uploaded form — trailing newline, comment, whitespace. Confusing auth failures and duplicate CA rows rather than a break-in.Repair: stronger input in the existing test. Post a deliberately non-canonical line (trailing
"\n", auser@hostcomment, leading spaces) and assertTenantForUserCA(sshca.AuthorizedKeyLine(pub))— lookup by the canonical form — resolves. Storing verbatim then fails the lookup, killing the mutant. Optionally also assert the listedPubkeyequals the canonical line exactly.8.
TestOpenTCPBridgesBytes— deadline lifecycle unpinnedRisk: clear-missing: every TCP/console session dies 10s after handshake (loud, user-visible, quickly found). Set-missing: a wedged agent pins the WS handler goroutine forever (quiet leak). Neither catastrophic, both worth pinning.
Repair: harness change + one new test; existing test keeps its (honest) name. A 10s constant can't be unit-tested; promote
consoleHandshakeTimeoutto a field onService(defaulted inNew, overridden by tests — same pattern asp.signal). Then:TestOpenStreamAbandonsAWedgedAgent: timeout 100ms, fake agent accepts the stream and never replies;OpenTCPmust return an error promptly. Kills the set-deleted mutant.TestOpenTCPBridgesBytes: timeout ~200ms, keep the existing 50ms... make it a post-handshake sleep longer than the injected timeout (e.g. 300ms), then the existing write/read round-trip. Kills the clear-deleted mutant — today's 50ms-vs-10s gap is what makes both mutants survivable. Fix the doc comment to say the clear is proven by outliving the (injected) handshake deadline.9.
TestApplyReportSkipsUnchangedLeases— extra writes per tickRisk: perf only — one redundant UPDATE per bridged VM per tick on SQLite's single connection. Real (it's the P1 perf lane's territory) but the least dangerous regression here.
Repair: new unit test carries the claim; existing test renamed. The dedup lives in
netTracker.writeThrough, whosewriteclosure is the natural counting seam — andtracker_test.goalready testsstatusTracker.writeThroughexactly this way, but nevernetTracker. AddTestNetTrackerWritesOnlyOnChange: counting closure; same value twice → one write; new value → second write; failing write → error surfaced, cache NOT updated, retry writes again (the not-cached-on-failure branch is also unpinned today). The applyReport-level test can't see writes without instrumenting the store; rename it to what it proves —TestApplyReportTracksTheReportedLease(that applyReport routes leases through the cache at all) — and let the tracker test own the dedup name.The self-referential constants — pattern verdict and repo sweep
Verdict: these are two different diseases, and the repair differs.
(a) Behaviour pinned, value floating —
maxKeyCommentLenis this: deleting the truncation FAILS the test (verified); only 128→4096 sails through. The parameterized assertion is correct test construction — the boundary behaviour is what the test owns. What's missing is a value pin where the value itself is load-bearing.(b) Pure tautology — the assertion copies the constant to both sides and proves only that an assignment happened.
assert.Equal(t, httpServerReadHeaderTimeout, srv.ReadHeaderTimeout)is this: it passes at 0s with a message claiming "the Slowloris defense is on" (verified below).Repair: keep the behaviour tests parameterized (they're right); for constants whose value is the security/operational content, add a per-package literal-pinning guard test —
assert.Equal(t, 30*time.Second, enrollRefillEvery)with the rationale in the message — so drifting a value is a conscious two-file edit. This is the house pattern already ("things can't pin their own defaults" → docs-match guard tests). For tautologies, the literal pin replaces the tautology rather than joining it. Do not pin taste-only values (that's noise); the list below marks which is which. Renaming-only is the honest fix nowhere in this trio — the limiter values are genuinely load-bearing (30ms refill ≈ no limiter; 10-entry cap ≈ prune-thrash on 11 IPs).Sweep results (assertions mentioning a prod constant on both sides, all packages)
Scripted sweep: extracted every constant from non-test files, flagged
assert./require.lines in same-package tests referencing them. ~40 raw hits, triaged:Tier 1 — tautologies or floating values where the value is the point (fix like the trio):
internal/server/boot/httpserver_test.go:18-19—assert.Equal(t, httpServerReadHeaderTimeout, srv.ReadHeaderTimeout)(and Idle). Verified: both constants set to 0 → package passes. The v0.0.6 Slowloris hardening is pinned by nothing. Pin literals20s/120s.internal/server/boot/headers_test.go:45,49—assert.Equal(t, hstsMaxAge, …Header.Get("Strict-Transport-Security")). Verified:max-age=0→ package passes. The header-presence half is real; the value needs a literal (or parse-and-assertmax-age ≥ 1y).internal/server/store/exposures_test.go:30,45-52,164-183— six assertions all relative toMinAllocatedHostPort. Allocation ordering is well tested; the base value floats — at80the allocator would hand out well-known ports and everything passes. One literal pin (30000) covers all six.internal/agent/vfkit/vfkit_test.go:142— asserts the composed socket path< maxSocketPath, butmaxSocketPath(103) stands in for the OS'ssun_pathlimit, which is external truth: bump the constant and the test follows while real binds fail. Assert against literal 103 with the sun_path rationale.internal/server/api/ratelimit_test.go— the filed pair (enrollRefillEvery,maxLimiterEntries), plusenrollBurstrides the same pattern inapi_test.go:928andratelimit_test.goloops. One guard test pinning all three limiter values.Tier 2 — behaviour pinned, value floats; pin only if the value is judged load-bearing:
internal/mcpserver/sshrun_test.go:226—outputCap(1 MiB): truncation behaviour pinned exactly likemaxKeyCommentLen; cap value floats (memory bound per exec — probably worth a pin).internal/server/api/abandoned_vm_test.go:50,83—abandonedVMReapGrace(15m) used as fake-clock input; grace behaviour pinned, window floats (1ms would reap freshly-created VMs).internal/agent/netenv/named_test.go:490—HasPrefix(NetTapName(…), netTapPrefix): prefix value floats; IFNAMSIZ budget depends on its length. Borderline.internal/agent/seed/seed_test.go:372-392— chrony drop-in paths asserted via the constants; the paths are guest-side contracts. Borderline.internal/smoke/mcp_test.go:433—delegationTTLis smoke's own request TTL asserted back from the cert it mints; low risk, smoke-local.internal/server/api/injectedkey_test.go:47— the filedmaxKeyCommentLen; behaviour already pinned, value pin optional.Retired from suspicion (correct constructions, no action):
internal/agent/syncclient/client_test.go:111—OnlineWindow ≥ 3×DefaultTickIntervalis a cross-package relationship assertion — this is the model to imitate, not a bug.internal/smoke/mcp_contract_test.go:53—remoteToolCountis smoke's independent copy of mcpserver's tool count (the deliberate tool-list tie); cross-package pin, good.internal/server/sshgate/gate_test.go:570—tenantExtis minted and checked in the same package; internal consistency is the contract.transport(DefaultMaxFrame,ALPN),pidfile/statelock(mode,lockName) — constants as call inputs, not expectations.Net: the trio is the visible edge of a repo-wide habit. Tier 1 is five sites (boot ×2, store exposures, vfkit socket-path, api limiter) where a value with a security or OS-truth rationale can drift to a useless setting with a green suite — the boot pair verified, the rest read. Suggest the guard-test convention lands as its own small issue so the pins arrive as one sweep, not nine drive-bys.
[claude 2026-08-23] Claiming the eight lying tests (design comment items 1-9 minus the self-referential constants trio, which are issue
7c8b320fand belong to another agent). Working branch: lying-tests. Discipline: every repaired test is proven by applying its mutant, observing the FAIL, and restoring the tree.[claude 2026-08-23] Done on branch
lying-tests(4 commits off main290ffad, not merged, not pushed). make ci green.Eight lying tests repaired; every one proven by applying its mutant to the production code, watching the named test FAIL with its intended message, and restoring (git diff clean on each source file, re-verified after rebasing onto main).
if false && n > 0now surfaces "FOREIGN KEY constraint failed" and fails.Two departures from the design comment:
e11ca29landed TestSnapshotHubRecomputeFansEachTenantItsOwnBytes while this branch was in flight; it kills the same broadcast mutant from its own test. Dropped my duplicate.Out of scope, worth filing: internal/server/web coverage is 89.2% against a floor of 95 in any checkout where 'make web' has not been run (dist/ is gitignored, only .gitkeep tracked). 'make ci' does not depend on 'web', so a fresh clone or worktree fails the coverage gate with no code change. Either make cover depend on web, or drop the floor to what an unbuilt dist yields.