Skip to content

👷 ci: derive test:node and test:bun scope - #196

Merged
taras merged 5 commits into
mainfrom
ci/derive-runtime-test-scope
Jul 28, 2026
Merged

👷 ci: derive test:node and test:bun scope#196
taras merged 5 commits into
mainfrom
ci/derive-runtime-test-scope

Conversation

@taras

@taras taras commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Closes #144.

Why

test:node and test:bun named individual test files. That selection dates
from 9db5eae and has only ever been path-renamed since, so the Node and Bun
jobs covered less of the repository with every test added — 19 of 105 files,
and 241 of the assertions Deno ran. Nothing signalled whether a new test was
runtime-portable, so the gap widened silently.

What changes

Scope is derived the way deno task test derives its own (#140/#142). A new
*.test.ts runs under all three runtimes by default; staying out of one is a
deliberate, reviewable act.

Before: 19 files under Node and Bun, hardcoded.
After: 1131 tests across 103 files, 0 failures, under each.

The measured corpus

I measured every discovered file individually under both runtimes rather than
trusting #144's counts, which were taken at 81 files.

discovered ran unchanged
Node (tsx --tsconfig tsconfig.node.json --test) 104 101
Bun (bun test) 104 101

The same three failed on both:

File Cause Disposition
scripts/tests/build-npm.test.ts calls Deno.execPath(); subject is the dnt build excluded
scripts/tests/cli-npm-bin.test.ts calls Deno.readTextFileSync; builds the npm package with dnt excluded
scripts/tests/bump-version.test.ts scripts/lib/bump-version.ts imported npm:zod@^4 fixed

bumpManifests is pure manifest manipulation and its five tests are portable —
only the import specifier was Deno-only. zod is already a root dependency and
packages/cli/deno.json already maps it, so that is a one-line fix, not a
boundary. Excluding it would have recorded an artifact as if it were a runtime
limitation, and the integrity test could never have told the two apart later.

Two measurement artifacts I ruled out, recorded so nobody re-derives them as
defects: a first sweep failed all seven CLI-launching suites under Node because
my harness invoked ./node_modules/.bin/tsx directly, leaving tsx off PATH
for the child processes those suites spawn; and worker-lifecycle hit a 300 s
cap under three-way parallelism while passing 11/11 in isolation.

How it works

deno.json workspace → listWorkspacePaths → each member's tests/ → *.test.ts
                                                                      ↓
                                          minus this runtime's exclusions
                                                                      ↓
                                    tsx --test <files>  /  bun test <files>
  • scripts/lib/test-files.tslistTestFiles(root) reuses the existing
    listWorkspacePaths, so discovery follows the same workspace contract as
    build-npm, bump-version and gen-publish-workflow. Walking concrete
    tests directories rather than the repository root keeps nested worktrees,
    site, and scripts/tests/fixtures out without naming them. Returns sorted
    repository-relative POSIX paths.
  • scripts/runtime-test-exclusions.ts — the one place a test opts out.
    Each entry names a discovered file, says why, and links the issue.
  • scripts/runtime-tests.ts — an Effection entrypoint over
    scripts/lib/runtime-tests.ts, matching the bump-version split. That is not
    decoration: it lets the exit-code mapping be asserted without importing a
    module that calls main() and exits.

Both the launcher and the integrity test call listTestFiles. Discovery logic
exists once.

Two Bun-specific changes come with the widened scope, both pre-existing gaps the
19-file list never reached:

  • The root declares every @executablemd/* workspace as a devDependency.
    Bun's isolated install links only what the root manifest depends on, and
    generated .xmd-eval/<uuid>.ts modules execute from the repository root — so
    they need those packages declared there to resolve. ♻️ refactor: own cross-runtime test infrastructure as @executablemd/test-support #188 had already added
    @executablemd/test-support for the same reason; this completes the set.
  • The Bun runner passes --timeout=300000. Bun caps each test at 5s by
    default where Deno and Node impose no comparable limit. Suites that spawn a
    CLI or a worker carry their own 15–180s timeboxes, and the cap preempted them;
    the ceiling lets those govern.

What must stay true

  • test:node and test:bun enumerate no test files.
  • Every exclusion names a file discovery finds, appears once per runtime, and
    carries a reason and an issue — checked by
    scripts/tests/runtime-exclusions.test.ts.
  • A failing child propagates its code; a signal-killed child exits 1, so a
    crashed runner cannot pass CI.
  • Execution and typechecking are separate axes. The manifest governs what runs;
    tsconfig.node.json lists only the portable runtime-test scripts.

What the integrity test cannot do, stated in the manifest's module comment:
it detects invalid, renamed, deleted or malformed exclusions, but nothing
re-runs excluded files, so it cannot show that one has become portable.
Removing a stale entry stays a manual act.

How to verify it

Four repository checks with the CI-pinned Deno 2.9.1: lint 0 errors,
typecheck clean, 148 passed / 0 failed, JSR Success. Plus:

pnpm install --frozen-lockfile
pnpm exec tsc --project tsconfig.node.json --noEmit    # 58 new files, 0 errors
pnpm test:node        # 1131 tests, 0 fail, 103 files
bun install
bun run test:bun      # 1131 pass, 0 fail, 103 files
deno task gen:publish-workflow                          # no drift

Both runs print each exclusion with its reason and issue before starting, so
the gap appears in the CI log rather than reading as full coverage.

Scope

Included

  • Derived discovery, the manifest, the launcher, and the integrity tests.
  • The bump-version specifier fix, which is what keeps the exclusion list to
    genuine boundaries.
  • tsconfig.node.json: packages/acp, packages/cli, packages/test-agent
    plus their paths, and the five portable runtime-test scripts. Not
    scripts/**/*.ts — that would pull in the deliberately malformed fixtures and
    the Deno-only build tools.

Intentionally unchanged

Risks and limitations

  • packages/test-agent/tests/worker-lifecycle.test.ts uses real sleep() calls
    for cancellation timing. It passes in both derived runs here, but it timed out
    under artificial parallelism during measurement, so it is the first place to
    look if the widened jobs flake on slower CI hardware. That would be a timing
    problem to fix, not grounds for an exclusion.
  • The Node launcher needs tsx on PATH; pnpm supplies it, and ✅ test: launch the CLI through the host runtime #186's
    launcher already carries the same requirement.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results — rechecked at
    7a365b9: 8 modified files and 5 new, 13 in total; 1131 tests across 103
    files under Node and Bun; 2 exclusions.

Closes #144.

Both scripts named individual test files. That list dates from 9db5eae and has
only ever been path-renamed since, so the Node and Bun jobs covered less of the
repository with every test added: 19 of 105 files, 241 of the assertions Deno
ran.

Scope is now derived the way `deno task test` derives its own. Discovery walks
each workspace member's `tests` directory through the existing
`listWorkspacePaths`, so a new `*.test.ts` joins all three runtime jobs without
editing anything. Walking concrete `tests` directories rather than the
repository root keeps nested worktrees, `site`, and the deliberately malformed
`scripts/tests/fixtures` out without naming them.

Measured the whole corpus individually under both runtimes rather than trusting
the counts in #144, which were taken at 81 files. Of 104 discovered, 101 ran
unchanged under each. Of the three that did not:

- build-npm and cli-npm-bin test the dnt toolchain and call Deno.execPath() /
  Deno.readTextFileSync. They are a real boundary, and are excluded.
- bump-version failed only because scripts/lib/bump-version.ts imported
  npm:zod@^4. That is an import specifier, not a runtime boundary, so it is
  fixed rather than excluded: zod is already a root dependency and
  packages/cli/deno.json already maps it.

Two exclusions remain. Each names a discovered file, says why, and links the
issue where the boundary is argued. The integrity test checks that structure;
its module comment is explicit that it cannot show an excluded test has become
portable, because nothing re-runs excluded files.

The launcher lives in scripts/lib/ with a thin entrypoint over it, matching
bump-version, so the exit-code mapping can be asserted without importing a
module that calls main() and exits.

tsconfig.node.json gains acp, cli and test-agent — 58 files typechecked for the
first time, with no new errors — plus the five portable runtime-test scripts.
Not scripts/**/*.ts: that would pull in the malformed fixtures and the Deno-only
build tools. Execution and typechecking stay separate axes.

Both runtimes now run 1127 tests across 103 files, 0 failures.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR #196: 👷 ci: derive test:node and test:bun scope

13 files, +425 / -24

Scope

🟡 449 lines changed. PRs under 400 receive more thorough review.

🟡 PR mixes config and source changes.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

taras added 4 commits July 28, 2026 11:27
`relativeTo` matched a URL pathname inside a walked filesystem path. Those
agree on POSIX and disagree on Windows — `/C:/repo/tests/` never occurs inside
`C:\repo\tests\a.test.ts` — so the function fell through to returning the whole
absolute path while its contract promised a repository-relative one, and the
launcher would have handed absolute paths to the runner.

It now compares `toPath(tests)` against the walked path with separators
normalized, requires the directory as a boundary-aware prefix so a sibling like
`tests-extra/` cannot match, and throws rather than returning an absolute path
when an entry falls outside. Covered by four cases including Windows-style
input, and the repository-relative assertion now rejects a drive letter.

Also corrects two claims that promised more than the code delivers: AGENTS.md
said "a new test file anywhere" when discovery covers `tests/` beneath
workspace members plus `scripts/tests/`, and the integrity suite's preamble
claimed to catch an exclusion outliving its reason, which it cannot — the
manifest already says so.

Drops comments restating the names beneath them, and restores the literal em
dash in the package description that a JSON rewrite had escaped.
test-bun failed while test-node and test-deno passed, on one root cause: the
Bun job runs `bun install` alone, and that links only the workspace packages
the root manifest depends on. `@executablemd/test-support` was the sole entry,
added by #188 for exactly this reason, so nothing else appeared under
node_modules/@executablemd.

Every failure followed from it. Eval blocks compile to `.xmd-eval/<uuid>.ts`
beside node_modules and could not resolve `@executablemd/core`, which is why
the provider, sample and agent suites reported ERROR in their rendered output
rather than failing an assertion outright.

It passed locally because pnpm links all nine, so the pnpm and Bun layouts
disagreed and only CI ran the Bun one. Declaring the packages at the root makes
`bun install` produce the same layout: verified by moving node_modules aside,
running `bun install` alone, and getting all nine links and 1131 passing tests
where the same layout previously failed 45.

The old hand-picked `test:bun` list never reached these suites, so the gap
existed before this branch and only became visible once the scope was derived.
The last three Bun failures were all `this test timed out after 5000ms`. Bun
caps each test at 5s by default; Deno and Node impose no comparable limit, so
nothing in the corpus was written for it.

The suites that failed spawn CLI subprocesses and real test-agent workers, and
carry their own timeboxes — 15s to 180s — chosen for exactly that work. Bun's
cap preempted them, killing the child and leaving `result.code` null rather
than the exit status the assertion expected.

Raising it past the longest leaves each suite's own timeout in charge, which is
what the other two runners already do. This is a runner default that does not
suit the corpus, not a defect in the tests.
The module comment claimed discovery covers every *.test.ts in the repository.
It covers tests/ beneath each workspace member plus scripts/tests/ — the
boundary AGENTS.md already states, and the one that keeps nested worktrees and
the malformed fixtures out.
@taras
taras merged commit 53d2c5b into main Jul 28, 2026
9 checks passed
@taras
taras deleted the ci/derive-runtime-test-scope branch July 28, 2026 16:08
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.

Derive test:node and test:bun scope instead of naming test files

1 participant