diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb5a591..331a68e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,10 @@ jobs: - run: pnpm typecheck - run: pnpm lint - run: pnpm conformance + # scripts/ is not a workspace package, so `pnpm test` (turbo) never + # reaches its test files — they get their own vitest run. + - name: Root scripts test suite + run: pnpm test:scripts - name: Compliance manifest drift check run: | pnpm compliance:emit-all --check || { diff --git a/package.json b/package.json index 6f34996..2902583 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "turbo run lint", "test": "turbo run test", "test:e2e": "turbo run test:e2e", + "test:scripts": "vitest run --config vitest.scripts.config.mjs", "test:stories": "turbo run test:stories", "test:visual": "pnpm --filter @repo/storybook exec concurrently -k -s first -n 'SB,VRT' -c 'magenta,blue' 'pnpm --filter @repo/storybook exec http-server storybook-static --port 6006 --silent' 'pnpm --filter @repo/storybook exec wait-on tcp:6006 && pnpm exec playwright test --config ../../playwright.config.ts'", "typecheck": "turbo run typecheck", @@ -45,6 +46,7 @@ "prettier": "^3.5.0", "turbo": "^2.4.0", "typescript": "^5.8.0", + "vitest": "^3.2.7", "zod": "^3.25.0" }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e42936..2b4dfd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ importers: typescript: specifier: ^5.8.0 version: 5.9.3 + vitest: + specifier: ^3.2.7 + version: 3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) zod: specifier: ^3.25.0 version: 3.25.76 diff --git a/scripts/compliance/emit-data-map.test.mjs b/scripts/compliance/emit-data-map.test.mjs index d5ca479..3739246 100644 --- a/scripts/compliance/emit-data-map.test.mjs +++ b/scripts/compliance/emit-data-map.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/compliance/emit-retention-policy.test.mjs b/scripts/compliance/emit-retention-policy.test.mjs index e09aa0a..5d92e3d 100644 --- a/scripts/compliance/emit-retention-policy.test.mjs +++ b/scripts/compliance/emit-retention-policy.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/compliance/emit-sub-processors.test.mjs b/scripts/compliance/emit-sub-processors.test.mjs index 1ae0a10..a88eefe 100644 --- a/scripts/compliance/emit-sub-processors.test.mjs +++ b/scripts/compliance/emit-sub-processors.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/coverage/aggregate.test.mjs b/scripts/coverage/aggregate.test.mjs index 676fe5f..d825217 100644 --- a/scripts/coverage/aggregate.test.mjs +++ b/scripts/coverage/aggregate.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/scripts/coverage/diff.test.mjs b/scripts/coverage/diff.test.mjs index 534735b..1cfba89 100644 --- a/scripts/coverage/diff.test.mjs +++ b/scripts/coverage/diff.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/scripts/coverage/mutate.test.mjs b/scripts/coverage/mutate.test.mjs index 8911e84..2de7664 100644 --- a/scripts/coverage/mutate.test.mjs +++ b/scripts/coverage/mutate.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/scripts/library-decisions/check.test.mjs b/scripts/library-decisions/check.test.mjs index 715fac8..41e4127 100644 --- a/scripts/library-decisions/check.test.mjs +++ b/scripts/library-decisions/check.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/library-decisions/revalidate.test.mjs b/scripts/library-decisions/revalidate.test.mjs index 41f5ba6..723ce45 100644 --- a/scripts/library-decisions/revalidate.test.mjs +++ b/scripts/library-decisions/revalidate.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/library-decisions/schema.test.mjs b/scripts/library-decisions/schema.test.mjs index 8ebd6b0..90b31b1 100644 --- a/scripts/library-decisions/schema.test.mjs +++ b/scripts/library-decisions/schema.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; diff --git a/scripts/work/cli.test.mjs b/scripts/work/cli.test.mjs index c7e169a..4ccbf5b 100644 --- a/scripts/work/cli.test.mjs +++ b/scripts/work/cli.test.mjs @@ -52,8 +52,10 @@ describe("pnpm work cli", () => { expect(out.length).toBeGreaterThan(0); }); - it("dispatch prints a plan", () => { + it("dispatch prints a plan or reports nothing is ready", () => { + // Runs against the real docs/work tree, so the outcome depends on + // whether a ready task exists — both branches are valid CLI output. const out = run("dispatch"); - expect(out).toContain("Dispatch plan"); + expect(out).toMatch(/Dispatch plan|No ready task to dispatch/); }); }); diff --git a/scripts/work/decompose.test.mjs b/scripts/work/decompose.test.mjs index a4096f9..89c27ac 100644 --- a/scripts/work/decompose.test.mjs +++ b/scripts/work/decompose.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/scripts/work/dispatch.test.mjs b/scripts/work/dispatch.test.mjs index 79402e0..8e582f2 100644 --- a/scripts/work/dispatch.test.mjs +++ b/scripts/work/dispatch.test.mjs @@ -12,7 +12,8 @@ import { function makeWorkTree({ epics }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "dispatch-")); for (const [epicId, epicData] of Object.entries(epics)) { - const epicDir = path.join(root, epicId); + // findNextTask/buildState walk /epics/ — mirror docs/work. + const epicDir = path.join(root, "epics", epicId); fs.mkdirSync(epicDir, { recursive: true }); fs.writeFileSync( path.join(epicDir, "_epic.md"), diff --git a/scripts/work/prd-ship.test.mjs b/scripts/work/prd-ship.test.mjs index 5823fb0..d047cbc 100644 --- a/scripts/work/prd-ship.test.mjs +++ b/scripts/work/prd-ship.test.mjs @@ -1,4 +1,6 @@ -import { test, describe } from "node:test"; +// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); +// node:assert keeps the original assertion style. +import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; diff --git a/scripts/work/state-builder.test.mjs b/scripts/work/state-builder.test.mjs index e3dad9d..fba70fd 100644 --- a/scripts/work/state-builder.test.mjs +++ b/scripts/work/state-builder.test.mjs @@ -12,7 +12,8 @@ import { function makeWorkTree({ epics }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-state-")); for (const [epicId, epicData] of Object.entries(epics)) { - const epicDir = path.join(root, epicId); + // buildState walks /epics/ — mirror the real docs/work layout. + const epicDir = path.join(root, "epics", epicId); fs.mkdirSync(epicDir, { recursive: true }); fs.writeFileSync( path.join(epicDir, "_epic.md"), @@ -85,6 +86,7 @@ describe("buildState", () => { epic1: { status: "in-progress", title: "epic1", + prd: null, stories: { story1: { status: "done", @@ -127,7 +129,7 @@ describe("buildState", () => { it("parses depends-on and blocks frontmatter arrays", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-dep-")); - const epicDir = path.join(root, "epic1"); + const epicDir = path.join(root, "epics", "epic1"); fs.mkdirSync(epicDir, { recursive: true }); fs.writeFileSync( path.join(epicDir, "_epic.md"), diff --git a/vitest.scripts.config.mjs b/vitest.scripts.config.mjs new file mode 100644 index 0000000..d672ad9 --- /dev/null +++ b/vitest.scripts.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig } from "vitest/config"; + +/** + * Runner for the root scripts/ test suite (pnpm test:scripts). + * + * scripts/ is not a workspace package, so `turbo run test` never reaches + * these files — this dedicated config wires them to a runner. The suite + * runs in CI's validate job alongside the other root-level gates. + * + * Named vitest.scripts.config.mjs (not vitest.config.*) on purpose so a + * bare `vitest` invocation at the repo root does not auto-discover it. + */ +export default defineConfig({ + test: { + environment: "node", + include: ["scripts/**/*.test.mjs"], + clearMocks: true, + restoreMocks: true, + // Several suites exec child node processes (work CLI, conformance + // fixtures) — keep a generous per-test ceiling. + testTimeout: 60_000, + hookTimeout: 60_000, + }, +});