v0.2.9
Typecheck the tests, and fix the four contracts that stopped them
include: ["src"] kept the whole test tree out of tsc. The reason was real:
tests/selftest augments TestHandle with a macro, and in one program that
declare module reached the source too — src/runtime/suite.ts, which builds a
handle before any macro is registered, was reported as missing a method nothing
had put there yet. So the tests were not typechecked at all, which is the worse
half of that trade.
tsconfig.test.json compiles them in their own program, and the augmentation
is optional now, which is what it always was: a macro exists on a handle once
Test.macro has registered it, and not before.
That surfaced four public contracts that were wrong, each of them only visible
from a test:
AnyFn = (...args: unknown[]) => unknownas a CONSTRAINT rejected every typed
function, because parameters are compared contravariantly —vi.fn((a: number, b: number) => a + b)did not compile.AnyFunction, withnever[], is the
constraint that admits them all;AnyFnstays as the value a bareSpy
stands for, andvi.fn()defaults to it so a spy with no implementation is
still callable.TestFnandTestCleanupreturnedvoid | Promise<void>, so
() => log.push('x')was a type error for returning whatpushreturns. The
runtime awaits the value and drops it; the types say so now.SuiteHookwas a union of "returns nothing" and "returns its undo", so a hook
that returned anything else was rejected for doing what the runtime already
ignores. It returnsunknown, andisSuiteHookCleanupis the guard that
picks the undo out — the same shapehookListbeside it already used.- The reporter tests built
FileResult,TestResult,SerializedErrorand
WorkerErrorMessagefixtures that the runtime never produces: missingname,
missingsuites/totals, and atypefield the message has no such thing
as. They were asserting about shapes nothing sends.
The rest is noUncheckedIndexedAccess on test fixtures, read through a
defined() helper rather than a ! — a fixture that stops producing the
element now fails on the line that reads it.
pnpm typecheck:tests runs in CI beside the build typecheck.
Turn on noUncheckedIndexedAccess
It was not missing here — it was explicitly false, in sixteen of the
seventeen tsconfigs. eon alone had it on, which is why nobody had seen
what it finds.
It stays a named deviation from upstream: @adonisjs/tsconfig sets
strictNullChecks and noImplicitAny but not this one. We keep it because
turning it on is what caught an as asserting a possibly-absent regex
group was a known value — the exact shape the flag exists to find. Doing
better than upstream is kept and written down, not reverted to parity.
Every site is restated rather than silenced: no !, no cast, no ?? 0
standing in for a branch that cannot happen. A reversed copy read by
value where an index walked a callback list backwards, the winner of a
scan kept as the value it found rather than its position, destructuring
where a length check was doing the proving, and an explicit break where a
loop condition already bounds the read.
Let a run say how long a finished worker may take to exit
exitGraceMs was a constant, and the wait it sizes is only "milliseconds"
until it is not: a large V8 coverage dump on a loaded CI runner takes
longer, and the pool then killed a worker that was about to finish
writing. The only way out was to stop asking for coverage. It is a
PoolConfig / RunConfig option now, still defaulting to 2 000.
The three Math.floor(cfg.x as number) reads that surrounded it are one
helper: narrowing an optional property does not survive to the next read
of it, which is why each ended in a cast asserting back what the check
above had just established.
Treat an empty expect() label as no label
expect(total, row.label) with an empty label is the ordinary way to reach
this, and the check was label === undefined, so the empty string went
through and opened the failure on a bare ": expected 3 to be 4".
The type is now checked alongside the value. expect() is public API a
JavaScript caller reaches too, and a symbol passed there throws inside the
template rather than labelling anything, turning a failed assertion into a
TypeError that says nothing about what actually broke.
Stop paying the exit grace on a file that never produced a result
Two paths out of the native pool were still wrong once the grace existed.
A worker can report a failure and then refuse to exit — the file blew up and
left something running. The grace is there to protect a result; there is none
on that path, so waiting it out only delayed the error the run already had,
under a message announcing the file had "finished". The error is now the only
diagnosis, and the worker is killed at once.
The stdout drain task was never stopped. It reads to EOF, and EOF only comes
when every holder of the write end is gone, so a worker we had to kill that
had itself spawned something leaves the task waiting on a file already
reported. It is now held by a guard that aborts it on every path out of
run_one_file, including the timeout that returns early.
Release 0.2.9
Let expect() take the message argument it documents compatibility with
expect(value, message) is Vitest's signature, and helix bills itself as
Vitest-compatible; Playwright has it too. Only the value was accepted, so
a failure inside a loop or a table-driven case said what broke but never
which row.
The label prefixes the matcher's own text rather than replacing it —
trading "expected 3 to be 4" for "the total after tax" would keep the
intent and lose the diagnosis. It threads through .not, .resolves and
.rejects, and through the promise-shape errors those raise.
Stop the native pool waiting forever on a worker that will not exit
A test that leaves a server, a timer or a connection running keeps its
worker alive after the result is framed. run_one_file then sat on an
unbounded child.wait(), so the run hung after its last file with
nothing printed at all — a passing suite that reads as a crash, and in
CI a whole budget burnt with no diagnostic to act on.
The TS pool has guarded this since 0.2.6, but the cutover to the native
orchestrator made that pool the exception path: coverage, --list-pinned
and a pluggable reporter instance reach it, an ordinary run does not. So
the guard was there and almost nobody ran it. Reproduced on a bare
helix test — 120s and still going, while the same file under
--coverage finished in 3s with the reason on stderr.
The wait is now bounded by a 2s exit grace, mirroring EXIT_GRACE_MS, and
the message is the TS pool's verbatim so the two paths cannot be told
apart. The result is kept: the test passed, only its cleanup did not.
Move the NAPI bindings to napi 3
The Rust needed no change; the toolchain did. napi-derive 3 writes one type-def
file per crate into NAPI_TYPE_DEF_TMP_FOLDER and panics outright when it sees
the old single-file TYPE_DEF_TMP_PATH — that variable is how it detects an
out-of-date toolchain, so the failure reads as "upgrade @napi-rs/cli" even
though the generator here is our own.
It also emits a function as a bare function name(...) where 2 emitted the
signature alone, so concatenating the name onto it produced
function xfunction x(...). The generator handles all three shapes now.
napi-build stays at 2 — there is no 3 on crates.io.
Verified by what the migration could break rather than by it compiling: the
generated src/native/generated.ts comes out byte-identical to the napi 2 one,
and the native binary is rebuilt and exercised by the JS suite.
Update the Rust dependencies within their ranges
Everything the existing semver ranges allow, so no manifest changes and no API
surface moves. fmt, strict clippy, tests and advisories all pass.
Clear strict clippy and the advisory list
Nothing in the root gate ran clippy or cargo-deny against a package workspace —
cargo test --all covered the ROOT workspace only, which is a handful of
crates. Five packages were failing strict clippy at the same time and nothing
said so.
Isolate a reporter that fails asynchronously
EventHandler returns void, and TypeScript ACCEPTS an async function for a
void return: a reporter written as async (payload) => … type-checks and its
rejection walked straight past the try/catch, which only ever saw a synchronous
throw. In a test runner that means one reporter awaiting something that fails
takes down the run it is reporting on.
The handler is still called synchronously — reporters observe in order and the
runtime relies on that — only a returned promise is followed. Fourteen call
sites, one place to fix.
Changes since v0.2.8.