Summary
When a consumer project measures test coverage of its own API leaves with vitest + @vitest/coverage-v8, any leaf reached only through slothlet composition reports near-zero coverage of its function body — even though the leaf executes and its output is asserted in tests. Module-level lines (the spec / default export) attribute; everything inside the exported functions reads as uncovered. The functions genuinely run; the coverage collector just never sees them. It looks like slothlet is "blocking" coverage, but it is purely a measurement artifact — worth documenting so future consumers don't burn a day on it (this one cost a long debugging session).
Root cause
The loader imports each leaf via a native dynamic import with a per-instance cache-bust query — src/lib/processors/loader.mjs (~line 273):
const fileUrl = url.pathToFileURL(filePath).href;
// Cache bust using instanceID to prevent cross-instance pollution
moduleUrl = `${fileUrl}?slothlet_instance=${instanceID}`;
// …
const module = await import(moduleUrl);
In a consumer project @cldmv/slothlet is an externalized node_modules dependency. vitest externalizes node_modules by default, so slothlet runs as native Node code and this import() is a native import that never enters vitest's module runner / module graph. @vitest/coverage-v8 only attributes execution for modules in that graph, so the leaf's execution instance (…/leaf.mjs?slothlet_instance=<id>) is invisible to it. The only coverage a leaf gets is the module-level baseline from coverage.all scanning src/** once — hence the ~20% floor with the whole function body showing uncovered.
This is not source-vs-dist. The slothlet-dev condition only changes which files resolve; it does not change externalization. A consumer pointed at slothlet's src (if the package shipped it) would still see native leaf imports and still lose the coverage. The axis is externalized-vs-inlined. slothlet's own repo doesn't hit this because there the loader is project code (transformed by vitest by default), so its import() rides the runner and the query-URL loads attribute via cross-file aggregation. A consumer has no equivalent by default.
Workaround (works today)
Add slothlet to deps.inline so vitest runs it through its module runner; the leaf imports then route through vitest and attribute correctly:
// vitest.config.mjs
export default defineConfig({
test: {
server: { deps: { inline: [/@cldmv\/slothlet/] } },
coverage: { provider: "v8", include: ["src/**"] }
}
});
Measured on a real consumer (@cldmv/git-embedded), same published dist slothlet, identical test suite, only the config line added:
| Metric |
externalized (default) |
deps.inline |
| Lines |
75% |
92% |
| Statements |
75% |
91% |
| Functions |
80% |
95% |
No test changes — purely a measurement fix; all tests stay green. Tradeoff: the shipped dist is re-processed by Vite in tests rather than loaded byte-for-byte by Node, a small fidelity cost.
Ask
- Document this in the testing/coverage docs (README coverage section or a dedicated guide): consumers measuring their own leaf coverage must inline
@cldmv/slothlet, with the snippet above and the one-line why. This is the primary ask.
- Optional, worth considering: a first-class path so a consumer can route leaf imports through its own test runner without inlining the whole package — e.g. an injectable importer (
slothlet({ …, import: customImport })) the loader uses instead of a bare import(), or a documented env/flag honored under coverage. That would remove the fidelity tradeoff entirely.
Environment
@cldmv/slothlet 3.12.x (behavior present since 3.7.x)
- vitest 4.x,
@vitest/coverage-v8 4.x, Node 26
- Per-file child-process runner (
@cldmv/vitest-runner), v8 provider
Summary
When a consumer project measures test coverage of its own API leaves with vitest +
@vitest/coverage-v8, any leaf reached only through slothlet composition reports near-zero coverage of its function body — even though the leaf executes and its output is asserted in tests. Module-level lines (thespec/defaultexport) attribute; everything inside the exported functions reads as uncovered. The functions genuinely run; the coverage collector just never sees them. It looks like slothlet is "blocking" coverage, but it is purely a measurement artifact — worth documenting so future consumers don't burn a day on it (this one cost a long debugging session).Root cause
The loader imports each leaf via a native dynamic import with a per-instance cache-bust query —
src/lib/processors/loader.mjs(~line 273):In a consumer project
@cldmv/slothletis an externalizednode_modulesdependency. vitest externalizesnode_modulesby default, so slothlet runs as native Node code and thisimport()is a native import that never enters vitest's module runner / module graph.@vitest/coverage-v8only attributes execution for modules in that graph, so the leaf's execution instance (…/leaf.mjs?slothlet_instance=<id>) is invisible to it. The only coverage a leaf gets is the module-level baseline fromcoverage.allscanningsrc/**once — hence the ~20% floor with the whole function body showing uncovered.This is not source-vs-dist. The
slothlet-devcondition only changes which files resolve; it does not change externalization. A consumer pointed at slothlet'ssrc(if the package shipped it) would still see native leaf imports and still lose the coverage. The axis is externalized-vs-inlined. slothlet's own repo doesn't hit this because there the loader is project code (transformed by vitest by default), so itsimport()rides the runner and the query-URL loads attribute via cross-file aggregation. A consumer has no equivalent by default.Workaround (works today)
Add slothlet to
deps.inlineso vitest runs it through its module runner; the leaf imports then route through vitest and attribute correctly:Measured on a real consumer (
@cldmv/git-embedded), same published dist slothlet, identical test suite, only the config line added:deps.inlineNo test changes — purely a measurement fix; all tests stay green. Tradeoff: the shipped
distis re-processed by Vite in tests rather than loaded byte-for-byte by Node, a small fidelity cost.Ask
@cldmv/slothlet, with the snippet above and the one-line why. This is the primary ask.slothlet({ …, import: customImport })) the loader uses instead of a bareimport(), or a documented env/flag honored under coverage. That would remove the fidelity tradeoff entirely.Environment
@cldmv/slothlet3.12.x (behavior present since 3.7.x)@vitest/coverage-v84.x, Node 26@cldmv/vitest-runner), v8 provider