Skip to content

Document: consumer coverage of composition-loaded leaves needs deps.inline for @cldmv/slothlet (externalized dep → native leaf imports bypass the v8 collector) #217

Description

@Shinrai

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

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreTouches core library / runtime source codepriority: lowMinor issue or improvement that can be deferred without impacttype: documentationRelates to docs, README updates, guides, or inline code comments

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions