diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97e57b7..4675ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,13 @@ jobs: node-version: ${{ matrix.node }} cache: npm - run: npm ci - - name: Run categorized tests + - name: Run Vitest run: npm run test:ci - name: Upload JUnit test results if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }}-node-${{ matrix.node }} - path: test-results/*.xml + path: test-results/vitest.junit.xml if-no-files-found: error - run: npm run pack:verify diff --git a/AGENTS.md b/AGENTS.md index afd1b31..ec41d6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,7 @@ Adding support for a new Coding Agent host starts with - All relative `.md` references across `skills/`, `references/`, `templates/`, `models/`, `docs/`, and `case-studies/` must resolve; `test/skills-docs/doc-link-graph.test.mjs` enforces this in `npm test`. -- After adding, moving, or renaming markdown docs, run `node --test test/skills-docs/doc-link-graph.test.mjs` before committing, +- After adding, moving, or renaming markdown docs, run `npx vitest run test/skills-docs/doc-link-graph.test.mjs` before committing, and regenerate the routing graph with `node scripts/doc-link-graph/cli.mjs skills/better-harness` (it rewrites `docs/better-harness-doc-links.mmd`, which the test checks for staleness). - Every reference doc shipped under `skills/better-harness/references/` must stay reachable from `SKILL.md` routing, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad36763..8b0c526 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ Useful focused checks: ```bash # Markdown links and the generated Harness routing graph -node --test test/skills-docs/doc-link-graph.test.mjs +npx vitest run test/skills-docs/doc-link-graph.test.mjs # Package and runtime-bundle boundaries npm run pack:verify @@ -95,8 +95,8 @@ unrelated cleanup, dependency changes, or local host state into the same change. Run the smallest relevant tests while developing, then the broader gate justified by the risk: -- Markdown moves or links: `node --test test/skills-docs/doc-link-graph.test.mjs`. -- Runtime logic: focused `node --test test/.test.mjs`, then `npm test`. +- Markdown moves or links: `npx vitest run test/skills-docs/doc-link-graph.test.mjs`. +- Runtime logic: focused `npx vitest run test/.test.mjs`, then `npm test`. - Package/runtime roots: `npm run pack:verify`. - Visual output: preview smoke, console inspection, and screenshot review. - Cross-platform command changes: verify argv-based execution and avoid diff --git a/docs/adapters/contributing-new-coding-agent.md b/docs/adapters/contributing-new-coding-agent.md index a6afe12..e735267 100644 --- a/docs/adapters/contributing-new-coding-agent.md +++ b/docs/adapters/contributing-new-coding-agent.md @@ -210,7 +210,7 @@ moved, regenerate and verify the link graph: ```bash node scripts/doc-link-graph/cli.mjs skills/better-harness -node --test test/skills-docs/doc-link-graph.test.mjs +npx vitest run test/skills-docs/doc-link-graph.test.mjs npm test npm run pack:verify git diff --check diff --git a/docs/community.md b/docs/community.md index 9596431..d3ab38f 100644 --- a/docs/community.md +++ b/docs/community.md @@ -30,7 +30,7 @@ This is the complete reference. For the common cases, see Start Here above. | Skill detail and reusable guidance | Yes | `skills//references/` or shared `references/` | Topic-scoped Markdown with stable headings and source boundaries | Loaded only when the skill or agent task needs it | Link/path check; consumer grep for any 2+ consumer claim | | Maturity model | Yes, additive only | `models/.md` plus `models/routing.md` | Stable `model_id`, aliases, audience, levels, dimensions, evidence sources, scoring and confidence rules | Selectable through model routing after routing-file registration | Model-routing checks, fixture/report sample, no mutation of built-in defaults unless intentional | | Detector or analysis signal | Yes | owning `models/.md`, `scripts//`, or `skills//references/` | Detector or signal id when stable, source evidence, emitted fields, false-positive and downgrade rules | Selected through model, capability, or skill routing; shared prose needs two visible workflow consumers | Fixtures or sample evidence ledger plus behavior, report-quality, or model-routing checks | -| Executable analysis capability | Yes, but only with tests | `scripts//cli.mjs` | CLI contract, JSON output shape, fixtures, scoped helpers | Called by skills, hooks, reports, or host adapters | `node --test`, fixture assertions, cross-platform command construction | +| Executable analysis capability | Yes, but only with tests | `scripts//cli.mjs` | CLI contract, JSON output shape, fixtures, scoped helpers | Called by skills, hooks, reports, or host adapters | `npm test` or focused `npx vitest run`, fixture assertions, cross-platform command construction | | Host evidence adapter | Yes | `scripts//platforms/.mjs` and `docs/adapters/README.md` | Matrix row with discovery paths, normalized evidence shape, smoke command, and split trigger if needed | Capability-specific host collection; not packaging | Host matrix entry, smoke command or explicit unavailable note | | Lifecycle enforcement | Yes, narrowly | `hooks/hooks.json.template` and `hooks/git-scripts//` | Hook event, mode, command contract, expected failure behavior | Installed from the template into host lifecycle points | Hook fixture/test or dry-run output; no shell-specific assumptions | | Reporting template family | Yes, with routing and runtime tests | `templates/reporting/` | `report-structure.md` owns Markdown structure; mode files own metadata, companion rules, imports/props, and validation | Selected by report generation and output mode | Template tests, parser checks, path checks, and visual preview/smoke when relevant | diff --git a/docs/specs/2026-08-13-vitest-migration.md b/docs/specs/2026-08-13-vitest-migration.md new file mode 100644 index 0000000..c60c3d1 --- /dev/null +++ b/docs/specs/2026-08-13-vitest-migration.md @@ -0,0 +1,97 @@ +# Make test failures directly visible + +## Traceability + +- Spec ID: vitest-migration +- Status: Implemented +- Supersedes the CI runner/reporting slice of + [Make test failures concise and actionable](2026-08-13-test-ci-diagnostics.md). + +## Intent + +Replace the repository's custom `node:test` CI orchestration with Vitest so a +failed GitHub Actions step names the failing file and test, prints the useful +assertion stack in the main log, links an annotation to the source, and retains +downloadable JUnit evidence. Remove the custom reporting lifecycle that reduced +successful output but hid the identity of failures behind dots and artifacts. + +## Acceptance Scenarios + +- AC-1: `npm test` runs the complete `test/**/*.test.mjs` population through + Vitest on the declared Node.js versions without requiring tests to adopt + Vitest assertions or mocks. +- AC-2: An intentionally failing isolated test produces a main-log summary + containing its relative file, full test name, assertion message, and source + location; diagnosing the failure does not require opening an artifact. +- AC-3: GitHub Actions uses Vitest's built-in default and `github-actions` + reporters plus its JUnit reporter, uploads the JUnit file on success or + failure, and publishes a test Job Summary. +- AC-4: The repository no longer owns a custom test scheduler or reporter for + ordinary CI execution; `test/support/run-ci.mjs` and + `test/support/github-actions-reporter.mjs` are removed. +- AC-5: Existing Node assertion behavior, dynamic skip behavior, per-test + cleanup, suite setup/teardown, filename filtering, and exit codes remain + observable after migration. +- AC-6: CI passes on Linux Node 22.20.0, macOS Node 22.20.0, Windows Node + 22.20.0, and Linux Node 24.x from the exact pushed head. +- AC-7: `npm run pack:verify` continues to exclude test infrastructure and + development dependencies from the published runtime artifact. + +## Non-goals + +- Rewriting Node `assert` calls to Vitest `expect` matchers. +- Adding coverage thresholds, browser tests, snapshots, or TypeScript test + compilation. +- Changing product behavior or weakening platform-specific tests. +- Keeping capability-by-capability subprocess scheduling when Vitest can report + file and suite ownership directly. + +## Plan and Tasks + +1. Add the exact stable Vitest version as a development dependency and add + local and CI configurations for the existing `.test.mjs` population. +2. Mechanically replace `node:test` imports with Vitest imports. Map per-test + cleanup from `t.after` to `t.onTestFinished`, retain context `skip`, and map + suite hooks to `beforeAll` and `afterAll`. +3. Replace `npm test` and `npm run test:ci` with Vitest run commands. Configure + the CI route with `default`, `github-actions`, and JUnit reporters so failures + stay human-readable while machine artifacts remain available. +4. Update the GitHub Actions upload boundary for the single Vitest JUnit file + and remove the custom runner and reporter. +5. Validate one isolated intentional failure, focused migrated tests, the full + suite, package verification, documentation links, and the exact-head CI + matrix before release. + +Decision rationale: Vitest 4.1.10 supports the repository's Node.js range and +ships the three required reporting surfaces. The test corpus primarily uses +`node:assert/strict`, so the migration changes lifecycle registration while +leaving nearly all assertions and fixtures unchanged. + +## Test and Review Evidence + +- AC-1/AC-5: `npm test` passes all 91 files and 1,305 Vitest cases. The five + result-count difference from the 1,310 Node baseline is accounted for by + removing four custom-runner component tests and representing the two adapter + cases directly instead of counting their former parent subtest. All 91 test + files import Vitest, and no `node:test`, `t.after`, or `context.test` route + remains. Focused lifecycle coverage passes 79/79 across cleanup, dynamic + skip, and filesystem-heavy suites. +- AC-2: the reporting contract launches Vitest against an intentionally failing + temporary fixture and passes only when the captured main log contains the + relative file, full test name, Node assertion message, and line 3 location. +- AC-3: a focused `npm run test:ci` writes and parses + `test-results/vitest.junit.xml`. A GitHub Actions environment simulation also + writes a non-empty Job Summary with the one-test pass result. +- AC-4: both custom support modules are absent, and no non-historical reference + to either module remains. +- AC-6: the exact-head four-platform PR matrix remains the merge gate; local + evidence does not claim remote Windows, macOS, or Linux status. +- AC-7: `npm run pack:verify` passes with 508 npm entries and 530 runtime zip + entries. Test files, Vitest configuration, and installed development modules + remain outside both artifacts. +- Documentation and installation: `npm ci` succeeds from the lockfile; the + regenerated documentation graph contains 39 files and 56 links, and its + focused suite passes 6/6. `git diff --check` is clean. +- Risk: Vitest file parallelism and cleanup timing can differ from `node:test`. + The fork pool preserves process isolation, cleanup is migrated explicitly, + and the full cross-platform suite remains the release gate. diff --git a/package-lock.json b/package-lock.json index 6eb11ec..6d021f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,17 +15,489 @@ "bin": { "better-harness": "scripts/better-harness.mjs" }, + "devDependencies": { + "vitest": "4.1.10" + }, "engines": { "node": ">=22.20.0 <25.0.0", "npm": ">=10.9.3 <12.0.0" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vscode/tree-sitter-wasm": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", "integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild-wasm": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.28.1.tgz", @@ -37,6 +509,724 @@ "engines": { "node": ">=18" } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index b6b800c..51f9920 100644 --- a/package.json +++ b/package.json @@ -80,8 +80,8 @@ "prepublishOnly": "npm run check", "preview": "node scripts/harness-analysis/canvas-preview-server.mjs", "preview:canvas": "node scripts/harness-analysis/canvas-preview-server.mjs", - "test": "node --test \"test/**/*.test.mjs\"", - "test:ci": "node test/support/run-ci.mjs" + "test": "vitest run", + "test:ci": "vitest run --config vitest.ci.config.mjs" }, "packageManager": "npm@10.9.3", "publishConfig": { @@ -96,5 +96,8 @@ "dependencies": { "@vscode/tree-sitter-wasm": "0.3.1", "esbuild-wasm": "0.28.1" + }, + "devDependencies": { + "vitest": "4.1.10" } } diff --git a/references/agent-customize/skill-eval.md b/references/agent-customize/skill-eval.md index a462b19..55001da 100644 --- a/references/agent-customize/skill-eval.md +++ b/references/agent-customize/skill-eval.md @@ -71,7 +71,7 @@ never edit the canonical Plugin Eval result to make the tools agree. For the repository's own `skills/better-harness` target, also run: ```text -node --test test/skills-docs/better-harness-skill.test.mjs test/skills-docs/doc-link-graph.test.mjs +npx vitest run test/skills-docs/better-harness-skill.test.mjs test/skills-docs/doc-link-graph.test.mjs node scripts/doc-link-graph/cli.mjs skills/better-harness npm run pack:verify ``` diff --git a/test/README.md b/test/README.md index d4143dd..931127a 100644 --- a/test/README.md +++ b/test/README.md @@ -2,9 +2,9 @@ Tests are grouped by the production capability that owns the behavior. Keep a new test beside its primary owner even when it crosses unit, integration, and -CLI boundaries. Shared fixtures stay in `test/fixtures`. CI-only reporters and -the categorized runner stay in `test/support`; they are not published with the -package and are not discovered as tests. +CLI boundaries. Shared fixtures stay in `test/fixtures`. Vitest discovers tests +directly from the capability directories; test support code is not published +with the package. ## Categories @@ -32,7 +32,8 @@ Run the complete suite: npm test ``` -Run the CI presentation locally, including compact progress and JUnit files: +Run the CI presentation locally, including human-readable failures, GitHub +Actions reporting when applicable, and the JUnit file: ```sh npm run test:ci @@ -41,16 +42,16 @@ npm run test:ci Run one capability: ```sh -node --test "test/sessions/*.test.mjs" -node --test "test/reporting/*.test.mjs" +npx vitest run test/sessions +npx vitest run test/reporting ``` Run one file: ```sh -node --test test/plugins/plugin-lifecycle.test.mjs +npx vitest run test/plugins/plugin-lifecycle.test.mjs ``` -Do not add a second test manifest. Both local and CI runners discover -`*.test.mjs` files from the capability directories; this document explains +Do not add a second test manifest. Both local and CI commands use the same +Vitest discovery configuration for `*.test.mjs` files; this document explains ownership and routing. diff --git a/test/agents/agent-asset-baseline.test.mjs b/test/agents/agent-asset-baseline.test.mjs index 72385b7..187c743 100644 --- a/test/agents/agent-asset-baseline.test.mjs +++ b/test/agents/agent-asset-baseline.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { ASSET_BASELINE_KIND, diff --git a/test/agents/agent-asset-integrity.test.mjs b/test/agents/agent-asset-integrity.test.mjs index 49e7685..8dde479 100644 --- a/test/agents/agent-asset-integrity.test.mjs +++ b/test/agents/agent-asset-integrity.test.mjs @@ -5,7 +5,7 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import test from "node:test"; +import { test } from "vitest"; import { ASSET_INTEGRITY_PROFILE, diff --git a/test/agents/agent-customize-architecture.test.mjs b/test/agents/agent-customize-architecture.test.mjs index fc3f850..c67d6b7 100644 --- a/test/agents/agent-customize-architecture.test.mjs +++ b/test/agents/agent-customize-architecture.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import * as inventory from "../../scripts/agent-customize/inventory.mjs"; import { diff --git a/test/agents/agent-customize.test.mjs b/test/agents/agent-customize.test.mjs index 7f96410..6297737 100644 --- a/test/agents/agent-customize.test.mjs +++ b/test/agents/agent-customize.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { collectAgentCustomizeInventory, diff --git a/test/agents/agent-guardrails-secret-scan.test.mjs b/test/agents/agent-guardrails-secret-scan.test.mjs index 6eda764..9938823 100644 --- a/test/agents/agent-guardrails-secret-scan.test.mjs +++ b/test/agents/agent-guardrails-secret-scan.test.mjs @@ -4,7 +4,7 @@ import { promises as fsPromises } from "node:fs"; import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { handleHookEvent, @@ -158,7 +158,7 @@ test("workspace-contained scanning refuses a swap to an external symlink at read const outsidePath = path.join(root, "outside.yaml"); const probePath = path.join(workspace, "symlink-probe"); const outsideKey = syntheticOpenAiKey(); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await mkdir(workspace, { recursive: true }); await writeFile(configPath, "name: safe\n"); await writeFile(outsidePath, `OPENAI_API_KEY=${outsideKey}\n`); @@ -261,7 +261,7 @@ test("secret-scan CLI hook mode reads JSON stdin and emits a block payload", () test("secret-scan CLI fails closed when an explicit path cannot be scanned", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-secret-scan-missing-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); const result = spawnSync(process.execPath, [scannerCli, "missing.txt", "--json"], { cwd: root, encoding: "utf8", @@ -276,7 +276,7 @@ test("secret-scan CLI fails closed when an explicit path cannot be scanned", asy test("secret-scan CLI reports complete coverage and exits zero for a clean explicit file", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-secret-scan-clean-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await writeFile(path.join(root, "clean.txt"), "ordinary fixture text\n"); const result = spawnSync(process.execPath, [scannerCli, "clean.txt", "--json"], { cwd: root, @@ -295,7 +295,7 @@ test("secret-scan CLI treats an explicitly supplied symbolic-link file as incomp const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-secret-scan-link-")); const target = path.join(root, "target.txt"); const link = path.join(root, "linked.txt"); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await writeFile(target, "safe fixture\n"); try { await symlink(target, link); @@ -320,7 +320,7 @@ test("secret-scan CLI treats symbolic links nested in a scanned directory as inc const scanRoot = path.join(root, "scan"); const target = path.join(root, "target.txt"); const link = path.join(scanRoot, "linked.txt"); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await mkdir(scanRoot, { recursive: true }); await writeFile(target, "safe fixture\n"); try { diff --git a/test/agents/agent-lint-monorepo.test.mjs b/test/agents/agent-lint-monorepo.test.mjs index 9ceacb8..b1c0500 100644 --- a/test/agents/agent-lint-monorepo.test.mjs +++ b/test/agents/agent-lint-monorepo.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { collectAgentInstructionGraph, diff --git a/test/agents/agent-lint.test.mjs b/test/agents/agent-lint.test.mjs index acd72b2..c2a6540 100644 --- a/test/agents/agent-lint.test.mjs +++ b/test/agents/agent-lint.test.mjs @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promis import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import test from "node:test"; +import { test } from "vitest"; import { collectAgentInstructionGraph, diff --git a/test/agents/coding-agent-asset-eval.test.mjs b/test/agents/coding-agent-asset-eval.test.mjs index 18a1fb7..866b6f0 100644 --- a/test/agents/coding-agent-asset-eval.test.mjs +++ b/test/agents/coding-agent-asset-eval.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { attachAssetEvaluations, diff --git a/test/agents/coding-agent-practices-inventory.test.mjs b/test/agents/coding-agent-practices-inventory.test.mjs index 7bf9c40..1eb0896 100644 --- a/test/agents/coding-agent-practices-inventory.test.mjs +++ b/test/agents/coding-agent-practices-inventory.test.mjs @@ -4,7 +4,7 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import test from "node:test"; +import { test } from "vitest"; import { collectQoderInventory } from "../../scripts/coding-agent-practices/inventory.mjs"; diff --git a/test/agents/harness-checkup.test.mjs b/test/agents/harness-checkup.test.mjs index 6d02c53..8b4ff3b 100644 --- a/test/agents/harness-checkup.test.mjs +++ b/test/agents/harness-checkup.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { applySourcePatch, @@ -670,7 +670,7 @@ test("source patch rejects symbolic links in the target path", async (t) => { "workspace:linked/settings.json": sha256(original), }, }; - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await mkdir(workspace, { recursive: true }); await mkdir(outside, { recursive: true }); await writeFile(path.join(outside, "settings.json"), original); @@ -709,7 +709,7 @@ test("source patch rejects a symbolic-link backup root", async (t) => { "workspace:settings.json": sha256(original), }, }; - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await mkdir(workspace, { recursive: true }); await mkdir(outside, { recursive: true }); await writeFile(sourcePath, original); diff --git a/test/cli/better-harness-cli.test.mjs b/test/cli/better-harness-cli.test.mjs index a596454..fa95009 100644 --- a/test/cli/better-harness-cli.test.mjs +++ b/test/cli/better-harness-cli.test.mjs @@ -3,7 +3,7 @@ import { spawn, spawnSync } from "node:child_process"; import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { pathToFileURL } from "node:url"; import { main, resolveDispatch } from "../../scripts/better-harness-cli/cli.mjs"; import { commandInventory } from "../../scripts/better-harness-cli/registry.mjs"; @@ -1109,7 +1109,7 @@ test("cloc CLI runs from a spaced symlink installation path", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "better-harness cloc install-")); const linkedClocDir = path.join(root, "linked cloc"); const workspace = path.join(root, "workspace with spaces"); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); await writeFixtureFile(workspace, "src/app.mjs", "export const value = 1;\n"); try { await symlink( diff --git a/test/cli/harness-doctor.test.mjs b/test/cli/harness-doctor.test.mjs index 3dacf96..78d7b3a 100644 --- a/test/cli/harness-doctor.test.mjs +++ b/test/cli/harness-doctor.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { chmod, mkdir, mkdtemp, rm, unlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { renderDoctor } from "../../scripts/harness-doctor/cli.mjs"; import { runHarnessDoctor } from "../../scripts/harness-doctor/index.mjs"; diff --git a/test/cli/harness-quickstart.test.mjs b/test/cli/harness-quickstart.test.mjs index 280f701..2bba2d5 100644 --- a/test/cli/harness-quickstart.test.mjs +++ b/test/cli/harness-quickstart.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { workspaceToQoderSlug } from "../../scripts/session-analysis/platforms/qoder.mjs"; diff --git a/test/cli/read-only-command.test.mjs b/test/cli/read-only-command.test.mjs index 7492054..adc456a 100644 --- a/test/cli/read-only-command.test.mjs +++ b/test/cli/read-only-command.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { LifecycleCliError, diff --git a/test/cli/terminal-demo.test.mjs b/test/cli/terminal-demo.test.mjs index 1491226..3e3ed82 100644 --- a/test/cli/terminal-demo.test.mjs +++ b/test/cli/terminal-demo.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { discoverHistoryRoots, diff --git a/test/governance/ai-contribution-census.test.mjs b/test/governance/ai-contribution-census.test.mjs index f24a4fb..4fa5303 100644 --- a/test/governance/ai-contribution-census.test.mjs +++ b/test/governance/ai-contribution-census.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { after, before, describe, test } from "node:test"; +import { afterAll, beforeAll, describe, test } from "vitest"; import { analyzeAiContributionCensus, @@ -116,11 +116,11 @@ async function makeFixtureRepo() { describe("AI contribution census", { concurrency: false }, () => { let repo; - before(async () => { + beforeAll(async () => { repo = await makeFixtureRepo(); }); - after(async () => { + afterAll(async () => { await rm(repo, { recursive: true, force: true }); }); diff --git a/test/governance/blast-radius.test.mjs b/test/governance/blast-radius.test.mjs index 4d0573a..d8aff67 100644 --- a/test/governance/blast-radius.test.mjs +++ b/test/governance/blast-radius.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { analyzeRepository, diff --git a/test/governance/cloc.test.mjs b/test/governance/cloc.test.mjs index 078774a..e72cc5d 100644 --- a/test/governance/cloc.test.mjs +++ b/test/governance/cloc.test.mjs @@ -4,7 +4,7 @@ import { existsSync } from "node:fs"; import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { analyzeCloc } from "../../scripts/cloc/analyze.mjs"; import { countFile, countFilesSummary } from "../../scripts/cloc/count-file.mjs"; diff --git a/test/governance/core-change-watch-scope.test.mjs b/test/governance/core-change-watch-scope.test.mjs index 36870ae..29b439f 100644 --- a/test/governance/core-change-watch-scope.test.mjs +++ b/test/governance/core-change-watch-scope.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { assertCompatibleAnalysisScope, diff --git a/test/governance/core-change-watch.test.mjs b/test/governance/core-change-watch.test.mjs index bffb5ab..d9b1f4e 100644 --- a/test/governance/core-change-watch.test.mjs +++ b/test/governance/core-change-watch.test.mjs @@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { fileRoleFor } from "../../scripts/core-change-watch/common.mjs"; import { analyzeChangeDrift } from "../../scripts/core-change-watch/change-drift.mjs"; diff --git a/test/governance/dependency-governance.test.mjs b/test/governance/dependency-governance.test.mjs index ae544fa..d113365 100644 --- a/test/governance/dependency-governance.test.mjs +++ b/test/governance/dependency-governance.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { analyzeDependencyGovernance, diff --git a/test/governance/review-trigger.test.mjs b/test/governance/review-trigger.test.mjs index 91eb767..b0a2169 100644 --- a/test/governance/review-trigger.test.mjs +++ b/test/governance/review-trigger.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { buildChangeTestEvidenceFindings, diff --git a/test/governance/scripts-refactor-contract.test.mjs b/test/governance/scripts-refactor-contract.test.mjs index b871a3d..e7feb70 100644 --- a/test/governance/scripts-refactor-contract.test.mjs +++ b/test/governance/scripts-refactor-contract.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; const ROOT = process.cwd(); const CLI = path.join(ROOT, "scripts", "better-harness.mjs"); diff --git a/test/governance/test-ci-reporting.test.mjs b/test/governance/test-ci-reporting.test.mjs deleted file mode 100644 index 0e1e488..0000000 --- a/test/governance/test-ci-reporting.test.mjs +++ /dev/null @@ -1,126 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import test from "node:test"; -import { fileURLToPath } from "node:url"; - -import { githubFailureAnnotation } from "../support/github-actions-reporter.mjs"; -import { discoverTestCategories, nodeTestFileArgument, runCiTests } from "../support/run-ci.mjs"; - -const REPORTER_PATH = fileURLToPath(new URL("../support/github-actions-reporter.mjs", import.meta.url)); - -test("CI test discovery finds capability tests without treating support or fixtures as categories", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-test-discovery-")); - try { - await mkdir(path.join(root, "sessions"), { recursive: true }); - await mkdir(path.join(root, "support"), { recursive: true }); - await mkdir(path.join(root, "fixtures"), { recursive: true }); - await writeFile(path.join(root, "sessions", "usage.test.mjs"), "export {};\n"); - await writeFile(path.join(root, "support", "runner.mjs"), "export {};\n"); - await writeFile(path.join(root, "fixtures", "helper.mjs"), "export {};\n"); - - assert.deepEqual(await discoverTestCategories(root), [{ - name: "sessions", - files: [path.join(root, "sessions", "usage.test.mjs")], - }]); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -test("CI test files use portable relative arguments on Windows", () => { - assert.equal(nodeTestFileArgument( - "D:\\a\\better-harness\\better-harness\\test\\agents\\example.test.mjs", - { - cwd: "D:\\a\\better-harness\\better-harness", - pathApi: path.win32, - }, - ), "./test/agents/example.test.mjs"); -}); - -test("GitHub failure annotations retain location and escape command data", () => { - const annotation = githubFailureAnnotation({ - name: "rejects bad, input: safely", - file: path.join(process.cwd(), "test", "governance", "example.test.mjs"), - line: 17, - column: 4, - details: { error: new Error("first line\nsecond 100%") }, - }); - - assert.ok(annotation.startsWith("::error file=test/governance/example.test.mjs,line=17,col=4,")); - assert.ok(annotation.includes("title=Test failed%3A rejects bad%2C input%3A safely")); - assert.ok(annotation.includes("first line%0Asecond 100%25")); -}); - -test("the custom reporter turns a real Node test failure into one GitHub annotation", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-test-reporter-")); - const failingTest = path.join(root, "failure.test.mjs"); - try { - await writeFile(failingTest, [ - 'import assert from "node:assert/strict";', - 'import test from "node:test";', - 'test("example reporter failure", () => assert.equal(1, 2));', - "", - ].join("\n")); - const childEnv = { ...process.env, GITHUB_ACTIONS: "true" }; - delete childEnv.NODE_TEST_CONTEXT; - const result = spawnSync(process.execPath, [ - "--test", - `--test-reporter=${REPORTER_PATH}`, - "--test-reporter-destination=stdout", - failingTest, - ], { - cwd: root, - encoding: "utf8", - env: childEnv, - }); - - assert.equal(result.status, 1, `${result.stdout}\n${result.stderr}`); - assert.equal(result.stdout.split("::error ").length - 1, 1, `${result.stdout}\n${result.stderr}`); - assert.ok(result.stdout.includes("title=Test failed%3A example reporter failure")); - assert.ok(result.stdout.includes("Expected values to be strictly equal")); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -test("the CI runner continues after a failed capability and summarizes every result", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-test-runner-")); - const resultsRoot = path.join(root, "results"); - const summaryPath = path.join(root, "summary.md"); - try { - await mkdir(path.join(root, "alpha")); - await mkdir(path.join(root, "omega")); - await writeFile(path.join(root, "alpha", "failure.test.mjs"), [ - 'import assert from "node:assert/strict";', - 'import test from "node:test";', - 'test("intentional failure", () => assert.equal(1, 2));', - "", - ].join("\n")); - await writeFile(path.join(root, "omega", "success.test.mjs"), [ - 'import test from "node:test";', - 'test("success after failure", () => {});', - "", - ].join("\n")); - - const status = await runCiTests([], { - cwd: root, - env: { ...process.env, GITHUB_ACTIONS: "false", GITHUB_STEP_SUMMARY: summaryPath }, - resultsRoot, - stderr: { write: () => true }, - stdio: "pipe", - stdout: { write: () => true }, - testRoot: root, - }); - - assert.equal(status, 1); - assert.deepEqual((await readdir(resultsRoot)).sort(), ["alpha.xml", "omega.xml"]); - const summary = await readFile(summaryPath, "utf8"); - assert.ok(summary.includes("| alpha | 1 | fail |")); - assert.ok(summary.includes("| omega | 1 | pass |")); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); diff --git a/test/governance/test-mapping.test.mjs b/test/governance/test-mapping.test.mjs index 6ec0e4b..fc540e0 100644 --- a/test/governance/test-mapping.test.mjs +++ b/test/governance/test-mapping.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { analyzeTestMappings, runHook } from "../../hooks/git-scripts/mapping-gate.mjs"; diff --git a/test/governance/vitest-reporting.test.mjs b/test/governance/vitest-reporting.test.mjs new file mode 100644 index 0000000..5bcb899 --- /dev/null +++ b/test/governance/vitest-reporting.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "vitest"; + +const ROOT = fileURLToPath(new URL("../../", import.meta.url)); +const VITEST_CLI_PATH = fileURLToPath(new URL("../../node_modules/vitest/vitest.mjs", import.meta.url)); + +test("Vitest's default reporter identifies an intentional failure in the main log", async () => { + const fixtureName = `.vitest-reporting-${randomUUID()}.test.mjs`; + const fixturePath = path.join(ROOT, "test", "governance", fixtureName); + const relativeFixture = path.relative(ROOT, fixturePath).split(path.sep).join("/"); + + try { + await writeFile(fixturePath, [ + 'import assert from "node:assert/strict";', + 'import { test } from "vitest";', + 'test("intentional visible failure", () => assert.equal(1, 2));', + "", + ].join("\n")); + + const childEnv = { + ...process.env, + FORCE_COLOR: "0", + NO_COLOR: "1", + }; + for (const key of ["VITEST", "VITEST_POOL_ID", "VITEST_WORKER_ID"]) { + delete childEnv[key]; + } + + const result = spawnSync(process.execPath, [ + VITEST_CLI_PATH, + "run", + relativeFixture, + "--reporter=default", + ], { + cwd: ROOT, + encoding: "utf8", + env: childEnv, + }); + const output = `${result.stdout}\n${result.stderr}`; + + assert.equal(result.status, 1, output); + assert.ok(output.includes(relativeFixture), output); + assert.ok(output.includes("intentional visible failure"), output); + assert.ok(output.includes("Expected values to be strictly equal"), output); + assert.ok(output.includes(`${fixtureName}:3:`), output); + } finally { + await rm(fixturePath, { force: true }); + } +}); diff --git a/test/governance/workflow-demand-diagnostics.test.mjs b/test/governance/workflow-demand-diagnostics.test.mjs index bfd002b..1a6c62b 100644 --- a/test/governance/workflow-demand-diagnostics.test.mjs +++ b/test/governance/workflow-demand-diagnostics.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import test from "node:test"; +import { test } from "vitest"; import { WORKFLOW_DEMAND_BUILT_IN_CAPABILITIES, diff --git a/test/governance/workspace-topology.test.mjs b/test/governance/workspace-topology.test.mjs index 39d5a1f..ba456c7 100644 --- a/test/governance/workspace-topology.test.mjs +++ b/test/governance/workspace-topology.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { main as topologyMain, parseArgs as parseTopologyArgs } from "../../scripts/workspace-topology/cli.mjs"; import { pathIdentityKey } from "../../scripts/workspace-topology/contract.mjs"; diff --git a/test/learning/intervention-ledger.test.mjs b/test/learning/intervention-ledger.test.mjs index a12fe33..c989840 100644 --- a/test/learning/intervention-ledger.test.mjs +++ b/test/learning/intervention-ledger.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { projectInterventionLedger, diff --git a/test/learning/learning-capture-evidence.test.mjs b/test/learning/learning-capture-evidence.test.mjs index 3b1ddf8..1dc15e7 100644 --- a/test/learning/learning-capture-evidence.test.mjs +++ b/test/learning/learning-capture-evidence.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildLearningCaptureEvidence, diff --git a/test/learning/learning-capture-state.test.mjs b/test/learning/learning-capture-state.test.mjs index f220665..aa28052 100644 --- a/test/learning/learning-capture-state.test.mjs +++ b/test/learning/learning-capture-state.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, utimes, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { loadPriorLearningCaptureState } from "../../scripts/harness-analysis/learning-capture-state.mjs"; import { projectInterventionLedger } from "../../scripts/harness-analysis/intervention-ledger.mjs"; diff --git a/test/learning/learning-loop-contract.test.mjs b/test/learning/learning-loop-contract.test.mjs index bb10dea..0420509 100644 --- a/test/learning/learning-loop-contract.test.mjs +++ b/test/learning/learning-loop-contract.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { LEARNING_LOOP_CHECK_IDS, diff --git a/test/learning/learning-loop-review.test.mjs b/test/learning/learning-loop-review.test.mjs index 41e1ea2..1d99b1e 100644 --- a/test/learning/learning-loop-review.test.mjs +++ b/test/learning/learning-loop-review.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildLearningLoopReview, diff --git a/test/learning/lifecycle-demand-signals.test.mjs b/test/learning/lifecycle-demand-signals.test.mjs index 2696623..1c94a56 100644 --- a/test/learning/lifecycle-demand-signals.test.mjs +++ b/test/learning/lifecycle-demand-signals.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildTaskEpisodes } from "../../scripts/session-analysis/episode-contract.mjs"; import { diff --git a/test/learning/native-learning-candidate-review.test.mjs b/test/learning/native-learning-candidate-review.test.mjs index f7d42f0..f655eed 100644 --- a/test/learning/native-learning-candidate-review.test.mjs +++ b/test/learning/native-learning-candidate-review.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { applyNativeLearningCandidateReview, diff --git a/test/plugins/host-plugin-artifact.test.mjs b/test/plugins/host-plugin-artifact.test.mjs index 8ec4373..b4b8db1 100644 --- a/test/plugins/host-plugin-artifact.test.mjs +++ b/test/plugins/host-plugin-artifact.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { artifactPackage, diff --git a/test/plugins/host-support-profile-model.test.mjs b/test/plugins/host-support-profile-model.test.mjs index c14bca5..e1b8ea1 100644 --- a/test/plugins/host-support-profile-model.test.mjs +++ b/test/plugins/host-support-profile-model.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; -import test from "node:test"; +import { test } from "vitest"; import { defineHostProfile, diff --git a/test/plugins/host-support-profiles.test.mjs b/test/plugins/host-support-profiles.test.mjs index 6059339..1b0dd0e 100644 --- a/test/plugins/host-support-profiles.test.mjs +++ b/test/plugins/host-support-profiles.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { readFileSync, readdirSync } from "node:fs"; -import test from "node:test"; +import { test } from "vitest"; import { getHostProfile, diff --git a/test/plugins/host-support.test.mjs b/test/plugins/host-support.test.mjs index eda7e46..d7b063f 100644 --- a/test/plugins/host-support.test.mjs +++ b/test/plugins/host-support.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { getHostDescriptor, @@ -113,7 +113,7 @@ test("selection profiles route every declared host home through the generic anal test("asset-integrity CLI accepts every declared asset host and rejects unknown hosts", (t) => { const isolatedRoot = mkdtempSync(path.join(os.tmpdir(), "better-harness-host-gate-")); - t.after(() => rmSync(isolatedRoot, { recursive: true, force: true })); + t.onTestFinished(() => rmSync(isolatedRoot, { recursive: true, force: true })); const cliPath = path.join(process.cwd(), "scripts", "better-harness.mjs"); for (const hostId of hostIdsFor(HOST_CAPABILITIES.ASSET_PRACTICES)) { const host = getHostDescriptor(hostId); diff --git a/test/plugins/plugin-lifecycle-architecture.test.mjs b/test/plugins/plugin-lifecycle-architecture.test.mjs index 98385aa..9e1cced 100644 --- a/test/plugins/plugin-lifecycle-architecture.test.mjs +++ b/test/plugins/plugin-lifecycle-architecture.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import * as lifecycle from "../../scripts/plugin-lifecycle/index.mjs"; diff --git a/test/plugins/plugin-lifecycle-command-definitions.test.mjs b/test/plugins/plugin-lifecycle-command-definitions.test.mjs index 917c108..171cce0 100644 --- a/test/plugins/plugin-lifecycle-command-definitions.test.mjs +++ b/test/plugins/plugin-lifecycle-command-definitions.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { groupCommand } from "../../scripts/better-harness-cli/registry.mjs"; import { diff --git a/test/plugins/plugin-lifecycle-model.test.mjs b/test/plugins/plugin-lifecycle-model.test.mjs index 6c3002b..d3ed74f 100644 --- a/test/plugins/plugin-lifecycle-model.test.mjs +++ b/test/plugins/plugin-lifecycle-model.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { assertLifecycleControlled, diff --git a/test/plugins/plugin-lifecycle-plan-model.test.mjs b/test/plugins/plugin-lifecycle-plan-model.test.mjs index 3b4eaf0..cda41a2 100644 --- a/test/plugins/plugin-lifecycle-plan-model.test.mjs +++ b/test/plugins/plugin-lifecycle-plan-model.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { listHostProfiles } from "../../scripts/host-support/index.mjs"; import { buildPluginLifecyclePlan } from "../../scripts/plugin-lifecycle/index.mjs"; diff --git a/test/plugins/plugin-lifecycle-status-row.test.mjs b/test/plugins/plugin-lifecycle-status-row.test.mjs index 5208a79..e3fd33a 100644 --- a/test/plugins/plugin-lifecycle-status-row.test.mjs +++ b/test/plugins/plugin-lifecycle-status-row.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { getHostProfile, getHostSurface } from "../../scripts/host-support/index.mjs"; import { inspectPluginLifecycle } from "../../scripts/plugin-lifecycle/index.mjs"; diff --git a/test/plugins/plugin-lifecycle-target-resolution.test.mjs b/test/plugins/plugin-lifecycle-target-resolution.test.mjs index ab9b304..c9d7fcd 100644 --- a/test/plugins/plugin-lifecycle-target-resolution.test.mjs +++ b/test/plugins/plugin-lifecycle-target-resolution.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { resolvePlanTarget, diff --git a/test/plugins/plugin-lifecycle.test.mjs b/test/plugins/plugin-lifecycle.test.mjs index adebdc2..8852ae2 100644 --- a/test/plugins/plugin-lifecycle.test.mjs +++ b/test/plugins/plugin-lifecycle.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { getHostProfile } from "../../scripts/host-support/index.mjs"; import { runHarnessDoctor } from "../../scripts/harness-doctor/index.mjs"; diff --git a/test/plugins/plugin-manifests.test.mjs b/test/plugins/plugin-manifests.test.mjs index aed8081..de3a714 100644 --- a/test/plugins/plugin-manifests.test.mjs +++ b/test/plugins/plugin-manifests.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { existsSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; const ROOT = process.cwd(); diff --git a/test/reporting/better-harness-evidence-bundle.test.mjs b/test/reporting/better-harness-evidence-bundle.test.mjs index 42ca35e..f0032e0 100644 --- a/test/reporting/better-harness-evidence-bundle.test.mjs +++ b/test/reporting/better-harness-evidence-bundle.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { EVIDENCE_BUNDLE_KIND, diff --git a/test/reporting/cursor-canvas-report.test.mjs b/test/reporting/cursor-canvas-report.test.mjs index aac5883..716a68d 100644 --- a/test/reporting/cursor-canvas-report.test.mjs +++ b/test/reporting/cursor-canvas-report.test.mjs @@ -4,7 +4,7 @@ import { readFileSync } from "node:fs"; import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { renderCursorCanvasTsx } from "../../scripts/harness-analysis/renderers/cursor-canvas.mjs"; import { diff --git a/test/reporting/episode-evidence-review.test.mjs b/test/reporting/episode-evidence-review.test.mjs index 2af71e4..4d18578 100644 --- a/test/reporting/episode-evidence-review.test.mjs +++ b/test/reporting/episode-evidence-review.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { applyEpisodeReviews, diff --git a/test/reporting/findings-recommend.test.mjs b/test/reporting/findings-recommend.test.mjs index 410e3de..ba380f9 100644 --- a/test/reporting/findings-recommend.test.mjs +++ b/test/reporting/findings-recommend.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { enrichFindingWithRecommendation, diff --git a/test/reporting/harness-canvas-validation.test.mjs b/test/reporting/harness-canvas-validation.test.mjs index f6b05c0..fbf297a 100644 --- a/test/reporting/harness-canvas-validation.test.mjs +++ b/test/reporting/harness-canvas-validation.test.mjs @@ -3,7 +3,7 @@ import { execFile } from "node:child_process"; import { cp, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { promisify } from "node:util"; import { analyzeCanvasModuleBoundaries } from "../../scripts/harness-analysis/canvas-module-boundaries.mjs"; diff --git a/test/reporting/harness-component-snapshot-cli.test.mjs b/test/reporting/harness-component-snapshot-cli.test.mjs index 97e3f0a..c12d8e0 100644 --- a/test/reporting/harness-component-snapshot-cli.test.mjs +++ b/test/reporting/harness-component-snapshot-cli.test.mjs @@ -3,7 +3,7 @@ import { cp, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import test from "node:test"; +import { test } from "vitest"; const ROOT = process.cwd(); const CLI = path.join(ROOT, "scripts", "harness-component-snapshot", "cli.mjs"); @@ -13,7 +13,7 @@ async function fixtureWorkspace(t) { const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-component-cli-")); const workspace = path.join(root, "workspace"); await cp(FIXTURE, workspace, { recursive: true }); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); return { root, workspace }; } diff --git a/test/reporting/harness-component-snapshot.test.mjs b/test/reporting/harness-component-snapshot.test.mjs index de200ec..9e6e971 100644 --- a/test/reporting/harness-component-snapshot.test.mjs +++ b/test/reporting/harness-component-snapshot.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { collectAgentCustomizeInventory } from "../../scripts/agent-customize/index.mjs"; import { @@ -28,7 +28,7 @@ async function fixtureWorkspace(t) { const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-component-snapshot-")); const workspace = path.join(root, "workspace"); await cp(FIXTURE, workspace, { recursive: true }); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); return workspace; } @@ -228,7 +228,7 @@ test("population references reject cross-project reuse and preserve explicit rel cp(FIXTURE, firstWorkspace, { recursive: true }), cp(FIXTURE, secondWorkspace, { recursive: true }), ]); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); const first = await createHarnessComponentSnapshot({ workspace: firstWorkspace }); const second = await createHarnessComponentSnapshot({ workspace: secondWorkspace }); @@ -255,7 +255,7 @@ test("workspace evidence roots are scoped to each snapshot assembly", async (t) cp(FIXTURE, secondWorkspace, { recursive: true }), ]); await writeFile(path.join(secondWorkspace, ".qoder", "workflows", "review.yml"), "name: changed\n", "utf8"); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); try { await symlink(firstWorkspace, workspaceAlias, process.platform === "win32" ? "junction" : "dir"); @@ -330,7 +330,7 @@ test("workspace scope rejects user-home aliases and regular files", async (t) => const workspace = await fixtureWorkspace(t); const previousQoderHome = process.env.QODER_HOME; process.env.QODER_HOME = path.join(workspace, ".qoder"); - t.after(() => { + t.onTestFinished(() => { if (previousQoderHome === undefined) delete process.env.QODER_HOME; else process.env.QODER_HOME = previousQoderHome; }); @@ -359,7 +359,7 @@ test("tilde QODER_HOME aliases cannot masquerade as project scope", async (t) => const originalHomedir = os.homedir; process.env.QODER_HOME = "~/.qoder"; os.homedir = () => root; - t.after(async () => { + t.onTestFinished(async () => { os.homedir = originalHomedir; if (previousQoderHome === undefined) delete process.env.QODER_HOME; else process.env.QODER_HOME = previousQoderHome; diff --git a/test/reporting/harness-findings-repair.test.mjs b/test/reporting/harness-findings-repair.test.mjs index d5500ad..5aed5f1 100644 --- a/test/reporting/harness-findings-repair.test.mjs +++ b/test/reporting/harness-findings-repair.test.mjs @@ -3,7 +3,7 @@ import { execFile } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { promisify } from "node:util"; import { repairFindingsJsonData } from "../../scripts/harness-analysis/repair-findings-json.mjs"; diff --git a/test/reporting/harness-inspector.test.mjs b/test/reporting/harness-inspector.test.mjs index b2b3198..d3d4a45 100644 --- a/test/reporting/harness-inspector.test.mjs +++ b/test/reporting/harness-inspector.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { fileURLToPath } from "node:url"; import { diff --git a/test/reporting/harness-memory-review.test.mjs b/test/reporting/harness-memory-review.test.mjs index 77f655a..6e3fca6 100644 --- a/test/reporting/harness-memory-review.test.mjs +++ b/test/reporting/harness-memory-review.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { validateHarnessReportSource } from "../../scripts/harness-analysis/report-source.mjs"; import { diff --git a/test/reporting/harness-report-quality.test.mjs b/test/reporting/harness-report-quality.test.mjs index 01c4c54..f15ef95 100644 --- a/test/reporting/harness-report-quality.test.mjs +++ b/test/reporting/harness-report-quality.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { evaluateHarnessCanvasQuality, diff --git a/test/reporting/harness-report-render-cli.test.mjs b/test/reporting/harness-report-render-cli.test.mjs index c568f7f..ae33372 100644 --- a/test/reporting/harness-report-render-cli.test.mjs +++ b/test/reporting/harness-report-render-cli.test.mjs @@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs"; import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { evaluateHtmlReport, renderHtml } from "../../scripts/harness-analysis/renderers/html.mjs"; import { RENDER_REPORT_PLATFORMS, renderReport } from "../../scripts/harness-analysis/render-report.mjs"; diff --git a/test/reporting/harness-report-run.test.mjs b/test/reporting/harness-report-run.test.mjs index cce04b2..e8b4ca4 100644 --- a/test/reporting/harness-report-run.test.mjs +++ b/test/reporting/harness-report-run.test.mjs @@ -4,7 +4,7 @@ import { readFileSync } from "node:fs"; import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { ANALYZE_EVIDENCE_TOKEN_BUDGET, diff --git a/test/reporting/harness-report-source.test.mjs b/test/reporting/harness-report-source.test.mjs index 028953a..728469a 100644 --- a/test/reporting/harness-report-source.test.mjs +++ b/test/reporting/harness-report-source.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { createHarnessReportSource, validateHarnessReportSource } from "../../scripts/harness-analysis/report-source.mjs"; import { buildObservationManifest } from "../../scripts/session-analysis/observation-manifest.mjs"; diff --git a/test/reporting/html-report-interactions.test.mjs b/test/reporting/html-report-interactions.test.mjs index 002d1ca..4e02d03 100644 --- a/test/reporting/html-report-interactions.test.mjs +++ b/test/reporting/html-report-interactions.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { installHtmlReportInteractions, diff --git a/test/reporting/inspector-cross-provider.test.mjs b/test/reporting/inspector-cross-provider.test.mjs index b53f555..bc5d36f 100644 --- a/test/reporting/inspector-cross-provider.test.mjs +++ b/test/reporting/inspector-cross-provider.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import os from "node:os"; -import test from "node:test"; +import { test } from "vitest"; import { fileURLToPath } from "node:url"; import { collectMultiPlatformSessionSummaries } from "../../scripts/commit-session-link/index.mjs"; diff --git a/test/reporting/maturity-models.test.mjs b/test/reporting/maturity-models.test.mjs index 8e6689b..8a4f12a 100644 --- a/test/reporting/maturity-models.test.mjs +++ b/test/reporting/maturity-models.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { AGENT_WORK_LOOP_DIMENSIONS, diff --git a/test/reporting/practice-findings.test.mjs b/test/reporting/practice-findings.test.mjs index 2941a6e..9a944c0 100644 --- a/test/reporting/practice-findings.test.mjs +++ b/test/reporting/practice-findings.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { projectAgentLintPracticeEvidence } from "../../scripts/harness-analysis/practice-findings.mjs"; import { collectAgentLintPracticeEvidence } from "../../scripts/harness-analysis/task-loop-source.mjs"; diff --git a/test/reporting/preview-servers.test.mjs b/test/reporting/preview-servers.test.mjs index 68b9232..f93f2e7 100644 --- a/test/reporting/preview-servers.test.mjs +++ b/test/reporting/preview-servers.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { canvasMediaCandidatesForApplication, diff --git a/test/reporting/report-source-review.test.mjs b/test/reporting/report-source-review.test.mjs index 5e8eebb..7e3d740 100644 --- a/test/reporting/report-source-review.test.mjs +++ b/test/reporting/report-source-review.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { applyReportSourceReview } from "../../scripts/harness-analysis/apply-source-review.mjs"; import { buildHarnessReviewPacket, compileHarnessLeadDecision } from "../../scripts/harness-analysis/report-review-packet.mjs"; diff --git a/test/reporting/score-finding-consistency.test.mjs b/test/reporting/score-finding-consistency.test.mjs index c00e61e..cce9433 100644 --- a/test/reporting/score-finding-consistency.test.mjs +++ b/test/reporting/score-finding-consistency.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { AGENT_WORK_LOOP_CURRENT_TASK_DIMENSION_IDS, diff --git a/test/reporting/task-loop-report.test.mjs b/test/reporting/task-loop-report.test.mjs index d2d0db3..307a293 100644 --- a/test/reporting/task-loop-report.test.mjs +++ b/test/reporting/task-loop-report.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import test from "node:test"; +import { test } from "vitest"; import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; diff --git a/test/reporting/task-loop-repository-evidence.test.mjs b/test/reporting/task-loop-repository-evidence.test.mjs index a56aa7c..133315d 100644 --- a/test/reporting/task-loop-repository-evidence.test.mjs +++ b/test/reporting/task-loop-repository-evidence.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildTaskLoopRepositoryEvidence } from "../../scripts/harness-analysis/task-loop-repository-evidence.mjs"; diff --git a/test/reporting/task-loop-source.test.mjs b/test/reporting/task-loop-source.test.mjs index 2676efc..2181c56 100644 --- a/test/reporting/task-loop-source.test.mjs +++ b/test/reporting/task-loop-source.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { buildCheckupScan } from "../../scripts/coding-agent-practices/checkup/scan.mjs"; import { freezeSessionPopulation } from "../../scripts/session-analysis/session-population.mjs"; diff --git a/test/sessions/commit-session-link.test.mjs b/test/sessions/commit-session-link.test.mjs index 184b8c0..a0d9a16 100644 --- a/test/sessions/commit-session-link.test.mjs +++ b/test/sessions/commit-session-link.test.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { fileURLToPath } from "node:url"; import { @@ -158,7 +158,7 @@ function fixtureSession(overrides = {}) { test("collectCommitFacts parses numstat, subject, and session trailers (AC-1, AC-2)", async (t) => { const root = await createFixtureRepo(); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); const { repoRoot, commits } = collectCommitFacts({ workspace: root, limit: 10 }); assert.equal(commits.length, 2); @@ -177,7 +177,7 @@ test("collectCommitFacts parses numstat, subject, and session trailers (AC-1, AC test("collectCommitFacts rejects unknown commits", async (t) => { const root = await createFixtureRepo(); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); assert.throws( () => collectCommitFacts({ workspace: root, commit: "0000000000000000000000000000000000000000" }), @@ -222,7 +222,7 @@ test("collectCommitFacts preserves adversarial Git paths through --numstat -z (A return; } const root = await mkdtemp(path.join(os.tmpdir(), "commit-session-paths-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); git(root, ["init", "--initial-branch=main"]); await mkdir(path.join(root, "src"), { recursive: true }); const paths = ["src/tab\tname.mjs", "src/line\nname.mjs"]; @@ -235,7 +235,7 @@ test("collectCommitFacts preserves adversarial Git paths through --numstat -z (A test("reference-shape checkpoint refs resolve three ULIDs to a distinct session UUID (AC-9, AC-13)", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "commit-session-checkpoints-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); git(root, ["init", "--initial-branch=main"]); await mkdir(path.join(root, "src"), { recursive: true }); await writeFile(path.join(root, "src", "app.mjs"), "export const value = 0;\n"); @@ -272,7 +272,7 @@ test("reference-shape checkpoint refs resolve three ULIDs to a distinct session test("legacy entire/checkpoints/v1 metadata resolves without checking out the branch (AC-9)", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "commit-session-checkpoint-branch-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); git(root, ["init", "--initial-branch=main"]); const checkpointId = "a3b2c4d5e6f7"; const sessionId = "legacy-session-id"; @@ -288,7 +288,7 @@ test("legacy entire/checkpoints/v1 metadata resolves without checking out the br test("render-session prefers the newest self-contained transcript when one Entire session has several checkpoints (AC-9, AC-12)", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "commit-session-entire-transcript-")); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); git(root, ["init", "--initial-branch=main"]); await mkdir(path.join(root, "src"), { recursive: true }); await writeFile(path.join(root, "src", "app.mjs"), "export const value = 1;\n"); @@ -1084,7 +1084,7 @@ test("miniMarkdownToHtml renders the bounded subset and escapes HTML (AC-7)", () test("cli correlate runs end-to-end against a fixture repo (AC-1)", async (t) => { const root = await createFixtureRepo(); - t.after(() => rm(root, { recursive: true, force: true })); + t.onTestFinished(() => rm(root, { recursive: true, force: true })); const result = spawnSync( process.execPath, diff --git a/test/sessions/session-analysis-claude-facets.test.mjs b/test/sessions/session-analysis-claude-facets.test.mjs index 793a9e2..3df80a3 100644 --- a/test/sessions/session-analysis-claude-facets.test.mjs +++ b/test/sessions/session-analysis-claude-facets.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { access, writeFile } from "node:fs/promises"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { analyzeClaudeStyleSessions, diff --git a/test/sessions/session-analysis-core-facts.test.mjs b/test/sessions/session-analysis-core-facts.test.mjs index 40ad85b..9e2b298 100644 --- a/test/sessions/session-analysis-core-facts.test.mjs +++ b/test/sessions/session-analysis-core-facts.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildTaskEpisodes } from "../../scripts/session-analysis/episode-contract.mjs"; import { diff --git a/test/sessions/session-analysis-fs.test.mjs b/test/sessions/session-analysis-fs.test.mjs index 2190f9d..2a39bac 100644 --- a/test/sessions/session-analysis-fs.test.mjs +++ b/test/sessions/session-analysis-fs.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { walkFiles } from "../../scripts/session-analysis/fs.mjs"; import { collectSkillFiles } from "../../scripts/agent-customize/core/items.mjs"; diff --git a/test/sessions/session-analysis-providers.test.mjs b/test/sessions/session-analysis-providers.test.mjs index 892640e..b8c4e35 100644 --- a/test/sessions/session-analysis-providers.test.mjs +++ b/test/sessions/session-analysis-providers.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { createAnalyzer, diff --git a/test/sessions/session-analysis-time.test.mjs b/test/sessions/session-analysis-time.test.mjs index 32425b2..372705b 100644 --- a/test/sessions/session-analysis-time.test.mjs +++ b/test/sessions/session-analysis-time.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { normalizeTimestamp, timestampMillis } from "../../scripts/session-analysis/time.mjs"; diff --git a/test/sessions/session-analysis.test.mjs b/test/sessions/session-analysis.test.mjs index 26483cb..f095f99 100644 --- a/test/sessions/session-analysis.test.mjs +++ b/test/sessions/session-analysis.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { createAnalyzer } from "../../scripts/session-analysis.mjs"; import { diff --git a/test/sessions/session-episode-contract.test.mjs b/test/sessions/session-episode-contract.test.mjs index 85ef851..b4d3a93 100644 --- a/test/sessions/session-episode-contract.test.mjs +++ b/test/sessions/session-episode-contract.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildTaskEpisodes, diff --git a/test/sessions/session-population.test.mjs b/test/sessions/session-population.test.mjs index 1938bd3..74c9ab4 100644 --- a/test/sessions/session-population.test.mjs +++ b/test/sessions/session-population.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; async function populationModule() { return import("../../scripts/session-analysis/session-population.mjs"); diff --git a/test/sessions/session-result-facts.test.mjs b/test/sessions/session-result-facts.test.mjs index 78fe44a..542fded 100644 --- a/test/sessions/session-result-facts.test.mjs +++ b/test/sessions/session-result-facts.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { parseResultFacts } from "../../scripts/session-analysis/result-facts.mjs"; diff --git a/test/sessions/session-selection-plan.test.mjs b/test/sessions/session-selection-plan.test.mjs index 70ade64..aa016fe 100644 --- a/test/sessions/session-selection-plan.test.mjs +++ b/test/sessions/session-selection-plan.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildObservationManifest } from "../../scripts/session-analysis/observation-manifest.mjs"; import { CodexSessionAnalyzer } from "../../scripts/session-analysis/platforms/codex.mjs"; @@ -407,7 +407,10 @@ test("selection entry collection bounds concurrency and never asks adapters for assert.ok(optionsSeen.every((options) => options.includeContent === false)); }); -test("Qoder and Codex insight adapters apply the same validated AI plan", async (context) => { +test.each([ + { label: "Qoder", platform: "qoder", Analyzer: QoderSessionAnalyzer }, + { label: "Codex", platform: "codex", Analyzer: CodexSessionAnalyzer }, +])("$label insight adapter applies the same validated AI plan", async ({ platform, Analyzer }) => { const sessions = Array.from({ length: 6 }, (_, index) => ({ sessionId: `session-${index}`, workspace: "/workspace/project", @@ -454,38 +457,34 @@ test("Qoder and Codex insight adapters apply the same validated AI plan", async const precomputedEntries = sessions.map((session) => buildSessionSelectionEntry(session, eventsBySession.get(session.sessionId) ?? [])); - for (const [name, Analyzer] of [["qoder", QoderSessionAnalyzer], ["codex", CodexSessionAnalyzer]]) { - await context.test(name, async () => { - const analyzer = new Analyzer(); - let readCount = 0; - analyzer.resolveScope = async () => ({ - platform: name, - workspace: "/workspace/project", - since: null, - until: null, - sinceTime: null, - untilTime: null, - includeGlobalCapabilities: false, - }); - analyzer.discoverSourceRoots = async () => []; - analyzer.discoverSessions = async () => sessions; - analyzer.readSession = async (session) => { - readCount += 1; - return eventsBySession.get(session.sessionId) ?? []; - }; + const analyzer = new Analyzer(); + let readCount = 0; + analyzer.resolveScope = async () => ({ + platform, + workspace: "/workspace/project", + since: null, + until: null, + sinceTime: null, + untilTime: null, + includeGlobalCapabilities: false, + }); + analyzer.discoverSourceRoots = async () => []; + analyzer.discoverSessions = async () => sessions; + analyzer.readSession = async (session) => { + readCount += 1; + return eventsBySession.get(session.sessionId) ?? []; + }; - const result = await analyzer.analyze({ - command: "insights", - selectionPlan: adapterPlan, - selectionEntries: precomputedEntries, - }); + const result = await analyzer.analyze({ + command: "insights", + selectionPlan: adapterPlan, + selectionEntries: precomputedEntries, + }); - assert.equal(result.selection.requestedStrategy, "ai-plan"); - assert.equal(result.selection.analyzedCount, 2); - assert.deepEqual(result.sessions.map((session) => session.sessionId), ["session-0", "session-1"]); - assert.deepEqual(result.selection.strata, ["closed", "unvalidated"]); - assert.equal(result.selection.plan.planner, "ai"); - assert.equal(readCount, 2); - }); - } + assert.equal(result.selection.requestedStrategy, "ai-plan"); + assert.equal(result.selection.analyzedCount, 2); + assert.deepEqual(result.sessions.map((session) => session.sessionId), ["session-0", "session-1"]); + assert.deepEqual(result.selection.strata, ["closed", "unvalidated"]); + assert.equal(result.selection.plan.planner, "ai"); + assert.equal(readCount, 2); }); diff --git a/test/sessions/session-selection.test.mjs b/test/sessions/session-selection.test.mjs index 6207618..8d241d5 100644 --- a/test/sessions/session-selection.test.mjs +++ b/test/sessions/session-selection.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { selectSessions, selectionSummary } from "../../scripts/session-analysis/selection.mjs"; import { buildObservationManifest } from "../../scripts/session-analysis/observation-manifest.mjs"; diff --git a/test/sessions/session-usage-activity.test.mjs b/test/sessions/session-usage-activity.test.mjs index dbd4b8f..fb258e6 100644 --- a/test/sessions/session-usage-activity.test.mjs +++ b/test/sessions/session-usage-activity.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { buildTaskLoopSourceCandidate } from "../../scripts/harness-analysis/task-loop-source.mjs"; import { buildDailyUsageActivity } from "../../scripts/session-analysis/daily-usage.mjs"; diff --git a/test/sessions/session-usage-report.test.mjs b/test/sessions/session-usage-report.test.mjs index 0133219..05150dc 100644 --- a/test/sessions/session-usage-report.test.mjs +++ b/test/sessions/session-usage-report.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { fileURLToPath } from "node:url"; import { diff --git a/test/sessions/session-usage-summary.test.mjs b/test/sessions/session-usage-summary.test.mjs index d6093b8..dfab1a0 100644 --- a/test/sessions/session-usage-summary.test.mjs +++ b/test/sessions/session-usage-summary.test.mjs @@ -4,7 +4,7 @@ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import test from "node:test"; +import { test } from "vitest"; import { workspaceToQoderSlug } from "../../scripts/session-analysis/platforms/qoder.mjs"; import { buildUsageSummary } from "../../scripts/session-analysis/usage-summary.mjs"; diff --git a/test/sessions/session-workspace-match.test.mjs b/test/sessions/session-workspace-match.test.mjs index 9155c7e..e726a6f 100644 --- a/test/sessions/session-workspace-match.test.mjs +++ b/test/sessions/session-workspace-match.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { test } from "vitest"; import { SESSION_WORKSPACE_SCOPE_KIND, diff --git a/test/sessions/session-workspace-provider.test.mjs b/test/sessions/session-workspace-provider.test.mjs index 254fd5d..2014444 100644 --- a/test/sessions/session-workspace-provider.test.mjs +++ b/test/sessions/session-workspace-provider.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { CodexSessionAnalyzer } from "../../scripts/session-analysis/platforms/codex.mjs"; import { diff --git a/test/skills-docs/better-harness-skill.test.mjs b/test/skills-docs/better-harness-skill.test.mjs index bef93e2..f41b085 100644 --- a/test/skills-docs/better-harness-skill.test.mjs +++ b/test/skills-docs/better-harness-skill.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; const ROOT = process.cwd(); const SKILL_PATH = "skills/better-harness/SKILL.md"; diff --git a/test/skills-docs/coding-agent-platform-notes.test.mjs b/test/skills-docs/coding-agent-platform-notes.test.mjs index b286b3f..11879ab 100644 --- a/test/skills-docs/coding-agent-platform-notes.test.mjs +++ b/test/skills-docs/coding-agent-platform-notes.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; const ROOT = process.cwd(); diff --git a/test/skills-docs/doc-link-graph.test.mjs b/test/skills-docs/doc-link-graph.test.mjs index 6777300..a17873e 100644 --- a/test/skills-docs/doc-link-graph.test.mjs +++ b/test/skills-docs/doc-link-graph.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync, existsSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { buildGraph, diff --git a/test/skills-docs/pi-prompt-template.test.mjs b/test/skills-docs/pi-prompt-template.test.mjs index aabc1e6..69f4f84 100644 --- a/test/skills-docs/pi-prompt-template.test.mjs +++ b/test/skills-docs/pi-prompt-template.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; -import test from "node:test"; +import { test } from "vitest"; import { fileURLToPath } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); diff --git a/test/support/github-actions-reporter.mjs b/test/support/github-actions-reporter.mjs deleted file mode 100644 index f25370c..0000000 --- a/test/support/github-actions-reporter.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -function escapeCommandData(value) { - return String(value) - .replaceAll("%", "%25") - .replaceAll("\r", "%0D") - .replaceAll("\n", "%0A"); -} - -function escapeCommandProperty(value) { - return escapeCommandData(value) - .replaceAll(":", "%3A") - .replaceAll(",", "%2C"); -} - -function errorDetail(error) { - const cause = error?.cause ?? error; - if (!cause) return "Test failed without an error detail."; - return String(cause.stack ?? cause.message ?? cause).slice(0, 6000); -} - -function repositoryPath(file, cwd) { - if (!file) return null; - const filePath = file.startsWith("file:") ? fileURLToPath(file) : file; - const relative = path.relative(cwd, filePath); - return relative && !relative.startsWith(`..${path.sep}`) && relative !== ".." - ? relative.split(path.sep).join("/") - : filePath.split(path.sep).join("/"); -} - -export function githubFailureAnnotation(data, { cwd = process.cwd() } = {}) { - const properties = []; - const file = repositoryPath(data.file, cwd); - if (file) properties.push(`file=${escapeCommandProperty(file)}`); - if (Number.isInteger(data.line)) properties.push(`line=${data.line}`); - if (Number.isInteger(data.column)) properties.push(`col=${data.column}`); - properties.push(`title=${escapeCommandProperty(`Test failed: ${data.name}`)}`); - return `::error ${properties.join(",")}::${escapeCommandData(errorDetail(data.details?.error))}\n`; -} - -export function plainFailure(data, { cwd = process.cwd() } = {}) { - const file = repositoryPath(data.file, cwd) ?? "unknown file"; - const location = Number.isInteger(data.line) ? `${file}:${data.line}` : file; - return `\nFAIL ${location} — ${data.name}\n${errorDetail(data.details?.error)}\n`; -} - -export default async function* githubActionsReporter(source) { - const seen = new Set(); - for await (const event of source) { - if (event.type === "test:pass" && !event.data.skip && !event.data.todo) { - yield "."; - continue; - } - if (event.type === "test:summary") { - yield "\n"; - continue; - } - if (event.type !== "test:fail" || event.data.skip || event.data.todo) continue; - const key = `${event.data.file ?? ""}:${event.data.line ?? ""}:${event.data.name}`; - if (seen.has(key)) continue; - seen.add(key); - yield "X"; - yield process.env.GITHUB_ACTIONS === "true" - ? githubFailureAnnotation(event.data) - : plainFailure(event.data); - } -} diff --git a/test/support/run-ci.mjs b/test/support/run-ci.mjs deleted file mode 100644 index be67f9e..0000000 --- a/test/support/run-ci.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import { appendFile, mkdir, readdir, rm } from "node:fs/promises"; -import path from "node:path"; -import { performance } from "node:perf_hooks"; -import { spawnSync } from "node:child_process"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const SUPPORT_DIR = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(SUPPORT_DIR, "../.."); -const TEST_ROOT = path.join(REPO_ROOT, "test"); -const RESULTS_ROOT = path.join(REPO_ROOT, "test-results"); -const REPORTER_PATH = path.join(SUPPORT_DIR, "github-actions-reporter.mjs"); -const REPORTER_SPECIFIER = pathToFileURL(REPORTER_PATH).href; - -export function nodeTestFileArgument(filePath, { - cwd = REPO_ROOT, - pathApi = path, -} = {}) { - const relativePath = pathApi.relative(cwd, filePath); - if (!relativePath || pathApi.isAbsolute(relativePath)) { - throw new Error("test files must be on the same filesystem root as the test runner"); - } - const portablePath = relativePath.split(pathApi.sep).join("/"); - return portablePath.startsWith(".") ? portablePath : `./${portablePath}`; -} - -export async function discoverTestCategories(testRoot = TEST_ROOT) { - const entries = await readdir(testRoot, { withFileTypes: true }); - const categories = []; - for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { - if (!entry.isDirectory()) continue; - const directory = path.join(testRoot, entry.name); - const files = (await readdir(directory, { withFileTypes: true })) - .filter((file) => file.isFile() && file.name.endsWith(".test.mjs")) - .map((file) => path.join(directory, file.name)) - .sort(); - if (files.length > 0) categories.push({ name: entry.name, files }); - } - return categories; -} - -function durationLabel(durationMs) { - return `${(durationMs / 1000).toFixed(1)}s`; -} - -async function writeStepSummary(rows, summaryPath) { - if (!summaryPath) return; - const markdown = [ - "## Test results by capability", - "", - "| Capability | Files | Status | Duration |", - "| --- | ---: | --- | ---: |", - ...rows.map((row) => `| ${row.name} | ${row.files} | ${row.status === 0 ? "pass" : "fail"} | ${durationLabel(row.durationMs)} |`), - "", - ].join("\n"); - await appendFile(summaryPath, markdown, "utf8"); -} - -export async function runCiTests(selectedNames = process.argv.slice(2), { - cwd = REPO_ROOT, - env = process.env, - resultsRoot = RESULTS_ROOT, - stderr = process.stderr, - stdio = "inherit", - stdout = process.stdout, - testRoot = TEST_ROOT, -} = {}) { - const discovered = await discoverTestCategories(testRoot); - const selected = selectedNames.length === 0 - ? discovered - : discovered.filter((category) => selectedNames.includes(category.name)); - const missing = selectedNames.filter((name) => !discovered.some((category) => category.name === name)); - if (missing.length > 0) { - stderr.write(`Unknown test capabilities: ${missing.join(", ")}\n`); - return 64; - } - - await mkdir(resultsRoot, { recursive: true }); - const childEnv = { ...env }; - delete childEnv.NODE_TEST_CONTEXT; - const rows = []; - for (const category of selected) { - const junitPath = path.join(resultsRoot, `${category.name}.xml`); - await rm(junitPath, { force: true }); - const grouped = env.GITHUB_ACTIONS === "true"; - if (grouped) stdout.write(`::group::Tests: ${category.name}\n`); - else stdout.write(`\n## Tests: ${category.name}\n`); - - const started = performance.now(); - const result = spawnSync(process.execPath, [ - "--test", - `--test-reporter=${REPORTER_SPECIFIER}`, - "--test-reporter=junit", - "--test-reporter-destination=stdout", - `--test-reporter-destination=${junitPath}`, - ...category.files.map((filePath) => nodeTestFileArgument(filePath, { cwd })), - ], { - cwd, - env: childEnv, - stdio, - windowsHide: true, - }); - const durationMs = performance.now() - started; - const status = Number.isInteger(result.status) ? result.status : 1; - rows.push({ name: category.name, files: category.files.length, status, durationMs }); - if (result.error) stderr.write(`${result.error.message}\n`); - if (grouped) stdout.write("::endgroup::\n"); - } - - await writeStepSummary(rows, env.GITHUB_STEP_SUMMARY); - const failed = rows.filter((row) => row.status !== 0); - stdout.write(`\nTest capabilities: ${rows.length - failed.length} passed, ${failed.length} failed.\n`); - if (failed.length > 0) stderr.write(`Failed capabilities: ${failed.map((row) => row.name).join(", ")}\n`); - return failed.length === 0 ? 0 : 1; -} - -const isMain = process.argv[1] - && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url; -if (isMain) process.exitCode = await runCiTests(); diff --git a/vitest.ci.config.mjs b/vitest.ci.config.mjs new file mode 100644 index 0000000..0b751a5 --- /dev/null +++ b/vitest.ci.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig, mergeConfig } from "vitest/config"; + +import baseConfig from "./vitest.config.mjs"; + +const reporters = [ + "default", + ...(process.env.GITHUB_ACTIONS === "true" ? ["github-actions"] : []), + ["junit", { outputFile: "test-results/vitest.junit.xml" }], +]; + +export default mergeConfig(baseConfig, defineConfig({ + test: { reporters }, +})); diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 0000000..978c012 --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + exclude: ["test/fixtures/**", "test/support/**"], + hookTimeout: 120_000, + include: ["test/**/*.test.mjs"], + pool: "forks", + testTimeout: 120_000, + teardownTimeout: 30_000, + }, +});