test: cover bar Config and the bar/panel helper duplicates #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/t7-tests"
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?
Two gaps closed in
test.luau: theConfigmatrix forbar.luau, and a driftguard over the five helpers the two entries are forced to duplicate.
test.luauis the only file touched; nothing under
timew/changed.How
bar.luauis drivenThe luau CLI gives each required module its own global table, so a
noctaliaassigned in
test.luauis invisible insidebar.luau—Config.getwould callthe host's real
getConfigand get nothing.No generated file was needed.
getfenvis present in the luau CLI, andgetfenv(bar.formatElapsed)returns that module's global table, which iswritable. Stubbing
noctalia,barWidgetanduiin it makes the module's ownglobals resolve to the stubs.
bar.luauis unchanged and keeps no test hook itwould not otherwise have.
That one stub also makes
onClick,onConfigChangedandupdatecallable,which is what puts bar's private helpers in reach — see the drift section.
1. Config matrix
Everything asserted is something a hand-edited
~/.config/noctalia/config.tomlcan produce and the settings UI cannot:
"20"->20(numeric string coerces)99->28,1->10(clamped to the manifest bounds)"big"->17,""->17,nil->17(default, nevernil)taggiven a number42->"42"(stringified before it can reachshQuote)taggiven""ornil->"focus"; a usable stored value winsgetdoes not re-read the host (getConfigcall count stays 1)Config.invalidate()lets a new value throughonConfigChanged()— the real production path — drops the cache and repaintsThe last one matters because passing
getConfiga computed key switches offnoctalia plugins lint's static cross-check, so thaterror()is the only thingleft that catches a
plugin.tomlkey missed inDEFAULTS.2. bar/panel drift
requirebetween plugin entries is impossible at API 3, sotrim,unquote,shQuote,formatElapsedandelapsedexist twice.Reachability, and how each pair is compared:
formatElapsedpanel.formatElapsedandpanel.Fmt.clockshQuoteonClick, which interpolates the tag into the/bin/sh -cstringTimew.tagArgtrimonClick's callback, which trims a failing command's stdout on its way to the toastParse.trimunquotesync's callback (reached byupdate();ticksSinceSyncstarts atRESYNC_TICKS), which publishestimew.tagsParse.unquoteelapsedrender, which publishestimew.elapsedTextThe one gap: panel's
elapsedis a local, absent from panel's export table,and reaching it would mean stubbing the panel's whole render tree. So that pair
is not compared copy-against-copy. Instead bar's copy is pinned against the rule
the duplication exists to preserve — the clock is
os.time() - startEpoch, neveraccumulated — by freezing
os.timein the module env and asserting the publishedtimew.elapsedTextequalspanel.Fmt.clock(1062 - 1000). A tick counter fails it.Also not observable on either side:
elapsed'sif not active then return 0branch, since both callers only invoke it when active.
formatElapsedinputs are0, 1, 59, 60, 61, 3599, 3600, 3601, 93600, -1, -5, 62.7, 3599.9, -0.5— the fractional and negative cases are new here; theexisting loop at lines 35-45 already pins the literal values.
Liveness
Every new assert group was broken one at a time, the failure captured, and the
file restored byte-identically (
cmpclean against a pre-mutation copy in alleight cases).
Breaking an assert only proves the assert runs. The drift group's actual job is
catching a source edit, so
timew/bar.luauwas mutated five times as well, onceper duplicated helper, and restored each time (
git statusclean afterwards):(The last one trips the pre-existing literal loop first, since it sits earlier in
the file; the drift assert at 386 catches it too.)
Gate
No runtime verification and no
timewinvocation: every new assert runs oncanned stdout and stubbed host calls.
Verdict: REQUEST_CHANGES
Two of the new blocks claim coverage they do not have, and both fixes are small and I have run them. Everything else holds up: the
getfenvtrick is the right call, the shQuote expectations are correct POSIX rather than frozen implementation output, and I could not find a bar/panel helper drift that slips past the comparisons.Findings
1. (P1)
test.luau:300— thegetConfigstub ignores its key, so the entire Config matrix passes with a hardcoded keyThe stub takes no parameter and hands the same
hostRawback for every key. Nothing in the matrix can therefore tellnoctalia.getConfig(key)apart fromnoctalia.getConfig("tag"). I changed exactly that one line intimew/bar.luau:Green. In the shell that mutant is
icon_sizereading the tag string:tonumber("focus")is nil, so the glyph pins to 17 forever and the settings entry silently stops working — which is the exact class of bug this matrix is here to catch. Key the stub:That needs
configWithto seed one key (hostRaw = { [key] = value }at :331) and the four bare assignments to become tables ({ icon_size = 24 }at :351,{ icon_size = 12 }at :354,{ icon_size = 21 }at :358,{ tag = tag }at :393). I applied precisely that and re-ran both ways:2. (P2)
test.luau:410—barEnv.update()is dead here;syncCbis the trim loop's leftover, notupdate()'s resyncThe comment says
update()is the only global that reachessyncand thatticksSinceSyncstarts atRESYNC_TICKSso the first tick resyncs. By the time line 418 runs, that is no longer true: eachlastCb(...)in the trim loop above invokesonClick's callback, which callssync(), which setsticksSinceSync = 0.update()then bumps it to 1,1 >= 10is false, andsync()is never called.syncCbbinds to therunAsyncthe trim loop's lastsync()left inlastCb.Two experiments, both green when they should not be:
So bar's resync branch is claimed as the driver and is not exercised at all. It is also live fragility, not just a wrong comment — one innocuous extra call before line 418 rebinds
syncCbtoonClick's callback and the failure blames the wrong helper:unquoteis fine there. Capture the callback bar registers instead of guessing whichrunAsyncwas last. Change the watch stub at :311:declare
local barPokebeside :296, and replace :409-419's comment and capture with:Verified: clean source
ok; the extra-onClickreordering that broke the original nowok; and the drift detection stays live — bar'sunquotemade identity fails at :427, bar'selapsedreturning a constant fails at :435.3. (P2)
test.luau:373—trimandunquoteexist three times, not twice, and panel's private pair is unguardedThe block's premise is "five helpers exist twice over ... Nothing but this block stops one copy being edited and the other left behind."
trimandunquotealso exist aspanel.luau:252andpanel.luau:257, separate fromParse.trim/Parse.unquote, with eleven live call sites between them. Only theParsepair is compared:shQuoteandformatElapseddo not have this hole —Timew.tagArgreaches panel's onlyshQuote, andpanel.formatElapsedis the private copy. I am not asking for more asserts:panel.luau:47-50says the private copies die with the call-site move, and guarding code that is scheduled for deletion is the wrong trade. Just stop the comment overstating what the block covers:4. (P3)
test.luau:392— the onClick loops silently depend onactive == falseonClickbranches on bar's module state, and nothing above establishes or asserts it. Both loops assume thetimew startbranch. Appending a probe that makes bar tracking first:Correct today, but the message blames
shQuotefor somethingshQuotedid not do. One assert naming the precondition, right before the loop, keeps a future failure pointed at the right thing:Verified
Worktree
/.worktrees/t7-tests, clean before and after every experiment. Sources restored withgit checkout --or from pre-mutation copies andcmp-checked byte-identical each time.Baseline:
Source mutations that the suite caught (good):
Source mutations that the suite missed (findings 1-3):
Assert liveness, broken directly rather than via the source:
Worktree afterwards:
Answers
3. The
elapsedsubstitute — keep it, and do not exportelapsedfrom panel.The substitute is not testing the test. Its expected value is built from two inputs — 1062 from the frozen clock, 1000 from the canned stdout — never from the code under test, and it fires on a real regression: bar's
elapsedreturning a constant trips :430. It earns its place.Exporting
elapsedin a later task would be worse, not better. Panel'sactiveandstartEpochare module-level locals (panel.luau:213,215), sogetfenvcannot reach them the way it reaches panel's globals. I addedelapsed = elapsedto panel's export table and printed the result mid-test:A copy-against-copy comparison then has two outcomes and both are bad: compare while bar is tracking and you get
62 == 0, a permanent failure; compare while both are idle and you get0 == 0, a tautology that only exercises theif not activebranch you correctly identified as dead. Exporting one word does not close this gap — driving panel's sync does, and that costs the whole render tree. The invariant pin is the right trade. I would drop the "reaching it would mean stubbing the panel's whole render tree" framing from the PR body only insofar as it implies exporting would suffice; it would not.4. The shQuote expectations — all correct, and the new loop freezes nothing.
The new drift loop compares
lastCmdagainst a computedpanel.Timew.tagArg(tag). There is no literal in it, so the "drift test freezes a wrong value" hazard does not apply to this patch at all. The literals live in the pre-existingtagArgasserts at :278-284. I checked all six against a real/bin/shround-trip rather than against the implementation:Each is the canonical POSIX close-escape-reopen form and each round-trips to the original byte for byte. The newline case at :282 is right too — a literal LF inside single quotes is data. So:
focusright,deep workright,it'sright,$(id) \id`right (both$and backtick are inert inside single quotes),x'; rm -rf /; #` right.5. Weight —
shownis justified, the branch call is right, one assert is thin.shownis not decorative. I checked Luau's%qrather than assuming:%qreally does emit a backslash and a real newline, so without the gsub the"\t\nx\n\t"case splits its own failure message across three lines. Two call sites, one of which genuinely needs it — keep it.The
elapsedif not activebranch: you were right to add no assert. It is unobservable on both sides, and in fact it is dead in production too — bar's only caller isrender()underif active, and panel's ispanel.luau:1181behind the same guard. Asserting it would have been coverage of a branch that cannot happen. Not a finding, just confirmation you called it correctly.The thin one is
hostReads == 1at :355. It is live and not trivially satisfiable — the explicithostReads = 0andConfig.invalidate()immediately above make it immune to earlier warming, and deleting the cache outright fires it. But theConfig.get(...) == 24half of the same assert already fails on that mutation on its own, so the counter is a second lock on the same door and pins a performance property rather than a contract. Cheap; not worth removing, worth knowing.Nothing else asserts an implementation detail.
published["timew.startEpoch"] == 1000is a contract — the panel reads that key. The formatElapsed drift loop repeats six of its fourteen inputs from the pre-existing loop at :35-45; that is six numbers in a table literal and not worth trimming.6. Diff hygiene — clean. Only
test.luau, 159 insertions and zero deletions, so no existing assert is reordered or touched. No new committed file;*.md,secretspec.tomlandforge.share absent as required. All twenty-five new assert messages name an invariant rather than restating the expression — "an emptied text field is no opinion, not a literal empty tag" and "the published clock is os.time() minus the start epoch, never a tick count" are the pattern throughout, and I found no exception.On the
getfenvtrick itself: sound, keep it. It is the only way to reach bar's private helpers without adding a test hook to production code, and it adds nothing tobar.luau. The risk is not the mechanism, it is stub width — and finding 1 is exactly that risk landing: a stub wide enough that a real regression walks through it. Narrow that one stub and the technique is stable enough to keep.All four findings applied in
518da00. I re-ran every mutation you named, before and after, in the worktree.1. (P1) The
getConfigstub ignored its keyApplied exactly your suggestion:
getConfig = function(key) ... return hostRaw[key] end,hostRaw = { [key] = value }inconfigWith, and the four bare assignments converted to tables ({ icon_size = 24 },{ icon_size = 12 },{ icon_size = 21 },{ tag = tag }).Your mutant,
bar.luau:59->local raw = noctalia.getConfig("tag"):The matrix now fails on the first row, as it should:
hostRaw["icon_size"]isnilwhen the key is hardcoded totag.2. (P2)
barEnv.update()was dead;syncCbwas the trim loop's leftoverApplied your suggestion: the watch stub captures the registered callback into a new
local barPoke, and the block isbarEnv.update()thenbarPoke(). I added one line beyond your patch, because otherwise a missing registration fails asattempt to call a nil value, which names no invariant:The two experiments that were green when they should not have been, plus the reordering hazard:
Your
if false thenmutant on bar's resync branch staysok, which is the intended outcome of this fix rather than a remaining hole: the block no longer claims that branch as its driver, and the comment now says so. Everything the block is actually for is still live:3. (P2) The drift comment overstated its coverage
Comment replaced with your wording verbatim; no asserts added, for the reason you gave (
panel.luau:47-50).4. (P3) The onClick loops depended on
active == falsesilentlyApplied your suggestion verbatim. Proof it points at the right thing now — driving bar into the tracking state immediately above the loop:
Previously that same probe blamed
shQuoteat:448.Follow-up, not this PR
Nothing here needed a new export from
timew/panel.luauortimew/bar.luau; I agree with your answer onelapsedand did not export it. No production file is touched by this PR.Gate
Still 159 insertions and zero deletions against
main: no pre-existing assert was weakened, reordered or reformatted.