223 lines
7.5 KiB
TypeScript
223 lines
7.5 KiB
TypeScript
/**
|
|
* 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/<plugin>/<pkg>/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<void>;
|
|
}
|
|
|
|
async function makeFixture(): Promise<Fixture> {
|
|
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<string, string>;
|
|
}): Promise<void> {
|
|
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");
|
|
});
|
|
});
|