ubq 0.0.7(beta)

This page as markdown

Status

What is built, and the known limits.

What exists, by step of the build order in 04-architecture.md, section 13. Update this file when a step ends.

Commands#

Command Does
npm test runs the tool's own tests with ubq itself: node src/cli.ts
npm run typecheck must print nothing
npm run build makes the package in dist/ (D32): bundled JavaScript, type declarations, package.json
npm run docs builds, then makes docs/api.md from the type declarations (scripts/api-docs.ts). A test fails if the file is not current.
npm run bench builds, then times ubq and other tools. -- --check fails if a ubq row is over its budget. -- --only ubq runs only ubq rows.

Checks before each commit#

npx tsc                 # must print nothing
node src/cli.ts         # the tool runs its own tests: 92 pass
npm run bench -- --only ubq --check   # speed guards (builds first)

The suite needs bun and deno on the path for all tests. A test skips itself, with a reason, if its runtime is not installed (8 tests need deno).

The tool needs Node 24. If node -v shows an older version, look for a newer one on the machine (a cloud session has /opt/node24/bin), and put it first on the path.

The budgets of the benchmark are for the machine of the first measurements. On a different machine, all rows can be over budget. Then run the benchmark on the commit before the change and on the change, and compare the two.

If a test fails, node src/cli.ts failures prints the failures of the last run again, and node src/cli.ts failures <run> prints an older run.

Lessons for work on this repository#

Step 0: done (2026-11-11)#

Step 1: done (2026-11-11)#

Source files:

File Contents
src/index.ts public exports, starts the plain program role
src/test.ts test, describe, the loop that runs tests one at a time
src/assert.ts the assertion functions
src/format.ts values as text, differences with paths
src/stack.ts stack traces as user code locations
src/state.ts the AsyncLocalStorage for the current test
src/emit.ts publishes events on diagnostics_channel
src/plain.ts runner role for a plain program
src/sink-text.ts compact text sink
src/hint.ts errors for wrong guesses
src/project.ts project root, relative paths

Checks (test/plain.ts, test/types.ts):

Known limits:

Step 2: done (2026-11-11)#

Source files:

File Contents
src/coverage.ts V8 collector: start, snapshot, flatten
src/preload.ts node --import ubqtest/preload: starts coverage before user modules load
src/store.ts .ubq/runs.db: schema, writer sink, retention
src/query.ts uncovered, covers, failures, last result
src/cli.ts the ubq command: failures, show, uncovered, covers, sql

Run the CLI from source with node src/cli.ts <command>.

Checks (test/coverage.ts, fixture project test/fixtures/cov/):

What was learned:

Known limits:

Step 3: done, with one target not met (2026-11-11)#

Source files:

File Contents
src/runner.ts finds test files, orders them, starts a process for each, reads events, totals
src/sink-pipe.ts a child writes its events as JSON lines to fd 3
src/cli.ts now also runs tests: ubq, ubq <file or folder>..., ubq <file>:<line>

How a run works:

Checks (test/runner.test.ts): 38 tests in 4 files, run by ubq.

What was learned:

Speed: see 04-architecture.md, section 12. One file meets its target. 200 files take 885 ms against a target of 650 ms.

Not done in step 3:

Step 4: done, without hang reruns (2026-11-11)#

Source files:

File Contents
src/capture.ts console capture: stdout and stderr text becomes output events
src/probe.ts reads the stack of a stuck process through the inspector
src/watchdog.ts worker thread that stops a stuck process that has no runner

What it does:

Checks: test/runner.test.ts, fixtures hang.ts, busy.ts, output.ts, orphan.ts. 43 tests.

Not done:

Known limits:

Step 5: done for coverage, not for mocks and hang reports (2026-11-11)#

Source files:

File Contents
src/instrument.ts adds a counter to each function and branch of a source file, and maps each counter to a source range
src/counters.ts counter regions and snapshots, with no parser, so that a process can read counters and not load the parser
src/bun-preload.ts bun --preload ubqtest/bun-preload: a load plugin that adds the counters, and the collector that reads them

What it does:

Checks (test/bun.test.ts):

Decisions in this step are D21 and D22 in 02-decisions.md.

Not done:

Known limits:

Step 6: done (2026-11-12)#

Done:

Checks: test/select.test.ts (IDs, --failed, --changed, configuration), test/history.test.ts (history, diff, --repeat, --baseline). test/setup.ts has the helper that makes a project in a temporary folder.

