/** * Unit test for check-test-runner-imports.ts. * * Run with: `bun test scripts/lint/check-test-runner-imports.test.ts` * * Uses Bun's native test runner because this file is repo tooling, not a * workspace package — the canonical-runner-wrapper rule (openspec/AGENTS.md * §7) governs `packages///src/**\/*.test.ts`, not `scripts/`. * Importing the rule-enforcer's own test from `@suaveplan/testing/runner` * would create a circular dependency between the gate and the wrapper it * polices. */ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { EXEMPT_PACKAGES, findViolations, formatViolations, } from "./check-test-runner-imports.ts"; interface Fixture { readonly root: string; cleanup(): Promise; } async function makeFixture(): Promise { const root = await mkdtemp(join(tmpdir(), "check-runner-imports-")); return { root, async cleanup() { await rm(root, { recursive: true, force: true }); }, }; } async function writePackage(args: { root: string; plugin: string; pkg: string; name: string; files: Record; }): Promise { const pkgDir = join(args.root, "packages", args.plugin, args.pkg); await mkdir(pkgDir, { recursive: true }); await writeFile( join(pkgDir, "package.json"), JSON.stringify({ name: args.name }) ); for (const [relPath, content] of Object.entries(args.files)) { const full = join(pkgDir, relPath); await mkdir(join(full, ".."), { recursive: true }); await writeFile(full, content); } } describe("check-test-runner-imports", () => { let fixture: Fixture; beforeEach(async () => { fixture = await makeFixture(); }); afterEach(async () => { await fixture.cleanup(); }); it("reports zero violations when every package uses @suaveplan/testing/runner", { timeout: 5000, }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "good-pkg", name: "@suaveplan/good-pkg", files: { "src/feature/feature.test.ts": 'import { describe, it, expect } from "@suaveplan/testing/runner";\n' + 'describe("ok", () => { it("passes", () => { expect(1).toBe(1); }); });\n', }, }); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toEqual([]); expect(formatViolations(violations)).toContain( "All non-exempt packages use the canonical" ); }); it("flags a direct vitest import in a non-exempt package", { timeout: 5000, }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "bad-pkg", name: "@suaveplan/bad-pkg", files: { "src/feature/feature.test.ts": 'import { describe, it, expect } from "vitest";\n', }, }); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toHaveLength(1); expect(violations[0]?.packageName).toBe("@suaveplan/bad-pkg"); expect(violations[0]?.file).toBe( "packages/demo/bad-pkg/src/feature/feature.test.ts" ); expect(violations[0]?.line).toBe(1); expect(violations[0]?.source).toContain('from "vitest"'); const message = formatViolations(violations); expect(message).toContain("❌ Direct vitest / bun:test imports"); expect(message).toContain("@suaveplan/bad-pkg"); expect(message).toContain("openspec/AGENTS.md §7"); }); it("flags a direct bun:test import in a non-exempt package", { timeout: 5000, }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "bad-bun", name: "@suaveplan/bad-bun", files: { "src/x.test.ts": "import { describe } from 'bun:test';\n", }, }); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toHaveLength(1); expect(violations[0]?.source).toContain("'bun:test'"); }); it("skips every package in the exempt allowlist", { timeout: 5000, }, async () => { for (const exemptName of EXEMPT_PACKAGES) { const slug = exemptName.replace("@suaveplan/", ""); await writePackage({ root: fixture.root, plugin: "dev", pkg: slug, name: exemptName, files: { "src/x.test.ts": 'import { describe, it, expect } from "vitest";\n' + "import { describe as bunDescribe } from 'bun:test';\n", }, }); } const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toEqual([]); }); it("ignores packages without a src/ directory", { timeout: 5000, }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "no-src", name: "@suaveplan/no-src", files: { "README.md": "# no src here\n", }, }); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toEqual([]); }); it("does not flag re-exports from a barrel (only `import` statements)", { timeout: 5000, }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "reexport", name: "@suaveplan/reexport", files: { "src/x.ts": 'export { describe } from "vitest";\n', }, }); const violations = await findViolations({ repoRoot: fixture.root }); // The rule scope is `import` statements; re-exports from vitest in // non-test code are caught by other rules (and are nonsensical for // a non-exempt package — they would mean shipping vitest at // runtime). The check stays import-statement-scoped to keep the // signal high. expect(violations).toEqual([]); }); it("ignores packages whose package.json is missing or unparsable", { timeout: 5000, }, async () => { const pkgDir = join(fixture.root, "packages", "demo", "no-manifest"); await mkdir(join(pkgDir, "src"), { recursive: true }); await writeFile( join(pkgDir, "src", "x.test.ts"), 'import { describe } from "vitest";\n' ); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toEqual([]); }); it("scans .tsx as well as .ts", { timeout: 5000 }, async () => { await writePackage({ root: fixture.root, plugin: "demo", pkg: "tsx-pkg", name: "@suaveplan/tsx-pkg", files: { "src/x.test.tsx": 'import { describe } from "vitest";\n', }, }); const violations = await findViolations({ repoRoot: fixture.root }); expect(violations).toHaveLength(1); expect(violations[0]?.file).toEndWith(".tsx"); }); });