diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30653784e5..b3dd654b10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -949,26 +949,6 @@ jobs: with: path: .turbo/cache key: turbo-tests-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} - # The miner/MCP packages compile IN PLACE (tsconfig outDir "." -- lib/foo.ts emits lib/foo.js next to - # its source, gitignored). The two --force builds above therefore leave compiled .js siblings for every - # .ts under packages/loopover-{miner,mcp}/{lib,bin} -- and once those exist, Vite resolves the packages' - # NodeNext .js-suffixed import specifiers to the REAL compiled files instead of falling back to the .ts - # source (the fallback vitest.config.ts's coverage globs document depending on). That breaks the - # coverage run twice over: vitest --changed's import-graph tracing can't relate a changed .ts to the - # tests importing it (the module graph holds the .js identity), and executed-module coverage is - # attributed to the .js ids, flatlining every changed .ts to 0% -- the 2026-07-24 "(0%/99%) - # codecov/patch on every packages/** PR" incident, introduced when the 3-shard consolidation moved - # these builds into the same job as the coverage run (the shards never built these packages). - # Reproduced/bisected locally on vitest 4.1.9: with lib/*.js present the scoped run selected 1 test - # file and reported the changed attempt-cli.ts at 0%; after this clean it selected 4 and reported 100%. - # Nothing in the coverage run needs the built output at runtime: both CLI harnesses spawn the .ts - # entrypoints via --experimental-strip-types (test/unit/support/{mcp,miner}-cli-harness.ts), and the - # one artifact-dependent test (check-miner-package.test.ts's npm-pack dry-run) is excluded from the - # coverage run below and re-run after a rebuild in "Pack-integrity test" at the end of this job. - # -fdX removes ONLY gitignored files, so tracked sources are untouchable by construction; the binary - # verification above already ran, and "Save Turborepo cache" already captured the build outputs. - - name: Remove in-place package build output before coverage - run: git clean -fdX packages/loopover-miner packages/loopover-mcp - name: Prepare test reports dir run: mkdir -p reports/junit - name: Test with coverage @@ -1032,16 +1012,10 @@ jobs: --exclude "test/unit/mcp-discovery.test.ts" ) fi - # check-miner-package.test.ts is ALWAYS excluded from the coverage run -- its npm-pack dry-run - # needs the in-place build output that "Remove in-place package build output before coverage" - # just deleted (see that step's comment for why the emit must not exist here). It runs in - # "Pack-integrity test" at the end of this job instead, after a rebuild, with coverage disabled -- - # it only spawns scripts/check-miner-package.ts as a subprocess, so it never contributed - # v8-visible coverage in this run anyway. - EXCLUDE_ARGS+=(--exclude "test/unit/check-miner-package.test.ts") if [ "$SKIP_MINER_TEST_HARNESS" = "true" ]; then echo "Skipping the self-contained miner-package tests (no packages/loopover-miner-relevant paths changed)." EXCLUDE_ARGS+=( + --exclude "test/unit/check-miner-package.test.ts" --exclude "test/unit/miner-calibration-types.test.ts" ) fi @@ -1176,18 +1150,6 @@ jobs: override_commit: ${{ github.event.pull_request.head.sha }} override_pr: ${{ github.event.pull_request.number }} fail_ci_if_error: false - # Rebuild the in-place emit (deleted before the coverage run -- see "Remove in-place package build - # output before coverage") and run the one test that genuinely needs it: check-miner-package.test.ts's - # npm-pack dry-run over the built package layout. Coverage stays disabled -- the test only spawns - # scripts/check-miner-package.ts as a subprocess, so excluding it from the coverage run loses nothing. - # Gated exactly like its old in-suite inclusion: every push, or a PR whose changed paths matched the - # minerTestHarness filter (the same condition SKIP_MINER_TEST_HARNESS negates above). The implicit - # success() on this if means it never runs after a failed suite, matching the old in-suite behavior. - - name: Pack-integrity test (rebuilds in-place emit) - if: ${{ github.event_name == 'push' || needs.changes.outputs.minerTestHarness == 'true' }} - run: | - npx turbo run build:tsc build:verify --filter=@loopover/miner --force - npx vitest run --coverage.enabled=false test/unit/check-miner-package.test.ts # Single required status check. Branch protection points at "validate"; this # aggregates the path-aware jobs (the PR-only dependency review gate lives inside diff --git a/vitest.config.ts b/vitest.config.ts index a42547bfca..ca03371938 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,38 @@ +import { existsSync } from "node:fs"; +import { dirname, isAbsolute, resolve as resolvePath } from "node:path"; import { defineConfig } from "vitest/config"; const junitPath = process.env.VITEST_JUNIT_PATH; +// In-process imports must resolve the miner/mcp packages' NodeNext ".js"-suffixed specifiers to the +// .ts SOURCE even when the in-place compiled .js exists on disk (both packages emit next to their +// sources -- tsconfig outDir "."; CI builds them before the coverage run, and local checkouts +// accumulate the same gitignored emit). Without this, Vite resolves the literal .js file, v8 +// coverage attributes every hit to the built-file identity (flatlining the .ts to 0% -- the +// 2026-07-24 codecov/patch (0%/99%) incident), and --changed's import-graph tracing can't relate a +// changed .ts to the tests importing it. Deleting the emit before the coverage run instead (the +// first attempt at fixing that incident) broke the OTHER class of tests: subprocess-spawning ones +// (the CLI harnesses and the MCP stdio tests) run under plain Node outside Vite's resolver and +// genuinely need the built .js at spawn time. This plugin serves both: in-process resolution always +// lands on the .ts (correct identity for coverage + tracing), while spawned subprocesses keep the +// built artifacts on disk. Scoped to exactly the two in-place-emit packages -- engine emits to +// dist/ and must keep resolving through its package boundary unchanged. +const IN_PLACE_EMIT_RE = /packages\/loopover-(?:miner|mcp)\/(?:lib|bin)\/.*\.js$/; +const preferTsSourceForInPlaceEmit = { + name: "loopover:prefer-ts-source-for-in-place-emit", + enforce: "pre" as const, + resolveId(source: string, importer: string | undefined) { + if (!importer || !source.endsWith(".js")) return null; + if (!source.startsWith("./") && !source.startsWith("../") && !isAbsolute(source)) return null; + const candidate = isAbsolute(source) ? source : resolvePath(dirname(importer), source); + if (!IN_PLACE_EMIT_RE.test(candidate)) return null; + const tsSibling = `${candidate.slice(0, -3)}.ts`; + return existsSync(tsSibling) ? tsSibling : null; + }, +}; + export default defineConfig({ + plugins: [preferTsSourceForInPlaceEmit], ssr: { noExternal: ["agents", "partyserver"], },