Not done in step 6:

Known limits:

Step 7: done (2026-11-12)#

Source files:

File Contents
src/fixtures.ts cleanup, env, testdir, and the cleanup functions of the file
src/spy.ts spy, spyOn
src/clock.ts fakeClock
src/real.ts the real timers and clocks, for the library itself
src/snapshot.ts assert.snapshot: the snapshot file, compare, write
src/mock.ts mockImport: resolve and load hooks, registered at the first call

Done (05-api.md, sections 6 and 7):

Checks: test/snapshot.test.ts, test/clock.test.ts, test/fixtures.test.ts, fixtures test/fixtures/fix.ts, test/fixtures/mock.ts, and test/fixtures/leak.ts. For Bun and Deno: test/bun.test.ts, test/plain.test.ts.

All parts of step 7 are built. Its check passes: a wrong mock does not compile, and mocks work on Node and Bun.

Known limits:

Step 8: done (2026-11-12)#

Done:

Known limits:

Ready for a release, but not released (2026-11-12)#

Conversion from a different test tool: first version (2026-11-12)#

D43. ubq convert, ubq guide convert (CONVERT.md), and the use-ubq skill (ubq skill). Checks: test/convert.test.ts.

Not checked yet: a conversion of a real project by a different agent. The user plans that check.

Known limits:

A Windows machine for trials (2026-11-12)#

The user has a Windows 11 VM (ARM64, Node 24.11.1). ssh winlocal runs a command in it. The copy of the repository is in C:\Users\isaacs\ubq-vm\ubqtest. It has no access to GitHub: copy files to it with tar through ssh. It has Bun 1.4.4, Deno 2.9.8 (the ARM build), and the browser of Playwright. The suite passes there with each runtime as the runner, with no skipped test.

The first run there found that Node 24.11 prints "ExperimentalWarning: SQLite is an experimental feature" for each ubq command. Node 24.22 (CI) and Node 26 do not. src/sqlite.ts loads the module with no warning.

It also found that the type checker did not start for an x64 runtime on an ARM machine: the native compiler of the project is for ARM, and ubq looked only for the one of the CPU of the runtime. src/typecheck.ts now looks for each.

Windows gaps closed (2026-11-12)#

All were found or checked in the VM.

Not checked: that only the user of the server can connect to its named pipe on Windows.

A flaky test, and its cause: the test of hang reports asked the store of the shared fixture project (test/fixtures) for the failures of the last run. Test files run in parallel, and a different file can store a run there in that time. The test now has a project of its own. A test must not ask a shared store for the last run. test/coverage.test.ts and test/bun.test.ts both use the store of test/fixtures/cov: this can fail in the same way, and it is not changed.

CI on each runtime, and the release workflow (2026-11-12)#

.github/workflows/ci.yml runs the suite with Node, with Bun, and with Deno as the runner, on Linux, macOS, and Windows. Each job installs the three runtimes: npm and the build need Node, and the suite has tests that start each runtime.

.github/workflows/release.yml runs on a v* tag. It builds dist/, runs the suite on Node, and runs npm stage publish in dist/. The tests of Bun, of Deno, and of the browser skip there.

The steps of a release: set the version in package.json, push the tag v<version>, approve the staged version on npmjs.com, then gh workflow run site.yml.

Known limit: Deno runs the suite with the built runner (deno run -A dist/cli.js), not from the source. With a module hook, Deno 2.9.6 does not load @sveltejs/acorn-typescript for a module that require loaded, so deno run -A src/cli.ts cannot add counters to a file that is not in the cache. The built package has the parser in its bundle, so a project that installs ubq does not have this problem.

Facts about the runtimes that the tests now allow for:

Tests in a browser: done (2026-11-12)#

D45. Source: src/browser.ts (the ubqtest/browser export), src/browser-server.ts (the browser key of the configuration), assert.eventually in src/assert.ts, ubq browser install in src/cli.ts. Tests: test/browser.test.ts. They skip if the browser is not installed. CI installs it.

src/state.ts keeps the state of the current test on globalThis, because browser.js is a separate file of the built package with its own copy of each module.

Checked by hand with the built package, and not by a test: the runner on Bun and on Deno.

To do next:

Coverage of code that runs in a page: done (D47). Source: src/browser-coverage.ts. Not done: a bundle of many files, and a source map in a separate file.

