diff --git a/tests/scripts/development-scripts.test.ts b/tests/scripts/development-scripts.test.ts index 50fea4f0..e76b4836 100644 --- a/tests/scripts/development-scripts.test.ts +++ b/tests/scripts/development-scripts.test.ts @@ -14,17 +14,6 @@ afterEach(async () => { }); describe("development scripts", () => { - it.each([ - { message: "feat: add side chats", expectedStatus: 0 }, - { message: "Merge pull request #123 from owner/feature", expectedStatus: 0 }, - { message: 'Revert "feat: add side chats"', expectedStatus: 1 }, - { message: "v5.1.0", expectedStatus: 1 }, - { message: "fixup! feat: add side chats", expectedStatus: 1 }, - { message: "squash! feat: add side chats", expectedStatus: 1 }, - ])("enforces the commit message policy for $message", ({ message, expectedStatus }) => { - expect(runCommitlint(message)).toBe(expectedStatus); - }); - it("fails style builds when CSS files are missing from the style order file", async () => { const cwd = await styleOrderFixture(); @@ -66,28 +55,6 @@ describe("development scripts", () => { ); }); - it("preserves generated bindings when generation fails", async () => { - const cwd = await tempWorkspace(); - const generatedDir = path.join(cwd, "src", "generated", "app-server"); - await mkdir(generatedDir, { recursive: true }); - await writeFile(path.join(generatedDir, "Existing.ts"), "export type Existing = true;\n"); - await writeAppServerCompatibility(cwd, "0.144.4"); - const { generateAppServerTypes } = await import(pathToFileURL(path.join(repoRoot, "scripts", "generate-app-server-types.mjs")).href); - - await expect( - generateAppServerTypes({ - cwd, - readCodexVersion: () => "0.144.4", - runCommand: async () => { - throw new Error("generation failed"); - }, - }), - ).rejects.toThrow("generation failed"); - - await expect(readFile(path.join(generatedDir, "Existing.ts"), "utf8")).resolves.toBe("export type Existing = true;\n"); - await expect(readdir(path.join(cwd, "src", "generated"))).resolves.toEqual(["app-server"]); - }); - it("checks normalized generated bindings without replacing the tracked tree", async () => { const cwd = await tempWorkspace(); const generatedDir = path.join(cwd, "src", "generated", "app-server"); @@ -141,85 +108,6 @@ describe("development scripts", () => { ); }); - it("reads app-server compatibility policy from the declared baseline in API baseline checks", async () => { - const cwd = await tempWorkspace(); - await mkdir(path.join(cwd, "src", "app-server"), { recursive: true }); - const { createApiBaselineReport } = await import(pathToFileURL(path.join(repoRoot, "scripts", "api-baseline.mjs")).href); - - await writeJson(path.join(cwd, "package.json"), { - version: "1.0.0", - devDependencies: { - obsidian: "~1.12.3", - }, - }); - await writeJson(path.join(cwd, "package-lock.json"), { - packages: { - "node_modules/obsidian": { - version: "1.12.3", - }, - }, - }); - await writeJson(path.join(cwd, "manifest.json"), { - minAppVersion: "1.12.0", - }); - await writeJson(path.join(cwd, "versions.json"), { - "1.0.0": "1.12.0", - }); - await writeFile( - path.join(cwd, "README.md"), - [ - "## Compatibility", - "", - "| Key | Version | Notes |", - "| --- | --- | --- |", - "| `codex.testedCliVersion` | `0.139.0` | Tested CLI. |", - "| `manifest.minAppVersion` | `1.12.0` | Minimum app version. |", - "| `obsidian` API types | `1.12.3` | Compile-time API package. |", - ].join("\n"), - ); - await mkdir(path.join(cwd, "src", "app-server", "connection"), { recursive: true }); - await writeJson(path.join(cwd, "src", "app-server", "connection", "compatibility.json"), { - codexAppServer: { - testedCliVersion: "0.139.0", - typeGeneration: { - arguments: ["app-server", "generate-ts", "--experimental"], - }, - initialize: { - capabilities: { - experimentalApi: true, - requestAttestation: false, - }, - }, - }, - }); - - const report = await createApiBaselineReport({ - cwd, - readCodexVersion: () => "0.139.0", - }); - - expect(report.codex.recordedTestedCliVersion).toBe("0.139.0"); - expect(report.codex.generationArguments).toEqual(["app-server", "generate-ts", "--experimental"]); - expect(report.codex.readmeMatchesRecordedVersion).toBe(true); - expect(report.codex.localCliMatchesRecordedVersion).toBe(true); - expect(report.codex.initializeExperimentalApi).toBe(true); - expect(report.codex.initializeRequestAttestationDisabled).toBe(true); - expect(report.failures).toEqual([]); - - const missingLocalCliReport = await createApiBaselineReport({ - cwd, - readCodexVersion: () => null, - }); - expect(missingLocalCliReport.failures).toContain("local codex --version could not be read."); - }); - - it("rejects the obsolete recorded-only API baseline mode", () => { - const result = runNodeScript("scripts/api-baseline.mjs", ["--recorded-only"], repoRoot); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("Usage: node scripts/api-baseline.mjs [--json]"); - }); - it("reports app-server provenance drift between the recorded, README, and local CLI versions", async () => { const cwd = await apiBaselineFixture({ recordedCodexVersion: "0.144.4", readmeCodexVersion: "0.144.0" }); const { createApiBaselineReport } = await import(pathToFileURL(path.join(repoRoot, "scripts", "api-baseline.mjs")).href); @@ -230,30 +118,28 @@ describe("development scripts", () => { expect(report.failures).toContain("local Codex CLI 0.144.5 does not match recorded tested CLI 0.144.4."); }); - it("checks generated app-server bindings in CI and release paths", async () => { - const [checkWorkflow, releaseWorkflow, releasePreflight] = await Promise.all([ - readFile(path.join(repoRoot, ".github", "workflows", "check.yml"), "utf8"), - readFile(path.join(repoRoot, ".github", "workflows", "release.yml"), "utf8"), - readFile(path.join(repoRoot, "scripts", "release", "preflight.mjs"), "utf8"), - ]); - - for (const workflow of [checkWorkflow, releaseWorkflow]) { - expect(workflow).toContain("npm install --global"); - expect(workflow).toContain("node scripts/app-server-compatibility.mjs --tested-cli-version"); - expect(workflow).toContain("npm run generate:app-server-types:check"); - } - expect(releasePreflight).toContain('run("npm", ["run", "generate:app-server-types:check"]);'); - }); - it("reports representative CSS usage policy failures", async () => { const cwd = await cssUsageFixture({ "src/styles/10-component.css": [ ".codex-panel__used { display: block; }", ".codex-panel__test-only { display: block; }", ".codex-panel__unused { display: block; }", + ".codex-panel__item { display: block; }", + ".codex-panel__item-detail { display: block; }", + ".codex-panel__tokens {", + " --codex-panel-used-size: 1px;", + " --codex-panel-unused-size: 2px;", + " width: var(--codex-panel-used-size);", + " /*", + " --codex-panel-commented-size: 3px;", + " width: var(--codex-panel-unused-size);", + " */", + "}", ].join("\n"), "src/component.ts": [ 'export const className = "codex-panel__used";', + 'export const detailClassName = "codex-panel__item-detail";', + 'export const tokenClassName = "codex-panel__tokens";', "export const dynamicClassName = `codex-panel__task-step--$" + "{status}`;", ].join("\n"), "tests/component.test.ts": 'expect("codex-panel__test-only").toBeTruthy();\n', @@ -268,98 +154,11 @@ describe("development scripts", () => { expect(result.stderr).toContain("codex-panel__task-step--"); expect(result.stderr).toContain("codex-panel__test-only"); expect(result.stderr).toContain("codex-panel__unused"); - }); - - it("reports unused panel custom properties", async () => { - const cwd = await cssUsageFixture({ - "src/styles/10-component.css": [ - ".codex-panel__used {", - " --codex-panel-used-size: 1px;", - " --codex-panel-unused-size: 2px;", - " width: var(--codex-panel-used-size);", - " /*", - " --codex-panel-commented-size: 3px;", - " width: var(--codex-panel-unused-size);", - " */", - "}", - ].join("\n"), - "src/component.ts": 'export const className = "codex-panel__used";\n', - }); - - const result = runNodeScript("scripts/check-css-usage.mjs", [], cwd); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("Unused CSS custom property candidates:"); + expect(result.stderr).toContain(" codex-panel__item\n"); expect(result.stderr).toContain("--codex-panel-unused-size"); expect(result.stderr).not.toContain("--codex-panel-used-size\n"); expect(result.stderr).not.toContain("--codex-panel-commented-size"); }); - - it("does not count a longer source class name as usage", async () => { - const cwd = await cssUsageFixture({ - "src/styles/10-component.css": [".codex-panel__item { display: block; }", ".codex-panel__item-detail { display: block; }"].join("\n"), - "src/component.ts": 'export const className = "codex-panel__item-detail";\n', - }); - - const result = runNodeScript("scripts/check-css-usage.mjs", [], cwd); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("Unused CSS class candidates:"); - expect(result.stderr).toContain(" codex-panel__item\n"); - }); - - it("prepares release notes from conventional commits since the previous tag", async () => { - const cwd = await tempWorkspace(); - await writeJson(path.join(cwd, "package.json"), { version: "2.3.2" }); - await writeJson(path.join(cwd, "package-lock.json"), { - version: "2.3.2", - packages: { "": { version: "2.3.2" } }, - }); - await writeJson(path.join(cwd, "manifest.json"), { version: "2.3.2", minAppVersion: "1.12.0" }); - await writeJson(path.join(cwd, "versions.json"), { "2.3.2": "1.12.0" }); - runGit(["init"], cwd); - runGit(["config", "user.name", "Codex Panel Tests"], cwd); - runGit(["config", "user.email", "tests@example.com"], cwd); - runGit(["add", "."], cwd); - runGit(["commit", "-m", "chore: establish release baseline"], cwd); - runGit(["tag", "2.3.2"], cwd); - await writeFile(path.join(cwd, "change.txt"), "side chats\n"); - runGit(["add", "change.txt"], cwd); - runGit(["commit", "-m", "feat(chat): add side chat support"], cwd); - - const result = runNodeScript("scripts/release/prepare.mjs", ["2.4.0"], cwd); - - expect(result.status).toBe(0); - await expect(readFile(path.join(cwd, ".github", "release-notes", "2.4.0.md"), "utf8")).resolves.toBe( - "## Changes\n\n- Add side chat support.\n", - ); - await expect(readJson(path.join(cwd, "package.json"))).resolves.toMatchObject({ version: "2.4.0" }); - }); - - it("fails release prepare before changing version files when release notes already exist", async () => { - const cwd = await tempWorkspace(); - await mkdir(path.join(cwd, ".github", "release-notes"), { recursive: true }); - - const packageJson = { version: "2.3.2" }; - const packageLockJson = { version: "2.3.2", packages: { "": { version: "2.3.2" } } }; - const manifestJson = { version: "2.3.2", minAppVersion: "1.12.0" }; - const versionsJson = { "2.3.2": "1.12.0" }; - - await writeJson(path.join(cwd, "package.json"), packageJson); - await writeJson(path.join(cwd, "package-lock.json"), packageLockJson); - await writeJson(path.join(cwd, "manifest.json"), manifestJson); - await writeJson(path.join(cwd, "versions.json"), versionsJson); - await writeFile(path.join(cwd, ".github", "release-notes", "2.3.3.md"), "## Changes\n\n- Existing\n"); - - const result = runNodeScript("scripts/release/prepare.mjs", ["2.3.3"], cwd); - - expect(result.status).toBe(1); - expect(result.stderr).toContain(".github/release-notes/2.3.3.md already exists"); - await expect(readJson(path.join(cwd, "package.json"))).resolves.toEqual(packageJson); - await expect(readJson(path.join(cwd, "package-lock.json"))).resolves.toEqual(packageLockJson); - await expect(readJson(path.join(cwd, "manifest.json"))).resolves.toEqual(manifestJson); - await expect(readJson(path.join(cwd, "versions.json"))).resolves.toEqual(versionsJson); - }); }); async function tempWorkspace(): Promise { @@ -446,24 +245,6 @@ function runNodeScript(script: string, args: string[] = [], cwd = repoRoot, env: }); } -function runCommitlint(message: string): number | null { - return spawnSync(process.execPath, [path.join(repoRoot, "node_modules", "@commitlint", "cli", "cli.js")], { - cwd: repoRoot, - encoding: "utf8", - input: `${message}\n`, - shell: false, - }).status; -} - -function runGit(args: string[], cwd: string): void { - const result = spawnSync("git", args, { cwd, encoding: "utf8", shell: false }); - if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); -} - async function writeJson(file: string, value: unknown): Promise { await writeFile(file, `${JSON.stringify(value, null, 2)}\n`); } - -async function readJson(file: string): Promise { - return JSON.parse(await readFile(file, "utf8")); -} diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 2e90634f..2f0cbda6 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -173,7 +173,15 @@ const policyCases = [ 'import { renderPreactRoot } from "../../../shared/dom/preact-root.dom";', "export const value = 1;", ), - policyCase("no-restricted-css-policy.grit", "src/styles/escape.css", ".escape { color: #fff; }", ".safe { color: var(--text-normal); }"), + policyCase("no-restricted-css-policy.grit", "src/styles/escape.css", ".escape { color: #fff; }", ".safe { color: var(--text-normal); }", { + invalid: [ + { path: "src/styles/has.css", source: ".panel:has(.child) { color: var(--text-normal); }" }, + { path: "src/styles/where.css", source: ".panel:where(.child) { color: var(--text-normal); }" }, + { path: "src/styles/id.css", source: "#panel { color: var(--text-normal); }" }, + { path: "src/styles/universal.css", source: ".panel * { color: var(--text-normal); }" }, + ], + valid: [{ path: "src/styles/shallow.css", source: ".panel .child:hover { color: var(--text-normal); }" }], + }), ]; afterEach(async () => { @@ -191,70 +199,46 @@ describe("Biome Grit policies", () => { expect(covered).toEqual(configured); }); - it.each(policyCases)("$plugin rejects its invalid source", async (testCase) => { - const result = await lintPolicyCase(testCase, "invalid"); + it.each(policyCases)("$plugin rejects invalid sources and accepts valid sources", async (testCase) => { + const result = await lintPolicyCase(testCase); expect(result.status, result.output).toBe(1); - expect(result.pluginErrors, result.output).toBeGreaterThan(0); - }); - - it.each(policyCases)("$plugin accepts its valid source", async (testCase) => { - const result = await lintPolicyCase(testCase, "valid"); - - expect(result.status, result.output).toBe(0); - expect(result.pluginErrors, result.output).toBe(0); - }); - - it.each([ - ["the relational :has pseudo-class", ".panel:has(.child) { color: var(--text-normal); }"], - ["class selectors hidden inside :where", ".panel:where(.child) { color: var(--text-normal); }"], - ["ID selectors", "#panel { color: var(--text-normal); }"], - ["universal selectors", ".panel * { color: var(--text-normal); }"], - ])("no-restricted-css-policy.grit rejects %s", async (_name, invalidSource) => { - const result = await lintPolicyCase( - { - plugin: "no-restricted-css-policy.grit", - invalid: { path: "src/styles/invalid.css", source: invalidSource }, - }, - "invalid", - ); - - expect(result.status, result.output).toBe(1); - expect(result.pluginErrors, result.output).toBeGreaterThan(0); - }); - - it("no-restricted-css-policy.grit accepts shallow panel selectors", async () => { - const result = await lintPolicyCase( - { - plugin: "no-restricted-css-policy.grit", - valid: { path: "src/styles/valid.css", source: ".panel .child:hover { color: var(--text-normal); }" }, - }, - "valid", - ); - - expect(result.status, result.output).toBe(0); - expect(result.pluginErrors, result.output).toBe(0); + for (const target of result.invalidTargets) expect(result.pluginErrorFiles, result.output).toContain(target); + for (const target of result.validTargets) expect(result.errorFiles, result.output).not.toContain(target); }); }); function policyCase(plugin, invalidPath, invalidSource, validSource, options = {}) { return { plugin, - invalid: { path: invalidPath, source: invalidSource }, - valid: { path: options.validPath ?? invalidPath, source: validSource }, + invalid: [{ path: invalidPath, source: invalidSource }, ...(options.invalid ?? [])], + valid: [{ path: options.validPath ?? invalidPath, source: validSource }, ...(options.valid ?? [])], }; } -async function lintPolicyCase(testCase, variant) { +async function lintPolicyCase(testCase) { const plugin = (await projectPlugins()).find((candidate) => path.basename(pluginPath(candidate)) === testCase.plugin); if (!plugin) throw new Error(`Missing configured Grit policy: ${testCase.plugin}`); const workspace = await mkdtemp(path.join(tmpdir(), "codex-panel-grit-policy-")); workspaces.add(workspace); - const fixture = testCase[variant]; - const fixturePath = path.join(workspace, fixture.path); - await mkdir(path.dirname(fixturePath), { recursive: true }); - await Promise.all([writeFile(fixturePath, fixture.source), writePluginConfig(workspace, plugin)]); - return biomeLint(workspace, fixture.path); + const [invalidTargets, validTargets] = await Promise.all([ + writeFixtures(workspace, "invalid", testCase.invalid), + writeFixtures(workspace, "valid", testCase.valid), + writePluginConfig(workspace, plugin), + ]); + return { ...biomeLint(workspace, [...invalidTargets, ...validTargets]), invalidTargets, validTargets }; +} + +async function writeFixtures(workspace, variant, fixtures) { + return Promise.all( + fixtures.map(async (fixture) => { + const target = path.join(variant, fixture.path).replaceAll(path.sep, "/"); + const fixturePath = path.join(workspace, target); + await mkdir(path.dirname(fixturePath), { recursive: true }); + await writeFile(fixturePath, fixture.source); + return target; + }), + ); } async function projectPlugins() { @@ -271,14 +255,21 @@ async function writePluginConfig(workspace, plugin) { await writeFile(path.join(workspace, "biome.json"), JSON.stringify(config)); } -function biomeLint(workspace, target) { - const result = spawnSync(biomeBin, ["lint", target, "--config-path", workspace, "--reporter=json", "--max-diagnostics=none"], { +function biomeLint(workspace, targets) { + const result = spawnSync(biomeBin, ["lint", ...targets, "--config-path", workspace, "--reporter=json", "--max-diagnostics=none"], { cwd: workspace, encoding: "utf8", }); const report = JSON.parse(result.stdout.slice(result.stdout.indexOf("{"))); + const errorDiagnostics = report.diagnostics.filter((diagnostic) => diagnostic.severity === "error"); + const diagnosticFiles = (diagnostics) => + diagnostics + .map((diagnostic) => (typeof diagnostic.location?.path === "string" ? diagnostic.location.path : diagnostic.location?.path?.file)) + .filter(Boolean) + .map((file) => path.relative(workspace, path.resolve(workspace, file)).replaceAll(path.sep, "/")); return { - pluginErrors: report.diagnostics.filter((diagnostic) => diagnostic.category === "plugin" && diagnostic.severity === "error").length, + errorFiles: diagnosticFiles(errorDiagnostics), + pluginErrorFiles: diagnosticFiles(errorDiagnostics.filter((diagnostic) => diagnostic.category === "plugin")), status: result.status, output: `${result.stdout}\n${result.stderr}`, };