Add workspace source resolution for accurate coverage reporting - #741
Draft
sroussey wants to merge 15 commits into
Draft
Add workspace source resolution for accurate coverage reporting#741sroussey wants to merge 15 commits into
sroussey wants to merge 15 commits into
Conversation
sroussey
force-pushed
the
claude/coverage-dist-bundle-fix-ew0vj8
branch
from
August 11, 2026 03:08
8d27385 to
ced2825
Compare
Coverage Report
File CoverageNo changed files found. |
This was referenced Aug 13, 2026
sroussey
force-pushed
the
claude/coverage-dist-bundle-fix-ew0vj8
branch
from
August 13, 2026 03:53
ced2825 to
97a0b75
Compare
sroussey
marked this pull request as draft
August 13, 2026 04:42
This was referenced Aug 13, 2026
`packages/test` reaches everything it exercises by package specifier, which `exports` resolves to `dist/node.js`. Under v8 coverage that bundle is the file instrumented, so executed lines were attributed to `packages/ai/dist/*` and `packages/ai/src/**` read as barely covered — a package scored worse the more of its behavior lived behind its public entry point. Measured on the `ai-model` section: every executed line landed on a dist bundle (task-graph/dist 29.7%, ai/dist 23.8%, util/dist 14.0%) and `packages/ai/src` was absent from the report entirely. The fix is a resolver, not a per-package config. `workspaceSourcePlugin` lets normal resolution run first — so conditional exports still pick the node/browser/bun target — and rewrites only the result, `<pkg>/dist/<entry>.js` to `<pkg>/src/<entry>.ts`. Every package, every subpath export, and every package added later is covered with no list to maintain. Unlike `use-source` it writes nothing into `dist`, so it cannot clobber a build or leave a tree that needs `use-dist` afterwards. `WORKGLOW_TEST_TARGET=dist` opts out; the Bun runner still resolves `exports` natively, so the nightly parity workflow exercises the bundles either way. The coverage denominator is now stated explicitly rather than left to vitest's default of "files loaded during the run" — that default omits the modules no test imports at all, which are exactly the ones a coverage report exists to surface, and makes a package's file list depend on which section CI happened to run. `scripts/workspaceSource.test.ts` fails if any published runtime entry lacks a source counterpart: such an entry keeps resolving to its bundle, and the only symptom is one package's coverage collapsing back onto `dist/*`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016khjNuSErZBHzW3aUw2SP4
The source-redirect plugin rewrites the RESULT of resolution, so resolution itself still goes through the package's `exports`, which point at ./dist/*. With no built entry there, Vite's resolution fails first, `this.resolve` yields nothing, the plugin returns null, and the run dies with a generic `Cannot find package '@workglow/ai/worker' imported from …` that blames the manifest and names neither the plugin nor anything to do about it. Keeps the workspace list as WorkspacePackage rather than bare names, so the owning directory is available at the point resolution fails, and throws a message naming the specifier, the owner, the importer and the remedy. Throwing rather than warning is right: an unresolvable @workglow/* specifier already fails the run, so this replaces a misleading message with an actionable one. The remedy branches on whether the owner's dist holds any built entries, since "never built" and "a new exports subpath was added without rebuilding" call for different actions and the second reads as wrong advice to someone looking at a populated dist. An empty dist directory — what `bun run clean` and `use-dist --no-build` both leave behind — counts as never built. ownerOf and unresolvedWorkspaceMessage are separated out as pure functions because resolveId needs Vite's plugin context to drive and cannot be unit tested; the message was additionally verified end to end by emptying dist and running a suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
Resolving @workglow/* to src is what makes the coverage numbers mean anything, but the plugin is attached to every project unconditionally — not only to coverage runs — so with the default in force no vitest job resolves a specifier through `exports` at all. There is also no `bun test` job in the blocking workflow. So after the source-resolution change, nothing that can block a merge loads a built bundle: a `bun build` entry that silently dropped a re-export would reach main and surface only in the nightly Bun parity run, which is explicitly informational, runs on a cron, and excludes six sections. Adds test-vitest-dist: reuses the existing build-output artifact and runs the unit tier with WORKGLOW_TEST_TARGET=dist. It is in cleanup's needs list, since cleanup deletes the artifact it downloads. Scoping the plugin to coverage runs instead would not have worked: scripts/test.ts adds --coverage whenever CI is set, so in CI every run is a coverage run and would still resolve to src. Also skips --coverage for a dist-targeted run. The denominator names package sources, so such a run reported all ~1286 of them at 0% — not a measurement of anything, and it is what lets the new job reuse test:vitest:unit unchanged and produce no fragment for merge-vitest-coverage. The CLAUDE.md and vitest.config.ts notes claimed bundle integrity was covered by the nightly parity run; both now say what actually guards it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
The coverage-flag test spawned the runner with `{...process.env, CI: "1"}` and
let WORKGLOW_TEST_TARGET come from the ambient environment. The new
test-vitest-dist job exports that variable for its whole step, so inside that
job the source-target case inherited `dist` and became a second copy of the
dist case — asserting `--coverage` is present while the runner correctly
omitted it. It failed in the one job it was added to support.
Both cases now state the target explicitly, so the assertions hold whatever the
runner is invoked under.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
`examples` is a first-class workspace group in both workspaceSource.ts and testDiscovery.ts, and all three example packages — @workglow/cli, @workglow/eval and @workglow/web — are published with none marked private. They carry 24 test files between them. The denominator listed only packages/* and providers/*, so their source was rewritten to src, executed by their own tests, and then left out of the measurement entirely: a coverage run from examples/cli reported `All files 0%` with no file rows at all. Pins coverage.root to the config's own directory rather than making the globs absolute. coverage.root is the documented base for include/exclude, and this config is invoked from package directories too (vitest run --config ../../vitest.config.ts), where a repo-relative glob would otherwise match nothing. It also does not depend on whether vitest accepts absolute glob patterns. Excludes examples/*/src/test/**: those dirs hold the example suites plus the occasional non-`.test.` helper the filename rules cannot catch, and counting a test helper is what the adjacent excludes already exist to prevent. Guards the invariant that broke, in workspaceSource.test.ts: every entry in WORKSPACE_GROUPS must be a prefix of some coverage.include glob, read from the actual config so the two cannot drift apart again. A missing group is invisible in a coverage report — it shows a shorter file list, not an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H797qbH356jjznKgUax63o
…pecifier causes (#759) Two guards on the dist-targeted test path. `WORKGLOW_TEST_TARGET` had two independent readers, each treating anything that was not literally "dist" as source, silently. A workflow edited to "dist " or Dist turned test-vitest-dist into a byte-identical rerun of test-vitest-unit: green, a full runner slot spent, no bundle coverage. `resolveTestTarget` is now the single reader — unset and empty mean source, everything else throws naming the value, with no lowercasing and no prefix matching. `unresolvedWorkspaceMessage` offered only "never built" or "stale dist", but resolution fails from the IMPORTER's node_modules: under isolated linking an undeclared workspace dependency, or a subpath absent from the owner's exports, fails while the owner's dist is fully populated. The causes are now ranked, so the built/stale pair is offered last and labelled as the remaining likely cause. Claude-Session: https://claude.ai/code/session_01RomTUtZSTgUbFCYqFs4pcu Co-authored-by: Claude <noreply@anthropic.com>
`resolveId` — the one function the workspace-source plugin exists for — had no test, and nothing asserted the plugin reaches the generated projects. `workspaceSource.test.ts` covered only the pure helpers, and both CI modes pass under either resolution: `test-vitest-unit` and `test-vitest-dist` run the same files and the same assertions, differing only in which files load, so no suite distinguished "resolved to src" from "resolved to dist". The failure that leaves is silent. Hoisting `plugins: [workspaceSourcePlugin(__dirname)]` from the per-project object to the root `defineConfig` — a natural "one instance instead of N" cleanup, and the exact mistake the comment there warns about — makes every project resolve `@workglow/*` through `exports` to dist again. All tests pass, merge-vitest-coverage succeeds, and the only symptom is entry-point behavior reading as uncovered. The same silence covers a regression inside `resolveId`. Extract the hook body into `resolveWorkspaceSourceId(packages, context, source, importer, options)`, which takes the plugin context as a parameter so a recording stub can stand in for Vite; `workspaceSourcePlugin` becomes a one-line adapter. `WorkspaceResolveContext` declares `resolve` with method syntax on purpose — parameter bivariance is what lets Vite's real `PluginContext` satisfy it — and is generic in the result so the adapter's return type stays `ResolvedId`, which is what Vite's `resolveId` hook is declared to return. Both together are what keep the call site cast-free (checked against the installed vite typings, where the non-generic form needs one). New tests: the rewrite, a non-workspace specifier short-circuiting before `this.resolve`, an external resolution left alone, dist output with no source twin left on the built file, the unresolved-specifier diagnostic (a branch with zero execution anywhere before this), and `skipSelf: true` plus verbatim forwarding of the hook's own options. Plus a "plugin attachment" block that re-imports the real `vitest.config.ts` and asserts every project carries `workglow:workspace-source` under the default target and none does under `dist`. Both directions stub `WORKGLOW_TEST_TARGET` explicitly and unstub afterwards: `test-vitest-dist` runs this file with that variable ambient, so a test reading the ambient value would pass in one CI job and fail in the other.
`test-vitest-dist` is the one blocking job that resolves `@workglow/*` through `exports`, but it runs the UNIT tier only — while the source rewrite applies to EVERY vitest job. The integration/rag/provider suites previously loaded the bundles and now load src, so a bundle reachable only from an `.integration.test.ts` file lost its blocking check. Two entries lost every check: `@workglow/openrouter/ai-runtime` and `@workglow/huggingface-inference/ai-runtime`, imported only from provider-api integration files, whose section the nightly Bun parity run also excludes. Concretely: a `bun build` change dropping `registerOpenRouterInline` from `providers/openrouter/dist/ai-runtime.js` leaves the file in place, satisfies the dist-must-exist requirement, passes all of CI, and breaks consumers only after publish. `PublishedEntryImports.test.ts` makes the check total instead of tier-shaped: it enumerates every workspace manifest's `exports`, resolves each subpath under the Node conditions only (`node`/`import`/`default`, walked in declaration order the way Node does, so `types`/`browser`/`bun` are stepped over rather than entered), and dynamically imports each resulting specifier, asserting the module is non-empty. Under `WORKGLOW_TEST_TARGET=dist` that one unit-tier file loads every published bundle; under the default target it costs nothing, since it loads the same source the rest of the suite already does. Adding `workglow` to `packages/test`'s devDependencies is the larger half: it brings the meta-package's own entries and, transitively, the provider bundles those re-export. The enumeration is local rather than shared with `scripts/lib/sourceStubs`: `stubSpecsFor` returns dist targets rather than import specifiers, and `packages/test` is a `composite` project rooted at `./src`, so importing from `scripts/` would put those files in its program and break `build-types`. Anti-vacuity assertions (over 60 entries across over 20 packages, every target `./dist/**.js`) keep a mis-typed walk from passing as a short list, and both exemption maps are staleness-checked against the enumeration. Two exemptions, each with its reason: `@workglow/cli` (uncheckable — an example app `packages/test` does not depend on, so under isolated linking the specifier does not resolve from here at all) and `workglow/auto-bootstrap` (imported, but exempt from the non-empty assertion: it registers providers as a side effect and exports nothing by design). New packages default to checked.
Three findings from the coverage-derivation review, fixed together because
they all come from the same place: a hand-copied list of workspace groups
and a denominator that was not derived from it.
H1 — bundle-identity checking was lost for every non-unit tier. Only
`test-vitest-dist` sets `WORKGLOW_TEST_TARGET=dist`, and it runs the unit
tier, so nothing checks that the built bundles are wired correctly beyond
"the entry loads and exports something". The failure that hides there is
CLASS identity: a provider's `register*Inline` constructs its provider class
from a relative import while that class extends `AiProvider` imported by
specifier, so inlining `@workglow/ai` into `<provider>/ai-runtime.js`
produces an object that is no longer `instanceof` the base every consumer
holds — and nothing goes red.
Two new UNIT-tier files strengthen the dist sweep rather than adding a CI
job (`register*Inline` needs no API key, so this is affordable where it
already runs):
- `PublishedEntryIdentity.test.ts` registers every provider publishing both
`./ai` and `./ai-runtime` and asserts each registered provider is an
instanceof an `AiProvider` that `@workglow/ai` publishes, and serves at
least one run function. Verified non-vacuous by rebuilding
`@workglow/anthropic`'s `ai-runtime` with `@workglow/ai` inlined: the
check fails, naming the package.
- `PublishedEntryExportParity.test.ts` imports each published entry
alongside the source it was built from and compares export NAME sets. A
bundle that lost a re-export still resolves and evaluates, so the existing
"exports something" bound passes over it.
Asserting on the SERVICE registry instead would have been vacuous: the
global DI container is stashed on a `Symbol.for` key so duplicated bundle
copies share one instance, and tokens are plain strings.
M1 — the workspace group list was written out by hand in four places.
`scripts/lib/workspaceGroups.ts` now derives it from the root manifest's
`workspaces` field (Node-portable, since `vitest.config.ts` loads it under
Node) and throws on a pattern that does not reduce to one scannable
directory. `WORKSPACE_GROUPS` is gone; `PACKAGE_GROUPS` and the coverage
`include` globs derive from it. `PublishedEntryImports.test.ts` cannot
import from `scripts/` (composite project rooted at `./src`), so it
re-derives locally, with a comment saying why.
M2 — `examples/web` diluted the denominator. It declares
`publishConfig.access: "none"`, `exports: {}`, and no `main`/`bin`: none of
its source is published API. `WorkspacePackage` grows a `publishes` field
and non-publishing workspaces are subtracted from `coverage.exclude` by
path. The gate is `access: "none"`, NOT `private` — `packages/test`,
`providers/aws` and `providers/cloudflare` are private and the latter two
carry real suites. The two comments justifying the old behavior were
factually wrong and are rewritten.
Also:
- `coverage.exclude` splices `coverageConfigDefaults.exclude`, not
`configDefaults.exclude` (a test-file list); the two entries the latter
was silently supplying are now stated.
- `**/testing/**` dropped from `coverage.exclude`: those 11 files are
published API (`@workglow/task-graph/test`, `@workglow/util/test`).
- `listWorkspacePackages` re-throws a non-ENOENT manifest error naming the
path instead of dropping the package silently.
- The workspace scan and the source-resolving plugin are hoisted out of the
per-project map — one shared, stateless instance instead of ~500 manifest
reads at config load.
The line-95 coverage guard was tautological once both sides derive, so it is
replaced by tests that fail when the derivation is bypassed, when a declared
group scans to nothing, when the denominator stops matching those groups,
when the plugin stops being shared, and when the `examples/web` exclusion
outlives the property that justifies it.
Reported coverage numbers will move: dropping `examples/web`'s 37 files
raises the figure, un-excluding the 11 published `testing/` files nudges it.
Claude-Session: https://claude.ai/code/session_01UW1Qr5mxetAQr61YKEY9nz
Co-authored-by: Claude <noreply@anthropic.com>
sroussey
force-pushed
the
claude/coverage-dist-bundle-fix-ew0vj8
branch
from
August 16, 2026 16:34
758c0b2 to
27b4958
Compare
…st target
Every vitest project now attaches the source-resolving plugin unconditionally,
so under the default target nothing resolves `@workglow/*` through `exports`.
`publish-all`'s test step therefore stopped touching the bundles it is about to
version-bump and push, and `WORKGLOW_TEST_TARGET=dist` existed only as an inline
`env:` block on one CI job.
- `test:vitest:dist` is the one definition of the dist target, unit tier only
(matching the CI job; the other tiers want keys, databases and model
downloads). The variable is set INSIDE the script, so dropping it now means
deleting the script call.
- `publish-all` runs it after `rebuild`/`format` and before `bunset`.
- The `test-vitest-dist` job invokes the script instead of restating the
variable.
The dist run also had no precondition. `use-source` writes stubs carrying
`SOURCE_STUB_SENTINEL` and nothing read it, so under a stubbed dist
`@workglow/ai` and `@workglow/ai/worker` collapse onto one source module: every
cross-entry `instanceof` succeeds trivially and export-name parity compares a
file with itself. `assertNoSourceStubs` now runs at CONFIG LOAD under the dist
target, which kills the whole run before any suite can pass vacuously.
`containsSourceStubSentinel` is the node-portable half of the existing Bun-only
`isSourceStub`, since vitest.config.ts is loaded by Vite under node.
`shared.env` hands the validated target down to the workers: `packages/test` is
a composite program rooted at ./src and cannot import scripts/lib/*, so
re-deriving it there would reintroduce the silent-comparison bug
`resolveTestTarget` exists to remove.
`scripts/` was in no CI type gate — `typecheck:budget` globs packages|providers
and `typecheck:tests` globs packages/*/tsconfig.test.json — while now holding
vitest.config.ts's resolution logic. `tsconfig.scripts.json` + `typecheck:scripts`
close that, and the first run surfaced four real errors, all fixed here:
three `WorkspacePackage` literals missing the required `publishes`, a
`TestProjectConfiguration` callback annotation that ignores the union's string
variant, a missing root `vite` devDependency behind
`import type { Plugin } from "vite"`, and a `coverage.root` key that vitest 4
neither types nor reads (its provider derives coverage roots from the resolved
project configs), so it was inert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…nk it The sweep skipped the exact regression it exists to catch. A candidate whose `ai-runtime` module exports no `register*Inline` was pushed onto `withoutInlineRegistrar` and `continue`d, contributing NO assertion, and the only place that surfaced was a message printed on failure. Sixteen workspace packages publish both `./ai` and `./ai-runtime`, and every bound was `> 4`, so eleven of the sixteen could drop their registrar and leave the file green. Measured rather than reasoned: renaming `registerAnthropicInline` and rebuilding the provider left all six assertions passing, with `@workglow/anthropic` gone from the sweep and nothing saying so. - `NO_INLINE_REGISTRAR` declares the skips, beside `NEEDS_NATIVE_RUNTIME`. Sole member today is `@workglow/mlx`, whose registrar is `registerMlx` — no `Inline` suffix — because `MlxProvider` stays unavailable until an mlx-lm runtime is bundled. `withoutInlineRegistrar` is compared for EQUALITY against its keys, so an undeclared skip fails. - The exemption-pinning test covers both maps: every key names a real candidate, every reason is longer than a word. - `MINIMUM_RUNTIME_CANDIDATES` replaces `candidates.length > 4`, and `checkable.length` is now an equality against candidates minus the declared native-runtime exemptions rather than a second floor. - The `providers.size > 4` bound becomes two statements that cannot be satisfied by a shrunken sweep: the registered package set EQUALS the checkable set minus the declared no-registrar packages, and each registration is checked for at least one provider name, collected into an offenders array so one no-opping registrar reports itself. - The base-class allowance is now two-sided and target-keyed: `dist` requires exactly 2 distinct classes (two bundles really loaded), `source` exactly 1 (the resolution plugin really attached). `<= 2` was satisfied by either, so it could not tell a real dist run from a source run mislabelled as one. This is the in-process proof that the loaded modules are bundles. With the rename still applied the new file fails, naming `@workglow/anthropic` in both the undeclared-skip check and the registered-package set; reverted and rebuilt, it passes on both targets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgxtp7mQECdh7F2UT9CVwN
…d74u5n-published-entry-guards test: declare the identity sweep's skips instead of letting them shrink it
…import `vitest.config.ts` called `assertNoSourceStubs` as a MODULE-LEVEL side effect whenever the target was `dist`, and `scripts/workspaceSource.test.ts` imports that module with `WORKGLOW_TEST_TARGET` stubbed to `dist` to check plugin attachment. `scripts/*.test.ts` is unit-tier, so on a `use-source` tree an ordinary `bun run test:vitest:unit` died there with advice to run `use-dist` — undoing the documented no-build dev mode the run had never left. CI stayed green because CI trees are really built. Two further call sites in the same file import the config with AMBIENT env, so under `test:vitest:dist` they re-ran the whole scan too. The check is now a `configResolved` hook on a guard plugin attached to every project under `dist`. Vitest resolves each project's config at startup, so the run still dies before any suite can report a pass over stubs; the verdict is computed once and re-thrown, so one scan serves all twelve projects instead of printing a 41-entry message twelve times. Rejected alternatives: `buildStart` (vitest does not reliably drive build hooks for a test-only server, while `configResolved` is guaranteed by `resolveConfig` per project); exporting `buildProjects(target)` (leaves the config module itself un-exercised, which is what makes the attachment test worth having); an early check in `scripts/test.ts` (misses `vitest run --project <n>` and adds a second place that knows the rule). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
… anchor coverage globs Four related follow-ups to the dist-target work. **The parity sweep is skipped under the source target.** Under `source` the resolving plugin rewrites `import(specifier)` to exactly the path `sourceCounterpart()` computes, so both sides are the SAME module and ~90 cases assert `X === X`. They now skip, so the report distinguishes "checked" from "not applicable" instead of showing 97 green rows that compared nothing. Nothing stops being loaded: `PublishedEntryImports.test.ts` imports every published specifier unconditionally and does carry signal under source. An anti-vacuity test asserts the target is one of the two known values, so a broken `test.env` cannot make every case skip and leave `test-vitest-dist` green having compared nothing. **The stub guard now covers every entry `use-source` writes.** It read only `exports`, but `use-source` also stubs `bin` targets, and two are named by no export entry at all: `examples/eval` declares NO `exports`, only a `bin`, and `examples/cli`'s `bin` is not among its export targets. Both were stubbed and unguarded, while `publish-workspaces.ts` (a full dist walk) did refuse them. `guardedDistTargets` adds them — deliberately not via `stubSpecsFor`, which also maps each target to a source counterpart and throws for a target that is neither `.js` nor `.d.ts`; the guard has no use for that mapping and should not acquire a new way to fail during config resolution. **An unreadable entry is unproven, not built.** `probeSourceStub` returns three states, because the two callers fail safe in opposite directions: removal must not delete an artifact it could not read, while the dist guard must not accept one as a real bundle. The guard's message has a section for each. **Coverage globs are anchored at the repo root.** A `--project` run — every package's own `test` script, and `turbo run test -- --coverage` — starts in a package directory, so a relative pattern's meaning depends on which directory the provider resolves it against. Measured on `@vitest/coverage-v8` 4.1.10 the two forms give an identical denominator from both the repo root and a package directory, so this is defensive rather than a fix for an observed miss; it is worth pinning because a denominator that silently loses its untested half reads as a BETTER number, not as an error. The per-package `/src/**` exclusions are anchored to match, or they subtract from nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJRf3YFa8DjmjsZvXz8xDT
…hook Run the dist stub guard in a plugin hook; widen it; anchor the coverage globs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Vite plugin that resolves workspace package imports from built bundles to source files during testing, enabling accurate v8 coverage attribution and eliminating duplicate module identities from mixed import graphs.
Changes
scripts/lib/workspaceSource.ts— New module providing:listWorkspacePackages(): Scans workspace groups (packages/,providers/,examples/) to enumerate all packagesdistToSource(): Maps dist entry points back to their source counterparts (e.g.,packages/ai/dist/node.js→packages/ai/src/node.ts)workspaceSourcePlugin(): Vite resolver plugin that intercepts workspace specifier resolution and rewrites dist paths to source paths after normal resolution completesscripts/workspaceSource.test.ts— Comprehensive test suite ensuring:vitest.config.ts— Integration:workspaceSourcePluginto each test project whenWORKGLOW_TEST_TARGET !== "dist"packages/*/src/**/*.{ts,tsx}andproviders/*/src/**/*.{ts,tsx}rather than relying on vitest's default (which omits untested modules).gitignore— Adds coverage output directories (coverage/,.nyc_output/).claude/CLAUDE.md— Documents the coverage strategy andWORKGLOW_TEST_TARGETenvironment variableImplementation Details
The plugin works by letting normal Vite resolution run first (preserving conditional
exportsbehavior for node/browser/bun targets), then rewriting only the resolved path. This approach requires no per-package configuration and automatically covers all packages and subpath exports.The test suite uses
stubSpecsFor()fromsourceStubs.tsto enumerate the same entry points thatuse-sourcestubs from, ensuring the two mechanisms cannot drift into disagreement about what a package exports.WORKGLOW_TEST_TARGET=distrestores the old behavior for verifying bundle integrity; the Bun runner's nativeexportsresolution provides an additional guard via the nightly parity workflow.https://claude.ai/code/session_016khjNuSErZBHzW3aUw2SP4