Web site: done (2026-11-12)#

D44. npm run site makes _site/ from site/ and the markdown documents. test/site.test.ts checks that each page has a markdown copy and that each link goes to a file. The Site workflow deploys it to a Cloudflare Worker (D46). The maintainer starts it by hand after the version is on npm.

Smaller gaps closed (2026-11-12)#

Commands that work on a change: done (2026-11-12)#

D41. ubq --changed selects the tests that ran the changed part of a file (selectChanged in src/runner.ts). ubq uncovered --diff and ubq mutate --diff work on the lines that differ from a git commit (changedLines in src/git.ts). Checks: test/diff.test.ts, test/select.test.ts.

Known limits:

Coverage of JSX files: done on Bun and Deno (2026-11-12)#

D40. A .tsx or .jsx file gets counters, so coverage, ubq uncovered, ubq covers, and --changed work for it under the runner.

Known limits:

Bun inspector coverage as an option: done (2026-11-12)#

D39. "bunInspectorCoverage": true gives a plain bun test/foo.ts coverage from node:inspector, with places from a guess (src/bun-coverage.ts). D39 lists what it gives and what it does not. The issue to Bun is https://github.com/oven-sh/bun/issues/48712. Checks: test/bun.test.ts.

Background server: done (2026-11-12)#

ubq server and ubq server stop (D36, D37, 11-server.md). Source: src/server.ts, src/pool.ts, src/warm.ts.

Known limits:

Mutation check: done (2026-11-12)#

ubq mutate <file> and ubq mutate <file>:<line> (D35, 01-agent-ideation.md, section 7). Source: src/mutate.ts, the list of changes in src/instrument.ts (mutants), the change in a test process in src/counters.ts (mutated).

Known limits:

Hang reruns: D14 done (2026-11-12)#

The part of step 4 that was not built. Source: src/hang.ts.

Known limits:

Source counters on Node: D25 done (2026-11-12)#

Step 3: the runner uses counters on Node, for its children and for a run of one file in its own process. UBQ_COVERAGE=v8 goes back to V8.

The text below is the state after steps 1 and 2.

Source files:

File Contents
src/counter-collector.ts the collector for counters, and the cache of changed source in .ubq/counters/
src/counters-preload.ts node --import ubqtest/counters-preload: a load hook that adds the counters
src/bun-preload.ts now uses the same collector and cache

Known limits:

Report from a Cloudflare Workers project (2026-11-13)#

An agent tried to convert a project with two Workers from vitest (@cloudflare/vitest-pool-workers) to ubq 0.0.3, and went back: that version could not run tests in the Workers runtime (workerd). The report is 12-agent-feedback-0.0.3.md.

Done from the report:

A trial showed that workerd can run the test API (03-measurements.md, section 14). The answer is D48, below.

Not done, from the same report:

Tests of a Cloudflare Worker: first version done (2026-11-13)#

D48. Source: src/workers-host.ts (the process that starts workerd), src/workers-entry.ts (the module that workerd runs), src/workers.ts (ubqtest/workers), src/workerd.ts (the global that tells the test API where it is). Checks: test/workers.test.ts, with the Worker in test/fixtures/workers/ (D1 with migrations and a trigger, a Durable Object, waitUntil, a scheduled handler).

Known gaps, for future work:

Second report from the Workers project (2026-11-13)#

13-agent-feedback-0.0.4.md: one of the two Workers converted (163 tests pass in workerd). The other did not load. Done from the report:

Not done:

Third report from the Workers project (2026-11-13)#

14-agent-feedback-0.0.5.md: both Workers run under ubq 0.0.5 (225 tests, 3.0 s, the same as vitest). The report lists the hand work that a less able model can get wrong. Done from it:

Done after the user answered Q11 (D49, D50):

Not done:

The summary of ubq uncovered (2026-11-13)#

The user asked about two points of the output.

Changed after that (D51): a test helper (test/setup.ts) is not in the list, and ubq uncovered exits with 1 if it lists code. In this repository the list is empty, because the collectors leave out the library, which here is src/.

Not done: the store keeps the rows of a test file that is gone (its coverage and its test_file row). ubq covers can name a test of such a file.

Coverage of ubq by its own tests: started (2026-11-13)#

D52: the files of ubq are project files in this repository. ubq uncovered lists 291 ranges in 24 of the 28 files that test processes load. 25 more files run only in child processes.

