Generate small reusable component partitions with explicit provider dependencies, GitHub-hosted concurrency, fail-closed evidence, and exact mobile store gates. Include the literal-i18n fix from #206 so validation covers the exact post-merge tree.
155 lines
5.2 KiB
JavaScript
155 lines
5.2 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { GRAPH_VERSION } from "./component-node-catalog.mjs";
|
|
import { loadRepositoryTestGraph } from "./load-graph.mjs";
|
|
import { validateDependencyCiResults } from "./result-schema.mjs";
|
|
|
|
const requiredRoles = ["superuser", "admin", "customer", "subuser"];
|
|
|
|
const platformDefinitions = {
|
|
android: {
|
|
lanes: ["chromium-mobile", "ct-chromium-mobile"],
|
|
roleLane: "chromium-mobile",
|
|
},
|
|
apple: {
|
|
lanes: ["webkit-mobile"],
|
|
roleLane: "webkit-mobile",
|
|
},
|
|
};
|
|
|
|
export function normalizeStorePlatform(platform) {
|
|
const normalized = String(platform || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!platformDefinitions[normalized]) {
|
|
throw new Error(`Unsupported store platform: ${platform || "<empty>"}`);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function expectedPlatformResults(platform, graph) {
|
|
const normalized = normalizeStorePlatform(platform);
|
|
if (!graph?.nodes || !Array.isArray(graph.topologicalOrder)) {
|
|
throw new Error("A resolved dependency graph is required for platform verification.");
|
|
}
|
|
return platformDefinitions[normalized].lanes.flatMap((lane) =>
|
|
graph.topologicalOrder
|
|
.map((nodeId) => graph.nodes.get(nodeId))
|
|
.filter((node) => node.lanes.includes(lane))
|
|
.map((node) => ({ nodeId: node.id, lane }))
|
|
);
|
|
}
|
|
|
|
export function evaluatePlatformResult(platform, manifest, graph) {
|
|
const normalized = normalizeStorePlatform(platform);
|
|
const definition = platformDefinitions[normalized];
|
|
const failures = [];
|
|
|
|
try {
|
|
validateDependencyCiResults(manifest);
|
|
} catch (error) {
|
|
failures.push(`dependency manifest:${error instanceof Error ? error.message : String(error)}`);
|
|
return { platform: normalized, lanes: definition.lanes, required: [], failures, passed: false };
|
|
}
|
|
|
|
if (manifest.profile !== "full") {
|
|
failures.push(`dependency manifest:profile ${manifest.profile} is not full`);
|
|
}
|
|
if (manifest.graphVersion !== GRAPH_VERSION) {
|
|
failures.push(`dependency manifest:graph version ${manifest.graphVersion} is not ${GRAPH_VERSION}`);
|
|
}
|
|
if (manifest.requiredCi !== true) {
|
|
failures.push("dependency manifest:Required CI failed");
|
|
}
|
|
|
|
const expected = expectedPlatformResults(normalized, graph);
|
|
const expectedKeys = new Set(expected.map(({ nodeId, lane }) => `${nodeId}|${lane}`));
|
|
const relevant = manifest.results.filter((result) => definition.lanes.includes(result.lane));
|
|
const byKey = new Map();
|
|
|
|
for (const result of relevant) {
|
|
const key = `${result.nodeId}|${result.lane}`;
|
|
if (!expectedKeys.has(key)) {
|
|
failures.push(`${result.nodeId}/${result.lane}:unexpected result`);
|
|
continue;
|
|
}
|
|
if (byKey.has(key)) {
|
|
failures.push(`${result.nodeId}/${result.lane}:duplicate result`);
|
|
continue;
|
|
}
|
|
byKey.set(key, result);
|
|
}
|
|
|
|
const coveredRoles = new Set();
|
|
for (const { nodeId, lane } of expected) {
|
|
const result = byKey.get(`${nodeId}|${lane}`);
|
|
if (!result) {
|
|
failures.push(`${nodeId}/${lane}:missing result`);
|
|
continue;
|
|
}
|
|
if (result.status !== "success") {
|
|
failures.push(`${nodeId}/${lane}:${result.status}`);
|
|
}
|
|
if (result.expectedTests < 1) {
|
|
failures.push(`${nodeId}/${lane}:no expected tests`);
|
|
}
|
|
if (result.executedTests + result.skippedTests !== result.expectedTests) {
|
|
failures.push(
|
|
`${nodeId}/${lane}:accounted for ${result.executedTests} green and ${result.skippedTests} intentionally skipped of ${result.expectedTests}`
|
|
);
|
|
}
|
|
if (lane === definition.roleLane) {
|
|
for (const role of result.roles) {
|
|
coveredRoles.add(role);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const role of requiredRoles) {
|
|
if (!coveredRoles.has(role)) {
|
|
failures.push(`${definition.roleLane}:role ${role} missing`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
platform: normalized,
|
|
lanes: [...definition.lanes],
|
|
required: expected.map(({ nodeId, lane }) => `${nodeId}/${lane}`),
|
|
failures,
|
|
passed: failures.length === 0,
|
|
};
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const value = argv[index];
|
|
if (!value.startsWith("--")) throw new Error(`Unexpected argument: ${value}`);
|
|
const [name, inline] = value.slice(2).split("=", 2);
|
|
args[name] = inline ?? argv[++index];
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export async function main(argv = process.argv.slice(2)) {
|
|
const args = parseArgs(argv);
|
|
if (!args.manifest) throw new Error("--manifest is required.");
|
|
const manifest = JSON.parse(await fs.readFile(path.resolve(args.manifest), "utf8"));
|
|
const { graph } = await loadRepositoryTestGraph(await fs.realpath(process.cwd()));
|
|
const result = evaluatePlatformResult(args.platform, manifest, graph);
|
|
if (!result.passed) {
|
|
throw new Error(`${result.platform} mobile test gate failed: ${result.failures.join(", ")}`);
|
|
}
|
|
console.log(`${result.platform} mobile test gate passed: ${result.required.join(", ")}`);
|
|
return result;
|
|
}
|
|
|
|
const currentFile = fileURLToPath(import.meta.url);
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
|
|
await main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|