Files
pikasTech-HWLAB/scripts/m5-mvp-e2e-dry-run.mjs
T

633 lines
26 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import crypto from "node:crypto";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const planPath = "fixtures/mvp/m5-e2e/dry-run-plan.json";
const checklistPath = "fixtures/mvp/m5-e2e/dev-acceptance-checklist.json";
const matrixPath = "docs/reference/MVP-e2e-acceptance.md";
const runtimePath = "fixtures/mvp/runtime.json";
const contractPath = "protocol/mvp-e2e-contract.md";
const commonSchemaPath = "protocol/schemas/common.json";
const JSON_RPC_VERSION = "2.0";
let serviceIds = [];
let serviceIdSet = new Set();
const requiredArtifactFields = [
"serviceId",
"commitId",
"image",
"tag",
"digest",
"buildSource",
"deployEnv",
"healthTimestamp"
];
const requiredStepSequence = [
"step-dev-health",
"step-gateway-box-status",
"step-wiring",
"step-direct-hardware-call",
"step-audit",
"step-agent-session",
"step-agent-hardware-call",
"step-evidence",
"step-cleanup"
];
const requiredTraceStates = ["accepted", "dispatched", "executed", "completed"];
function readJSON(relativePath) {
return readFile(path.join(repoRoot, relativePath), "utf8").then((raw) => JSON.parse(raw));
}
function byKey(items, key) {
const map = new Map();
for (const item of items) {
assert.ok(item && typeof item === "object", `${key} item must be an object`);
assert.ok(item[key], `${key} item missing ${key}`);
assert.equal(map.has(item[key]), false, `${key} ${item[key]} must be unique`);
map.set(item[key], item);
}
return map;
}
function requireDevObject(label, object, environmentField = "environment") {
assert.equal(object[environmentField], ENVIRONMENT_DEV, `${label} ${environmentField}`);
}
function requireTimestamp(label, value) {
assert.equal(typeof value, "string", `${label} timestamp must be a string`);
assert.equal(Number.isNaN(Date.parse(value)), false, `${label} timestamp must parse`);
}
function requireServiceId(label, serviceId) {
assert.equal(serviceIdSet.has(serviceId), true, `${label} unknown serviceId ${serviceId}`);
}
function assertArrayIncludesAll(label, actual, expected) {
assert.ok(Array.isArray(actual), `${label} must be an array`);
for (const item of expected) {
assert.equal(actual.includes(item), true, `${label} missing ${item}`);
}
}
function assertNoProdTarget(value, trail = "plan") {
if (typeof value === "string") {
assert.equal(value.includes(":6666"), false, `${trail} must not target deprecated legacy port`);
assert.equal(/\bprod\b/i.test(value), false, `${trail} must not target PROD`);
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => assertNoProdTarget(item, `${trail}[${index}]`));
return;
}
if (value && typeof value === "object") {
for (const [key, nested] of Object.entries(value)) {
if (
key === "prohibitedActions" ||
key === "failureCriteria" ||
key === "failRule" ||
key === "digest" ||
key === "sha256"
) {
continue;
}
assertNoProdTarget(nested, `${trail}.${key}`);
}
}
}
function assertObjectHasOnlyDryRunSafety(plan) {
assert.equal(plan.mode, "dry-run", "plan mode");
assert.equal(plan.safety.allowNetwork, false, "network must be disabled");
assert.equal(plan.safety.allowDeploy, false, "deploy must be disabled");
assert.equal(plan.safety.allowBrowserE2E, false, "browser e2e must be disabled");
assert.equal(plan.safety.allowSecrets, false, "secret reads must be disabled");
assertArrayIncludesAll("prohibited actions", plan.safety.prohibitedActions, [
"real-dev-deploy",
"prod-deploy",
"prod-smoke",
"browser-e2e",
"heavyweight-e2e",
"secret-or-token-read",
"force-push",
"unidesk-runtime-substitution"
]);
}
function validateMeta(meta) {
assert.ok(meta && typeof meta === "object" && !Array.isArray(meta), "meta must be an object");
assert.ok(meta.traceId, "meta.traceId is required");
requireServiceId("meta.serviceId", meta.serviceId);
assert.equal(meta.environment, ENVIRONMENT_DEV, "meta.environment");
}
function validateRequest(envelope) {
assert.ok(envelope && typeof envelope === "object" && !Array.isArray(envelope), "request must be an object");
assert.equal(envelope.jsonrpc, JSON_RPC_VERSION, "request jsonrpc");
assert.notEqual(envelope.id, undefined, "request id");
assert.match(envelope.method, /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/, "request method");
if (Object.hasOwn(envelope, "params")) {
assert.ok(
envelope.params && typeof envelope.params === "object" && !Array.isArray(envelope.params),
"request params must be an object"
);
}
validateMeta(envelope.meta);
}
function validateResponse(envelope) {
assert.ok(envelope && typeof envelope === "object" && !Array.isArray(envelope), "response must be an object");
assert.equal(envelope.jsonrpc, JSON_RPC_VERSION, "response jsonrpc");
assert.notEqual(envelope.id, undefined, "response id");
const hasResult = Object.hasOwn(envelope, "result");
const hasError = Object.hasOwn(envelope, "error");
assert.notEqual(hasResult, hasError, "response must contain exactly one of result or error");
if (hasError) {
assert.ok(envelope.error && typeof envelope.error === "object", "response error must be an object");
assert.equal(Number.isInteger(envelope.error.code), true, "response error code");
assert.ok(envelope.error.message, "response error message");
}
validateMeta(envelope.meta);
}
function assertEndpointFreeze(plan, checklist, runtime, contract) {
assert.equal(plan.environment, ENVIRONMENT_DEV, "plan environment");
assert.equal(plan.endpoint, DEV_ENDPOINT, "plan endpoint");
assert.equal(checklist.environment, ENVIRONMENT_DEV, "checklist environment");
assert.equal(checklist.endpoint, DEV_ENDPOINT, "checklist endpoint");
assert.equal(runtime.endpoints.dev, DEV_ENDPOINT, "runtime fixture endpoint");
assert.equal(contract.includes(DEV_ENDPOINT), true, "MVP contract must include DEV endpoint");
assertNoProdTarget(plan);
}
function assertArtifacts(plan, checklist) {
assert.deepEqual(checklist.artifactObservabilityFields, requiredArtifactFields);
const artifacts = byKey(plan.artifacts, "serviceId");
const requiredServiceIds = new Set([
"hwlab-edge-proxy",
"hwlab-tunnel-client",
"hwlab-router",
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-agent-skills",
"hwlab-cli"
]);
for (const serviceId of requiredServiceIds) {
assert.equal(artifacts.has(serviceId), true, `artifact missing ${serviceId}`);
}
for (const artifact of artifacts.values()) {
requireServiceId(`artifact ${artifact.serviceId}`, artifact.serviceId);
for (const field of requiredArtifactFields) {
assert.ok(artifact[field], `artifact ${artifact.serviceId} missing ${field}`);
}
assert.equal(artifact.deployEnv, ENVIRONMENT_DEV, `artifact ${artifact.serviceId} deployEnv`);
requireTimestamp(`artifact ${artifact.serviceId}.healthTimestamp`, artifact.healthTimestamp);
if (artifact.digest === "not_applicable") {
assert.ok(
artifact.digestNotApplicableReason,
`artifact ${artifact.serviceId} must explain not_applicable digest`
);
} else {
assert.match(artifact.digest, /^sha256:[a-f0-9]{64}$/, `artifact ${artifact.serviceId} digest`);
}
}
return artifacts;
}
function assertHealth(plan, checklist, artifacts) {
const healthById = byKey(plan.health, "healthId");
const healthByComponent = byKey(plan.health, "component");
const healthByService = new Map();
for (const health of plan.health) {
if (!healthByService.has(health.serviceId)) {
healthByService.set(health.serviceId, health);
}
}
for (const contract of checklist.healthContracts) {
const health = healthByComponent.get(contract.component);
assert.ok(health, `health missing component ${contract.component}`);
assert.equal(health.serviceId, contract.serviceId, `health ${contract.component} serviceId`);
assert.equal(artifacts.has(health.serviceId), true, `health ${contract.component} missing artifact`);
assert.equal(health.status, "healthy", `health ${contract.component} status`);
assert.equal(health.deployEnv, ENVIRONMENT_DEV, `health ${contract.component} deployEnv`);
assert.equal(health.dryRun, true, `health ${contract.component} dryRun`);
requireTimestamp(`health ${contract.component}`, health.healthTimestamp);
}
assert.equal(healthByService.get("hwlab-edge-proxy").endpoint, `${DEV_ENDPOINT}/health`);
assert.equal(healthByService.get("hwlab-cloud-web").output.directHardwareCall, false);
assert.equal(healthByService.get("hwlab-gateway").output.patchPanelRequired, true);
assert.equal(healthByService.get("hwlab-box-simu").output.patchPanelOnlyPropagation, true);
assert.equal(healthByService.get("hwlab-agent-mgr").output.workerLeak, false);
return { byId: healthById, byService: healthByService, count: plan.health.length };
}
function assertAcceptanceSteps(plan, checklist) {
const checklistStepIds = new Set(checklist.smokeSteps.map((step) => step.id));
assert.deepEqual(
checklist.smokeSteps.map((step) => step.order),
checklist.smokeSteps.map((_, index) => index + 1),
"checklist smoke steps must be ordered"
);
const referencedStepIds = new Set();
for (const step of plan.steps) {
for (const acceptanceStepId of step.acceptanceStepIds) {
assert.equal(
checklistStepIds.has(acceptanceStepId),
true,
`${step.id} references unknown acceptance step ${acceptanceStepId}`
);
referencedStepIds.add(acceptanceStepId);
}
}
assertArrayIncludesAll("dry-run acceptance coverage", [...referencedStepIds], [
"static-contract-parse",
"endpoint-freeze",
"dev-ingress-health",
"edge-route",
"frp-tunnel",
"d601-router",
"cloud-surface",
"gateway-sim-patch-panel",
"agent-runtime",
"artifact-observability"
]);
}
function assertStepGraph(plan) {
const steps = plan.steps.slice().sort((a, b) => a.order - b.order);
assert.deepEqual(
steps.map((step) => step.id),
requiredStepSequence,
"M5 sequence must cover required dry-run flow"
);
assert.deepEqual(plan.expectedSequence, requiredStepSequence, "expectedSequence must match required sequence");
const seen = new Set();
for (const step of steps) {
assert.equal(seen.has(step.id), false, `duplicate step ${step.id}`);
assert.ok(Array.isArray(step.inputs) && step.inputs.length > 0, `${step.id} inputs`);
assert.ok(Array.isArray(step.outputs) && step.outputs.length > 0, `${step.id} outputs`);
for (const dependency of step.requires) {
assert.equal(seen.has(dependency), true, `${step.id} requires ${dependency} before it is produced`);
}
seen.add(step.id);
}
}
function assertProjectGatewayAndBoxes(plan) {
requireDevObject("project", plan.project);
assert.equal(plan.project.status, "active", "project status");
const gatewaySessions = byKey(plan.gatewaySessions, "gatewaySessionId");
assert.equal(gatewaySessions.has("gws_m5-0001"), true, "real gateway boundary session is required");
assert.equal(gatewaySessions.has("gws_m5-simu-0001"), true, "gateway simulator session is required");
for (const session of gatewaySessions.values()) {
assert.equal(session.projectId, plan.project.projectId, `gateway ${session.gatewaySessionId} project`);
assert.ok(["hwlab-gateway", "hwlab-gateway-simu"].includes(session.serviceId), "gateway serviceId");
assert.equal(session.status, "connected", `gateway ${session.gatewaySessionId} status`);
requireDevObject(`gateway ${session.gatewaySessionId}`, session);
}
const resources = byKey(plan.boxResources, "resourceId");
const capabilities = byKey(plan.boxCapabilities, "capabilityId");
for (const resource of resources.values()) {
assert.equal(resource.projectId, plan.project.projectId, `resource ${resource.resourceId} project`);
assert.equal(gatewaySessions.has(resource.gatewaySessionId), true, `resource ${resource.resourceId} gateway`);
assert.equal(resource.state, "available", `resource ${resource.resourceId} state`);
requireDevObject(`resource ${resource.resourceId}`, resource);
}
for (const capability of capabilities.values()) {
assert.equal(capability.projectId, plan.project.projectId, `capability ${capability.capabilityId} project`);
assert.equal(resources.has(capability.resourceId), true, `capability ${capability.capabilityId} resource`);
assert.match(capability.name, /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/);
}
return { gatewaySessions, resources, capabilities };
}
function assertWiring(plan, { gatewaySessions, resources, capabilities }) {
const wiring = plan.wiringConfig;
assert.equal(wiring.projectId, plan.project.projectId, "wiring project");
assert.equal(gatewaySessions.has(wiring.gatewaySessionId), true, "wiring gateway");
assert.equal(wiring.status, "active", "wiring status");
assert.equal(wiring.constraints.patchPanelOwnsRouting, true, "wiring must be patch-panel-owned");
assert.equal(wiring.constraints.directBoxBypassAllowed, false, "wiring must reject direct box bypass");
for (const connection of wiring.connections) {
for (const side of ["from", "to"]) {
const endpoint = connection[side];
assert.equal(resources.has(endpoint.resourceId), true, `wiring ${side} resource`);
assert.equal(capabilities.has(endpoint.capabilityId), true, `wiring ${side} capability`);
assert.equal(
capabilities.get(endpoint.capabilityId).resourceId,
endpoint.resourceId,
`wiring ${side} capability must belong to resource`
);
}
}
const patch = plan.patchPanelStatus;
assert.equal(patch.projectId, plan.project.projectId, "patch project");
assert.equal(patch.gatewaySessionId, wiring.gatewaySessionId, "patch gateway");
assert.equal(patch.wiringConfigId, wiring.wiringConfigId, "patch wiring");
assert.equal(patch.serviceId, "hwlab-patch-panel", "patch service");
assert.equal(patch.state, "active", "patch state");
requireDevObject("patch", patch);
requireTimestamp("patch observedAt", patch.observedAt);
const activeConnectionKeys = new Set(
patch.activeConnections.map(
(connection) =>
`${connection.fromResourceId}:${connection.fromPort}->${connection.toResourceId}:${connection.toPort}`
)
);
for (const connection of wiring.connections) {
const key = `${connection.from.resourceId}:${connection.from.port}->${connection.to.resourceId}:${connection.to.port}`;
assert.equal(activeConnectionKeys.has(key), true, `patch panel missing active connection ${key}`);
}
}
function assertRpcAndOperations(plan, { gatewaySessions, resources, capabilities }) {
const operations = byKey(plan.operations, "operationId");
const rpcByName = byKey(plan.rpc, "name");
for (const rpc of rpcByName.values()) {
validateRequest(rpc.request);
validateResponse(rpc.response);
assert.equal(rpc.request.meta.environment, ENVIRONMENT_DEV, `${rpc.name} request env`);
assert.equal(rpc.response.meta.environment, ENVIRONMENT_DEV, `${rpc.name} response env`);
assert.equal(rpc.request.meta.traceId, rpc.response.meta.traceId, `${rpc.name} trace continuity`);
assert.equal(rpc.request.params.dryRun, undefined, `${rpc.name} params must not use top-level dryRun`);
assert.equal(rpc.request.params.input.dryRun, true, `${rpc.name} input dryRun`);
assert.equal(rpc.response.result.operationId, rpc.operationId, `${rpc.name} response operation`);
assert.equal(operations.has(rpc.operationId), true, `${rpc.name} operation exists`);
}
assert.equal(rpcByName.has("direct-hardware-request"), true, "direct RPC is required");
assert.equal(rpcByName.has("agent-hardware-request"), true, "agent RPC is required");
for (const operation of operations.values()) {
assert.equal(operation.projectId, plan.project.projectId, `operation ${operation.operationId} project`);
assert.equal(gatewaySessions.has(operation.gatewaySessionId), true, `operation ${operation.operationId} gateway`);
assert.equal(resources.has(operation.resourceId), true, `operation ${operation.operationId} resource`);
assert.equal(capabilities.has(operation.capabilityId), true, `operation ${operation.operationId} capability`);
assert.equal(operation.status, "succeeded", `operation ${operation.operationId} status`);
assert.equal(operation.input.dryRun, true, `operation ${operation.operationId} dryRun`);
assert.equal(operation.output.patchPanelStatusId, plan.patchPanelStatus.patchPanelStatusId);
requireDevObject(`operation ${operation.operationId}`, operation);
}
const agentOperation = operations.get("op_m5-agent-0001");
assert.equal(agentOperation.agentSessionId, plan.agentSession.agentSessionId, "agent operation session");
assert.equal(agentOperation.workerSessionId, plan.workerSession.workerSessionId, "agent operation worker");
return { operations, rpcByName };
}
function assertAgent(plan, { gatewaySessions }) {
const { agentSession, workerSession } = plan;
assert.equal(agentSession.projectId, plan.project.projectId, "agent project");
assert.equal(agentSession.serviceId, "hwlab-agent-mgr", "agent service");
assert.equal(agentSession.status, "completed", "agent status");
assert.equal(agentSession.metadata.workerSessionId, workerSession.workerSessionId, "agent worker link");
requireDevObject("agent", agentSession);
assert.equal(workerSession.agentSessionId, agentSession.agentSessionId, "worker agent link");
assert.equal(workerSession.projectId, plan.project.projectId, "worker project");
assert.equal(workerSession.serviceId, "hwlab-agent-worker", "worker service");
assert.equal(workerSession.status, "completed", "worker status");
assert.equal(gatewaySessions.has(workerSession.gatewaySessionId), true, "worker gateway");
assert.equal(workerSession.metadata.scopedProjectId, plan.project.projectId, "worker project scope");
requireDevObject("worker", workerSession);
}
function assertTraceAuditEvidenceAndCleanup(plan, { operations }) {
const traceEvents = byKey(plan.traceEvents, "traceEventId");
const auditEvents = byKey(plan.auditEvents, "auditId");
const evidenceRecords = byKey(plan.evidenceRecords, "evidenceId");
const cleanupRecords = byKey(plan.cleanup, "cleanupId");
for (const trace of traceEvents.values()) {
assert.equal(trace.projectId, plan.project.projectId, `trace ${trace.traceEventId} project`);
requireServiceId(`trace ${trace.traceEventId}`, trace.serviceId);
requireDevObject(`trace ${trace.traceEventId}`, trace);
requireTimestamp(`trace ${trace.traceEventId}`, trace.occurredAt);
if (trace.operationId) {
assert.equal(operations.has(trace.operationId), true, `trace ${trace.traceEventId} operation`);
}
}
for (const operationId of ["op_m5-direct-0001", "op_m5-agent-0001"]) {
const states = plan.traceEvents
.filter((trace) => trace.operationId === operationId)
.map((trace) => trace.metadata.state);
assert.deepEqual(states, requiredTraceStates, `${operationId} trace lifecycle`);
}
for (const audit of auditEvents.values()) {
assert.equal(audit.projectId, plan.project.projectId, `audit ${audit.auditId} project`);
requireServiceId(`audit ${audit.auditId}`, audit.serviceId);
requireDevObject(`audit ${audit.auditId}`, audit);
requireTimestamp(`audit ${audit.auditId}`, audit.occurredAt);
assert.ok(audit.outcome, `audit ${audit.auditId} outcome`);
}
assert.equal(
[...auditEvents.values()].some((audit) => audit.action === "hardware.operation.request"),
true,
"hardware operation audit required"
);
assert.equal(auditEvents.has("aud_m5-cleanup"), true, "cleanup audit required");
for (const evidence of evidenceRecords.values()) {
assert.equal(evidence.projectId, plan.project.projectId, `evidence ${evidence.evidenceId} project`);
assert.equal(operations.has(evidence.operationId), true, `evidence ${evidence.evidenceId} operation`);
requireServiceId(`evidence ${evidence.evidenceId}`, evidence.serviceId);
requireDevObject(`evidence ${evidence.evidenceId}`, evidence);
requireTimestamp(`evidence ${evidence.evidenceId}`, evidence.createdAt);
}
for (const operationId of operations.keys()) {
assert.equal(
[...evidenceRecords.values()].some((evidence) => evidence.operationId === operationId),
true,
`evidence missing for ${operationId}`
);
}
for (const cleanup of cleanupRecords.values()) {
assert.equal(cleanup.status, "completed", `cleanup ${cleanup.cleanupId} status`);
assert.equal(cleanup.environment, ENVIRONMENT_DEV, `cleanup ${cleanup.cleanupId} environment`);
requireServiceId(`cleanup ${cleanup.cleanupId}`, cleanup.serviceId);
requireTimestamp(`cleanup ${cleanup.cleanupId}`, cleanup.completedAt);
}
assert.equal(cleanupRecords.get("cln_m5-worker").outputs.workspaceReleased, true, "worker cleanup release");
assert.equal(cleanupRecords.get("cln_m5-gateway-state").outputs.transientStateCleared, true);
return { traceEvents, auditEvents, evidenceRecords, cleanupRecords };
}
async function assertEvidenceFiles(evidenceRecords) {
for (const evidence of evidenceRecords.values()) {
const absolutePath = path.join(repoRoot, evidence.uri);
const content = await readFile(absolutePath);
const digest = crypto.createHash("sha256").update(content).digest("hex");
const stats = await stat(absolutePath);
assert.equal(digest, evidence.sha256, `evidence ${evidence.evidenceId} sha256`);
assert.equal(stats.size, evidence.sizeBytes, `evidence ${evidence.evidenceId} sizeBytes`);
}
}
function assertStepInputsOutputs(plan, indexes) {
const resolvers = {
health: (id) => indexes.healthIds.has(id),
gatewaySession: (id) => indexes.gatewaySessions.has(id),
boxResource: (id) => indexes.resources.has(id),
resource: (id) => indexes.resources.has(id),
capability: (id) => indexes.capabilities.has(id),
wiringConfig: (id) => id === plan.wiringConfig.wiringConfigId,
patchPanelStatus: (id) => id === plan.patchPanelStatus.patchPanelStatusId,
rpc: (id) => indexes.rpcByName.has(id),
operation: (id) => indexes.operations.has(id),
audit: (id) => indexes.auditEvents.has(id),
agentSession: (id) => id === plan.agentSession.agentSessionId,
workerSession: (id) => id === plan.workerSession.workerSessionId,
trace: (id) => indexes.traceIds.has(id),
evidence: (id) => indexes.evidenceRecords.has(id),
cleanup: (id) => indexes.cleanupRecords.has(id)
};
for (const step of plan.steps) {
for (const input of step.inputs) {
if (input === "endpoint") {
continue;
}
const [type, id] = input.split(":");
assert.ok(type && id, `${step.id} input ${input} must be typed`);
assert.ok(resolvers[type], `${step.id} input ${input} has unknown type ${type}`);
assert.equal(resolvers[type](id), true, `${step.id} input ${input} does not resolve`);
}
}
const allOutputs = new Set([
"route_m5-dev",
"tun_m5-d601",
"hwlab-dev",
"hwlab-cloud-api",
"hwlab-cloud-web",
...indexes.gatewaySessions.keys(),
...indexes.resources.keys(),
...indexes.capabilities.keys(),
plan.wiringConfig.wiringConfigId,
plan.patchPanelStatus.patchPanelStatusId,
...indexes.operations.keys(),
...indexes.traceIds,
...indexes.auditEvents.keys(),
plan.agentSession.agentSessionId,
plan.workerSession.workerSessionId,
...indexes.evidenceRecords.keys(),
...indexes.cleanupRecords.keys()
]);
for (const step of plan.steps) {
for (const output of step.outputs) {
assert.equal(allOutputs.has(output), true, `${step.id} output ${output} does not resolve`);
}
}
}
function assertDependencies(plan) {
assertArrayIncludesAll(
"M0-M4 dependencies",
plan.m0ToM4Dependencies.map((dependency) => dependency.id),
["m0-contract-audit", "m1-local-smoke", "m2-cloud-core", "m3-hardware-loop", "m4-agent-runtime"]
);
const dependencyStepIds = new Set(plan.steps.map((step) => step.id));
for (const dependency of plan.m0ToM4Dependencies) {
assert.ok(dependency.provides.length > 0, `${dependency.id} provides`);
assert.equal(dependency.status, "contract_only", `${dependency.id} status`);
for (const stepId of dependency.requiredFor) {
assert.equal(dependencyStepIds.has(stepId), true, `${dependency.id} references unknown ${stepId}`);
}
}
assert.equal(plan.realDevGate.command, "hwlab-cli test e2e --env dev --mvp");
assert.equal(plan.realDevGate.requiresAllBlockersClear, true);
assert.equal(plan.realDevGate.requiresDryRunPass, true);
assert.equal(plan.realDevGate.requiresHumanApprovalForRealDev, true);
}
const [plan, checklist, runtime, contract, matrix, commonSchema] = await Promise.all([
readJSON(planPath),
readJSON(checklistPath),
readJSON(runtimePath),
readFile(path.join(repoRoot, contractPath), "utf8"),
readFile(path.join(repoRoot, matrixPath), "utf8"),
readJSON(commonSchemaPath)
]);
serviceIds = commonSchema.$defs.serviceId.enum;
serviceIdSet = new Set(serviceIds);
assert.equal(matrix.includes("HWLAB MVP E2E"), true, "MVP E2E reference exists");
assertObjectHasOnlyDryRunSafety(plan);
assertEndpointFreeze(plan, checklist, runtime, contract);
const artifacts = assertArtifacts(plan, checklist);
const health = assertHealth(plan, checklist, artifacts);
assertAcceptanceSteps(plan, checklist);
assertStepGraph(plan);
const topology = assertProjectGatewayAndBoxes(plan);
assertWiring(plan, topology);
assertAgent(plan, topology);
const rpcAndOperations = assertRpcAndOperations(plan, topology);
const evidenceIndexes = assertTraceAuditEvidenceAndCleanup(plan, rpcAndOperations);
await assertEvidenceFiles(evidenceIndexes.evidenceRecords);
assertStepInputsOutputs(plan, {
...topology,
...rpcAndOperations,
...evidenceIndexes,
healthIds: new Set(health.byId.keys()),
traceIds: new Set(plan.traceEvents.map((trace) => trace.traceId))
});
assertDependencies(plan);
console.log(
[
`M5 MVP E2E dry-run passed: ${plan.steps.length} steps`,
`${artifacts.size} artifacts`,
`${health.count} health contracts`,
`${rpcAndOperations.operations.size} hardware operations`,
`${evidenceIndexes.evidenceRecords.size} evidence records`,
"0 network calls"
].join(", ")
);