test(guards): re-anchor nine assertions that could not fail - #450
Merged
Conversation
A survey instrumented all 82 test files and applied 35 source mutations, one full suite run each. Nine of the assertions it flagged as unable to fail sit on real guards; those nine are fixed here. Sixteen of the eighteen affected test files did not exist at the last release — this is debt the recent PR series produced, not debt it inherited. The failure mode is not the one #440 fixed. #440 fixed anchors that were *missing*, so a slice degenerated to "". These are anchors that are too far apart: the slice swallows a neighbouring function and something else in it satisfies the match. Six had a mutation that left the suite green, reproduced here before being fixed and re-applied afterwards: * canTransfer's "&& !tab.isTruncated" and handleDetach's whole ensureFullContent guard, each deletable — the transfer-a-truncated- buffer-then-auto-save-truncates-the-file path truncatedBufferGuard is named for. The two slices ran through canDetach and moveTabToWindow, whose character-identical guards satisfied the match. * appExit's unsaved-tab review, movable verbatim into a helper nobody calls: the slice spanned 969 lines for a 16-line function. * previewSanitize's "all {@html} sinks" list, which captured bare identifiers only, so a second raw sink spelled as a member expression was invisible. * the Destroyed handler's window_registry removal, deletable because the two tokens matched ~460 lines apart. * the recent-files storage key and cap, each compared with itself through an export that existed for that comparison alone. * every context-menu label in 26 languages, restated 26 times: t() falls back to English, so deleting the German entry was green. Two more could not fail by construction and are deleted rather than repaired: an assertion on a template literal declared in the same file, and a count of a pattern the line above had already proved absent. A third of that kind, the config's URI regexp compared with the constant it is assigned from, turned out to be reachable — rebuilding the regexp from the same source and flags fails it, which is the hand-copied-pattern regression the file exists for — so it stays. New: sourceTree.ts gains functionSource(text, name), which extracts a function from the AST by its own name instead of by naming whatever text follows it, and fails loudly on a rename or a duplicate. Four guards that were only ever stood in front of — canDetach, moveTabToWindow, and each of the two independent recent-list caps — are now asserted about directly, and appExit's confirmation must be acted on rather than merely present. Source changes are limited to dropping "export" from RECENT_FILES_KEY and RECENT_FILES_LIMIT, whose only importer was the test that compared them with themselves. No behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A survey instrumented all 82 test files (22,477 assertion invocations logged) and then applied 35 source mutations, one full suite run each. It found 33 assertion sites that cannot fail. Nine of them sit on real guards, and those nine are this PR. The rest are handled separately.
These tests are recent, and the series that is fixing them is the series that wrote them. 16 of the 18 affected test files did not exist at the last release. None of this is inherited.
The failure mode, and why #440 did not cover it
#440 fixed anchors that were missing —
source.slice(source.indexOf(absent))slices from-1, so the subject degenerated to the last character of the file and nodoesNotMatchcould fail against it. That is a hole in the helper, andsliceFrom/sliceBetween/offsetOfnow assert their anchors exist.This is the opposite end. Both anchors are present; they are too far apart. The slice runs past the function under test into a neighbour, and something in the neighbour satisfies the match. The helper cannot see it, because nothing is missing.
Delete the first one.
assert.match(detach, /ensureFullContent/)passes on the second.The six that reproduced
Every mutation below was applied to
masterbefore any fix, one fullnpm testper mutation. All six reproduced.1. Both partial-buffer transfer guards —
truncatedBufferGuard.test.tsThe file exists for one claim: a buffer holding only the first 50KB of a large file is never written back over that file. Two of its three source-level assertions did not hold it.
sliceBetween(viewer, 'canTransfer: (tabId)', 'transferPayload:')runs fromMarkdownViewer.svelte:504pastcanDetachat:508. Both predicates end in&& !tab.isTruncated, soassert.match(canTransfer, /isTruncated/)is satisfied by the neighbour. Same shape for the detach slice above.Both mutations are the transfer-a-truncated-buffer-then-auto-save-truncates-the-file path.
Fixed by
functionSource(viewer, name)— see below — one call per predicate and per mover. The two neighbours that were doing the covering,canDetachandmoveTabToWindow, were never asserted about at all; they are now, and the guard asserted on the movers is the whole four-line block rather than the bare identifier.2. The exit review can be deleted outright —
viewModeWithoutSaving.test.tssliceBetween(viewer, 'async function appExit()', 'async function toggleEdit')spans 969 lines.appExitis 16.Moving the unsaved-tab confirmation verbatim into a helper 20 lines below that nobody calls, and reducing
appExittoappWindow.close(), is green.Fixed by the file's own
pluck('appExit'), a comment- and string-aware brace counter that was already there for the transpiled harness. One assertion added: the answer has to be acted on, not merely asked for. Asking and then closing anyway loses the same buffer the dialog exists to protect, and/if \(\w+ !== 'discard'\) return;/leaves the identifier unpinned because which local holds the answer is not the contract.3. The
{@html}sink list saw only bare identifiers —previewSanitize.test.ts{@html tooltip.html}— already live atMarkdownViewer.svelte:3543— is invisible to that capture, so the list it compared was['sanitizedHtml']by construction. The assertion's stated purpose is that "adding a second injection point of the raw document is a failure instead of an unnoticed addition", and that is the one thing it could not do.Fixed by capturing the whole expression and comparing against an allowlist.
tooltip.htmlis on it with a reason rather than by accident: the footnote tooltip is a clone of a subtree ofmarkdownBody, i.e. of the DOM the sanitized sink already injected — the same bytes read back out of the document, not a second policy. Two assertions pin that direction (found viamarkdownBody?.querySelector, taken from.innerHTML, never fromrawContent), so a futuretooltip.htmlfed from the raw buffer is red.4. Closed windows are never deregistered —
windowOrganization.test.tswindow_registryis the struct field atwindow_runtime.rs:26.remove(window.label())is the watcher cleanup at:453. Nothing required them to be the same statement, and the registry removal in theDestroyedarm at:484is deletable without a test noticing — after which every closed window stays inlist_viewer_windowsas a move target that no longer exists.Fixed by slicing the
Destroyedarm and reading the removal inside it. The bareassert.match(runtime, /window_registry/)above it is dropped as subsumed.5. The recent-files key and cap were compared with themselves —
recentFilesMultiWindow.test.tsRECENT_FILES_KEYandRECENT_FILES_LIMITwere exported fromrecentFiles.tsonly for this test, and every assertion compared the module's value with itself.Renaming the key is green — and a rename silently orphans every existing user's list, because the literal appears nowhere else to contradict it. Changing the cap from 9 to 3 is green, because the boundedness test seeded
LIMIT + 5and then asserted the result wasLIMITlong.Fixed by writing both values out in the test. Both are on-disk format: a localStorage key and the length of a list the user can see are things a reader should be able to look up, and things that must not change without someone deciding to. The exports go with them — this is the only source change in the PR, and it is the shape that started the survey: an export that exists to be asserted against itself is not a contract with anyone.
The boundedness claim was alive but only half-alive: the source caps the list twice, in
promoteRecentFileand again inupdateStoredRecentFiles, and an open goes through both — so removing either alone was green and only removing both was red. Tightened: one new test drives each layer on its own.6.
t()falls back to English —editorContextMenuI18n.test.tst()returns the English string when a language lacks the key, so this can only fail when English lacks it — which the line above already proved. The 26-language loop restated line 65 twenty-six times.Deleting the German
menu.inlineCodeentry is green. German renders the English label, which is the user-visible regression.Fixed by reading
translations[lang]directly, without the fallback. These twelve labels are deliberately held to a stricter bar than the dictionary at large —i18nCoverage.test.tsreports per-locale gaps rather than failing on them, because 19 locales are missing >100 keys and nobody should have to translate a new English string 25 times before landing it. That trade does not apply to a fixed list of a dozen context-menu entries that all 26 languages already define, where an eleventh action means adding a row toFORMATTING_ACTIONSby hand.The three that no mutation can reach
Deleting is the right answer for two of them. The third turned out not to be in that class.
previewSanitize.test.ts:92assert.match(POC_STYLE, /^<style>/)—POC_STYLEis a template literal declared 59 lines above, in this file. Both sides are the test.editorContextMenuI18n.test.ts:161uiLanguage, one line afterassert.doesNotMatch(editor, /uiLanguage/). The line above must pass to reach it, and once it has the count is zero by construction.previewSanitize.test.ts:91MARKDOWN_SANITIZE_CONFIG.ALLOWED_URI_REGEXPagainst the constant it is assigned from one line earlier, measured as the same object reference..sourceand.flagsfails it. That is a copy of the URI pattern, which is precisely the regression this file exists for, so identity is the right comparison and the import stays. The identical assertion is duplicated atexportSanitize.test.ts:68.functionSourceSites 1 and 2 are the same defect: an anchor pair that names the neighbour rather than the subject.
sourceTree.tsgained AST helpers in #440 for exactly this reason, so the fix is a third one rather than another hand-rolled anchor pair:It reads a function bound to
namein any of the spellingssrc/uses —function f() {},const f = () => {},{ f: () => {} }, class methods — andassert.equalon the count rather than "the first one" means a rename or a second definition fails here, loudly, instead of quietly changing what the caller is asserting about. Pinned insourceTree.test.tsagainst string literals, so refactoring the app cannot make that test fail and changing the helper is the only thing that can.Mutation table
Green measured against
masterbefore the fix; red measured after, on the rebased branch. Each mutation applied tosrc/orsrc-tauri/, the suite run, the source restored.canTransfer: drop&& !tab.isTruncatedcanTransfer must refuse a tab whose buffer is still the preview slicehandleDetach: delete the four-lineensureFullContentguardhandleDetach must complete the buffer before the payload is built, and stop when it cannotcanDetach: drop&& !tab.isTruncated(new coverage)canDetach must refuse a tab whose buffer is still the preview slicemoveTabToWindow: delete the four-line guard (new coverage)moveTabToWindow must complete the buffer before the payload is built, and stop when it cannotappExit: exit review moved verbatim into an uncalled helperThe input did not match /tabManager\.tabs\.some\(\(t\) => t\.isDirty …appExit: ask, then close regardless of the answer (new coverage)an answer other than 'discard' stops the exit{@html unsafe.rawHtml}beside the sanitized sinkunexpected {@html} sink — what the preview injects is the shared sanitizer outputand taken from the DOM the shared sanitizer already filteredwindow_runtime.rs: drop the registry removal from theDestroyedarma destroyed window must be deregistered, or it is still offered as a move targetRECENT_FILES_KEY:'recent-files'→'markpad-recent'Expected values to be strictly deep-equalRECENT_FILES_LIMIT:9→3promoteRecentFile caps what it returnspromoteRecentFileonlypromoteRecentFile caps what it returnsupdateStoredRecentFilesonlyand the stored list is capped even when the mutation is notthe list stays bounded no matter how the entries arrivedi18n.ts: delete the Germanmenu.inlineCodeentrymenu.inlineCode is translated for de; it currently falls back to the English labelpreviewSanitize.test.ts:92sanitize.ts: config rebuilds the URI regexp from.source+.flagsValues have same structure but are not reference-equal— the site was reachable, so it is kepteditorContextMenuI18n.test.ts:161:160. Deleted.One correction to the survey's own report: the mutation for site 2 as literally worded — delete the review block from
appExitand close immediately — is red, caught bywindowStateRestore.test.ts:107, which slicesappExitwith the tight'\n\t}'end anchor. The dead-assertion claim holds for the harder variant in the table, where the block stays inside the 969-line span.What did not change
No
src/orsrc-tauri/behaviour. The only source edit isexport const→conston the two recent-files constants (case 5), which is the one exception the audit allows and only because the assertion that needed them is gone.No existing assertion was weakened. Two were dropped as strictly subsumed by a stronger one alongside them (
windowOrganization's bare/window_registry/,editorContextMenuI18n'suiLanguagecount) and two were dropped as unfalsifiable. Everything else is the same claim on a tighter subject, plus six guards that had never been asserted about at all.Verification
npm test594/594,npm run check0 errors 0 warnings,npm run build,cargo test150/150 — all measured after rebasing onto5be7c18(#448), not before.🤖 Generated with Claude Code