mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
Keep static feedback concise while retaining complete diagnostic reports, visible lint errors, module-boundary checks, and the 3% duplication gate. Upload complete reports after static failures and restore their outputs from Turbo caches. Preserve unambiguous, checkout-relative clone locations. Bind both lint caches to the website, documentation configuration and experiment sources actually scanned by oxlint, including ignore policy. Guard this ownership by comparing live checker discovery with resolved Turbo inputs. A real website syntax error now invalidates a cached pass. Validation: full current-head CI and the separate Go lane passed. Seven real-checker regressions passed; failure-status and stale-report mutations were rejected, and restored source passed. Actual Turbo restored both reports byte-for-byte; full static/docs/workflow checks passed. Closes #2238. Refs #2190. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
215 lines
6.8 KiB
TypeScript
215 lines
6.8 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { join, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const script = fileURLToPath(
|
|
new URL("../../../scripts/ci/static-check-report.mjs", import.meta.url),
|
|
);
|
|
const repository = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const fixture = async () => {
|
|
const root = await createTestTempDirectory("rea-static-report-");
|
|
await Promise.all([mkdir(join(root, "src")), mkdir(join(root, "tests"))]);
|
|
await writeFile(
|
|
join(root, ".oxlintrc.json"),
|
|
JSON.stringify({
|
|
rules: { "no-debugger": "error", "no-unused-vars": "warn" },
|
|
}),
|
|
);
|
|
await writeFile(
|
|
join(root, ".jscpd.json"),
|
|
JSON.stringify({ threshold: 3, minLines: 5, minTokens: 50 }),
|
|
);
|
|
return root;
|
|
};
|
|
const invoke = (root: string, mode: string, ...arguments_: string[]) =>
|
|
spawnSync(process.execPath, [script, mode, ...arguments_], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
timeout: 15_000,
|
|
});
|
|
const lintDiagnostics = async (root: string) =>
|
|
z
|
|
.object({
|
|
diagnostics: z.array(
|
|
z.object({
|
|
severity: z.string(),
|
|
code: z.string().optional(),
|
|
filename: z.string(),
|
|
}),
|
|
),
|
|
})
|
|
.parse(
|
|
JSON.parse(
|
|
await readFile(join(root, ".cache/static-checks/oxlint.json"), "utf8"),
|
|
),
|
|
).diagnostics;
|
|
|
|
it("includes every actually linted file in both cached lint task inputs", () => {
|
|
const scan = spawnSync(
|
|
process.execPath,
|
|
[join(repository, "node_modules/oxlint/bin/oxlint"), "--debug=files"],
|
|
{ cwd: repository, encoding: "utf8", timeout: 15_000 },
|
|
);
|
|
expect(scan.error).toBeUndefined();
|
|
expect(scan.status, scan.stderr).toBe(0);
|
|
const files = scan.stdout
|
|
.trim()
|
|
.split(/\r?\n/u)
|
|
.map((path) => path.split(sep).join("/"));
|
|
expect(files.length).toBeGreaterThan(0);
|
|
const dry = spawnSync(
|
|
process.execPath,
|
|
[
|
|
join(repository, "node_modules/turbo/bin/turbo"),
|
|
"run",
|
|
"lint",
|
|
"lint:built",
|
|
"--dry=json",
|
|
],
|
|
{
|
|
cwd: repository,
|
|
encoding: "utf8",
|
|
timeout: 15_000,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
},
|
|
);
|
|
expect(dry.error).toBeUndefined();
|
|
expect(dry.status, dry.stderr).toBe(0);
|
|
const { tasks } = z
|
|
.object({
|
|
tasks: z.array(
|
|
z.object({
|
|
task: z.string(),
|
|
inputs: z.record(z.string(), z.string()),
|
|
}),
|
|
),
|
|
})
|
|
.parse(JSON.parse(dry.stdout));
|
|
const lintTasks = tasks.filter(({ task }) =>
|
|
["lint", "lint:built"].includes(task),
|
|
);
|
|
expect(lintTasks).toHaveLength(2);
|
|
for (const task of lintTasks)
|
|
expect(
|
|
files.filter((file) => !Object.hasOwn(task.inputs, file)),
|
|
`${task.task} omits actual checker inputs`,
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("summarizes warnings without losing their complete diagnostic records", async () => {
|
|
const root = await fixture();
|
|
await writeFile(join(root, "src/warning.js"), "const forgotten = 1;\n");
|
|
const result = invoke(root, "lint");
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toContain("0 errors, 1 warnings");
|
|
expect(result.stdout.split("\n").length).toBeLessThan(10);
|
|
expect(await lintDiagnostics(root)).toEqual([
|
|
expect.objectContaining({
|
|
severity: "warning",
|
|
code: "eslint(no-unused-vars)",
|
|
filename: "src/warning.js",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("keeps lint errors failing and visible alongside the retained warnings", async () => {
|
|
const root = await fixture();
|
|
await writeFile(
|
|
join(root, "src/fault.js"),
|
|
"const forgotten = 1;\ndebugger;\n",
|
|
);
|
|
const result = invoke(root, "lint");
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("1 errors, 1 warnings");
|
|
expect(result.stderr).toContain("src/fault.js:2:1");
|
|
expect(result.stderr).toContain("eslint(no-debugger)");
|
|
expect(await lintDiagnostics(root)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ severity: "warning" }),
|
|
expect.objectContaining({ severity: "error" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("keeps malformed source failing with its parser diagnostic", async () => {
|
|
const root = await fixture();
|
|
await writeFile(join(root, "src/malformed.js"), "const value = ;\n");
|
|
const result = invoke(root, "lint");
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("src/malformed.js:1:");
|
|
expect(await lintDiagnostics(root)).toContainEqual(
|
|
expect.objectContaining({ severity: "error" }),
|
|
);
|
|
});
|
|
|
|
it("preserves the duplication threshold and complete clone locations", async () => {
|
|
const root = await fixture();
|
|
const source = [
|
|
"export function calculate(value) {",
|
|
...Array.from({ length: 34 }, (_, index) => ` value += ${index + 1};`),
|
|
" return value;",
|
|
"}",
|
|
].join("\n");
|
|
await Promise.all([
|
|
writeFile(join(root, "src/same.ts"), source),
|
|
writeFile(join(root, "tests/same.ts"), source),
|
|
]);
|
|
const result = invoke(root, "duplicates");
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("Duplication:");
|
|
expect(result.stdout.split("\n").length).toBeLessThan(15);
|
|
const report = z
|
|
.object({
|
|
statistics: z.object({ total: z.object({ percentage: z.number() }) }),
|
|
duplicates: z.array(
|
|
z.object({
|
|
firstFile: z.object({ name: z.string() }),
|
|
secondFile: z.object({ name: z.string() }),
|
|
}),
|
|
),
|
|
})
|
|
.parse(
|
|
JSON.parse(
|
|
await readFile(
|
|
join(root, ".cache/static-checks/jscpd/jscpd-report.json"),
|
|
"utf8",
|
|
),
|
|
),
|
|
);
|
|
expect(report.statistics.total.percentage).toBeGreaterThan(3);
|
|
expect(report.duplicates).toHaveLength(1);
|
|
expect(
|
|
[
|
|
report.duplicates[0]?.firstFile.name,
|
|
report.duplicates[0]?.secondFile.name,
|
|
].sort(),
|
|
).toEqual(["src/same.ts", "tests/same.ts"]);
|
|
});
|
|
|
|
it.each([
|
|
["lint", "oxlint.json"],
|
|
["duplicates", "jscpd/jscpd-report.json"],
|
|
])(
|
|
"removes stale %s evidence when the checker cannot run",
|
|
async (mode, name) => {
|
|
const root = await fixture();
|
|
const reportPath = join(root, ".cache/static-checks", name);
|
|
await mkdir(join(root, ".cache/static-checks/jscpd"), { recursive: true });
|
|
await writeFile(reportPath, '{"previous_success":true}');
|
|
const result = invoke(root, mode, "--rea-invalid-checker-option");
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("rea-invalid-checker-option");
|
|
await expect(access(reportPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|