What happened
bun run lint:knip (the step "knip — dead modules and undeclared imports" in
package-health.yml) exits 1 on local develop at 09f0751a:
Unresolved imports (2)
examples/chat/backend/main.ts tests/unit/examples/ChatSampleUsageTexts.test.ts
examples/chat/backend/main.ts tests/unit/examples/ChatSecretStartupOrder.test.ts
Both findings are false positives. Both tests arrived with the #791 work
(203f58bf, 09725e89, 2026-09-22). Each one passes the repository-relative literal
'examples/chat/backend/main.ts' as the first element of the args array to
spawn('bun', [...], { cwd: repositoryRoot }). The file exists, and bun finds it
because the spawn's cwd is the repository root.
origin/develop has neither test, so CI has not flagged this yet. The job goes
red on the next develop push.
This is not a regression from a dependency bump. bun.lock pinned knip 6.37.0
at both commits, and 6.37.0 and 6.38.0 report the same two lines.
Mechanism
The finding comes from knip's source-code script parser, not from the import
graph. Traced in knip 6.38.0's dist/:
typescript/visitors/calls.js, handleCallExpression: in a file that
imports node:child_process, a call to spawn, spawnSync, execFile or
execFileSync goes through getSafeScriptFromArgs(executable, argsArray)
(typescript/ast-nodes.js). That function joins the string-literal
executable with every string-literal element of the args array into a
shell script and skips non-literal elements. The two scripts here are
bun examples/chat/backend/main.ts --help and
bun examples/chat/backend/main.ts --port --http-port --seeds --data-dir.
The parser is documented under "Source code" at
https://knip.dev/features/script-parser, but that page does not say which
directory it resolves against.
graph/build.js passes those scripts to the binaries parser with
cwd: dirname(filePath), which is the directory of the test file. knip
never reads the cwd option of the call.
binaries/resolvers/bun.js: the first positional argument is neither a bun
subcommand nor a manifest script, so the resolver tries join(cwd, file),
which is tests/unit/examples/examples/chat/backend/main.ts. That path does
not exist, so the resolver re-parses the words as a command.
binaries/fallback.js then turns a binary that contains / into an entry
input.
util/create-input-handler.js cannot resolve that entry relative to the
test file's directory and reports it as unresolved.
Measured: when one literal is changed to the test-file-relative
'../../../examples/chat/backend/main.ts', that file's finding disappears and
the other one remains. knip resolves against the test's directory. With
cwd: repositoryRoot, though, bun would get the wrong path from that literal.
What did you expect
lint:knip exits 0 without a new ignore entry, and both tests keep spawning
the same file from the same working directory.
Reproduction
bun install --frozen-lockfile
bun run lint:knip # exit 1, the two lines above
Environment
actor-ts at 09f0751a (local develop)
- Bun 1.4.2, Windows 11
- knip 6.38.0 (locked); 6.37.0 gives the same result
Fix sketch
Resolve the script the way the tests already mean it: as an absolute path,
join(repositoryRoot, 'examples', 'chat', 'backend', 'main.ts'), keeping
cwd: repositoryRoot. Every other spawn of a repository script in tests/
already does this, which is why none of them trips the parser:
CoverageGate.test.ts uses
SCRIPT = join(REPOSITORY_ROOT, 'scripts', 'coverage-gate.mjs'), and both
StressHarness* tests and DocSampleHarnessEndToEnd.test.ts do the same.
getSafeScriptFromArgs skips a non-literal element, so knip no longer sees a
path it resolves against the wrong directory. The tests themselves still
guarantee the file exists, because they fail if bun cannot load it.
Rejected alternatives:
- An
ignoreUnresolved entry in knip.jsonc. It silences the finding by name
without removing its cause, and the next test that spawns an example with a
relative literal would need another entry.
- A test-file-relative literal. knip accepts it, but under this
cwd it is the
wrong path for bun.
What happened
bun run lint:knip(the step "knip — dead modules and undeclared imports" inpackage-health.yml) exits 1 on localdevelopat09f0751a:Both findings are false positives. Both tests arrived with the #791 work
(203f58bf, 09725e89, 2026-09-22). Each one passes the repository-relative literal
'examples/chat/backend/main.ts'as the first element of the args array tospawn('bun', [...], { cwd: repositoryRoot }). The file exists, and bun finds itbecause the spawn's
cwdis the repository root.origin/develophas neither test, so CI has not flagged this yet. The job goesred on the next
developpush.This is not a regression from a dependency bump.
bun.lockpinned knip 6.37.0at both commits, and 6.37.0 and 6.38.0 report the same two lines.
Mechanism
The finding comes from knip's source-code script parser, not from the import
graph. Traced in knip 6.38.0's
dist/:typescript/visitors/calls.js,handleCallExpression: in a file thatimports
node:child_process, a call tospawn,spawnSync,execFileorexecFileSyncgoes throughgetSafeScriptFromArgs(executable, argsArray)(
typescript/ast-nodes.js). That function joins the string-literalexecutable with every string-literal element of the args array into a
shell script and skips non-literal elements. The two scripts here are
bun examples/chat/backend/main.ts --helpandbun examples/chat/backend/main.ts --port --http-port --seeds --data-dir.The parser is documented under "Source code" at
https://knip.dev/features/script-parser, but that page does not say which
directory it resolves against.
graph/build.jspasses those scripts to the binaries parser withcwd: dirname(filePath), which is the directory of the test file. knipnever reads the
cwdoption of the call.binaries/resolvers/bun.js: the first positional argument is neither a bunsubcommand nor a manifest script, so the resolver tries
join(cwd, file),which is
tests/unit/examples/examples/chat/backend/main.ts. That path doesnot exist, so the resolver re-parses the words as a command.
binaries/fallback.jsthen turns a binary that contains/into an entryinput.
util/create-input-handler.jscannot resolve that entry relative to thetest file's directory and reports it as
unresolved.Measured: when one literal is changed to the test-file-relative
'../../../examples/chat/backend/main.ts', that file's finding disappears andthe other one remains. knip resolves against the test's directory. With
cwd: repositoryRoot, though, bun would get the wrong path from that literal.What did you expect
lint:knipexits 0 without a new ignore entry, and both tests keep spawningthe same file from the same working directory.
Reproduction
Environment
actor-tsat09f0751a(localdevelop)Fix sketch
Resolve the script the way the tests already mean it: as an absolute path,
join(repositoryRoot, 'examples', 'chat', 'backend', 'main.ts'), keepingcwd: repositoryRoot. Every other spawn of a repository script intests/already does this, which is why none of them trips the parser:
CoverageGate.test.tsusesSCRIPT = join(REPOSITORY_ROOT, 'scripts', 'coverage-gate.mjs'), and bothStressHarness*tests andDocSampleHarnessEndToEnd.test.tsdo the same.getSafeScriptFromArgsskips a non-literal element, so knip no longer sees apath it resolves against the wrong directory. The tests themselves still
guarantee the file exists, because they fail if bun cannot load it.
Rejected alternatives:
ignoreUnresolvedentry inknip.jsonc. It silences the finding by namewithout removing its cause, and the next test that spawns an example with a
relative literal would need another entry.
cwdit is thewrong path for bun.