From 61bb05da0f80f5af05309f56a0d1b36884a85bfa Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 26 Jun 2026 11:39:55 +0900 Subject: [PATCH] Reduce script test process overhead --- scripts/api-baseline.mjs | 260 +++++++++++----------- tests/scripts/development-scripts.test.ts | 16 +- tests/scripts/grit-policy.test.mjs | 2 +- 3 files changed, 141 insertions(+), 137 deletions(-) diff --git a/scripts/api-baseline.mjs b/scripts/api-baseline.mjs index 520cf5f9..4fcc2de8 100644 --- a/scripts/api-baseline.mjs +++ b/scripts/api-baseline.mjs @@ -1,19 +1,28 @@ import { spawnSync } from "node:child_process"; import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const args = new Set(process.argv.slice(2)); -const asJson = args.has("--json"); const validArgs = new Set(["--json"]); -for (const arg of args) { - if (!validArgs.has(arg)) { - console.error("Usage: node scripts/api-baseline.mjs [--json]"); - process.exit(1); +if (isMain()) { + const args = new Set(process.argv.slice(2)); + const asJson = args.has("--json"); + for (const arg of args) { + if (!validArgs.has(arg)) { + console.error("Usage: node scripts/api-baseline.mjs [--json]"); + process.exit(1); + } } -} -function fail(message) { - failures.push(message); + const report = await createApiBaselineReport(); + if (asJson) { + console.log(JSON.stringify(report, null, 2)); + } else { + printReport(report); + } + + if (report.failures.length > 0) process.exit(1); } function parseSemver(value) { @@ -41,6 +50,109 @@ function readCodexVersion() { return parseSemver(`${result.stdout}\n${result.stderr}`)?.version ?? null; } +export async function createApiBaselineReport(options = {}) { + const failures = []; + const fail = (message) => { + failures.push(message); + }; + const inputs = await readBaselineInputs(options.cwd ?? process.cwd()); + const readmeBaselines = readCompatibilityBaselines(inputs.readme); + + const codexReadmeVersion = readmeBaselines.codexTestedCliVersion; + const codexLocalVersion = options.readCodexVersion ? await options.readCodexVersion() : readCodexVersion(); + const codexReadmeSemver = parseSemver(codexReadmeVersion); + const codexLocalSemver = parseSemver(codexLocalVersion); + + const obsidianMinVersion = inputs.manifestJson.minAppVersion; + const obsidianReadmeApiTypesVersion = readmeBaselines.obsidianApiTypesVersion; + const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion; + const obsidianVersionEntry = inputs.versionsJson[inputs.packageJson.version] ?? null; + const obsidianSpec = inputs.packageJson.devDependencies?.obsidian ?? null; + const obsidianLockVersion = inputs.packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null; + const obsidianSpecSemver = parseSemver(obsidianSpec); + const obsidianLockSemver = parseSemver(obsidianLockVersion); + const obsidianMinSemver = parseSemver(obsidianMinVersion); + + const appServerGenerationExperimental = + inputs.appServerGenerateSource.includes("app-server") && + inputs.appServerGenerateSource.includes("generate-ts") && + inputs.appServerGenerateSource.includes("--experimental"); + const initializeExperimentalApi = /experimentalApi:\s*true/.test(inputs.clientSource); + const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(inputs.clientSource); + + if (!codexReadmeSemver) { + fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z."); + } + if (!codexLocalSemver) { + fail("local codex --version could not be read."); + } + if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) { + fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`); + } + if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental."); + if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true."); + if (!initializeRequestAttestationDisabled) fail("app-server initialize must declare requestAttestation: false."); + + if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z."); + if (obsidianMinSemver && obsidianMinSemver.patch !== 0) { + fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`); + } + if (!parseSemver(obsidianReadmeMinVersion)) { + fail("README.md Compatibility table must define `manifest.minAppVersion` as X.Y.Z."); + } else if (obsidianReadmeMinVersion !== obsidianMinVersion) { + fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`); + } + if (obsidianVersionEntry !== obsidianMinVersion) { + fail(`versions.json must map ${inputs.packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`); + } + if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version."); + if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z."); + if (packageRangeKind(obsidianSpec) !== "patch") { + fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`); + } + if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) { + fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`); + } + if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) { + fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`); + } + if (!parseSemver(obsidianReadmeApiTypesVersion)) { + fail("README.md Compatibility table must define `obsidian` API types as X.Y.Z."); + } else if (obsidianReadmeApiTypesVersion !== obsidianLockVersion) { + fail( + `README Obsidian API types ${displayValue(obsidianReadmeApiTypesVersion)} does not match package-lock obsidian ${displayValue(obsidianLockVersion)}.`, + ); + } + + return { + codex: { + policy: "managed by minor version", + readmeTestedCliVersion: codexReadmeVersion, + readmeTestedMinor: minorKey(codexReadmeSemver), + localCliVersion: codexLocalVersion, + localCliMinor: minorKey(codexLocalSemver), + localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null, + appServerGenerationExperimental, + initializeExperimentalApi, + initializeRequestAttestationDisabled, + }, + obsidian: { + policy: "manifest declares the minimum app version; obsidian npm provides compile-time API types in the same minor", + minAppVersion: obsidianMinVersion, + minAppVersionMinor: minorKey(obsidianMinSemver), + readmeMinAppVersion: obsidianReadmeMinVersion, + versionsJsonCurrentMinAppVersion: obsidianVersionEntry, + readmeApiTypesVersion: obsidianReadmeApiTypesVersion, + packageDependency: obsidianSpec, + packageDependencyRange: packageRangeKind(obsidianSpec), + packageDependencyMinor: minorKey(obsidianSpecSemver), + lockedPackageVersion: obsidianLockVersion, + lockedPackageMinor: minorKey(obsidianLockSemver), + }, + failures, + }; +} + function readCompatibilityBaselines(readme) { const section = markdownSection(readme, "Compatibility"); const table = readMarkdownTableValues(section); @@ -112,121 +224,15 @@ function displayValue(value) { return value ?? "(missing)"; } -const failures = []; -const inputs = await readBaselineInputs(); -const readmeBaselines = readCompatibilityBaselines(inputs.readme); - -const codexReadmeVersion = readmeBaselines.codexTestedCliVersion; -const codexLocalVersion = readCodexVersion(); -const codexReadmeSemver = parseSemver(codexReadmeVersion); -const codexLocalSemver = parseSemver(codexLocalVersion); - -const obsidianMinVersion = inputs.manifestJson.minAppVersion; -const obsidianReadmeApiTypesVersion = readmeBaselines.obsidianApiTypesVersion; -const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion; -const obsidianVersionEntry = inputs.versionsJson[inputs.packageJson.version] ?? null; -const obsidianSpec = inputs.packageJson.devDependencies?.obsidian ?? null; -const obsidianLockVersion = inputs.packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null; -const obsidianSpecSemver = parseSemver(obsidianSpec); -const obsidianLockSemver = parseSemver(obsidianLockVersion); -const obsidianMinSemver = parseSemver(obsidianMinVersion); - -const appServerGenerationExperimental = - inputs.appServerGenerateSource.includes("app-server") && - inputs.appServerGenerateSource.includes("generate-ts") && - inputs.appServerGenerateSource.includes("--experimental"); -const initializeExperimentalApi = /experimentalApi:\s*true/.test(inputs.clientSource); -const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(inputs.clientSource); - -if (!codexReadmeSemver) { - fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z."); -} -if (!codexLocalSemver) { - fail("local codex --version could not be read."); -} -if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) { - fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`); -} -if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental."); -if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true."); -if (!initializeRequestAttestationDisabled) fail("app-server initialize must declare requestAttestation: false."); - -if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z."); -if (obsidianMinSemver && obsidianMinSemver.patch !== 0) { - fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`); -} -if (!parseSemver(obsidianReadmeMinVersion)) { - fail("README.md Compatibility table must define `manifest.minAppVersion` as X.Y.Z."); -} else if (obsidianReadmeMinVersion !== obsidianMinVersion) { - fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`); -} -if (obsidianVersionEntry !== obsidianMinVersion) { - fail(`versions.json must map ${inputs.packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`); -} -if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version."); -if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z."); -if (packageRangeKind(obsidianSpec) !== "patch") { - fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`); -} -if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) { - fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`); -} -if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) { - fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`); -} -if (!parseSemver(obsidianReadmeApiTypesVersion)) { - fail("README.md Compatibility table must define `obsidian` API types as X.Y.Z."); -} else if (obsidianReadmeApiTypesVersion !== obsidianLockVersion) { - fail( - `README Obsidian API types ${displayValue(obsidianReadmeApiTypesVersion)} does not match package-lock obsidian ${displayValue(obsidianLockVersion)}.`, - ); -} - -const report = { - codex: { - policy: "managed by minor version", - readmeTestedCliVersion: codexReadmeVersion, - readmeTestedMinor: minorKey(codexReadmeSemver), - localCliVersion: codexLocalVersion, - localCliMinor: minorKey(codexLocalSemver), - localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null, - appServerGenerationExperimental, - initializeExperimentalApi, - initializeRequestAttestationDisabled, - }, - obsidian: { - policy: "manifest declares the minimum app version; obsidian npm provides compile-time API types in the same minor", - minAppVersion: obsidianMinVersion, - minAppVersionMinor: minorKey(obsidianMinSemver), - readmeMinAppVersion: obsidianReadmeMinVersion, - versionsJsonCurrentMinAppVersion: obsidianVersionEntry, - readmeApiTypesVersion: obsidianReadmeApiTypesVersion, - packageDependency: obsidianSpec, - packageDependencyRange: packageRangeKind(obsidianSpec), - packageDependencyMinor: minorKey(obsidianSpecSemver), - lockedPackageVersion: obsidianLockVersion, - lockedPackageMinor: minorKey(obsidianLockSemver), - }, - failures, -}; - -if (asJson) { - console.log(JSON.stringify(report, null, 2)); -} else { - printReport(report); -} - -if (failures.length > 0) process.exit(1); - -async function readBaselineInputs() { +async function readBaselineInputs(cwd) { const [packageJson, packageLockJson, manifestJson, versionsJson, readme, clientSource, appServerGenerateSource] = await Promise.all([ - readJson("package.json"), - readJson("package-lock.json"), - readJson("manifest.json"), - readJson("versions.json"), - readFile("README.md", "utf8"), - readFile("src/app-server/connection/client.ts", "utf8"), - readFile("scripts/generate-app-server-types.mjs", "utf8"), + readJson(cwd, "package.json"), + readJson(cwd, "package-lock.json"), + readJson(cwd, "manifest.json"), + readJson(cwd, "versions.json"), + readFile(path.join(cwd, "README.md"), "utf8"), + readFile(path.join(cwd, "src/app-server/connection/client.ts"), "utf8"), + readFile(path.join(cwd, "scripts/generate-app-server-types.mjs"), "utf8"), ]); return { @@ -240,8 +246,12 @@ async function readBaselineInputs() { }; } -async function readJson(file) { - return JSON.parse(await readFile(file, "utf8")); +async function readJson(cwd, file) { + return JSON.parse(await readFile(path.join(cwd, file), "utf8")); +} + +function isMain() { + return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false; } function printReport(report) { diff --git a/tests/scripts/development-scripts.test.ts b/tests/scripts/development-scripts.test.ts index 6fee9ee2..c693be95 100644 --- a/tests/scripts/development-scripts.test.ts +++ b/tests/scripts/development-scripts.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -48,10 +48,9 @@ describe("development scripts", () => { it("reads app-server initialize flags from the connection client in API baseline checks", async () => { const cwd = await tempWorkspace(); - const binDir = path.join(cwd, "bin"); - await mkdir(binDir, { recursive: true }); await mkdir(path.join(cwd, "scripts"), { recursive: true }); await mkdir(path.join(cwd, "src", "app-server", "connection"), { 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", @@ -100,16 +99,11 @@ describe("development scripts", () => { ].join("\n"), ); - const codexBin = path.join(binDir, "codex"); - await writeFile(codexBin, "#!/usr/bin/env node\nconsole.log('codex 0.139.0');\n"); - await chmod(codexBin, 0o755); - - const result = runNodeScript("scripts/api-baseline.mjs", ["--json"], cwd, { - PATH: `${binDir}${path.delimiter}${process.env["PATH"] ?? ""}`, + const report = await createApiBaselineReport({ + cwd, + readCodexVersion: () => "0.139.0", }); - expect(result.status).toBe(0); - const report = JSON.parse(result.stdout); expect(report.codex.initializeExperimentalApi).toBe(true); expect(report.codex.initializeRequestAttestationDisabled).toBe(true); expect(report.failures).toEqual([]); diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index f4d5f589..27231c37 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -17,7 +17,7 @@ const projectPluginByName = new Map( const APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE = "Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol, and chat request handling may consume server request protocol at their app-server boundaries; feature state and UI must use Panel-owned models."; -describe("GritQL source policy", () => { +describe.concurrent("GritQL source policy", () => { it("keeps project-wide source-shape policies enforceable as Biome plugin diagnostics", async () => { const cwd = await tempBiomeWorkspace([ "no-handwritten-reexports.grit",