Worked review 4: a package with a test suite
The last of the four reviews is a package, not a script: a package.json, one source file, and one node:test suite — three files instead of one running program, which is why every transcript in this lesson is a real, separately-run command rather than a single <NodeRunner>. Four bugs from the AI bug catalog are planted across it: a wrong exports order, a ^0.x dependency, a test with no assertion, and a test that waits on a real timer instead of mock.timers.
The package
Section titled “The package”package.json:
{ "name": "wr4-demo", "version": "1.0.0", "type": "module", "exports": { ".": { "default": "./index.js", "types": "./index.d.ts" } }, "dependencies": { "left-pad": "^0.0.3" }, "scripts": { "test": "node --test" }}index.js:
// @skip-verify multi-file package -- imported by index.test.js below, not run standaloneexport function double(n) { return n * 2 - 1; // BUG 3: off by one -- should just be n * 2}index.test.js:
// @skip-verify multi-file package -- imports ./index.js as a sibling fileimport { test } from 'node:test';import { double } from './index.js';
test('doubles a number', function () { double(4); // BUG 3 (continued): no assertion at all -- this can never fail});
test('resolves eventually', function (t, done) { setTimeout(function () { done(); }, 300); // BUG 4: a real timer, not mock.timers -- slows the suite for no real reason});Find the bugs before reading on
Section titled “Find the bugs before reading on”Four planted bugs — one packaging convention, one version-range trap, one missing check, one slow test. Try to name all four before opening the answers below.
Bug 1 — "default" listed before "types" in the exports map
Symptom: Node resolves the first matching condition in an exports map — with "default" first, it always wins, and "types" underneath it is dead configuration a TypeScript consumer’s editor and tsc will never actually reach.
Evidence: this produces no runtime error at all in plain Node.js — require/import in JavaScript never looks at the "types" condition to begin with. The only way to observe the bug is to actually try consuming this package’s types from a TypeScript project and notice the editor falls back to implicit any instead of the real declarations.
Fix: list the more specific condition first: { "types": "./index.d.ts", "default": "./index.js" }.
Bug 2 — "left-pad": "^0.0.3"
Symptom: for a 0.0.x version, npm’s caret (^) allows no upgrades at all — not even patch releases — which is a sharper restriction than the same ^ on a 1.x or even a 0.1.x dependency permits.
Evidence: not a crash — this is a version-range choice, not a bug that fails a build. It is worth flagging in review because it is easy to assume ^ always means “patch and minor upgrades allowed,” when for anything under 0.0.x it actually means “pinned exactly.”
Fix: nothing is strictly broken here, but a reviewer should confirm the author actually intended “never auto-update this dependency at all” — if not, waiting for the dependency to reach at least 0.1.0 (or pinning explicitly with no caret, to make the intent obvious) avoids the surprise.
Bug 3 — double(4); with no assertion, on top of a genuinely wrong double
Symptom: index.js’s double returns n * 2 - 1 — double(4) is 7, not 8 — and the test that calls it never checks the return value at all.
Evidence: the real node --test transcript below: both tests report a pass, including this one, despite the underlying function being wrong.
Fix: assert.equal(double(4), 8); — and once the assertion exists, fixing the actual bug in index.js (n * 2) is what makes the suite green again.
Bug 4 — setTimeout(function () { done(); }, 300); with no mock.timers
Symptom: the test genuinely waits 300 real milliseconds to prove nothing more than “a callback eventually fires” — a fact that does not require waiting on a real clock to verify.
Evidence: the real transcript’s own per-test timing, quoted below: this one test alone takes roughly 300ms, while every other test in the suite takes under a millisecond. Multiply that by however many similar tests a real suite accumulates, and a “fast” test suite becomes minutes long for no functional reason.
Fix: node:test’s built-in mock.timers: enable it, register the same setTimeout, then call t.mock.timers.tick(300) to advance the mocked clock instantly instead of waiting on a real one.
The fixed version
Section titled “The fixed version”package.json:
{ "name": "wr4-demo", "version": "1.0.0", "type": "module", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } }, "dependencies": { "left-pad": "^0.0.3" }, "scripts": { "test": "node --test" }}index.js:
// @skip-verify multi-file package -- imported by index.test.js below, not run standaloneexport function double(n) { return n * 2; // FIX: was n * 2 - 1}index.test.js:
// @skip-verify multi-file package -- imports ./index.js as a sibling fileimport { test } from 'node:test';import assert from 'node:assert/strict';import { double } from './index.js';
test('doubles a number', function () { assert.equal(double(4), 8); // FIX: a real, reachable assertion});
test('resolves eventually', function (t) { t.mock.timers.enable({ apis: ['setTimeout'] }); // FIX: mock the timer instead of waiting for real return new Promise(function (resolve) { setTimeout(resolve, 300); t.mock.timers.tick(300); // advances the mocked clock instantly });});What the tools caught
Section titled “What the tools caught”npm ci with no lockfile present (the state of the buggy package as checked in):
npm error code EUSAGEnpm errornpm error The `npm ci` command can only install with an existing package-lock.json ornpm error npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 ornpm error later to generate a package-lock.json file, then try again.npm ci refuses to run at all without a committed lockfile — a real, immediate failure that has nothing to do with any of the four planted bugs, but is exactly the kind of thing a Dockerfile using npm ci (as the Dependencies lesson recommends) would catch on the very first build.
npm install (generates the lockfile), then npm ci again:
npm warn deprecated [email protected]: use String.prototype.padStart()
added 1 package, and audited 2 packages in 522ms
found 0 vulnerabilitiesnpm’s own deprecation warning calls out left-pad directly — a real, independent signal (unrelated to the ^0.0.3 version-range bug) that this specific dependency should not be a dependency of a new package at all in 2026, since the standard library has covered its job since ES2017.
node --test on the buggy version:
✔ doubles a number (0.4ms)✔ resolves eventually (302.7ms)ℹ tests 2ℹ suites 0ℹ pass 2ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 367.2Both tests pass — double’s real bug (returning 7 instead of 8) is completely invisible to this test run, and resolves eventually costs 300+ real milliseconds to confirm something that needed no real waiting at all.
node --test on the fixed version:
✔ doubles a number (0.4ms)✔ resolves eventually (0.4ms)ℹ tests 2ℹ suites 0ℹ pass 2ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 53.6Same two tests, same “2 passed” headline — but now doubles a number is a real check (it would fail loudly if double regressed back to its buggy form), and resolves eventually finishes in under half a millisecond instead of 300+, because mock.timers.tick(300) advances a simulated clock instead of waiting on a real one.