Skip to content

Ship the CLI as TypeScript and build out the test suite - #142

Merged
kilianc merged 2 commits into
mainfrom
claude/test-suite-status-f84d26
Aug 11, 2026
Merged

Ship the CLI as TypeScript and build out the test suite#142
kilianc merged 2 commits into
mainfrom
claude/test-suite-status-f84d26

Conversation

@kilianc

@kilianc kilianc commented Aug 11, 2026

Copy link
Copy Markdown
Owner

The CLI was the last JavaScript left in the project. It is TypeScript now, and it ships that way — no build step, and a test suite that actually covers it.

Shipping TypeScript without compiling

bin points straight at the .ts sources and Node strips the types as it loads them, so what you read in cli/ is exactly what npm installs. tsc never emits; it only type-checks.

That costs a Node floor, and the boundary is sharp:

Node node a.ts
22.0.0 fails
22.12.0 fails
22.17.1 fails
22.18.0 works

So engines is >=22.18. This is a breaking change — hence the bump to 0.3.0. The floor sitting at patch level is deliberate rather than a typo, and is spelled out in the README, the CI matrix comment and tools/Dockerfile.

erasableSyntaxOnly keeps the source to constructs that can genuinely be erased — no enums, namespaces or constructor parameter properties, all of which need real codegen and would fail at a user's runtime rather than in CI.

Compiling was considered and rejected: once Node 20 is out of scope either way, the only remaining gap is Node 22 installs more than a year behind on patches, which is not worth a permanent build step and the class of "works from source, broken from the tarball" bugs that cannot exist today.

The bins are now thin shims

Not tidying. The old entry points were top-level scripts that a test could only exercise by spawning a process, and V8 coverage cannot see into one — they would have scored 0% however well tested they were. The logic moved into cli/lib, so argv parsing, the UDP client, the stream store and the server wiring are each importable and testable on their own.

Three layers of tests

Layer Count Covers
test/unit 212 CLI modules, webapp libraries, all five Preact components via jsdom
test/integration 14 The real bins as processes, over a real UDP socket
test/e2e 15 The built webapp in Chromium, fed by the real client bin

The run fails below 90% line, branch and function coverage. It currently sits at 99.8 / 95.8 / 97.0.

Node cannot load .tsx at any version — stripping is erasure and JSX needs a real transform — so tools/tsx-hook.ts runs components through esbuild on the way into the test runner, reusing the JSX settings from tools/build.ts so a component cannot behave differently under test than in the browser. Source maps are inline, so coverage still reports against the original .tsx lines.

Chromium is heavy enough to deserve its own image: tools/Dockerfile.e2e, behind make test-e2e and a separate CI job. The Playwright version is pinned exactly in both places, because each release expects a specific browser revision.

Two fixes that fell out of writing this

Scroll-to-pause was broken in production. StreamView attached its wheel listener in an effect keyed on [onPause]. It ran once against a null ref while no stream was selected, and never re-ran once the scroller appeared — so scrolling to pause did nothing for anyone who picked a stream from the empty state. It only worked if a stream was already selected at load, via deep link or the remembered stream, which is why it went unnoticed. Guarded now at both the unit and browser level; the regression test was verified by reverting the fix and watching it fail.

The suite hung about one run in five. Closing the server while a long-poll was in flight left engine.io's grace timer armed, holding the process open. Tracked down by dumping async-hook creation stacks rather than papering over it with --test-force-exit; shutdown now waits for the disconnect and sweeps what survives.

Notes for the reviewer

  • npm run dist must precede npm test — two tests assert the built webapp is served. CI and the make targets handle it; a bare npm test on a fresh clone will not.
  • app/src/main.tsx is the one coverage exclusion: an 11-line bootstrap, exercised for real by the e2e suite, which loads the actual bundle.
  • Not published to npm yet.

Verified before pushing: typecheck clean, 238 unit/integration passing, 15 e2e passing across repeated runs, runtime Docker image builds and serves, and make dev still works.

🤖 Generated with Claude Code

The CLI was the last JavaScript left in the project. It is TypeScript now,
and it ships that way: `bin` points straight at the .ts sources and Node
strips the types as it loads them, so what you read in cli/ is exactly what
npm installs -- nothing is compiled, bundled or minified on the way. tsc
never emits, it only type-checks.