Coverage of child processes: done (D53). Source: src/child-coverage.ts, src/child-preload.ts, src/child-read.ts. Checks: test/children.test.ts. With it, ubq uncovered here sees 50 of the 53 source files, with 533 uncovered ranges. The suite takes 14.75 s with it and 14.25 s without (this repository uses the preload as TypeScript source, which is the slow form).

Checked by hand with the built package, on a project with a command, a child with a new environment, and a server that the test stops: the runner on Node and on Deno gives the same coverage. The runner on Bun does not have the code of the child with a new environment.

To do next, in this order:

  1. Coverage from more than one run, merged: done (D54). Source: src/merge.ts, the table other_coverage in src/store.ts. Checks: test/merge.test.ts. CI keeps the store of each job, and the job coverage merges them and prints the list of ubq uncovered. It does not fail for the list yet.
  2. Tests for the 533 uncovered ranges that are real gaps.
  3. A comment that marks code as not measured, with a reason. The user agreed to it, for what stays after 1 and 2.
  4. Deno children: done. The spawn functions add --preload to the arguments of a deno run child (D53).

Fourth report from the Workers project (2026-11-13)#

15-agent-feedback-0.0.6.md: the conversion was smooth (17 TODO comments, 15 type errors, 225 tests pass on the first run after the TODOs). Done from it:

Not done, on purpose: the command does not delete vi.restoreAllMocks() in the middle of a test, or the lines of a restore variable (restore = m.restore). The code after such a call can need the real method, and the command cannot know what the restore function of a helper of the project does. The command makes only the changes that are certain (D43).

Build output, source maps, and the way to find the merge flow (2026-11-13)#

D55. Done:

Checked by hand: the suite with the built runner on Deno, with maps. No dist/ file is in the list.

Not done:

Tests for the code that no test ran: in progress (2026-11-13)#

The merged list of CI had 593 ranges. The work goes file by file, largest first. The method: write inputs for the branches in the list, read each result, correct what is wrong, and keep the checked results as tests.

A local run counts only Node, and it had 465 ranges after this work. The next files: src/test.ts (33), src/runner.ts (33), src/cli.ts (26), src/vitest-config.ts (20).

The merged list of CI went from 326 to 279 ranges with tests of the runner, the command line, the server, mockImport, and the type check (test/runner-edges.test.ts, test/mock.test.ts, and more cases in test/cli.test.ts, test/server.test.ts, test/api-errors.test.ts, test/typecheck.test.ts, test/select.test.ts). The cases found three defects, all corrected:

Some ranges are not reachable by a test of this repository. The preload modules (src/child-coverage.ts, src/counter-collector.ts, src/load-hook.ts, src/child-preload.ts) load before the hooks that add counters, in each process. Their code has no counters there. These wait for the comment that marks code as not measured.

CI caches the npm downloads and the browser, and runs npm run typecheck in one job. Each job saves 5 to 20 s of setup. The test steps are most of the time (45 to 205 s; Windows is the slowest).

Later in the same day, the merged list went to 247 ranges, and 5 ranges have the comment of D56. More defects found and corrected:

The user's decision on fallbacks in an expression (?? '', || 0) for a value that a test never leaves out: use x! if the value is always set, or a D56 comment if not. Each one has a short comment that gives the reason. The merged list is now 204 ranges, and 24 ranges have a D56 comment. New tests: the store commands with no runs, a write of bytes, ubq skill and ubq convert from the built package, intervals with no time, and a doctor report of few stray files.

The fake clock runs setInterval(fn) with no time at once, then each 1 ms. Node waits 1 ms for the first call. The test keeps the behavior of the fake clock; a change waits for the user.

The rest of the list: branches of ? : for cases that tests do not make, paths for a runtime or a system, catch blocks for errors of the file system, and paths of Workers configs.

Later the same day, each range of the list got a test or a D56 comment. The merged list of CI went to 11 ranges, then fewer. About 160 ranges have a D56 comment. Two kinds of reason:

A unit test of a module that the library loads (coverage.ts, counters.ts, stack.ts, child-read.ts) imports a copy with a query ('../src/stack.ts?copy'). The library loaded its own copy before coverage started, so V8 has no block counts for that copy.

Defects found and corrected: ubq server --foreground with a server that already runs printed the listen error and did not end (its folder watcher kept it alive).

Found, not changed (each waits for a decision):