Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/scripts/check-web-pal-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function coverageClaimsFromFile(coverage) {
invalid.push(`${PENDING_FIELD} must be an object of name -> owner`);
} else {
for (const [name, owner] of Object.entries(pendingEntries)) {
if (typeof owner !== "string" || owner.length === 0) {
if (typeof owner !== "string" || owner.trim().length === 0) {
invalid.push(`${PENDING_FIELD}.${name} must name an owning lane`);
continue;
}
Expand Down
74 changes: 65 additions & 9 deletions desktop/scripts/web-pal-census.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ function modulePathForImport(specifier, sourcePath) {
}
if (!specifier.startsWith(".")) return null;

const candidate = path.resolve(path.dirname(sourcePath), specifier);
const absoluteSourcePath = path.isAbsolute(sourcePath)
? sourcePath
: path.resolve(REPO_DIR, sourcePath);
const candidate = path.resolve(path.dirname(absoluteSourcePath), specifier);
const candidates = [
candidate,
`${candidate}.ts`,
Expand Down Expand Up @@ -521,16 +524,58 @@ export function buildManifest(renderer, rustCommands) {
};
}

export async function generateManifest({ outputPath = MANIFEST_PATH } = {}) {
export async function createManifest() {
const [renderer, rustCommands] = await Promise.all([
extractRendererCommands(),
extractRustCommands(),
]);
const manifest = buildManifest(renderer, rustCommands);
return buildManifest(renderer, rustCommands);
}

export async function generateManifest({ outputPath = MANIFEST_PATH } = {}) {
const manifest = await createManifest();
await writeFile(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
return manifest;
}

function namesForSection(manifest, section) {
const commands = manifest?.[section]?.commands;
if (!Array.isArray(commands)) {
throw new Error(`Manifest is missing ${section}.commands`);
}
return commands.map((command) =>
typeof command === "string" ? command : command?.name,
);
}

function describeNameDrift(section, committedNames, currentNames) {
const committed = new Set(committedNames);
const current = new Set(currentNames);
const added = currentNames.filter((name) => !committed.has(name));
const removed = committedNames.filter((name) => !current.has(name));
return `${section} command drift (added: ${added.join(", ") || "none"}; removed: ${removed.join(", ") || "none"})`;
}

export async function checkCommittedManifest({
manifestPath = MANIFEST_PATH,
} = {}) {
const [committed, current] = await Promise.all([
readFile(manifestPath, "utf8").then(JSON.parse),
createManifest(),
]);
for (const section of ["renderer", "rust"]) {
const committedNames = namesForSection(committed, section);
const currentNames = namesForSection(current, section);
if (
committedNames.length !== currentNames.length ||
committedNames.some((name, index) => name !== currentNames[index])
) {
throw new Error(describeNameDrift(section, committedNames, currentNames));
}
}
return current;
}

function printSummary(manifest) {
const rendererCount = manifest.renderer.commandCount;
const rustCount = manifest.rust.commandCount;
Expand Down Expand Up @@ -560,10 +605,21 @@ const isMain =
process.argv[1] &&
pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
if (isMain) {
generateManifest()
.then(printSummary)
.catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
const args = process.argv.slice(2);
const unknownArgs = args.filter((argument) => argument !== "--check");
const action =
unknownArgs.length > 0
? Promise.reject(
new Error(`Unknown argument(s): ${unknownArgs.join(", ")}`),
)
: args.includes("--check")
? checkCommittedManifest().then((manifest) => {
printSummary(manifest);
console.log("Committed manifest matches current command names");
})
: generateManifest().then(printSummary);
action.catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}
22 changes: 22 additions & 0 deletions desktop/src/platform/web/coverage-guard.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { checkCoverage } from "../../../scripts/check-web-pal-coverage.mjs";
import { createManifest } from "../../../scripts/web-pal-census.mjs";

// Enforces that every renderer-invoked Tauri command has an explicit browser
// classification (implemented / noop / capability-off). Adding a new invoke()
Expand All @@ -16,6 +17,12 @@ test("web PAL coverage: every renderer command is classified", async () => {
const coverage = JSON.parse(
await readFile(new URL("./coverage.json", import.meta.url), "utf8"),
);
const currentManifest = await createManifest();
assert.deepEqual(
currentManifest.renderer.commands.map((command) => command.name),
manifest.renderer.commands.map((command) => command.name),
"renderer command census drift; regenerate docs/web-pal-commands.json",
);
const result = checkCoverage(manifest, coverage);
assert.deepEqual(result.missing, [], "unaccounted renderer commands");
assert.deepEqual(
Expand All @@ -27,3 +34,18 @@ test("web PAL coverage: every renderer command is classified", async () => {
assert.deepEqual(result.invalid, [], "invalid coverage entries");
assert.equal(result.ok, true);
});

test("web PAL coverage rejects blank and unknown pending owners", () => {
const result = checkCoverage(
{ renderer: { commands: [{ name: "known" }] } },
{
implemented: [],
noop: [],
"capability-off": [],
pending: { known: " ", unknown: "lane" },
},
);
assert.deepEqual(result.invalid, ["pending.known must name an owning lane"]);
assert.deepEqual(result.unknown, ["unknown"]);
assert.equal(result.ok, false);
});
48 changes: 47 additions & 1 deletion desktop/src/platform/web/web-pal-census.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
checkCommittedManifest,
extractRendererCommandsFromSource,
extractRustCommandsFromSource,
} from "../../../scripts/web-pal-census.mjs";

const execFileAsync = promisify(execFile);
const DESKTOP_DIR = fileURLToPath(new URL("../../../", import.meta.url));
const REPO_DIR = path.resolve(DESKTOP_DIR, "..");
const CENSUS_SCRIPT = fileURLToPath(
new URL("../../../scripts/web-pal-census.mjs", import.meta.url),
);

test("AST census extracts single-line, multi-line, aliased, and raw invocations", async () => {
const fixture = await readFile(
new URL(
Expand Down Expand Up @@ -55,3 +68,36 @@ test("Rust command extraction handles module paths and cfg attributes", () => {
`);
assert.deepEqual(commands, ["first_command", "second_command"]);
});

test("census check is independent of the caller working directory", async () => {
for (const cwd of [REPO_DIR, DESKTOP_DIR]) {
const { stdout } = await execFileAsync(
process.execPath,
[CENSUS_SCRIPT, "--check"],
{ cwd },
);
assert.match(stdout, /Renderer commands: 294 distinct/);
assert.match(stdout, /Committed manifest matches current command names/);
}
});

test("census check rejects committed command-name drift", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "buzz-census-test-"));
const manifestPath = path.join(directory, "web-pal-commands.json");
try {
const manifest = JSON.parse(
await readFile(
path.join(DESKTOP_DIR, "docs/web-pal-commands.json"),
"utf8",
),
);
manifest.renderer.commands.pop();
await writeFile(manifestPath, JSON.stringify(manifest), "utf8");
await assert.rejects(
checkCommittedManifest({ manifestPath }),
/renderer command drift/,
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
Loading