That costs a Node floor. Type stripping is unflagged from 22.18 on and
genuinely absent before it -- 22.17 cannot load a .ts file at all -- so
`engines` is >=22.18 and this is a breaking change, hence 0.3.0. The floor
being at patch level is deliberate rather than a typo, and is spelled out in
the README, the CI matrix and tools/Dockerfile. `erasableSyntaxOnly` keeps
the source to constructs that can actually be erased: no enums, namespaces
or constructor parameter properties, all of which need real codegen and
would fail at a user's runtime rather than in CI.

The bins are now thin shims over cli/lib. That is not tidying: the old entry
points were top-level scripts that a test could only exercise by spawning a
process, and V8 coverage cannot see into one, so they would have scored zero
however well tested they were. The logic moved into modules that a test can
import, and the argv parsing, the UDP client, the stream store and the
server wiring are each testable on their own.

Tests come in three layers. test/unit covers the CLI modules, the webapp's
libraries and all five Preact components against jsdom; test/integration
runs the real bins as processes over a real UDP socket, which is the only
thing that proves the published .ts actually executes; test/e2e drives the
built webapp in Chromium, fed by the real client bin. 238 node:test cases
and 15 Playwright ones, and the run now fails below 90% line, branch and
function coverage. It currently sits at 99.8/95.8/97.0.

Node cannot load .tsx at any version -- stripping is an erasure pass and JSX
needs a real transform -- so tools/tsx-hook.ts runs the components through
esbuild on the way into the test runner, reusing the JSX settings from
tools/build.ts so a component cannot behave differently under test than in
the browser. Source maps are inline, so coverage still reports against the
original .tsx lines. Chromium is heavy enough to deserve its own image, so
it lives in tools/Dockerfile.e2e behind `make test-e2e` and a separate CI
job, and the Playwright version is pinned exactly in both places because
each release expects a specific browser revision.

Two fixes fell out of writing this. StreamView attached its wheel-to-pause
listener in an effect keyed on [onPause], which ran once against a null ref
while no stream was selected and never again once the scroller appeared --
so scrolling to pause did nothing at all for anyone who picked a stream from
the empty state, and worked only if one was already selected at load. And
closing the server while a long-poll was in flight left engine.io's grace
timer armed, which held the process open and made the suite hang roughly one
run in five; shutdown now waits for the disconnect and sweeps what survives.

One operational note: two tests assert the built webapp is served, so
`npm run dist` has to precede `npm test`. CI and the make targets do it.
#141 landed the query language and field extraction while this branch was
converting the CLI to TypeScript and building the test suite. The two touch
the same files, so this is a real merge rather than a fast-forward.

The conflicts themselves were only import blocks -- main added imports to the
same lines this branch had rewritten to explicit .ts specifiers -- and main's
side won on content, with the extensions reapplied. Three things needed more
than that.

test/query.test.js moved to test/unit/query.test.ts. Left where it was it
would have matched none of the new globs and silently stopped running. It also
bundled the module into a data: URL with esbuild, a workaround for node:test
not being able to import TypeScript; that is no longer true, and the direct
import matters beyond tidiness, because coverage cannot attribute a data: URL
back to app/src/lib/query.ts -- the bundled version scored the module at zero
however much it exercised.

The ~800 lines main added are now covered: query.ts, json.ts, highlight.ts and
SearchBar.tsx, plus format.ts's renderFields and defaultExpanded and prefs'
jsonView and per-stream fields. The existing tests moved to the new APIs --
buildFilter is gone, StreamView no longer filters, and Line and Prefs both
grew fields. 402 tests, and the 90% gate holds on the merged tree rather than
on a shrunken surface.

Two fixes carried over. The scroll-to-pause bug was reintroduced by main's
rewrite of StreamView -- the wheel listener keyed on [onPause] again -- so the
fix is reapplied, and the e2e case for it now reaches the stream through the
sidebar, which is the path that was broken. And highlight() threw on a needle
whose regexp already carried `g`, since it appends `g` unconditionally;
unreachable through parseQuery, which strips stateful flags, but it is an
exported helper and the fix is one call.

The e2e suite follows the feature: field filtering, the syntax card, marking
hits, field extraction, and payload collapsing, 21 cases in all.
@kilianc
kilianc merged commit 0789066 into main Aug 11, 2026
3 checks passed
@kilianc
kilianc deleted the claude/test-suite-status-f84d26 branch August 22, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant