Reduce script test process overhead

This commit is contained in:
murashit 2026-06-26 11:39:55 +09:00
parent 3759f76e90
commit 61bb05da0f
3 changed files with 141 additions and 137 deletions

View file

@ -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) {

View file

@ -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([]);

View file

@ -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",