test: add dev m3 hardware loop smoke
This commit is contained in:
@@ -20,6 +20,15 @@ const requiredDocs = [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m0-contract-audit.md"
|
||||
];
|
||||
const requiredDevM3Issue = "pikasTech/HWLAB#38";
|
||||
const requiredDevM3ValidationCommands = [
|
||||
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
|
||||
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
|
||||
];
|
||||
const requiredDevM3Docs = [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m3-hardware-loop.md"
|
||||
];
|
||||
|
||||
const statusValues = new Set(["pass", "blocked", "not_run", "not_applicable", "failed"]);
|
||||
const blockerTypes = new Set([
|
||||
@@ -52,6 +61,11 @@ function assertStatus(value, label) {
|
||||
assert.ok(statusValues.has(value), `${label} must be one of ${Array.from(statusValues).join(", ")}`);
|
||||
}
|
||||
|
||||
function assertTimestamp(value, label) {
|
||||
assertString(value, label);
|
||||
assert.ok(!Number.isNaN(Date.parse(value)), `${label} must be an RFC 3339 timestamp`);
|
||||
}
|
||||
|
||||
function assertRepoRelativePath(value, label) {
|
||||
assertString(value, label);
|
||||
assert.ok(!path.isAbsolute(value), `${label} must be repo-relative`);
|
||||
@@ -96,6 +110,11 @@ async function validateReport(relativePath) {
|
||||
const label = relativePath;
|
||||
|
||||
assertObject(report, label);
|
||||
if (report.issue === requiredDevM3Issue || report.taskId === "dev-m3-hardware-loop") {
|
||||
await validateDevM3Report(report, label);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const field of [
|
||||
"$schema",
|
||||
"$id",
|
||||
@@ -206,6 +225,149 @@ async function validateReport(relativePath) {
|
||||
}
|
||||
}
|
||||
|
||||
async function validateDevM3Report(report, label) {
|
||||
for (const field of [
|
||||
"$schema",
|
||||
"$id",
|
||||
"reportVersion",
|
||||
"issue",
|
||||
"taskId",
|
||||
"commitId",
|
||||
"acceptanceLevel",
|
||||
"devOnly",
|
||||
"prodDisabled",
|
||||
"sourceContract",
|
||||
"validationCommands",
|
||||
"runtimeTarget",
|
||||
"safetyGates",
|
||||
"liveChecks",
|
||||
"liveOperation",
|
||||
"blockers",
|
||||
"summary"
|
||||
]) {
|
||||
assert.ok(Object.hasOwn(report, field), `${label} missing ${field}`);
|
||||
}
|
||||
|
||||
assertString(report.$schema, `${label}.$schema`);
|
||||
assertString(report.$id, `${label}.$id`);
|
||||
assert.equal(report.reportVersion, "v1", `${label}.reportVersion`);
|
||||
assert.equal(report.issue, requiredDevM3Issue, `${label}.issue`);
|
||||
assert.equal(report.taskId, "dev-m3-hardware-loop", `${label}.taskId`);
|
||||
assert.match(report.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.commitId`);
|
||||
assert.equal(report.acceptanceLevel, "dev_m3_hardware_loop", `${label}.acceptanceLevel`);
|
||||
assert.equal(report.devOnly, true, `${label}.devOnly`);
|
||||
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
|
||||
|
||||
assertObject(report.sourceContract, `${label}.sourceContract`);
|
||||
assertStatus(report.sourceContract.status, `${label}.sourceContract.status`);
|
||||
assertStringArray(report.sourceContract.documents, `${label}.sourceContract.documents`, {
|
||||
minLength: requiredDevM3Docs.length
|
||||
});
|
||||
assertString(report.sourceContract.summary, `${label}.sourceContract.summary`);
|
||||
const documents = new Set(report.sourceContract.documents);
|
||||
assertUnique(report.sourceContract.documents, `${label}.sourceContract.documents`);
|
||||
for (const requiredDoc of requiredDevM3Docs) {
|
||||
assert.ok(documents.has(requiredDoc), `${label}.sourceContract.documents missing ${requiredDoc}`);
|
||||
}
|
||||
for (const [index, documentPath] of report.sourceContract.documents.entries()) {
|
||||
const absolutePath = assertRepoRelativePath(
|
||||
documentPath,
|
||||
`${label}.sourceContract.documents[${index}]`
|
||||
);
|
||||
await access(absolutePath, fsConstants.R_OK);
|
||||
}
|
||||
|
||||
assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: 2 });
|
||||
assertUnique(report.validationCommands, `${label}.validationCommands`);
|
||||
for (const requiredCommand of requiredDevM3ValidationCommands) {
|
||||
assert.ok(
|
||||
report.validationCommands.includes(requiredCommand),
|
||||
`${label}.validationCommands missing ${requiredCommand}`
|
||||
);
|
||||
}
|
||||
|
||||
assertObject(report.runtimeTarget, `${label}.runtimeTarget`);
|
||||
assert.equal(report.runtimeTarget.endpoint, "http://74.48.78.17:6667", `${label}.runtimeTarget.endpoint`);
|
||||
assert.equal(report.runtimeTarget.namespace, "hwlab-dev", `${label}.runtimeTarget.namespace`);
|
||||
assert.equal(report.runtimeTarget.environment, "dev", `${label}.runtimeTarget.environment`);
|
||||
assert.equal(report.runtimeTarget.requiredBoxSimulators, 2, `${label}.runtimeTarget.requiredBoxSimulators`);
|
||||
assert.equal(report.runtimeTarget.requiredGatewaySimulators, 2, `${label}.runtimeTarget.requiredGatewaySimulators`);
|
||||
assert.equal(report.runtimeTarget.realHardwareAllowed, false, `${label}.runtimeTarget.realHardwareAllowed`);
|
||||
assert.equal(report.runtimeTarget.prodAllowed, false, `${label}.runtimeTarget.prodAllowed`);
|
||||
|
||||
assertObject(report.safetyGates, `${label}.safetyGates`);
|
||||
for (const field of [
|
||||
"liveFlagRequired",
|
||||
"confirmDevRequired",
|
||||
"confirmedNonProductionRequired",
|
||||
"prodForbidden",
|
||||
"realHardwareForbidden",
|
||||
"secretReadForbidden",
|
||||
"forcePushForbidden",
|
||||
"unideskRuntimeSubstitutionForbidden"
|
||||
]) {
|
||||
assert.equal(report.safetyGates[field], true, `${label}.safetyGates.${field}`);
|
||||
}
|
||||
|
||||
assertArray(report.liveChecks, `${label}.liveChecks`);
|
||||
assert.ok(report.liveChecks.length >= 8, `${label}.liveChecks must include DEV M3 checks`);
|
||||
assertUnique(report.liveChecks.map((check) => check.id), `${label}.liveChecks`);
|
||||
for (const [index, check] of report.liveChecks.entries()) {
|
||||
const checkLabel = `${label}.liveChecks[${index}]`;
|
||||
assertObject(check, checkLabel);
|
||||
for (const field of ["id", "status", "summary", "evidence"]) {
|
||||
assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`);
|
||||
}
|
||||
assertString(check.id, `${checkLabel}.id`);
|
||||
assertStatus(check.status, `${checkLabel}.status`);
|
||||
assertString(check.summary, `${checkLabel}.summary`);
|
||||
assertStringArray(check.evidence, `${checkLabel}.evidence`, { minLength: 1 });
|
||||
if (Object.hasOwn(check, "blockerClass")) {
|
||||
assert.ok(blockerTypes.has(check.blockerClass), `${checkLabel}.blockerClass must be known`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(report, "localRuntimeObservations")) {
|
||||
assertArray(report.localRuntimeObservations, `${label}.localRuntimeObservations`);
|
||||
for (const [index, observation] of report.localRuntimeObservations.entries()) {
|
||||
const observationLabel = `${label}.localRuntimeObservations[${index}]`;
|
||||
assertObject(observation, observationLabel);
|
||||
for (const field of ["id", "status", "command", "summary"]) {
|
||||
assert.ok(Object.hasOwn(observation, field), `${observationLabel} missing ${field}`);
|
||||
assertString(observation[field], `${observationLabel}.${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertObject(report.liveOperation, `${label}.liveOperation`);
|
||||
assertStatus(report.liveOperation.status, `${label}.liveOperation.status`);
|
||||
for (const field of ["operationId", "traceId", "auditId", "evidenceId", "summary"]) {
|
||||
assertString(report.liveOperation[field], `${label}.liveOperation.${field}`);
|
||||
}
|
||||
|
||||
assertArray(report.blockers, `${label}.blockers`);
|
||||
assertUnique(
|
||||
report.blockers.map((blocker) => `${blocker.type}::${blocker.scope}`),
|
||||
`${label}.blockers`
|
||||
);
|
||||
for (const [index, blocker] of report.blockers.entries()) {
|
||||
const blockerLabel = `${label}.blockers[${index}]`;
|
||||
assertObject(blocker, blockerLabel);
|
||||
for (const field of ["type", "scope", "status", "summary"]) {
|
||||
assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`);
|
||||
}
|
||||
assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type must be a known blocker type`);
|
||||
assertString(blocker.scope, `${blockerLabel}.scope`);
|
||||
assert.ok(blockerStates.has(blocker.status), `${blockerLabel}.status must be open, acknowledged, or closed`);
|
||||
assertString(blocker.summary, `${blockerLabel}.summary`);
|
||||
}
|
||||
|
||||
assertObject(report.summary, `${label}.summary`);
|
||||
assertStatus(report.summary.status, `${label}.summary.status`);
|
||||
assertTimestamp(report.summary.observedAt, `${label}.summary.observedAt`);
|
||||
assertString(report.summary.result, `${label}.summary.result`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reportFiles = await collectReportFiles();
|
||||
assert.ok(reportFiles.length >= 1, "expected at least one dev-gate report JSON file");
|
||||
|
||||
Reference in New Issue
Block a user