Files
pleno-vue/scripts/test-graph/verify-platform-result.mjs
T
Jeppe B 12a2d73b32 Replace monolithic E2E matrices with dependency-aware component CI
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.
2026-07-20 23:52:15 +02:00

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;
});
}