chore(check): type-check scripts/, and fix an assertion that could not fail - #432
Merged
Conversation
…t fail
`npm run check` runs `svelte-check --tsconfig ./tsconfig.json`, which inherits
`.svelte-kit/tsconfig.json`. SvelteKit generates that file's `include` list from
a fixed set of directories — `src/`, `test/`, `tests/` — and `scripts/` is not
among them. All 89 test files were therefore invisible to the type checker.
Measured: a deliberate `const x: number = "not a number"` in
scripts/foldKeys.test.ts still produced `438 FILES 0 ERRORS`. After this change
the same injection reports `ERROR "scripts/foldKeys.test.ts" 31:7 "Type 'string'
is not assignable to type 'number'."`.
TypeScript does not merge `include` across `extends`, so the root tsconfig now
carries a copy of the generated list plus `scripts/`. `@types/node` is added —
the suite imports `node:test`, `node:fs` and `node:assert/strict`, none of which
were typed. `allowImportingTsExtensions` is enabled because the tests import
their subjects with an explicit `.ts` extension, which is what `tsx` resolves;
the project is `noEmit`, so there is no output path for the extension to be
wrong in.
That surfaced 38 pre-existing errors across 13 files, all fixed rather than
suppressed: two `makeTab()` fixtures declared `Tab` while omitting the required
`collapsedHeaders`; monaco-editor's ESM internals had no declarations and are
now declared with the shapes the test uses (removing an `as any`); a `Proxy`
was cast in and back out for no reason; a `@ts-expect-error` in vite.config.js
became stale once `process` was typed.
Second, unrelated to the above but found by the same reading:
scripts/issue261EditorPdf.test.ts sliced the viewer from `@media print {`, a
marker MarkdownViewer.svelte does not contain. `indexOf` returns -1,
`slice(-1)` yields the file's last character, and the `assert.doesNotMatch`
against it could not fail. 37b3693 removed that print block from the component
and added this assertion in the same commit, so it was degenerate from birth.
The claim is still real — a component-scoped print rule would outrank the
global one — so it is re-anchored on the whole component instead of a slice.
A guarded `slice()` helper already existed, copied verbatim into two test
files. It moves to scripts/sourceTree.ts as `sliceBetween`, joined by
`sliceFrom` for the tail-of-file shape that produced both known instances of
this bug.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
pushed a commit
that referenced
this pull request
Aug 3, 2026
…bounds
Two holes in the test tooling, both demonstrated before they were fixed.
1. `enclosingFunctionName` matched `\n\t…function NAME(` and nothing else, so a
call inside `const f = async () => {}` was attributed to whichever classic
`function` preceded it. Planting
const renderRawBypass = async (raw: string) => {
return (await invoke('render_markdown', { content: raw })) as string;
};
in MarkdownViewer.svelte left all 524 tests green. It is now answered from
the real AST via `svelte/compiler`, which the suite already depends on, so
every declaration form in `src/` — 451 classic, 74 arrow, class methods,
object shorthand — and every form nobody has written yet is covered by
construction. Same for `$effect` bodies: the regex needed a tab-indented
`});` to terminate, so a one-line effect merged into its successor and its
ungated `editor` read was masked by the next effect's `editorReady`.
2. 71 `indexOf`-derived slice and ordering bounds had no `-1` guard, the defect
#432 fixed in two places. `sliceFrom`/`sliceBetween` cover the slices;
`offsetOf` is added for the ordering comparisons, where `a < b` is also
satisfied by `a === -1`. 110 helper call sites replace 155 raw `indexOf`
bounds; 194 anchors were each corrupted in turn and 193 produced a failure
naming the missing anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 3, 2026
…bounds (#440) Two holes in the test tooling, both demonstrated before they were fixed. 1. `enclosingFunctionName` matched `\n\t…function NAME(` and nothing else, so a call inside `const f = async () => {}` was attributed to whichever classic `function` preceded it. Planting const renderRawBypass = async (raw: string) => { return (await invoke('render_markdown', { content: raw })) as string; }; in MarkdownViewer.svelte left all 524 tests green. It is now answered from the real AST via `svelte/compiler`, which the suite already depends on, so every declaration form in `src/` — 451 classic, 74 arrow, class methods, object shorthand — and every form nobody has written yet is covered by construction. Same for `$effect` bodies: the regex needed a tab-indented `});` to terminate, so a one-line effect merged into its successor and its ungated `editor` read was masked by the next effect's `editorReady`. 2. 71 `indexOf`-derived slice and ordering bounds had no `-1` guard, the defect #432 fixed in two places. `sliceFrom`/`sliceBetween` cover the slices; `offsetOf` is added for the ordering comparisons, where `a < b` is also satisfied by `a === -1`. 110 helper call sites replace 155 raw `indexOf` bounds; 194 anchors were each corrupted in turn and 193 produced a failure naming the missing anchor. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> 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.
Two defects in the test tooling. They are independent; they share a PR because both were found by reading the same file.
1. The test files were never type-checked
npm run checkissvelte-check --tsconfig ./tsconfig.json, and that config extends.svelte-kit/tsconfig.json. SvelteKit generates that file'sincludefrom a fixed list of directories:scripts/is not on it, and never was. All 89 test files sat outside the program.Measured, both ways
A deliberate
const injectedTypeError: number = "not a number";appended toscripts/foldKeys.test.ts:COMPLETED 439 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMSERROR "scripts/foldKeys.test.ts" 31:7 "Type 'string' is not assignable to type 'number'."The practical cost of the gap: renaming an exported function gave a contributor a green
npm run checkand dozens of red tests with no tooling pointing at the cause. In one measured rename (loadMarkdown→openDocument), 26 of 36 failures wereTypeError: … is not a function.Why the fix is in the root tsconfig
.svelte-kit/tsconfig.jsonis regenerated bysvelte-kit syncon everycheck, so editing it is not an option. SvelteKit exposes no configuration for extra include directories. The supported path is the one the roottsconfig.jsonalready documents in its own comment:So the root config now carries a copy of the generated list, rewritten one level up, plus
scripts/.test/andtests/are kept even though neither directory exists: dropping them would silently re-create this exact bug for whoever adds one. The comment says to re-sync on a SvelteKit upgrade.Two supporting changes:
@types/node(devDependency). Every test file importsnode:test,node:fsandnode:assert/strict. None of them were typed — the first run reported 343 errors, ~300 of which wereCannot find module 'node:test'.allowImportingTsExtensions. 18 imports across 7 test files name their subject with an explicit.tsextension (await import('../src/lib/utils/markdown.ts')), which is whattsx— the loadernpm testuses — resolves. The flag requiresnoEmit, which this project already sets, so there is no output path for the extension to be wrong in. The alternative, rewriting 18 working imports to.jsto satisfy a config default, is churn with runtime risk on the side that actually runs.The 38 errors it surfaced
Across 13 files. None suppressed — no
any, no@ts-ignore, no exclusions.An import path can only end with a '.ts' extension…allowImportingTsExtensions, aboveConversion of type 'TransferableTab' to 'Record<string, unknown>' may be a mistake{ ...snapshotTab(tab) }is an anonymous object type, which does get an implicit index signature — no cast at allCould not find a declaration file for module 'monaco-editor/esm/vs/…'scripts/monaco-internals.d.tsdeclares the three internalspasteUrlContext.test.tsdrives, with the shapes it uses. This removes an existingas anyonMonarchTokenizer'{ id: string; … }' is not assignable to type 'Tab'makeTab()intabTransfer.test.tsandlossyDecodeSaveGuard.test.tsboth declare: Tabwhile omitting the requiredcollapsedHeaders: Set<string>Argument of type 'FakeElement' is not assignable to 'Element'asElement()helper, matching theas unknown as ParentNodethe same file already used forFakeRootConversion of type 'Record<string, unknown>' to 'SettingsStore'…Proxytarget was cast toRecord<string, unknown>going in and back toSettingsStorecoming out. Dropping both casts types it correctlyUnused '@ts-expect-error' directive(vite.config.js)// @ts-expect-error process is a nodejs globalbecame stale the moment@types/nodelandedTwo of these are real defects rather than annotation noise. The
collapsedHeadersomissions mean both fixtures have been claiming to beTabwhile not being one — precisely the class of drift this change exists to catch.2. An assertion that could not fail
scripts/issue261EditorPdf.test.ts:76:src/lib/MarkdownViewer.sveltecontains no@mediarule of any kind. Verified directly:slice(-1)takes the last character, not zero characters. Soassert.doesNotMatch(viewerPrintStyles, …)at line 91 was testing a one-character string against a multi-line pattern. It could not fail.It was degenerate from birth. 37b3693 (#359) removed this block from
MarkdownViewer.svelte:and added the assertion guarding against its return — in the same commit. Deleting the subject is what made the anchor miss.
What the claim was, and whether it still holds
It holds.
src/styles.csssets.markdown-body { padding: 0.75in !important }for print. A component-scoped rule inMarkdownViewer.sveltecompiles to.markdown-body.svelte-xxxx, which outranks it on specificity even with both!important. Re-introducing that block would silently drop the page margins from PDF export again.So the assertion is re-anchored, not deleted: same regex intent, searched over the whole component instead of over a slice that does not exist.
Nothing new is asserted — the pattern is the old one with the slice's anchor folded into it.
Falsifiability, checked by re-introducing the regression
The block was re-inserted into
MarkdownViewer.sveltetwo ways and the file restored afterwards:The second row is the point. The old form depended on a literal
\tin the anchor; any re-introduction that indented differently, or that lived outside a@media print {line matching that exact prefix, sailed through. And with the marker absent entirely, as today, it could not fail at all.The sweep
Every
.slice()/.substring()/.substr()inscripts/whose bounds come from.indexOf()/.lastIndexOf()/.search(), found by walking each file's TypeScript AST rather than grepping (several span multiple lines):72 sites across 23 files.
Degenerate-vs-fragile was then measured rather than eyeballed:
String.prototype.slice/substring/substrwere wrapped to record any call receiving-1as a bound, and the full suite was run.The one degenerate site is
issue261EditorPdf.test.ts:76—.slice(-1)against a 149,066-character subject. The only other-1bounds observed anywhere in the suite were literalslice(0, -1)/slice(1, -1)quote-trimming on 2–4 character strings.A separate instrumented run confirmed all 72 sites execute during
npm test, so "not degenerate" means "the anchor was found", not "never ran".Helper, not per-site guards
The identical guarded helper was already copied verbatim into two files (
findCollapsedMatches.test.ts:7-13,lossyDecodeSaveGuard.test.ts:26-33). It moves intoscripts/sourceTree.ts— the module whose own header records that three copies ofwalk()were collapsed into it for the same reason — and gains the shape it was missing:sliceBetween(source, start, end)— the lifted helper, both duplicates deleted and routed through it (12 call sites).sliceFrom(source, start)— new, for the tail-of-file shape. This is the shape that produced both known instances of this bug, and neither existing copy covered it.Migrated in this PR: the two de-duplicated files plus every site in
issue261EditorPdf.test.ts— 17 anchors, verified individually (below).The remaining 66 sites in 22 files are reported, not migrated. Justification: they are fragile, not broken; the helper's two shapes do not cover all of them (several search the end anchor from a bespoke offset); and rewriting 66 call sites across 22 files inside a PR that also changes
tsconfig.jsonwould make the diff unreviewable. Several of those files are also being deleted onchore/drop-tests-that-only-detect-renames, so migrating them now would conflict for no benefit. The helper is in the shared module and has 3 users on day one, so the next one is a one-line import rather than a fourth copy.Every migrated anchor, broken and confirmed loud
Each of the 17 anchors was replaced with
'NO SUCH ANCHOR ZZZ'in turn, that test file run, and the file restored:Each failed with
expected to find "NO SUCH ANCHOR ZZZ"rather than an assertion about the wrong subject.Not covered
checkedReadMigration7,externalChangeReload8,windowStateRestore7,truncatedBufferGuard6,viewModeWithoutSaving5,reopenDirtyDocument/tabTransferHandoff/windowClosePerTab4 each,editorPdfExport/settingsPersistence/tabTransfer3 each,printFindHighlight2, and 1 each in 10 more. All find their anchors today.scrollSyncInput.test.tsis still half-guarded. It got a top-levelassert.notEqual(index, -1)on its start anchor; its end anchor (editor.indexOf('\n\t$effect(() => {', syncStart + 1)) is unguarded, and a miss there would slice to the end of the file rather than to the end of the effect. Left alone — it is one of the 66, and fixing it in isolation would repeat the pattern this section is about.build-test-bundle.mjsis inside the newincludeglob but has no type errors to report; nothing was done to it.AGENTS.mdis untouched. It still says "Frontend: No test framework currently configured", which was already stale before this PR and is reported in AGENTS.md: three stale statements send contributors the wrong way #385. Not this PR's file to correct.cargo testwas not run.Verification
npm test562 passed, 0 failed ·npm run check644 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS·npm run buildclean · type-error injection caught (644 FILES 1 ERRORS) and removed · 17/17 broken anchors fail loudly · working tree clean,MarkdownViewer.svelteandfoldKeys.test.tsrestored byte-identical after every experiment.🤖 Generated with Claude Code