#!/usr/bin/env node import assert from "node:assert/strict"; import { access, readdir, readFile } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { ACTIVE_DEV_BROWSER_ENDPOINT, ACTIVE_DEV_PUBLIC_ENDPOINT, REPORT_LIFECYCLE_VERSION, deprecatedPublicEndpoints, findDeprecatedPublicEndpoints, findForbiddenActiveDeprecatedEndpoints, reportIsHistorical } from "../internal/dev-report-lifecycle.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const reportsDir = path.join(repoRoot, "reports/dev-gate"); const contractIssue = "pikasTech/HWLAB#31"; const issueFamily = Object.freeze({ DEV_DEPLOY_APPLY: "pikasTech/HWLAB#33", DEV_GATE_PREFLIGHT: "pikasTech/HWLAB#34", DEV_ARTIFACT_PUBLISH: "pikasTech/HWLAB#35", DEV_EDGE_HEALTH: "pikasTech/HWLAB#36", DEV_M4_AGENT_LOOP: "pikasTech/HWLAB#37", DEV_M3_HARDWARE_LOOP: "pikasTech/HWLAB#38", DEV_M5_GATE_REPORT: "pikasTech/HWLAB#39", DEV_BASE_IMAGE_PREFLIGHT: "pikasTech/HWLAB#40", DEV_EVIDENCE_BLOCKER_AGGREGATOR: "pikasTech/HWLAB#41", DEV_M5_GATE_AGGREGATOR_V2: "pikasTech/HWLAB#58", DEV_CLOUD_WORKBENCH_LIVE: "pikasTech/HWLAB#7" }); const allowedIssues = new Set([ contractIssue, ...Object.values(issueFamily) ]); const requiredPreflightIssue = issueFamily.DEV_GATE_PREFLIGHT; const requiredMilestoneIds = ["M0", "M1", "M2", "M3", "M4", "M5"]; const evidenceLevels = new Set(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]); const devCloudWorkbenchRuntimeIdentityMaxSkewMs = 15 * 60 * 1000; const syntheticAcceptedCodeAgentModels = new Set(["gpt-5"]); const deploymentPreflightCodeAgentErrorCode = "deployment_identity_preflight"; const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]); const requiredValidationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ]; const requiredAggregatorV2Commands = [ "node --check scripts/dev-evidence-blocker-aggregator.mjs", "node --check scripts/src/dev-evidence-blocker-aggregator.mjs", "node scripts/dev-evidence-blocker-aggregator.mjs --check", "node scripts/dev-evidence-blocker-aggregator.mjs --markdown", "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ]; const requiredDevDeployApplySupports = [ "pikasTech/HWLAB#63", "pikasTech/HWLAB#50", "pikasTech/HWLAB#7", "pikasTech/HWLAB#33", "pikasTech/HWLAB#17", "pikasTech/HWLAB#32", "pikasTech/HWLAB#30" ]; const requiredDevDeployApplyValidationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs", "node --check scripts/dev-deploy-apply.mjs", "node --check scripts/src/dev-deploy-apply.mjs", "node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked" ]; const requiredPreflightValidationCommands = [ "node --check scripts/dev-gate-preflight.mjs", "node --check scripts/src/dev-gate-preflight.mjs", "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/refresh-artifact-catalog.mjs", "node scripts/dev-gate-preflight.mjs" ]; const requiredSmokeCommand = "node scripts/m1-contract-smoke.mjs"; const requiredDryRunCommand = "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run"; const requiredDocs = [ "docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md" ]; const requiredDevM3Issue = issueFamily.DEV_M3_HARDWARE_LOOP; const requiredDevM3ValidationCommands = [ "node --check scripts/dev-m3-hardware-loop-smoke.mjs", "node --check scripts/validate-dev-m3-cardinality.mjs", "node scripts/validate-dev-m3-cardinality.mjs", "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run", "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod" ]; const legacyDevM3LiveCommand = "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 requiredDevEdgeIssue = issueFamily.DEV_EDGE_HEALTH; const requiredDevEdgeValidationCommands = [ "node --check scripts/dev-edge-health-smoke.mjs", "node --check scripts/src/dev-edge-health-smoke-lib.mjs", "node scripts/dev-edge-health-smoke.mjs --live --write-report" ]; const requiredDevEdgeDocs = [ "docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md", "docs/dev-edge-health.md" ]; const requiredDevM2Issue = "pikasTech/HWLAB#23"; const requiredDevM2ValidationCommands = [ "node --check scripts/m2-dev-deploy-smoke.mjs", "node scripts/m2-dev-deploy-smoke.mjs --dry-run", "node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report", "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ]; const requiredDevM2Docs = [ "docs/m2-dev-deploy-smoke.md", "docs/dev-acceptance-matrix.md", "fixtures/dev-deploy-smoke/dev-deploy-smoke.json" ]; const requiredDevCloudWorkbenchValidationCommands = [ "node --check scripts/dev-cloud-workbench-smoke.mjs", "node --check scripts/src/dev-cloud-workbench-smoke-lib.mjs", "node scripts/dev-cloud-workbench-smoke.mjs --source", "node scripts/dev-cloud-workbench-smoke.mjs --mobile", "node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture", "node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture" ]; const requiredDevCloudWorkbenchDomOnlyValidationCommands = [ "node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-dom-only.json", "node scripts/validate-dev-gate-report.mjs reports/dev-gate/dev-cloud-workbench-dom-only.json" ]; const requiredDevCloudWorkbenchLiveValidationCommands = [ "node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json", "node scripts/validate-dev-gate-report.mjs reports/dev-gate/dev-cloud-workbench-live.json" ]; const requiredDevCloudWorkbenchDocs = [ "docs/cloud-web-workbench.md", "docs/reference/cloud-workbench.md", "docs/reference/code-agent-chat-readiness.md" ]; const reportFamilyTemplates = new Map([ [ "dev-gate-report-contract", { issue: contractIssue, requiredDocs: ["docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md"], requiredValidationCommands, requiredSmokeCommand, requiredDryRunCommand } ], [ "dev-mvp-gate-report", { issue: issueFamily.DEV_M5_GATE_REPORT, requiredDocs: [ "docs/dev-gate-report.md", "docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md", "docs/m1-local-smoke.md", "docs/m2-dev-deploy-smoke.md", "docs/m3-hardware-loop.md", "docs/m4-agent-loop.md", "docs/m5-mvp-e2e.md", "docs/runtime-boundary-guard.md", "docs/topology-constraints.md", "docs/artifact-catalog.md", "docs/schema-drift-map.md", "protocol/README.md", "protocol/audit.md", "protocol/evidence-chain.md", "protocol/mvp-e2e-contract.md" ], requiredValidationCommands: [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ], requiredSmokeCommand: "node scripts/m1-contract-smoke.mjs", requiredDryRunCommand: "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run" } ], [ "dev-deploy-apply", { issue: issueFamily.DEV_DEPLOY_APPLY, requiredDocs: [ "docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md", "docs/dev-deploy-apply.md", "docs/artifact-catalog.md", "deploy/README.md" ], requiredValidationCommands: [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs", "node --check scripts/dev-deploy-apply.mjs", "node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked" ], requiredSmokeCommand, requiredDryRunCommand } ], [ "dev-artifact-publish", { issue: issueFamily.DEV_ARTIFACT_PUBLISH, requiredDocs: [ "docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md", "docs/artifact-catalog.md", "docs/dev-artifact-publish.md", "docs/dev-base-image-preflight.md" ], requiredValidationCommands: [ "node --check scripts/dev-artifact-publish.mjs", "node --check scripts/src/dev-artifact-services.mjs", "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/preflight-dev-base-image.mjs", "node --check scripts/src/dev-base-image-preflight.mjs", "node scripts/preflight-dev-base-image.mjs", "node scripts/dev-artifact-publish.mjs --preflight --no-report", "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ], requiredSmokeCommand, requiredDryRunCommand } ], [ "dev-m4-agent-loop", { issue: issueFamily.DEV_M4_AGENT_LOOP, requiredDocs: ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"], requiredValidationCommands: [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs", "node --check scripts/dev-m4-agent-loop-smoke.mjs", "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run", "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production" ], requiredSmokeCommand: "node scripts/m4-agent-loop-smoke.mjs", requiredDryRunCommand: "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run" } ] ]); const statusValues = new Set(["pass", "blocked", "degraded", "not_run", "not_applicable", "not_sent", "failed"]); const blockerTypes = new Set([ "contract_blocker", "environment_blocker", "network_blocker", "runtime_blocker", "agent_blocker", "observability_blocker", "safety_blocker" ]); const blockerStates = new Set(["open", "acknowledged", "closed"]); const artifactSourceStates = new Set(["source-present", "intentionally-disabled"]); const preflightConclusions = new Set(["ready", "blocked"]); const registryCapabilityDimensionIds = new Set([ "process-http-access", "docker-daemon-push-access", "k3s-pull-access" ]); const registryCapabilityDimensionKeys = new Set([ "processHttpAccess", "dockerDaemonPushAccess", "k3sPullAccess" ]); const requiredPreflightSupports = [ "pikasTech/HWLAB#7", "pikasTech/HWLAB#12", "pikasTech/HWLAB#22", "pikasTech/HWLAB#23", "pikasTech/HWLAB#29", "pikasTech/HWLAB#30", "pikasTech/HWLAB#31", "pikasTech/HWLAB#66" ]; const protectedAggregatorLiveCategories = Object.freeze({ M3: "hardware-loop-live", M4: "agent-loop-live-preflight", M5: "mvp-e2e-live" }); const protectedAggregatorMilestones = new Set(Object.keys(protectedAggregatorLiveCategories)); function assertObject(value, label) { assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`); } function assertString(value, label) { assert.equal(typeof value, "string", `${label} must be a string`); assert.ok(value.trim().length > 0, `${label} must not be empty`); assert.ok(!value.includes("\n"), `${label} must be a single line`); } function assertArray(value, label) { assert.ok(Array.isArray(value), `${label} must be an array`); } function assertStatus(value, label) { assertString(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 assertDbLayerBooleans(value, label) { assertObject(value, label); for (const field of ["configReady", "liveConnected", "connectionAttempted"]) { assertBoolean(value[field], `${label}.${field}`); } assertString(value.connectionResult, `${label}.connectionResult`); assertNoDbSecretMaterial(value, label); } function assertDbRuntimeReadiness(value, label) { assertDbLayerBooleans(value, label); for (const field of ["envInjected", "valuesRedacted", "secretMaterialRead", "liveDbEvidence"]) { assertBoolean(value[field], `${label}.${field}`); } assert.equal(value.valuesRedacted, true, `${label}.valuesRedacted`); assert.equal(value.secretMaterialRead, false, `${label}.secretMaterialRead`); assertObject(value.secret, `${label}.secret`); assertString(value.secret.secretName, `${label}.secret.secretName`); assertString(value.secret.secretKey, `${label}.secret.secretKey`); assert.equal(value.secret.redacted, true, `${label}.secret.redacted`); assert.equal(value.secret.secretValueRead, false, `${label}.secret.secretValueRead`); assertArray(value.evidence, `${label}.evidence`); if (value.status === "blocked") { assertString(value.blocker, `${label}.blocker`); } } function assertNoDbSecretMaterial(value, label) { const serialized = JSON.stringify(value); for (const forbidden of ["postgres://", "postgresql://", "password=", "://user:"]) { assert.equal(serialized.includes(forbidden), false, `${label} must not include ${forbidden}`); } } function assertRepoRelativePath(value, label) { assertString(value, label); assert.ok(!path.isAbsolute(value), `${label} must be repo-relative`); assert.ok(!value.startsWith(".."), `${label} must stay inside the repository`); const absolutePath = path.resolve(repoRoot, value); assert.ok( absolutePath.startsWith(`${repoRoot}${path.sep}`), `${label} must stay inside the repository` ); return absolutePath; } function assertStringArray(value, label, { minLength = 0 } = {}) { assertArray(value, label); assert.ok(value.length >= minLength, `${label} must contain at least ${minLength} item(s)`); for (const [index, item] of value.entries()) { assertString(item, `${label}[${index}]`); } } function assertUnique(values, label) { assert.equal(new Set(values).size, values.length, `${label} must be unique`); } function assertReportLifecycle(report, raw, label) { assertObject(report.reportLifecycle, `${label}.reportLifecycle`); const lifecycle = report.reportLifecycle; assert.equal(lifecycle.version, REPORT_LIFECYCLE_VERSION, `${label}.reportLifecycle.version`); assert.ok(["active", "historical"].includes(lifecycle.state), `${label}.reportLifecycle.state`); assert.equal(lifecycle.activeEndpoint, ACTIVE_DEV_PUBLIC_ENDPOINT, `${label}.reportLifecycle.activeEndpoint`); assert.equal(lifecycle.activeBrowserEndpoint, ACTIVE_DEV_BROWSER_ENDPOINT, `${label}.reportLifecycle.activeBrowserEndpoint`); assertString(lifecycle.summary, `${label}.reportLifecycle.summary`); if (lifecycle.state === "active") { assert.equal(lifecycle.deprecatedEndpoint, null, `${label}.reportLifecycle.deprecatedEndpoint`); assert.deepEqual( findForbiddenActiveDeprecatedEndpoints(report, raw), [], `${label} active report must not use deprecated public endpoints as active evidence` ); return; } assert.ok( deprecatedPublicEndpoints.includes(lifecycle.deprecatedEndpoint), `${label}.reportLifecycle.deprecatedEndpoint must name a deprecated public endpoint` ); assert.ok( lifecycle.summary.toLowerCase().includes("historical") || lifecycle.summary.toLowerCase().includes("deprecated"), `${label}.reportLifecycle.summary must explain the historical/deprecated state` ); const deprecatedMatches = findDeprecatedPublicEndpoints(raw); assert.ok( deprecatedMatches.length === 0 || deprecatedMatches.includes(lifecycle.deprecatedEndpoint), `${label}.reportLifecycle.deprecatedEndpoint must cover deprecated public endpoint evidence` ); } function primaryReportStatus(report) { return report.status ?? report.gateStatus ?? report.conclusion ?? report.artifactPublish?.status ?? report.summary?.status ?? (Array.isArray(report.blockers) && report.blockers.length > 0 ? "blocked" : "not_run"); } function statusIsPass(value) { return value === "pass"; } function validateHistoricalReport(report, label) { assert.equal(report.devOnly, true, `${label}.devOnly`); assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); assertString(report.issue, `${label}.issue`); assert.ok(allowedIssues.has(report.issue), `${label}.issue must be a known HWLAB DEV gate issue`); assertString(report.taskId ?? report.reportKind, `${label}.taskId`); assert.ok( ["blocked", "not_run", "not_applicable", "failed", "degraded"].includes(primaryReportStatus(report)), `${label} historical report must not present active green status` ); assert.ok( Array.isArray(report.blockers) && report.blockers.length >= 1, `${label}.blockers must explain why the report is historical` ); assertBlockers(report.blockers, `${label}.blockers`); } async function readJsonReport(relativePath) { const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); return { report: JSON.parse(raw), raw }; } async function collectReportFiles() { if (process.argv.length > 2) { return process.argv.slice(2).map((entry) => path.relative(repoRoot, path.resolve(process.cwd(), entry))); } return collectReportJsonFiles(reportsDir, "reports/dev-gate"); } async function collectReportJsonFiles(directory, relativeDirectory) { const entries = await readdir(directory, { withFileTypes: true }); const nested = await Promise.all(entries.map(async (entry) => { const relativePath = path.join(relativeDirectory, entry.name); if (entry.isDirectory()) { return collectReportJsonFiles(path.join(directory, entry.name), relativePath); } return entry.isFile() && entry.name.endsWith(".json") ? [relativePath] : []; })); return nested.flat().sort(); } async function validateReport(relativePath) { const { report, raw } = await readJsonReport(relativePath); const label = relativePath; assertObject(report, label); assertReportLifecycle(report, raw, label); if (reportIsHistorical(report)) { validateHistoricalReport(report, label); return; } if (report.reportKind === "dev-gate-preflight") { await validatePreflightReport(relativePath, report); return; } if (report.reportKind === "dev-m5-gate-aggregator") { await validateAggregatorV2Report(relativePath, report); return; } if (report.issue === requiredDevM3Issue || report.taskId === "dev-m3-hardware-loop") { await validateDevM3Report(report, label); return; } if (report.issue === requiredDevEdgeIssue || report.taskId === "dev-edge-health") { await validateDevEdgeReport(report, label); return; } if (report.issue === requiredDevM2Issue || report.taskId === "m2-dev-deploy-smoke") { await validateDevM2Report(report, label); return; } if ( report.issue === issueFamily.DEV_CLOUD_WORKBENCH_LIVE || report.taskId === "dev-cloud-workbench-live" ) { await validateDevCloudWorkbenchLiveReport(report, label, raw); return; } const template = reportFamilyTemplates.get(report.taskId); assert.ok( template, `${label}.taskId must be one of ${Array.from(reportFamilyTemplates.keys()).join(", ")}` ); for (const field of [ "$schema", "$id", "reportVersion", "issue", "taskId", "commitId", "acceptanceLevel", "devOnly", "prodDisabled", "sourceContract", "validationCommands", "localSmoke", "dryRun", "devPreconditions", "blockers" ]) { 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, template.issue, `${label}.issue`); assert.match(report.taskId, /^[a-z][a-z0-9-]*$/, `${label}.taskId`); assert.match(report.commitId, /^[a-f0-9]{7,40}$/, `${label}.commitId`); assert.match(report.acceptanceLevel, /^[a-z][a-z0-9_-]*$/, `${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`); assertString(report.sourceContract.summary, `${label}.sourceContract.summary`); assertStringArray(report.sourceContract.documents, `${label}.sourceContract.documents`, { minLength: template.requiredDocs.length }); const documents = new Set(report.sourceContract.documents); assertUnique(report.sourceContract.documents, `${label}.sourceContract.documents`); for (const requiredDoc of template.requiredDocs) { 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 template.requiredValidationCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } assertObject(report.localSmoke, `${label}.localSmoke`); assertStatus(report.localSmoke.status, `${label}.localSmoke.status`); assertStringArray(report.localSmoke.commands, `${label}.localSmoke.commands`, { minLength: 1 }); assertUnique(report.localSmoke.commands, `${label}.localSmoke.commands`); assert.ok( report.localSmoke.commands.includes(template.requiredSmokeCommand), `${label}.localSmoke.commands missing ${template.requiredSmokeCommand}` ); assertStringArray(report.localSmoke.evidence, `${label}.localSmoke.evidence`, { minLength: 1 }); assertString(report.localSmoke.summary, `${label}.localSmoke.summary`); assertObject(report.dryRun, `${label}.dryRun`); assertStatus(report.dryRun.status, `${label}.dryRun.status`); assertStringArray(report.dryRun.commands, `${label}.dryRun.commands`, { minLength: 1 }); assertUnique(report.dryRun.commands, `${label}.dryRun.commands`); assert.ok( report.dryRun.commands.includes(template.requiredDryRunCommand), `${label}.dryRun.commands missing ${template.requiredDryRunCommand}` ); assertStringArray(report.dryRun.evidence, `${label}.dryRun.evidence`, { minLength: 1 }); assertString(report.dryRun.summary, `${label}.dryRun.summary`); assertObject(report.devPreconditions, `${label}.devPreconditions`); assertStatus(report.devPreconditions.status, `${label}.devPreconditions.status`); assertStringArray(report.devPreconditions.requirements, `${label}.devPreconditions.requirements`, { minLength: 1 }); assertString(report.devPreconditions.summary, `${label}.devPreconditions.summary`); 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`); } if (Object.hasOwn(report, "notes")) { assertString(report.notes, `${label}.notes`); } if (Object.hasOwn(report, "gateStatus")) { assertStatus(report.gateStatus, `${label}.gateStatus`); } if (Object.hasOwn(report, "artifactPublish")) { assertObject(report.artifactPublish, `${label}.artifactPublish`); assertRegistryCapabilities(report.artifactPublish.registryCapabilities, `${label}.artifactPublish.registryCapabilities`); assertRegistryCapabilities(report.artifactPublish.publishPlan?.registryCapabilities, `${label}.artifactPublish.publishPlan.registryCapabilities`); assertArray(report.artifactPublish.services ?? [], `${label}.artifactPublish.services`); for (const [index, service] of report.artifactPublish.services.entries()) { const serviceLabel = `${label}.artifactPublish.services[${index}]`; assertObject(service, serviceLabel); assertString(service.serviceId, `${serviceLabel}.serviceId`); if (Object.hasOwn(service, "sourceCommitId")) { assertString(service.sourceCommitId, `${serviceLabel}.sourceCommitId`); } assertString(service.status, `${serviceLabel}.status`); assertString(service.image, `${serviceLabel}.image`); assertString(service.imageTag, `${serviceLabel}.imageTag`); assertShaOrNotPublished(service.digest, `${serviceLabel}.digest`); assertString(service.implementationState, `${serviceLabel}.implementationState`); assertString(service.sourceState, `${serviceLabel}.sourceState`); assert.ok( artifactSourceStates.has(service.sourceState), `${serviceLabel}.sourceState must be source-present or intentionally-disabled` ); if (Object.hasOwn(service, "distFreshness")) { assertObject(service.distFreshness, `${serviceLabel}.distFreshness`); assert.equal(service.distFreshness.status, "pass", `${serviceLabel}.distFreshness.status`); assert.equal(service.distFreshness.distPath, "web/hwlab-cloud-web/dist", `${serviceLabel}.distFreshness.distPath`); assert.equal( service.distFreshness.buildCommand, "node web/hwlab-cloud-web/scripts/build.mjs", `${serviceLabel}.distFreshness.buildCommand` ); assert.equal( service.distFreshness.freshnessCommand, "node web/hwlab-cloud-web/scripts/build.mjs", `${serviceLabel}.distFreshness.freshnessCommand` ); assertArray(service.distFreshness.files, `${serviceLabel}.distFreshness.files`); assertArray(service.distFreshness.mismatches, `${serviceLabel}.distFreshness.mismatches`); assert.equal(service.distFreshness.mismatches.length, 0, `${serviceLabel}.distFreshness.mismatches`); } if (service.sourceState === "intentionally-disabled") { assert.equal(service.entrypoint, null, `${serviceLabel}.entrypoint must be null when intentionally disabled`); } } } if (Object.hasOwn(report, "milestones")) { assertArray(report.milestones, `${label}.milestones`); assert.deepEqual( report.milestones.map((milestone) => milestone.id), requiredMilestoneIds, `${label}.milestones must cover M0-M5 in order` ); assertUnique( report.milestones.map((milestone) => milestone.id), `${label}.milestones` ); for (const [index, milestone] of report.milestones.entries()) { const milestoneLabel = `${label}.milestones[${index}]`; assertObject(milestone, milestoneLabel); for (const field of ["id", "status", "commands", "evidence", "summary"]) { assert.ok(Object.hasOwn(milestone, field), `${milestoneLabel} missing ${field}`); } assert.match(milestone.id, /^M[0-5]$/, `${milestoneLabel}.id`); assertStatus(milestone.status, `${milestoneLabel}.status`); assertStringArray(milestone.commands, `${milestoneLabel}.commands`, { minLength: 1 }); assertUnique(milestone.commands, `${milestoneLabel}.commands`); assertStringArray(milestone.evidence, `${milestoneLabel}.evidence`, { minLength: 1 }); assertUnique(milestone.evidence, `${milestoneLabel}.evidence`); assertString(milestone.summary, `${milestoneLabel}.summary`); if (Object.hasOwn(milestone, "blocker")) { assertString(milestone.blocker, `${milestoneLabel}.blocker`); } } } if (Object.hasOwn(report.devPreconditions, "commands")) { assertStringArray(report.devPreconditions.commands, `${label}.devPreconditions.commands`, { minLength: 1 }); assertUnique(report.devPreconditions.commands, `${label}.devPreconditions.commands`); } if (Object.hasOwn(report.devPreconditions, "evidence")) { assertStringArray(report.devPreconditions.evidence, `${label}.devPreconditions.evidence`, { minLength: 1 }); assertUnique(report.devPreconditions.evidence, `${label}.devPreconditions.evidence`); } if (report.taskId === "dev-deploy-apply") { assertDevDeployApplyReport(report, label); } } async function assertDocumentSet(documentsValue, label, requiredDocs) { assertStringArray(documentsValue, label, { minLength: requiredDocs.length }); const documents = new Set(documentsValue); assertUnique(documentsValue, label); for (const requiredDoc of requiredDocs) { assert.ok(documents.has(requiredDoc), `${label} missing ${requiredDoc}`); } for (const [index, documentPath] of documentsValue.entries()) { const absolutePath = assertRepoRelativePath(documentPath, `${label}[${index}]`); await access(absolutePath, fsConstants.R_OK); } } function assertBlockers(blockers, label) { assertArray(blockers, label); assertUnique( blockers.map((blocker) => `${blocker.type}::${blocker.scope}`), label ); for (const [index, blocker] of blockers.entries()) { const blockerLabel = `${label}[${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`); } } function assertCommandList(value, label, { minLength = 1 } = {}) { assertStringArray(value, label, { minLength }); assertUnique(value, label); for (const command of value) { assert.ok(!command.includes("--prod"), `${label} must not include PROD command`); assert.ok(!command.includes("--production"), `${label} must not include production command`); } } function assertDevDeployApplyReport(report, label) { assert.equal(report.issue, issueFamily.DEV_DEPLOY_APPLY, `${label}.issue`); assertStringArray(report.supports, `${label}.supports`, { minLength: requiredDevDeployApplySupports.length }); for (const supportedIssue of requiredDevDeployApplySupports) { assert.ok(report.supports.includes(supportedIssue), `${label}.supports missing ${supportedIssue}`); } for (const requiredCommand of requiredDevDeployApplyValidationCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } assertObject(report.devDeployApply, `${label}.devDeployApply`); const plan = report.devDeployApply; for (const field of [ "conclusion", "applyBoundary", "target", "artifactPlan", "workloadPlan", "servicePlan", "templateJobReplacementPolicy", "templateJobReplacements", "manualCommands", "rollbackHint", "remainingBlockers" ]) { assert.ok(Object.hasOwn(plan, field), `${label}.devDeployApply missing ${field}`); } assertObject(plan.conclusion, `${label}.devDeployApply.conclusion`); assert.ok(["blocked", "ready"].includes(plan.conclusion.status), `${label}.devDeployApply.conclusion.status`); assertString(plan.conclusion.reason, `${label}.devDeployApply.conclusion.reason`); assert.equal(plan.conclusion.blockerCount, report.blockers.length, `${label}.devDeployApply.conclusion.blockerCount`); assertObject(plan.applyBoundary, `${label}.devDeployApply.applyBoundary`); assert.equal(plan.applyBoundary.defaultNoWrite, plan.mode !== "apply", `${label}.devDeployApply.applyBoundary.defaultNoWrite`); assert.equal(plan.applyBoundary.mutationAttempted, report.devDeployApply.mutationAttempted, `${label}.devDeployApply.applyBoundary.mutationAttempted`); assertStringArray(plan.applyBoundary.applyRequiresFlags, `${label}.devDeployApply.applyBoundary.applyRequiresFlags`, { minLength: 3 }); for (const flag of ["--apply", "--confirm-dev", "--confirmed-non-production"]) { assert.ok(plan.applyBoundary.applyRequiresFlags.includes(flag), `${label}.devDeployApply.applyBoundary.applyRequiresFlags missing ${flag}`); } assertStringArray(plan.applyBoundary.forbiddenActions, `${label}.devDeployApply.applyBoundary.forbiddenActions`, { minLength: 6 }); for (const forbiddenAction of ["prod-deploy", "secret-read", "runtime-substitute", "heavy-master-e2e"]) { assert.ok(plan.applyBoundary.forbiddenActions.includes(forbiddenAction), `${label}.devDeployApply.applyBoundary.forbiddenActions missing ${forbiddenAction}`); } assertString(plan.applyBoundary.writeScope, `${label}.devDeployApply.applyBoundary.writeScope`); assertString(plan.applyBoundary.noWriteScope, `${label}.devDeployApply.applyBoundary.noWriteScope`); assertObject(plan.target, `${label}.devDeployApply.target`); assert.equal(plan.target.environment, "dev", `${label}.devDeployApply.target.environment`); assert.equal(plan.target.namespace, "hwlab-dev", `${label}.devDeployApply.target.namespace`); assert.equal(plan.target.endpoint, "http://74.48.78.17:16667", `${label}.devDeployApply.target.endpoint`); assert.equal(plan.target.prodDisabled, true, `${label}.devDeployApply.target.prodDisabled`); assertObject(plan.artifactPlan, `${label}.devDeployApply.artifactPlan`); for (const field of ["expectedArtifactCommit", "deployCommitId", "catalogCommitId", "sourceCommitId"]) { assertString(plan.artifactPlan[field], `${label}.devDeployApply.artifactPlan.${field}`); } assertStringArray(plan.artifactPlan.serviceCommitIds, `${label}.devDeployApply.artifactPlan.serviceCommitIds`, { minLength: 1 }); assert.equal(typeof plan.artifactPlan.matchesSourceCommit, "boolean", `${label}.devDeployApply.artifactPlan.matchesSourceCommit`); assert.equal(typeof plan.artifactPlan.published, "boolean", `${label}.devDeployApply.artifactPlan.published`); assert.equal(typeof plan.artifactPlan.registryVerified, "boolean", `${label}.devDeployApply.artifactPlan.registryVerified`); assert.ok(Number.isInteger(plan.artifactPlan.imageCount), `${label}.devDeployApply.artifactPlan.imageCount`); assertStringArray(plan.artifactPlan.unpublishedServices, `${label}.devDeployApply.artifactPlan.unpublishedServices`); assertArray(plan.workloadPlan, `${label}.devDeployApply.workloadPlan`); assert.ok(plan.workloadPlan.length >= 1, `${label}.devDeployApply.workloadPlan must not be empty`); assertArray(plan.servicePlan, `${label}.devDeployApply.servicePlan`); assert.ok(plan.servicePlan.length >= 1, `${label}.devDeployApply.servicePlan must not be empty`); for (const [index, workload] of plan.workloadPlan.entries()) { const workloadLabel = `${label}.devDeployApply.workloadPlan[${index}]`; assertObject(workload, workloadLabel); for (const field of ["kind", "name", "namespace", "serviceIds", "containers"]) { assert.ok(Object.hasOwn(workload, field), `${workloadLabel} missing ${field}`); } assert.equal(workload.namespace, "hwlab-dev", `${workloadLabel}.namespace`); assertStringArray(workload.serviceIds, `${workloadLabel}.serviceIds`, { minLength: 1 }); assertArray(workload.containers, `${workloadLabel}.containers`); } assertObject(plan.templateJobReplacementPolicy, `${label}.devDeployApply.templateJobReplacementPolicy`); assert.equal(plan.templateJobReplacementPolicy.namespace, "hwlab-dev", `${label}.devDeployApply.templateJobReplacementPolicy.namespace`); assert.equal(plan.templateJobReplacementPolicy.status, "active", `${label}.devDeployApply.templateJobReplacementPolicy.status`); assertArray(plan.templateJobReplacementPolicy.allowedJobs, `${label}.devDeployApply.templateJobReplacementPolicy.allowedJobs`); const allowedTemplateJobNames = plan.templateJobReplacementPolicy.allowedJobs.map((job) => job.name); assert.deepEqual( allowedTemplateJobNames, ["hwlab-agent-worker-template", "hwlab-cli-template"], `${label}.devDeployApply.templateJobReplacementPolicy.allowedJobs` ); assertArray(plan.templateJobReplacements, `${label}.devDeployApply.templateJobReplacements`); for (const [index, replacement] of plan.templateJobReplacements.entries()) { const replacementLabel = `${label}.devDeployApply.templateJobReplacements[${index}]`; assertObject(replacement, replacementLabel); for (const field of ["namespace", "jobName", "allowed", "desiredSuspended", "oldImages", "newImages", "replace", "result", "reason"]) { assert.ok(Object.hasOwn(replacement, field), `${replacementLabel} missing ${field}`); } assert.equal(replacement.namespace, "hwlab-dev", `${replacementLabel}.namespace`); assert.ok(allowedTemplateJobNames.includes(replacement.jobName), `${replacementLabel}.jobName must be allowlisted`); assert.equal(replacement.allowed, true, `${replacementLabel}.allowed`); assert.equal(replacement.desiredSuspended, true, `${replacementLabel}.desiredSuspended`); assertArray(replacement.oldImages, `${replacementLabel}.oldImages`); assertArray(replacement.newImages, `${replacementLabel}.newImages`); assert.equal(typeof replacement.replace, "boolean", `${replacementLabel}.replace`); assert.ok( ["not_needed", "planned", "replaced", "not_found", "not_evaluated", "blocked", "delete_failed", "delete_succeeded_apply_failed"].includes(replacement.result), `${replacementLabel}.result` ); assertString(replacement.reason, `${replacementLabel}.reason`); } if (Object.hasOwn(plan, "cloudWebRollout")) { assertObject(plan.cloudWebRollout, `${label}.devDeployApply.cloudWebRollout`); for (const field of [ "serviceId", "namespace", "status", "sourceCommitId", "image", "imageTag", "digest", "publishState", "rolloutRevision", "liveImage", "imageMatchesDesired", "verificationCommand", "readCommand" ]) { assert.ok(Object.hasOwn(plan.cloudWebRollout, field), `${label}.devDeployApply.cloudWebRollout missing ${field}`); } assert.equal(plan.cloudWebRollout.serviceId, "hwlab-cloud-web", `${label}.devDeployApply.cloudWebRollout.serviceId`); assert.equal(plan.cloudWebRollout.namespace, "hwlab-dev", `${label}.devDeployApply.cloudWebRollout.namespace`); assert.ok(["observed", "blocked", "not_evaluated"].includes(plan.cloudWebRollout.status), `${label}.devDeployApply.cloudWebRollout.status`); assertString(plan.cloudWebRollout.sourceCommitId, `${label}.devDeployApply.cloudWebRollout.sourceCommitId`); assertString(plan.cloudWebRollout.image, `${label}.devDeployApply.cloudWebRollout.image`); assertString(plan.cloudWebRollout.imageTag, `${label}.devDeployApply.cloudWebRollout.imageTag`); assertShaOrNotPublished(plan.cloudWebRollout.digest, `${label}.devDeployApply.cloudWebRollout.digest`); if (plan.cloudWebRollout.rolloutRevision !== null) { assertString(plan.cloudWebRollout.rolloutRevision, `${label}.devDeployApply.cloudWebRollout.rolloutRevision`); } if (plan.cloudWebRollout.liveImage !== null) { assertString(plan.cloudWebRollout.liveImage, `${label}.devDeployApply.cloudWebRollout.liveImage`); } assert.equal(typeof plan.cloudWebRollout.imageMatchesDesired, "boolean", `${label}.devDeployApply.cloudWebRollout.imageMatchesDesired`); assert.match(plan.cloudWebRollout.verificationCommand, /rollout status deployment\/hwlab-cloud-web/u, `${label}.devDeployApply.cloudWebRollout.verificationCommand`); assert.match(plan.cloudWebRollout.verificationCommand, /^KUBECONFIG=(?:[A-Za-z0-9_@%+=:,./-]+|'[^']+') kubectl /u, `${label}.devDeployApply.cloudWebRollout.verificationCommand`); assert.match(plan.cloudWebRollout.readCommand, /get deployment hwlab-cloud-web/u, `${label}.devDeployApply.cloudWebRollout.readCommand`); assert.match(plan.cloudWebRollout.readCommand, /^KUBECONFIG=(?:[A-Za-z0-9_@%+=:,./-]+|'[^']+') kubectl /u, `${label}.devDeployApply.cloudWebRollout.readCommand`); if (plan.cloudWebRollout.status === "blocked") { assertString(plan.cloudWebRollout.blocker, `${label}.devDeployApply.cloudWebRollout.blocker`); } } assertObject(plan.manualCommands, `${label}.devDeployApply.manualCommands`); assert.ok(["blocked", "ready"].includes(plan.manualCommands.status), `${label}.devDeployApply.manualCommands.status`); assertCommandList(plan.manualCommands.beforeHumanApproval, `${label}.devDeployApply.manualCommands.beforeHumanApproval`); assertCommandList(plan.manualCommands.afterHumanApproval, `${label}.devDeployApply.manualCommands.afterHumanApproval`, { minLength: 0 }); assertString(plan.manualCommands.summary, `${label}.devDeployApply.manualCommands.summary`); if (plan.conclusion.status === "blocked") { assert.equal(plan.manualCommands.status, "blocked", `${label}.devDeployApply.manualCommands.status`); assert.equal(plan.manualCommands.afterHumanApproval.length, 0, `${label}.devDeployApply.manualCommands.afterHumanApproval`); } if (plan.conclusion.status === "ready") { assert.equal(plan.manualCommands.status, "ready", `${label}.devDeployApply.manualCommands.status`); assert.ok( plan.manualCommands.afterHumanApproval.includes("node scripts/dev-deploy-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report"), `${label}.devDeployApply.manualCommands.afterHumanApproval missing apply command` ); } assertObject(plan.rollbackHint, `${label}.devDeployApply.rollbackHint`); assert.equal(plan.rollbackHint.namespace, "hwlab-dev", `${label}.devDeployApply.rollbackHint.namespace`); assertString(plan.rollbackHint.strategy, `${label}.devDeployApply.rollbackHint.strategy`); assertCommandList(plan.rollbackHint.captureBeforeApply, `${label}.devDeployApply.rollbackHint.captureBeforeApply`); assertCommandList(plan.rollbackHint.deploymentRollbackCommands, `${label}.devDeployApply.rollbackHint.deploymentRollbackCommands`); assertCommandList(plan.rollbackHint.jobCleanupCommands, `${label}.devDeployApply.rollbackHint.jobCleanupCommands`); assertCommandList(plan.rollbackHint.postRollbackChecks, `${label}.devDeployApply.rollbackHint.postRollbackChecks`); assertArray(plan.remainingBlockers, `${label}.devDeployApply.remainingBlockers`); assert.equal(plan.remainingBlockers.length, report.blockers.length, `${label}.devDeployApply.remainingBlockers.length`); for (const [index, blocker] of plan.remainingBlockers.entries()) { const blockerLabel = `${label}.devDeployApply.remainingBlockers[${index}]`; assertObject(blocker, blockerLabel); for (const field of ["type", "scope", "status", "summary", "unblockHint"]) { assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`); } assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type must be known`); assertString(blocker.unblockHint, `${blockerLabel}.unblockHint`); } } function assertShaOrNotPublished(value, label) { assertString(value, label); assert.ok( value === "not_published" || /^sha256:[a-f0-9]{64}$/.test(value), `${label} must be not_published or a sha256 digest` ); } function assertRegistryCapabilityDimension(dimension, label, expectedId) { assertObject(dimension, label); for (const field of [ "id", "status", "requiredForPublish", "requiredForDeploy", "endpoint", "probeKind", "summary", "evidence" ]) { assert.ok(Object.hasOwn(dimension, field), `${label} missing ${field}`); } assert.equal(dimension.id, expectedId, `${label}.id`); assert.ok(registryCapabilityDimensionIds.has(dimension.id), `${label}.id must be a known registry capability dimension`); assertStatus(dimension.status, `${label}.status`); assert.equal(typeof dimension.requiredForPublish, "boolean", `${label}.requiredForPublish`); assert.equal(typeof dimension.requiredForDeploy, "boolean", `${label}.requiredForDeploy`); assertString(dimension.endpoint, `${label}.endpoint`); assertString(dimension.probeKind, `${label}.probeKind`); assertString(dimension.summary, `${label}.summary`); assertObject(dimension.evidence, `${label}.evidence`); } function assertRegistryCapabilities(capabilities, label) { assertObject(capabilities, label); for (const field of [ "version", "registryPrefix", "generatedAt", "classification", "dimensions", "interpretation" ]) { assert.ok(Object.hasOwn(capabilities, field), `${label} missing ${field}`); } assert.equal(capabilities.version, "v1", `${label}.version`); assertString(capabilities.registryPrefix, `${label}.registryPrefix`); assertTimestamp(capabilities.generatedAt, `${label}.generatedAt`); assertStatus(capabilities.classification, `${label}.classification`); assertObject(capabilities.dimensions, `${label}.dimensions`); assert.deepEqual( new Set(Object.keys(capabilities.dimensions)), registryCapabilityDimensionKeys, `${label}.dimensions must contain processHttpAccess, dockerDaemonPushAccess, and k3sPullAccess` ); assertRegistryCapabilityDimension(capabilities.dimensions.processHttpAccess, `${label}.dimensions.processHttpAccess`, "process-http-access"); assertRegistryCapabilityDimension(capabilities.dimensions.dockerDaemonPushAccess, `${label}.dimensions.dockerDaemonPushAccess`, "docker-daemon-push-access"); assertRegistryCapabilityDimension(capabilities.dimensions.k3sPullAccess, `${label}.dimensions.k3sPullAccess`, "k3s-pull-access"); assert.equal(capabilities.dimensions.processHttpAccess.requiredForPublish, false, `${label}.dimensions.processHttpAccess.requiredForPublish`); assert.equal(capabilities.dimensions.dockerDaemonPushAccess.requiredForPublish, true, `${label}.dimensions.dockerDaemonPushAccess.requiredForPublish`); assert.equal(capabilities.dimensions.k3sPullAccess.requiredForDeploy, true, `${label}.dimensions.k3sPullAccess.requiredForDeploy`); assertObject(capabilities.interpretation, `${label}.interpretation`); for (const key of registryCapabilityDimensionKeys) { assertString(capabilities.interpretation[key], `${label}.interpretation.${key}`); } } function assertArtifactIdentity(identity, label, target) { assertObject(identity, label); for (const field of [ "source", "deployManifest", "artifactCatalog", "services", "serviceCommitIds", "matchesSource", "publishVerified", "refreshCommands" ]) { assert.ok(Object.hasOwn(identity, field), `${label} missing ${field}`); } assertObject(identity.source, `${label}.source`); assert.equal(identity.source.ref, target.ref, `${label}.source.ref`); assert.equal(identity.source.commitId, target.commitId, `${label}.source.commitId`); assert.equal(identity.source.shortCommitId, target.shortCommitId, `${label}.source.shortCommitId`); assertObject(identity.deployManifest, `${label}.deployManifest`); assert.equal(identity.deployManifest.path, "deploy/deploy.json", `${label}.deployManifest.path`); assert.match(identity.deployManifest.commitId, /^[a-f0-9]{7,40}$/, `${label}.deployManifest.commitId`); assert.equal(typeof identity.deployManifest.matchesSource, "boolean", `${label}.deployManifest.matchesSource`); assertObject(identity.artifactCatalog, `${label}.artifactCatalog`); assert.equal(identity.artifactCatalog.path, "deploy/artifact-catalog.dev.json", `${label}.artifactCatalog.path`); assert.match(identity.artifactCatalog.commitId, /^[a-f0-9]{7,40}$/, `${label}.artifactCatalog.commitId`); assert.ok(["contract-skeleton", "published"].includes(identity.artifactCatalog.artifactState), `${label}.artifactCatalog.artifactState`); assert.equal(typeof identity.artifactCatalog.ciPublished, "boolean", `${label}.artifactCatalog.ciPublished`); assert.equal(typeof identity.artifactCatalog.registryVerified, "boolean", `${label}.artifactCatalog.registryVerified`); assertString(identity.artifactCatalog.provenance, `${label}.artifactCatalog.provenance`); assert.equal(typeof identity.artifactCatalog.matchesSource, "boolean", `${label}.artifactCatalog.matchesSource`); assertObject(identity.artifactCatalog.digestCounts, `${label}.artifactCatalog.digestCounts`); for (const field of ["sha256", "notPublished", "invalid"]) { assert.equal(typeof identity.artifactCatalog.digestCounts[field], "number", `${label}.artifactCatalog.digestCounts.${field}`); } assertArray(identity.services, `${label}.services`); assert.ok(identity.services.length >= 1, `${label}.services must not be empty`); assertUnique(identity.services.map((service) => service.serviceId), `${label}.services`); for (const [index, service] of identity.services.entries()) { const serviceLabel = `${label}.services[${index}]`; assertObject(service, serviceLabel); for (const field of ["serviceId", "commitId", "matchesSource", "image", "imageTag", "digest", "publishState"]) { assert.ok(Object.hasOwn(service, field), `${serviceLabel} missing ${field}`); } assertString(service.serviceId, `${serviceLabel}.serviceId`); assert.match(service.commitId, /^[a-f0-9]{7,40}$/, `${serviceLabel}.commitId`); assert.equal(typeof service.matchesSource, "boolean", `${serviceLabel}.matchesSource`); assertString(service.image, `${serviceLabel}.image`); assertString(service.imageTag, `${serviceLabel}.imageTag`); assertShaOrNotPublished(service.digest, `${serviceLabel}.digest`); assert.ok(["skeleton-only", "published"].includes(service.publishState), `${serviceLabel}.publishState`); } assertStringArray(identity.serviceCommitIds, `${label}.serviceCommitIds`, { minLength: 1 }); assert.equal(typeof identity.matchesSource, "boolean", `${label}.matchesSource`); assert.equal(typeof identity.publishVerified, "boolean", `${label}.publishVerified`); assertObject(identity.refreshCommands, `${label}.refreshCommands`); assert.equal( identity.refreshCommands.blocked, `node scripts/refresh-artifact-catalog.mjs --target-ref ${target.ref} --blocked`, `${label}.refreshCommands.blocked` ); assert.equal( identity.refreshCommands.published, `node scripts/refresh-artifact-catalog.mjs --target-ref ${target.ref} --publish-report reports/dev-gate/dev-artifacts.json`, `${label}.refreshCommands.published` ); if (identity.publishVerified) { assert.equal(identity.artifactCatalog.ciPublished, true, `${label}.artifactCatalog.ciPublished`); assert.equal(identity.artifactCatalog.registryVerified, true, `${label}.artifactCatalog.registryVerified`); assert.equal(identity.artifactCatalog.digestCounts.sha256, identity.services.length, `${label}.artifactCatalog.digestCounts.sha256`); assert.equal(identity.artifactCatalog.digestCounts.notPublished, 0, `${label}.artifactCatalog.digestCounts.notPublished`); assert.equal(identity.artifactCatalog.digestCounts.invalid, 0, `${label}.artifactCatalog.digestCounts.invalid`); } } async function validateDevEdgeReport(report, label) { for (const field of [ "$schema", "$id", "reportVersion", "issue", "taskId", "commitId", "acceptanceLevel", "devOnly", "prodDisabled", "sourceContract", "validationCommands", "localSmoke", "dryRun", "devPreconditions", "blockers", "edgeHealth" ]) { 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, requiredDevEdgeIssue, `${label}.issue`); assert.equal(report.taskId, "dev-edge-health", `${label}.taskId`); assert.match(report.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.commitId`); assert.equal(report.acceptanceLevel, "dev_edge_health", `${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`); await assertDocumentSet( report.sourceContract.documents, `${label}.sourceContract.documents`, requiredDevEdgeDocs ); assertString(report.sourceContract.summary, `${label}.sourceContract.summary`); assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: 3 }); assertUnique(report.validationCommands, `${label}.validationCommands`); for (const requiredCommand of requiredDevEdgeValidationCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } for (const section of ["localSmoke", "dryRun", "devPreconditions"]) { assertObject(report[section], `${label}.${section}`); assertStatus(report[section].status, `${label}.${section}.status`); assertString(report[section].summary, `${label}.${section}.summary`); } assertStringArray(report.localSmoke.commands, `${label}.localSmoke.commands`, { minLength: 1 }); assertStringArray(report.localSmoke.evidence, `${label}.localSmoke.evidence`, { minLength: 1 }); assertStringArray(report.dryRun.commands, `${label}.dryRun.commands`, { minLength: 1 }); assertStringArray(report.dryRun.evidence, `${label}.dryRun.evidence`, { minLength: 1 }); assertStringArray(report.devPreconditions.requirements, `${label}.devPreconditions.requirements`, { minLength: 1 }); assertBlockers(report.blockers, `${label}.blockers`); assertObject(report.edgeHealth, `${label}.edgeHealth`); assertTimestamp(report.edgeHealth.generatedAt, `${label}.edgeHealth.generatedAt`); assert.equal(report.edgeHealth.endpoint, "http://74.48.78.17:16667", `${label}.edgeHealth.endpoint`); assert.ok( ["pass", "blocker", "not_run"].includes(report.edgeHealth.status), `${label}.edgeHealth.status must be pass, blocker, or not_run` ); assertString(report.edgeHealth.classification, `${label}.edgeHealth.classification`); assertObject(report.edgeHealth.contracts, `${label}.edgeHealth.contracts`); assertDbLayerBooleans( report.edgeHealth.contracts?.deploy?.cloudApiDb, `${label}.edgeHealth.contracts.deploy.cloudApiDb` ); assertDbRuntimeReadiness( report.edgeHealth.runtimeDbReadiness, `${label}.edgeHealth.runtimeDbReadiness` ); assertArray(report.edgeHealth.publicTcp ?? [], `${label}.edgeHealth.publicTcp`); assertArray(report.edgeHealth.publicHttp ?? [], `${label}.edgeHealth.publicHttp`); assertObject(report.edgeHealth.kubernetes ?? {}, `${label}.edgeHealth.kubernetes`); } async function validateDevM2Report(report, label) { for (const field of [ "$schema", "$id", "reportVersion", "issue", "taskId", "commitId", "acceptanceLevel", "devOnly", "prodDisabled", "status", "generatedAt", "endpoint", "frontendEndpoint", "sourceContract", "validationCommands", "localSmoke", "dryRun", "devPreconditions", "blockers", "runtimeSmoke" ]) { 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, requiredDevM2Issue, `${label}.issue`); assert.equal(report.taskId, "m2-dev-deploy-smoke", `${label}.taskId`); assert.match(report.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.commitId`); assert.equal(report.acceptanceLevel, "dev_m2_deploy_smoke", `${label}.acceptanceLevel`); assert.equal(report.devOnly, true, `${label}.devOnly`); assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); assert.equal(report.status, "pass", `${label}.status`); assertTimestamp(report.generatedAt, `${label}.generatedAt`); assert.equal(report.endpoint, "http://74.48.78.17:16667", `${label}.endpoint`); assert.equal(report.frontendEndpoint, "http://74.48.78.17:16666", `${label}.frontendEndpoint`); assertObject(report.sourceContract, `${label}.sourceContract`); assertStatus(report.sourceContract.status, `${label}.sourceContract.status`); await assertDocumentSet( report.sourceContract.documents, `${label}.sourceContract.documents`, requiredDevM2Docs ); assertString(report.sourceContract.summary, `${label}.sourceContract.summary`); assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: requiredDevM2ValidationCommands.length }); assertUnique(report.validationCommands, `${label}.validationCommands`); for (const requiredCommand of requiredDevM2ValidationCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } for (const section of ["localSmoke", "dryRun", "devPreconditions"]) { assertObject(report[section], `${label}.${section}`); assertStatus(report[section].status, `${label}.${section}.status`); assertStringArray(report[section].commands, `${label}.${section}.commands`, { minLength: 1 }); assertStringArray(report[section].evidence, `${label}.${section}.evidence`, { minLength: 1 }); assertString(report[section].summary, `${label}.${section}.summary`); } assertStringArray(report.devPreconditions.requirements, `${label}.devPreconditions.requirements`, { minLength: 1 }); for (const command of [ "curl -fsS --max-time 10 http://74.48.78.17:16667/health", "curl -fsS --max-time 10 http://74.48.78.17:16667/health/live", "curl -fsS --max-time 10 http://74.48.78.17:16666/" ]) { assert.ok( report.devPreconditions.commands.includes(command), `${label}.devPreconditions.commands missing ${command}` ); } assertBlockers(report.blockers, `${label}.blockers`); assertObject(report.runtimeSmoke, `${label}.runtimeSmoke`); assert.equal(report.runtimeSmoke.mode, "live-read-only", `${label}.runtimeSmoke.mode`); assert.equal(report.runtimeSmoke.status, "pass", `${label}.runtimeSmoke.status`); assertTimestamp(report.runtimeSmoke.generatedAt, `${label}.runtimeSmoke.generatedAt`); assert.equal(report.runtimeSmoke.apiEndpoint, "http://74.48.78.17:16667", `${label}.runtimeSmoke.apiEndpoint`); assert.equal(report.runtimeSmoke.frontendEndpoint, "http://74.48.78.17:16666", `${label}.runtimeSmoke.frontendEndpoint`); assertObject(report.runtimeSmoke.safety, `${label}.runtimeSmoke.safety`); assert.equal(report.runtimeSmoke.safety.environment, "dev", `${label}.runtimeSmoke.safety.environment`); for (const field of [ "prodTouched", "secretsRead", "restarts", "deployAttempted", "heavyE2E", "unideskRuntimeSubstitute" ]) { assert.equal(report.runtimeSmoke.safety[field], false, `${label}.runtimeSmoke.safety.${field}`); } assertArray(report.runtimeSmoke.probes, `${label}.runtimeSmoke.probes`); assert.ok(report.runtimeSmoke.probes.length >= 3, `${label}.runtimeSmoke.probes must include api and frontend probes`); assertObject(report.runtimeSmoke.legacyPublicEndpoint, `${label}.runtimeSmoke.legacyPublicEndpoint`); assert.equal( report.runtimeSmoke.legacyPublicEndpoint.endpoint, "http://74.48.78.17:6667", `${label}.runtimeSmoke.legacyPublicEndpoint.endpoint` ); assert.equal( report.runtimeSmoke.legacyPublicEndpoint.status, "deprecated", `${label}.runtimeSmoke.legacyPublicEndpoint.status` ); assert.equal( report.runtimeSmoke.legacyPublicEndpoint.activeGreenEligible, false, `${label}.runtimeSmoke.legacyPublicEndpoint.activeGreenEligible` ); } async function validateDevCloudWorkbenchLiveReport(report, label, raw) { for (const field of [ "$schema", "$id", "reportVersion", "issue", "taskId", "commitId", "acceptanceLevel", "devOnly", "prodDisabled", "status", "generatedAt", "mode", "url", "evidenceLevel", "devLive", "sourceIdentity", "runtimeIdentity", "sourceContract", "validationCommands", "localSmoke", "dryRun", "devPreconditions", "checks", "blockers", "safety" ]) { 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, issueFamily.DEV_CLOUD_WORKBENCH_LIVE, `${label}.issue`); assert.equal(report.taskId, "dev-cloud-workbench-live", `${label}.taskId`); assert.match(report.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.commitId`); assert.equal(report.acceptanceLevel, "dev_cloud_workbench_live", `${label}.acceptanceLevel`); assert.equal(report.devOnly, true, `${label}.devOnly`); assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); assertStatus(report.status, `${label}.status`); assert.equal( report.blockers.length === 0 ? ["pass", "degraded"].includes(report.status) : report.status === "blocked", true, `${label}.status must match blockers` ); assertTimestamp(report.generatedAt, `${label}.generatedAt`); assert.ok(["live", "dom-only"].includes(report.mode), `${label}.mode`); assert.equal(report.url, "http://74.48.78.17:16666/", `${label}.url`); if (report.mode === "dom-only") { assert.equal(report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-DOM-READONLY" : "BLOCKED", `${label}.evidenceLevel`); assert.equal(report.devLive, false, `${label}.devLive`); } else { assert.equal( report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED", `${label}.evidenceLevel` ); assert.equal(report.devLive, report.status === "pass", `${label}.devLive`); } assertDevCloudWorkbenchIdentity(report, label); const apiRuntimeReadiness = assertDevCloudWorkbenchApiRuntimeReadiness(report, label); assertDevCloudWorkbenchEndpoints(report, label); assertObject(report.sourceContract, `${label}.sourceContract`); assertStatus(report.sourceContract.status, `${label}.sourceContract.status`); await assertDocumentSet( report.sourceContract.documents, `${label}.sourceContract.documents`, requiredDevCloudWorkbenchDocs ); assertString(report.sourceContract.summary, `${label}.sourceContract.summary`); assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: requiredDevCloudWorkbenchValidationCommands.length }); const requiredModeCommands = report.mode === "dom-only" ? requiredDevCloudWorkbenchDomOnlyValidationCommands : requiredDevCloudWorkbenchLiveValidationCommands; const requiredCommands = [ ...requiredDevCloudWorkbenchValidationCommands, ...requiredModeCommands ]; assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: requiredCommands.length }); assertUnique(report.validationCommands, `${label}.validationCommands`); for (const requiredCommand of requiredCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } for (const section of ["localSmoke", "dryRun", "devPreconditions"]) { assertObject(report[section], `${label}.${section}`); assertStatus(report[section].status, `${label}.${section}.status`); assertStringArray(report[section].commands, `${label}.${section}.commands`, { minLength: 1 }); assertStringArray(report[section].evidence ?? [], `${label}.${section}.evidence`, { minLength: section === "devPreconditions" ? 0 : 1 }); assertString(report[section].summary, `${label}.${section}.summary`); } assertStringArray(report.devPreconditions.requirements, `${label}.devPreconditions.requirements`, { minLength: 4 }); assertArray(report.checks, `${label}.checks`); assert.ok(report.checks.length >= 3, `${label}.checks must include HTTP plus either deployment identity or browser journey checks`); assertUnique(report.checks.map((check) => check.id), `${label}.checks ids`); const checks = new Map(report.checks.map((check, index) => { const checkLabel = `${label}.checks[${index}]`; assertObject(check, checkLabel); for (const field of ["id", "status", "summary"]) { assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`); } assertString(check.id, `${checkLabel}.id`); assertStatus(check.status, `${checkLabel}.status`); assertString(check.summary, `${checkLabel}.summary`); if (Object.hasOwn(check, "evidence")) { assertStringArray(check.evidence, `${checkLabel}.evidence`, { minLength: 1 }); } if (Object.hasOwn(check, "observations")) { assertObject(check.observations, `${checkLabel}.observations`); } return [check.id, check]; })); const deploymentIdentityPreflight = report.status === "blocked" && report.safety?.liveMode === "deployment-identity-preflight" && checks.has("live-runtime-current-main") && checks.has("live-web-assets-current-main"); if (deploymentIdentityPreflight) { assertDevCloudWorkbenchDeploymentPreflight(report, label, checks); return; } if (report.mode === "dom-only") { assertDevCloudWorkbenchDomOnlyReport(report, label, checks); return; } for (const requiredCheck of [ "live-http-html", "live-browser-dom", "live-api-runtime-readiness", "live-code-agent-browser-journey" ]) { assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`); } const httpCheck = checks.get("live-http-html"); assert.equal(httpCheck.status, "pass", `${label}.live-http-html.status`); assert.ok(httpCheck.evidence.includes("HTTP 200"), `${label}.live-http-html.evidence missing HTTP 200`); assert.ok(httpCheck.evidence.includes("text/html; charset=utf-8"), `${label}.live-http-html.evidence missing content-type`); const dom = checks.get("live-browser-dom").observations; assertObject(dom, `${label}.live-browser-dom.observations`); assert.equal(dom.title, "HWLAB 云工作台", `${label}.live-browser-dom.title`); assert.equal(dom.workspaceHidden, false, `${label}.live-browser-dom.workspaceHidden`); assert.equal(dom.gateHidden, true, `${label}.live-browser-dom.gateHidden`); assert.equal(dom.helpHidden, true, `${label}.live-browser-dom.helpHidden`); assertObject(dom.coreControlsVisible, `${label}.live-browser-dom.coreControlsVisible`); for (const control of ["commandInput", "commandSend", "agentChatStatus", "conversationList", "hardwareList"]) { assert.equal(dom.coreControlsVisible[control], true, `${label}.live-browser-dom.coreControlsVisible.${control}`); } const apiRuntimeReadinessCheck = checks.get("live-api-runtime-readiness"); assert.equal( apiRuntimeReadinessCheck.status, apiRuntimeReadiness.status, `${label}.live-api-runtime-readiness.status` ); assert.deepEqual( apiRuntimeReadinessCheck.observations, apiRuntimeReadiness, `${label}.live-api-runtime-readiness.observations` ); const journey = checks.get("live-code-agent-browser-journey"); const acceptedJourneyStatuses = report.status === "degraded" ? ["pass", "degraded"] : [report.status]; assert.ok( acceptedJourneyStatuses.includes(journey.status), `${label}.live-code-agent-browser-journey.status` ); assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`); assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`); assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`); assert.equal(journey.observations.request.urlPath, "/v1/agent/chat", `${label}.live-code-agent-browser-journey.request.urlPath`); assertObject(journey.observations.response, `${label}.live-code-agent-browser-journey.response`); if (report.status === "blocked" && journey.status === "blocked") { assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey); return; } if (journey.status === "degraded") { assertDevCloudWorkbenchCodeAgentBackendTimeout(report, label, journey); assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey); return; } assert.equal(journey.observations.response.status, "completed", `${label}.live-code-agent-browser-journey.response.status`); for (const field of ["provider", "model", "backend", "traceId"]) { assertString(journey.observations.response[field], `${label}.live-code-agent-browser-journey.response.${field}`); } assert.equal(journey.observations.response.hasReply, true, `${label}.live-code-agent-browser-journey.response.hasReply`); assert.equal(journey.observations.response.error, null, `${label}.live-code-agent-browser-journey.response.error`); for (const forbiddenField of ["conversationId", "sessionId", "messageId", "reply"]) { assert.equal( Object.hasOwn(journey.observations.response, forbiddenField), false, `${label}.live-code-agent-browser-journey.response must not retain ${forbiddenField}` ); } assertObject(journey.observations.ui, `${label}.live-code-agent-browser-journey.ui`); assert.ok( ["已回复", "DEV-LIVE 回复"].includes(journey.observations.ui.agentChatStatus), `${label}.live-code-agent-browser-journey.ui.agentChatStatus` ); assert.equal(journey.observations.ui.completedMessageVisible, true, `${label}.live-code-agent-browser-journey.ui.completedMessageVisible`); assert.equal(journey.observations.ui.failedMessageVisible, false, `${label}.live-code-agent-browser-journey.ui.failedMessageVisible`); assertBlockers(report.blockers, `${label}.blockers`); assertObject(report.safety, `${label}.safety`); for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive"]) { assert.equal(report.safety[field], false, `${label}.safety.${field}`); } assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`); assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`); assertString(report.safety.statement, `${label}.safety.statement`); if (report.status === "degraded") { assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness); } assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey); } function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) { const response = journey.observations.response; assert.ok(["failed", "blocked"].includes(response.status), `${label}.live-code-agent-browser-journey.response.status`); assert.equal(response.hasReply, false, `${label}.live-code-agent-browser-journey.response.hasReply`); assertObject(response.error, `${label}.live-code-agent-browser-journey.response.error`); assert.ok( ["provider_unavailable", "provider_blocked", "untrusted_completion"].includes(response.error.code), `${label}.live-code-agent-browser-journey.response.error.code` ); if (response.error.providerStatus !== null && response.error.providerStatus !== undefined) { assert.ok( Number.isInteger(response.error.providerStatus) && response.error.providerStatus >= 100 && response.error.providerStatus <= 599, `${label}.live-code-agent-browser-journey.response.error.providerStatus` ); } for (const field of ["provider", "model", "backend", "traceId"]) { assertString(response[field], `${label}.live-code-agent-browser-journey.response.${field}`); } for (const forbiddenField of ["conversationId", "sessionId", "messageId", "reply"]) { assert.equal( Object.hasOwn(response, forbiddenField), false, `${label}.live-code-agent-browser-journey.response must not retain ${forbiddenField}` ); } assertObject(journey.observations.ui, `${label}.live-code-agent-browser-journey.ui`); assert.ok( blockedCodeAgentUiLabels.has(journey.observations.ui.agentChatStatus), `${label}.live-code-agent-browser-journey.ui.agentChatStatus` ); assert.equal(journey.observations.ui.completedMessageVisible, false, `${label}.live-code-agent-browser-journey.ui.completedMessageVisible`); assert.equal(journey.observations.ui.failedMessageVisible, true, `${label}.live-code-agent-browser-journey.ui.failedMessageVisible`); assertBlockers(report.blockers, `${label}.blockers`); assert.ok( report.blockers.some((blocker) => blocker.scope === "live-code-agent-browser-journey"), `${label}.blockers must include live-code-agent-browser-journey` ); assertObject(report.safety, `${label}.safety`); for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive"]) { assert.equal(report.safety[field], false, `${label}.safety.${field}`); } assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`); assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`); assertString(report.safety.statement, `${label}.safety.statement`); } function assertDevCloudWorkbenchCodeAgentBackendTimeout(report, label, journey) { assert.equal(report.status, "degraded", `${label}.status`); assertObject(journey.observations.classification, `${label}.live-code-agent-browser-journey.classification`); assert.equal(journey.observations.classification.terminalState, "backend-timeout", `${label}.live-code-agent-browser-journey.classification.terminalState`); assert.ok( Number.isInteger(journey.observations.classification.backendTimeoutMs) && journey.observations.classification.backendTimeoutMs >= 4500, `${label}.live-code-agent-browser-journey.classification.backendTimeoutMs` ); assert.match( journey.observations.classification.chineseSummary ?? "", /后端超时/u, `${label}.live-code-agent-browser-journey.classification.chineseSummary` ); assertArray(journey.observations.prompts, `${label}.live-code-agent-browser-journey.prompts`, { minLength: 1 }); assert.ok( journey.observations.prompts.every((prompt) => prompt.legacyWindow?.permanentFailureAround4500ms === false), `${label}.live-code-agent-browser-journey.prompts must not include old 4500ms permanent failure` ); assert.equal(journey.observations.legacyFailureWindowMs, 4500, `${label}.live-code-agent-browser-journey.legacyFailureWindowMs`); assertObject(report.safety, `${label}.safety`); assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`); assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`); } function assertDevCloudWorkbenchApiRuntimeReadiness(report, label) { assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`); if (Object.hasOwn(report.runtimeIdentity, "ready")) { assertBoolean(report.runtimeIdentity.ready, `${label}.runtimeIdentity.ready`); } if (Object.hasOwn(report.runtimeIdentity, "runtime")) { assertObject(report.runtimeIdentity.runtime, `${label}.runtimeIdentity.runtime`); for (const field of ["durable", "ready"]) { if (Object.hasOwn(report.runtimeIdentity.runtime, field)) { assertBoolean(report.runtimeIdentity.runtime[field], `${label}.runtimeIdentity.runtime.${field}`); } } if (Object.hasOwn(report.runtimeIdentity.runtime, "status")) { assertString(report.runtimeIdentity.runtime.status, `${label}.runtimeIdentity.runtime.status`); } if (Object.hasOwn(report.runtimeIdentity.runtime, "blocker")) { assertString(report.runtimeIdentity.runtime.blocker, `${label}.runtimeIdentity.runtime.blocker`); } } if (Object.hasOwn(report.runtimeIdentity, "readiness")) { assertObject(report.runtimeIdentity.readiness, `${label}.runtimeIdentity.readiness`); if (Object.hasOwn(report.runtimeIdentity.readiness, "ready")) { assertBoolean(report.runtimeIdentity.readiness.ready, `${label}.runtimeIdentity.readiness.ready`); } if (Object.hasOwn(report.runtimeIdentity.readiness, "status")) { assertString(report.runtimeIdentity.readiness.status, `${label}.runtimeIdentity.readiness.status`); } if (Object.hasOwn(report.runtimeIdentity.readiness, "durability")) { assertObject(report.runtimeIdentity.readiness.durability, `${label}.runtimeIdentity.readiness.durability`); if (Object.hasOwn(report.runtimeIdentity.readiness.durability, "ready")) { assertBoolean(report.runtimeIdentity.readiness.durability.ready, `${label}.runtimeIdentity.readiness.durability.ready`); } for (const field of ["status", "blocker", "blockedLayer", "queryResult"]) { if (Object.hasOwn(report.runtimeIdentity.readiness.durability, field)) { assertString(report.runtimeIdentity.readiness.durability[field], `${label}.runtimeIdentity.readiness.durability.${field}`); } } } } if (Object.hasOwn(report.runtimeIdentity, "blockerCodes")) { assertStringArray(report.runtimeIdentity.blockerCodes, `${label}.runtimeIdentity.blockerCodes`); } const runtime = report.runtimeIdentity.runtime ?? {}; const readiness = report.runtimeIdentity.readiness ?? {}; const durability = readiness.durability ?? {}; const blockerCodes = Array.isArray(report.runtimeIdentity.blockerCodes) ? report.runtimeIdentity.blockerCodes : []; const durableBlocked = runtime.blocker === "runtime_durable_adapter_query_blocked" || durability.blocker === "runtime_durable_adapter_query_blocked" || blockerCodes.includes("runtime_durable_adapter_query_blocked") || durability.blockedLayer === "durability_query" || runtime.connection?.queryResult === "query_blocked" || durability.queryResult === "query_blocked"; const degraded = report.runtimeIdentity.healthStatus === "degraded" || report.runtimeIdentity.ready === false || runtime.ready === false || durability.ready === false || runtime.durable === false || durableBlocked; const status = degraded ? "degraded" : "pass"; const result = { status, healthStatus: report.runtimeIdentity.healthStatus, ready: report.runtimeIdentity.ready === true, runtimeDurable: runtime.durable === true, runtimeReady: runtime.ready === true, durabilityReady: durability.ready === true, durableBlocked, blockerCodes, evidence: [ `api.status=${report.runtimeIdentity.healthStatus}`, `ready=${report.runtimeIdentity.ready === true}`, `runtime.durable=${runtime.durable === true}`, `runtime.ready=${runtime.ready === true}`, `durability.ready=${durability.ready === true}`, durableBlocked ? "runtime_durable_adapter_query_blocked" : null, ...blockerCodes.map((code) => `blocker=${code}`) ].filter(Boolean), summary: degraded ? "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance." : "Live API health and runtime durability are ready for browser journey evidence." }; if (["pass", "degraded"].includes(report.status)) { assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`); assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`); assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`); assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`); assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`); assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`); assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`); assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`); assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`); assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`); const generatedAtMs = Date.parse(report.generatedAt); const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt); assert.ok( Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs, `${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt` ); } if (report.status === "pass") { assert.equal( degraded, false, `${label}.runtimeIdentity degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted` ); } if (degraded) { assert.ok( ["degraded", "blocked"].includes(report.status), `${label}.status must be degraded or blocked when API health or runtime durability is degraded` ); assert.equal(report.devLive, false, `${label}.devLive must remain false for degraded/read-only workbench evidence`); assert.equal( report.evidenceLevel, report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED", `${label}.evidenceLevel` ); } return result; } function assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness) { assert.equal(apiRuntimeReadiness.status, "degraded", `${label}.live-api-runtime-readiness.status`); assert.equal(report.devPreconditions.status, "degraded", `${label}.devPreconditions.status`); const summaryText = [ report.reportLifecycle.summary, report.devPreconditions.summary, report.safety.statement ].join(" "); assert.match( summaryText, /degraded\/read-only mode|degraded\/read-only|read-only mode/u, `${label} degraded API/runtime report must be summarized as deployed UI usable in degraded/read-only mode` ); assert.doesNotMatch( summaryText, /full DEV-LIVE accepted|full DEV-LIVE acceptance passed|M3\/M4\/M5 DEV-LIVE accepted|M[345]\s+(?:DEV-LIVE\s+)?(?:accepted|acceptance passed|green)/u, `${label} degraded API/runtime report must not claim full M3/M4/M5 DEV-LIVE acceptance` ); } function assertCodeAgentRetainedApiFields(value, label) { assertArray(value, label); for (const field of ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv"]) { assert.ok(value.includes(field), `${label} missing ${field}`); } for (const field of value) { assert.ok( ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv", "error.providerStatus"].includes(field), `${label} contains unexpected retained field ${field}` ); } } function assertDevCloudWorkbenchDeploymentPreflight(report, label, checks) { for (const requiredCheck of [ "live-runtime-current-main", "live-http-html", "live-web-assets-current-main", "live-code-agent-browser-journey" ]) { assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`); } const runtimeCheck = checks.get("live-runtime-current-main"); const webAssetCheck = checks.get("live-web-assets-current-main"); const journey = checks.get("live-code-agent-browser-journey"); assert.ok(["pass", "blocked"].includes(runtimeCheck.status), `${label}.live-runtime-current-main.status`); assert.ok(["pass", "blocked"].includes(webAssetCheck.status), `${label}.live-web-assets-current-main.status`); assert.equal(journey.status, "blocked", `${label}.live-code-agent-browser-journey.status`); assert.ok( runtimeCheck.status === "blocked" || webAssetCheck.status === "blocked", `${label}.deployment preflight requires runtime or web asset drift` ); assertObject(runtimeCheck.observations, `${label}.live-runtime-current-main.observations`); assertObject(webAssetCheck.observations, `${label}.live-web-assets-current-main.observations`); assertObject(report.expectedRuntimeIdentity, `${label}.expectedRuntimeIdentity`); for (const field of ["status", "serviceId", "source", "commitId", "imageTag", "image", "summary"]) { assertString(report.expectedRuntimeIdentity[field], `${label}.expectedRuntimeIdentity.${field}`); } assert.ok(["observed", "not_observed"].includes(report.expectedRuntimeIdentity.status), `${label}.expectedRuntimeIdentity.status`); assertObject(report.deploymentIdentity, `${label}.deploymentIdentity`); assert.equal(report.deploymentIdentity.status, runtimeCheck.status, `${label}.deploymentIdentity.status`); assertString(report.deploymentIdentity.expectedCommit, `${label}.deploymentIdentity.expectedCommit`); assertString(report.deploymentIdentity.expectedImageTag, `${label}.deploymentIdentity.expectedImageTag`); assertString(report.deploymentIdentity.expectedSource, `${label}.deploymentIdentity.expectedSource`); assertString(report.deploymentIdentity.observedRuntimeCommit, `${label}.deploymentIdentity.observedRuntimeCommit`); assertString(report.deploymentIdentity.observedImageTag, `${label}.deploymentIdentity.observedImageTag`); if (report.deploymentIdentity.status === "blocked") { assertString(report.deploymentIdentity.reason, `${label}.deploymentIdentity.reason`); } assertString(report.deploymentIdentity.summary, `${label}.deploymentIdentity.summary`); assertStringArray(report.deploymentIdentity.evidence, `${label}.deploymentIdentity.evidence`, { minLength: 4 }); assert.deepEqual( runtimeCheck.observations, report.deploymentIdentity, `${label}.live-runtime-current-main.observations` ); assertObject(report.webAssetIdentity, `${label}.webAssetIdentity`); assert.equal(report.webAssetIdentity.status, webAssetCheck.status, `${label}.webAssetIdentity.status`); assertArray(report.webAssetIdentity.assets, `${label}.webAssetIdentity.assets`); assertStringArray(report.webAssetIdentity.mismatches, `${label}.webAssetIdentity.mismatches`, { minLength: report.webAssetIdentity.status === "blocked" ? 1 : 0 }); assertString(report.webAssetIdentity.summary, `${label}.webAssetIdentity.summary`); for (const [index, asset] of report.webAssetIdentity.assets.entries()) { const assetLabel = `${label}.webAssetIdentity.assets[${index}]`; assertObject(asset, assetLabel); for (const field of ["path", "status", "contentType", "sourceHash", "liveHash"]) { assertString(asset[field], `${assetLabel}.${field}`); } assert.ok(["match", "mismatch", "fetch_failed"].includes(asset.status), `${assetLabel}.status`); assert.ok(Number.isInteger(asset.sourceLength), `${assetLabel}.sourceLength`); assert.ok(Number.isInteger(asset.liveLength), `${assetLabel}.liveLength`); } assert.deepEqual( webAssetCheck.observations, report.webAssetIdentity, `${label}.live-web-assets-current-main.observations` ); assertDevCloudWorkbenchDeploymentPreflightJourney(report, label, journey); assertBlockers(report.blockers, `${label}.blockers`); if (runtimeCheck.status === "blocked") { assert.ok( report.blockers.some((blocker) => blocker.scope === "live-runtime-current-main"), `${label}.blockers must include runtime identity drift` ); } if (webAssetCheck.status === "blocked") { assert.ok( report.blockers.some((blocker) => blocker.scope === "live-web-assets-current-main"), `${label}.blockers must include web asset drift` ); } assertObject(report.safety, `${label}.safety`); for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive", "codeAgentBrowserJourneySkipped"]) { assert.equal(report.safety[field], field === "codeAgentBrowserJourneySkipped", `${label}.safety.${field}`); } assert.equal(report.safety.liveMode, "deployment-identity-preflight", `${label}.safety.liveMode`); assert.deepEqual( report.safety.retainedApiFields, ["runtime.commitId", "runtime.imageTag", "web asset hash/status"], `${label}.safety.retainedApiFields` ); assertString(report.safety.statement, `${label}.safety.statement`); } function assertDevCloudWorkbenchDeploymentPreflightJourney(report, label, journey) { assertStringArray(journey.evidence, `${label}.live-code-agent-browser-journey.evidence`, { minLength: 3 }); assert.ok( journey.evidence.includes("POST /v1/agent/chat not sent"), `${label}.live-code-agent-browser-journey.evidence missing not_sent proof` ); assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`); assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`); assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`); assert.equal(journey.observations.request.status, "not_sent", `${label}.live-code-agent-browser-journey.request.status`); assert.equal(journey.observations.request.urlPath, "/v1/agent/chat", `${label}.live-code-agent-browser-journey.request.urlPath`); assertObject(journey.observations.response, `${label}.live-code-agent-browser-journey.response`); assert.equal(journey.observations.response.status, "failed", `${label}.live-code-agent-browser-journey.response.status`); assert.equal(journey.observations.response.hasReply, false, `${label}.live-code-agent-browser-journey.response.hasReply`); assertObject(journey.observations.response.error, `${label}.live-code-agent-browser-journey.response.error`); assert.equal( journey.observations.response.error.code, deploymentPreflightCodeAgentErrorCode, `${label}.live-code-agent-browser-journey.response.error.code` ); for (const field of ["provider", "model", "backend", "traceId"]) { assert.equal(journey.observations.response[field], "not_observed", `${label}.live-code-agent-browser-journey.response.${field}`); } assertObject(journey.observations.classification, `${label}.live-code-agent-browser-journey.classification`); assert.equal(journey.observations.classification.status, "blocked", `${label}.live-code-agent-browser-journey.classification.status`); assert.equal( journey.observations.classification.reason, deploymentPreflightCodeAgentErrorCode, `${label}.live-code-agent-browser-journey.classification.reason` ); assert.equal( journey.observations.classification.codeAgentBrowserJourneySkipped, true, `${label}.live-code-agent-browser-journey.classification.codeAgentBrowserJourneySkipped` ); const journeyText = JSON.stringify(journey.observations); assert.equal( /"status"\s*:\s*200|completed|provider_unavailable|OPENAI_API_KEY/u.test(journeyText), false, `${label}.live-code-agent-browser-journey must not retain sent, completed, or secret-dependent evidence during deployment preflight` ); assert.ok( report.blockers.some((blocker) => blocker.scope === "live-code-agent-browser-journey"), `${label}.blockers must include live-code-agent-browser-journey` ); } function assertDevCloudWorkbenchDomOnlyReport(report, label, checks) { assert.equal(report.mode, "dom-only", `${label}.mode`); for (const requiredCheck of [ "live-runtime-current-main", "live-api-runtime-readiness", "live-http-html", "live-web-assets-current-main", "live-browser-dom", "live-gate-route", "live-help-markdown-route", "live-code-agent-browser-journey" ]) { assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`); } const runtimeCheck = checks.get("live-runtime-current-main"); const apiRuntimeReadinessCheck = checks.get("live-api-runtime-readiness"); const webAssetCheck = checks.get("live-web-assets-current-main"); const apiRuntimeReadiness = assertDevCloudWorkbenchApiRuntimeReadiness(report, label); assert.ok(["pass", "blocked"].includes(runtimeCheck.status), `${label}.live-runtime-current-main.status`); assert.equal( apiRuntimeReadinessCheck.status, apiRuntimeReadiness.status, `${label}.live-api-runtime-readiness.status` ); assert.deepEqual( apiRuntimeReadinessCheck.observations, apiRuntimeReadiness, `${label}.live-api-runtime-readiness.observations` ); assert.ok(["pass", "blocked"].includes(webAssetCheck.status), `${label}.live-web-assets-current-main.status`); assertObject(runtimeCheck.observations, `${label}.live-runtime-current-main.observations`); assertObject(webAssetCheck.observations, `${label}.live-web-assets-current-main.observations`); assertObject(report.expectedRuntimeIdentity, `${label}.expectedRuntimeIdentity`); assertObject(report.deploymentIdentity, `${label}.deploymentIdentity`); assert.equal(report.deploymentIdentity.status, runtimeCheck.status, `${label}.deploymentIdentity.status`); assertObject(report.webAssetIdentity, `${label}.webAssetIdentity`); assert.equal(report.webAssetIdentity.status, webAssetCheck.status, `${label}.webAssetIdentity.status`); assertArray(report.webAssetIdentity.assets, `${label}.webAssetIdentity.assets`); assertString(report.webAssetIdentity.summary, `${label}.webAssetIdentity.summary`); const domCheck = checks.get("live-browser-dom"); if (domCheck.status === "pass") { assertDevCloudWorkbenchDomObservations(domCheck.observations, `${label}.live-browser-dom`); } else { assert.ok(["blocked", "skip"].includes(domCheck.status), `${label}.live-browser-dom.status`); } const helpCheck = checks.get("live-help-markdown-route"); assert.ok(["pass", "blocked", "skip"].includes(helpCheck.status), `${label}.live-help-markdown-route.status`); const gateCheck = checks.get("live-gate-route"); if (gateCheck.status === "pass") { assertDevCloudWorkbenchGateRoute(gateCheck.observations, `${label}.live-gate-route`); } else { assert.ok(["blocked", "skip"].includes(gateCheck.status), `${label}.live-gate-route.status`); } assertDevCloudWorkbenchDomOnlyJourney(report, label, checks.get("live-code-agent-browser-journey")); assertBlockers(report.blockers, `${label}.blockers`); assertObject(report.safety, `${label}.safety`); for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive"]) { assert.equal(report.safety[field], false, `${label}.safety.${field}`); } assert.equal(report.safety.liveMode, "dom-only-readonly-no-code-agent-post", `${label}.safety.liveMode`); assert.equal(report.safety.codeAgentBrowserJourneySkipped, true, `${label}.safety.codeAgentBrowserJourneySkipped`); assert.equal(report.safety.codeAgentPostSent, false, `${label}.safety.codeAgentPostSent`); assertStringArray(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`, { minLength: 4 }); assertString(report.safety.statement, `${label}.safety.statement`); assert.equal(/Secret|M3\/M4\/M5|\/v1\/agent\/chat/u.test(report.safety.statement), true, `${label}.safety.statement`); if (apiRuntimeReadiness.status === "degraded") { assert.equal(report.status, "blocked", `${label}.status`); assert.match( report.safety.statement, /must not be used to infer M3\/M4\/M5 acceptance|claims M3\/M4\/M5 acceptance/u, `${label}.safety.statement` ); } const raw = JSON.stringify(report); assert.equal(/"method"\s*:\s*"POST"[\s\S]{0,200}"status"\s*:\s*(200|201|202)|"completed"/u.test(raw), false, `${label} must not include a sent or completed Code Agent POST`); } function assertDevCloudWorkbenchDomObservations(dom, label) { assertObject(dom, `${label}.observations`); assert.equal(dom.title, "HWLAB 云工作台", `${label}.title`); assert.equal(dom.workspaceHidden, false, `${label}.workspaceHidden`); assert.equal(dom.gateHidden, true, `${label}.gateHidden`); assert.equal(dom.helpHidden, true, `${label}.helpHidden`); assert.equal(dom.navLabelsPresent, true, `${label}.navLabelsPresent`); assert.equal(dom.defaultBackstageHidden, true, `${label}.defaultBackstageHidden`); assert.equal(dom.gateRouteAvailable, true, `${label}.gateRouteAvailable`); if (Object.hasOwn(dom, "codeAgentPostGuard")) { assertObject(dom.codeAgentPostGuard, `${label}.codeAgentPostGuard`); assert.equal(dom.codeAgentPostGuard.installed, true, `${label}.codeAgentPostGuard.installed`); assert.equal(dom.codeAgentPostGuard.blockedPath, "/v1/agent/chat", `${label}.codeAgentPostGuard.blockedPath`); assert.equal(dom.codeAgentPostGuard.attemptedCount, 0, `${label}.codeAgentPostGuard.attemptedCount`); } assertStringArray(dom.wiringHeaders, `${label}.wiringHeaders`, { minLength: 8 }); for (const column of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) { assert.ok(dom.wiringHeaders.includes(column), `${label}.wiringHeaders missing ${column}`); } assertArray(dom.m3WiringRows, `${label}.m3WiringRows`); assert.ok( dom.m3WiringRows.some((row) => Array.isArray(row) && row.includes("res_boxsimu_1") && row.includes("DO1") && row.includes("hwlab-patch-panel") && row.includes("res_boxsimu_2") && row.includes("DI1") ), `${label}.m3WiringRows missing required M3 route` ); assertObject(dom.coreControlsVisible, `${label}.coreControlsVisible`); for (const control of ["commandInput", "commandSend", "agentChatStatus", "conversationList", "hardwareList", "wiringTab", "recordsTab"]) { assert.equal(dom.coreControlsVisible[control], true, `${label}.coreControlsVisible.${control}`); } } function assertDevCloudWorkbenchGateRoute(gate, label) { assertObject(gate, `${label}.observations`); assert.equal(gate.pathname, "/gate", `${label}.pathname`); assert.equal(gate.title, "HWLAB 云工作台", `${label}.title`); assert.equal(gate.workspaceHidden, true, `${label}.workspaceHidden`); assert.equal(gate.gateHidden, false, `${label}.gateHidden`); assert.equal(gate.helpHidden, true, `${label}.helpHidden`); assert.equal(gate.diagnosticsTermsPresent, true, `${label}.diagnosticsTermsPresent`); if (Object.hasOwn(gate, "codeAgentPostGuard")) { assertObject(gate.codeAgentPostGuard, `${label}.codeAgentPostGuard`); assert.equal(gate.codeAgentPostGuard.installed, true, `${label}.codeAgentPostGuard.installed`); assert.equal(gate.codeAgentPostGuard.blockedPath, "/v1/agent/chat", `${label}.codeAgentPostGuard.blockedPath`); assert.equal(gate.codeAgentPostGuard.attemptedCount, 0, `${label}.codeAgentPostGuard.attemptedCount`); } } function assertDevCloudWorkbenchDomOnlyJourney(report, label, journey) { assert.equal(journey.status, "not_applicable", `${label}.live-code-agent-browser-journey.status`); assertStringArray(journey.evidence, `${label}.live-code-agent-browser-journey.evidence`, { minLength: 3 }); assert.ok( journey.evidence.includes("POST /v1/agent/chat not sent"), `${label}.live-code-agent-browser-journey.evidence missing not-sent proof` ); assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`); assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`); assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`); assert.equal(journey.observations.request.status, "not_applicable", `${label}.live-code-agent-browser-journey.request.status`); assert.equal(journey.observations.request.urlPath, "/v1/agent/chat", `${label}.live-code-agent-browser-journey.request.urlPath`); assertObject(journey.observations.response, `${label}.live-code-agent-browser-journey.response`); assert.equal(journey.observations.response.status, "not_applicable", `${label}.live-code-agent-browser-journey.response.status`); assert.equal(journey.observations.response.hasReply, false, `${label}.live-code-agent-browser-journey.response.hasReply`); assert.equal(journey.observations.response.error, null, `${label}.live-code-agent-browser-journey.response.error`); for (const field of ["provider", "model", "backend", "traceId"]) { assert.equal(journey.observations.response[field], "not_observed", `${label}.live-code-agent-browser-journey.response.${field}`); } assertObject(journey.observations.classification, `${label}.live-code-agent-browser-journey.classification`); assert.equal(journey.observations.classification.status, "not_applicable", `${label}.live-code-agent-browser-journey.classification.status`); assert.equal(journey.observations.classification.reason, "dom_only_no_code_agent_post", `${label}.live-code-agent-browser-journey.classification.reason`); assert.equal(journey.observations.classification.codeAgentBrowserJourneySkipped, true, `${label}.live-code-agent-browser-journey.classification.codeAgentBrowserJourneySkipped`); assertArray(journey.observations.networkEvents ?? [], `${label}.live-code-agent-browser-journey.networkEvents`); assert.equal((journey.observations.networkEvents ?? []).length, 0, `${label}.live-code-agent-browser-journey.networkEvents`); assert.equal(report.devLive, false, `${label}.devLive`); } function assertDevCloudWorkbenchIdentity(report, label) { assertObject(report.sourceIdentity, `${label}.sourceIdentity`); for (const field of ["status", "source", "commitId", "shortCommitId", "ref", "worktreeState", "reportCommitId", "summary"]) { assert.ok(Object.hasOwn(report.sourceIdentity, field), `${label}.sourceIdentity missing ${field}`); assertString(report.sourceIdentity[field], `${label}.sourceIdentity.${field}`); } assert.ok(["observed", "unknown"].includes(report.sourceIdentity.status), `${label}.sourceIdentity.status`); assert.ok(["git-head", "environment", "unknown"].includes(report.sourceIdentity.source), `${label}.sourceIdentity.source`); assert.match(report.sourceIdentity.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.sourceIdentity.commitId`); assert.match(report.sourceIdentity.shortCommitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.sourceIdentity.shortCommitId`); assert.ok(["clean", "dirty", "unknown"].includes(report.sourceIdentity.worktreeState), `${label}.sourceIdentity.worktreeState`); if (report.sourceIdentity.source === "environment") { assertString(report.sourceIdentity.environmentKey, `${label}.sourceIdentity.environmentKey`); } if (report.sourceIdentity.worktreeState === "clean" && report.sourceIdentity.commitId !== "unknown") { assert.match(report.sourceIdentity.reportCommitId, /^[a-f0-9]{7,40}$/, `${label}.sourceIdentity.reportCommitId`); assert.equal(report.commitId, report.sourceIdentity.reportCommitId, `${label}.commitId must match clean source reportCommitId`); } else { assert.equal(report.sourceIdentity.reportCommitId, "unknown", `${label}.sourceIdentity.reportCommitId`); assert.equal(report.commitId, "unknown", `${label}.commitId must be unknown when source worktree identity is dirty or unknown`); } if (report.sourceIdentity.dirty !== null) { assertBoolean(report.sourceIdentity.dirty, `${label}.sourceIdentity.dirty`); assert.equal(report.sourceIdentity.dirty, report.sourceIdentity.worktreeState === "dirty", `${label}.sourceIdentity.dirty`); } assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`); for (const field of ["status", "source", "endpoint", "serviceId", "environment", "healthStatus", "commitId", "commitSource", "imageTag", "observedAt", "summary"]) { assert.ok(Object.hasOwn(report.runtimeIdentity, field), `${label}.runtimeIdentity missing ${field}`); assertString(report.runtimeIdentity[field], `${label}.runtimeIdentity.${field}`); } assert.ok(["observed", "not_observed"].includes(report.runtimeIdentity.status), `${label}.runtimeIdentity.status`); assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`); assert.equal(report.runtimeIdentity.endpoint, "http://74.48.78.17:16667/health/live", `${label}.runtimeIdentity.endpoint`); assertTimestamp(report.runtimeIdentity.observedAt, `${label}.runtimeIdentity.observedAt`); assert.match(report.runtimeIdentity.commitId, /^([a-f0-9]{7,40}|unknown|not_observed)$/, `${label}.runtimeIdentity.commitId`); if (report.runtimeIdentity.status === "not_observed") { assert.equal(report.runtimeIdentity.commitId, "not_observed", `${label}.runtimeIdentity.commitId`); assertString(report.runtimeIdentity.reason, `${label}.runtimeIdentity.reason`); } else { assert.ok( ["hwlab-cloud-api", "unknown"].includes(report.runtimeIdentity.serviceId), `${label}.runtimeIdentity.serviceId` ); assert.ok(["dev", "unknown"].includes(report.runtimeIdentity.environment), `${label}.runtimeIdentity.environment`); } assert.ok( report.sourceIdentity.commitId === "unknown" || report.runtimeIdentity.commitId === "unknown" || report.runtimeIdentity.commitId === "not_observed" || report.runtimeIdentity.commitSource !== "source-git-head", `${label}.runtimeIdentity must not present source git HEAD as the deployed live revision` ); } function assertDevCloudWorkbenchEndpoints(report, label) { assertObject(report.endpoints, `${label}.endpoints`); assert.equal(report.endpoints.frontend, ACTIVE_DEV_BROWSER_ENDPOINT, `${label}.endpoints.frontend`); assert.equal(report.endpoints.api, ACTIVE_DEV_PUBLIC_ENDPOINT, `${label}.endpoints.api`); assert.equal(report.endpoints.edge, ACTIVE_DEV_PUBLIC_ENDPOINT, `${label}.endpoints.edge`); } function assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey) { const deprecatedEndpoints = findDeprecatedPublicEndpoints(raw); assert.deepEqual( deprecatedEndpoints, [], `${label} deployed workbench report must not contain deprecated public endpoint evidence` ); if (!["pass", "degraded"].includes(report.status)) { return; } assert.equal(report.devLive, report.status === "pass", `${label}.devLive`); assert.equal( report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : "DEV-LIVE-BROWSER-DEGRADED", `${label}.evidenceLevel` ); assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`); if (report.status === "pass") { assert.notEqual(report.runtimeIdentity.healthStatus, "degraded", `${label}.runtimeIdentity.healthStatus`); assert.equal(report.runtimeIdentity.ready, true, `${label}.runtimeIdentity.ready`); assert.equal(report.runtimeIdentity.runtime?.durable, true, `${label}.runtimeIdentity.runtime.durable`); assert.equal(report.runtimeIdentity.runtime?.ready, true, `${label}.runtimeIdentity.runtime.ready`); assert.equal(report.runtimeIdentity.readiness?.durability?.ready, true, `${label}.runtimeIdentity.readiness.durability.ready`); assert.notEqual( report.runtimeIdentity.runtime?.blocker, "runtime_durable_adapter_query_blocked", `${label}.runtimeIdentity.runtime.blocker` ); assert.notEqual( report.runtimeIdentity.readiness?.durability?.blocker, "runtime_durable_adapter_query_blocked", `${label}.runtimeIdentity.readiness.durability.blocker` ); assert.equal( (report.runtimeIdentity.blockerCodes ?? []).includes("runtime_durable_adapter_query_blocked"), false, `${label}.runtimeIdentity.blockerCodes` ); } assert.equal( report.sourceIdentity.reportCommitId === "unknown" || report.sourceIdentity.worktreeState === "clean", true, `${label}.sourceIdentity.reportCommitId must be unknown unless the source worktree is clean` ); if (journey.status === "degraded" && journey.observations.classification?.terminalState === "backend-timeout") { return; } const journeyText = JSON.stringify(journey.observations); assert.equal( /provider_unavailable|OPENAI_API_KEY|providerStatus|provider HTTP|HTTP 502/u.test(journeyText), false, `${label}.live-code-agent-browser-journey must not mix provider_unavailable, OPENAI_API_KEY, or providerStatus blockers into accepted evidence` ); assertAcceptedCodeAgentPayload( journey.observations.response, `${label}.live-code-agent-browser-journey.response` ); const networkEvents = journey.observations.networkEvents ?? []; assertArray(networkEvents, `${label}.live-code-agent-browser-journey.networkEvents`); assert.ok(networkEvents.length >= 1, `${label}.live-code-agent-browser-journey.networkEvents`); for (const [index, event] of networkEvents.entries()) { const eventLabel = `${label}.live-code-agent-browser-journey.networkEvents[${index}]`; assertObject(event, eventLabel); assert.equal(event.method, "POST", `${eventLabel}.method`); assert.equal(event.status, 200, `${eventLabel}.status`); assert.equal(event.urlPath, "/v1/agent/chat", `${eventLabel}.urlPath`); assertAcceptedCodeAgentPayload(event.body, `${eventLabel}.body`); } } function assertAcceptedCodeAgentPayload(payload, label) { assertObject(payload, label); assert.equal(payload.status, "completed", `${label}.status`); assertString(payload.provider, `${label}.provider`); assertString(payload.model, `${label}.model`); assertString(payload.backend, `${label}.backend`); assertString(payload.traceId, `${label}.traceId`); assert.equal(payload.hasReply, true, `${label}.hasReply`); assert.equal(payload.error, null, `${label}.error`); assert.equal( syntheticAcceptedCodeAgentModels.has(payload.model), false, `${label}.model must not use the known synthetic placeholder model` ); for (const forbiddenField of ["conversationId", "sessionId", "messageId", "reply", "content", "text", "output"]) { assert.equal( Object.hasOwn(payload, forbiddenField), false, `${label} must not retain ${forbiddenField}` ); } } 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}` ); } assert.ok( report.validationCommands.includes(legacyDevM3LiveCommand), `${label}.validationCommands missing legacy compatibility command ${legacyDevM3LiveCommand}` ); assertObject(report.runtimeTarget, `${label}.runtimeTarget`); assert.equal(report.runtimeTarget.endpoint, "http://74.48.78.17:16667", `${label}.runtimeTarget.endpoint`); if (Object.hasOwn(report.runtimeTarget, "frontendEndpoint")) { assert.equal( report.runtimeTarget.frontendEndpoint, "http://74.48.78.17:16666", `${label}.runtimeTarget.frontendEndpoint` ); } 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`); if (Object.hasOwn(report.runtimeTarget, "requiredPatchPanels")) { assert.equal(report.runtimeTarget.requiredPatchPanels, 1, `${label}.runtimeTarget.requiredPatchPanels`); } if (Object.hasOwn(report.runtimeTarget, "requiredRoute")) { assert.equal( report.runtimeTarget.requiredRoute, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1", `${label}.runtimeTarget.requiredRoute` ); } 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", "expectNonProdRequired", "confirmedNonProductionRequired", "dryRunPlanSupported", "prodForbidden", "realHardwareForbidden", "secretReadForbidden", "forcePushForbidden", "unideskRuntimeSubstitutionForbidden" ]) { assert.equal(report.safetyGates[field], true, `${label}.safetyGates.${field}`); } if (Object.hasOwn(report.safetyGates, "dryRunCallsLiveEndpoints")) { assert.equal(report.safetyGates.dryRunCallsLiveEndpoints, false, `${label}.safetyGates.dryRunCallsLiveEndpoints`); } if (Object.hasOwn(report.safetyGates, "defaultSourceReadOnly")) { assert.equal(report.safetyGates.defaultSourceReadOnly, true, `${label}.safetyGates.defaultSourceReadOnly`); } 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}`); } } } if (Object.hasOwn(report, "d601Observability")) { assertObject(report.d601Observability, `${label}.d601Observability`); for (const field of [ "runnerKubeconfigReadable", "runnerKubeconfigProbeExitCode", "runnerKubeconfigProbeStderr", "d601PublicEndpointsReachable", "d601K3sUnavailable", "classification", "sourceIssue", "inferenceRule" ]) { assert.ok(Object.hasOwn(report.d601Observability, field), `${label}.d601Observability missing ${field}`); } assertBoolean(report.d601Observability.runnerKubeconfigReadable, `${label}.d601Observability.runnerKubeconfigReadable`); if (Object.hasOwn(report.d601Observability, "runnerKubeconfigProbeOk")) { assertBoolean(report.d601Observability.runnerKubeconfigProbeOk, `${label}.d601Observability.runnerKubeconfigProbeOk`); } assert.ok( Number.isInteger(report.d601Observability.runnerKubeconfigProbeExitCode) || report.d601Observability.runnerKubeconfigProbeExitCode === null, `${label}.d601Observability.runnerKubeconfigProbeExitCode` ); assertString(report.d601Observability.runnerKubeconfigProbeStderr, `${label}.d601Observability.runnerKubeconfigProbeStderr`); assertBoolean(report.d601Observability.d601PublicEndpointsReachable, `${label}.d601Observability.d601PublicEndpointsReachable`); assertBoolean(report.d601Observability.d601K3sUnavailable, `${label}.d601Observability.d601K3sUnavailable`); if ( report.d601Observability.runnerKubeconfigReadable === false && report.d601Observability.d601PublicEndpointsReachable === true ) { assert.equal( report.d601Observability.d601K3sUnavailable, false, `${label}.d601Observability runner gap must not imply D601 k3s unavailable` ); } assertString(report.d601Observability.classification, `${label}.d601Observability.classification`); assertString(report.d601Observability.sourceIssue, `${label}.d601Observability.sourceIssue`); assertString(report.d601Observability.inferenceRule, `${label}.d601Observability.inferenceRule`); } if (Object.hasOwn(report, "readOnlySupplementalEvidence")) { assertArray(report.readOnlySupplementalEvidence, `${label}.readOnlySupplementalEvidence`); for (const [index, evidence] of report.readOnlySupplementalEvidence.entries()) { const evidenceLabel = `${label}.readOnlySupplementalEvidence[${index}]`; assertObject(evidence, evidenceLabel); for (const field of ["id", "status", "source", "summary", "evidence", "requiredFollowUp"]) { assert.ok(Object.hasOwn(evidence, field), `${evidenceLabel} missing ${field}`); } assertString(evidence.id, `${evidenceLabel}.id`); assert.ok( ["pass", "gap", "source-ready", "manifest-ready"].includes(evidence.status), `${evidenceLabel}.status must be pass, gap, source-ready, or manifest-ready` ); if (Array.isArray(evidence.source)) { assertStringArray(evidence.source, `${evidenceLabel}.source`, { minLength: 1 }); for (const [sourceIndex, sourcePath] of evidence.source.entries()) { assertRepoRelativePath(sourcePath, `${evidenceLabel}.source[${sourceIndex}]`); } } else { assertRepoRelativePath(evidence.source, `${evidenceLabel}.source`); } assertString(evidence.summary, `${evidenceLabel}.summary`); assertObject(evidence.evidence, `${evidenceLabel}.evidence`); assertString(evidence.requiredFollowUp, `${evidenceLabel}.requiredFollowUp`); if (Object.hasOwn(evidence, "validationCommand")) { assertString(evidence.validationCommand, `${evidenceLabel}.validationCommand`); } if (Object.hasOwn(evidence, "blockerClass") && evidence.blockerClass !== null) { assert.ok(blockerTypes.has(evidence.blockerClass), `${evidenceLabel}.blockerClass must be known`); } } } if (Object.hasOwn(report, "dryRunPlan")) { const plan = report.dryRunPlan; assertObject(plan, `${label}.dryRunPlan`); assert.equal(plan.mode, "plan-only", `${label}.dryRunPlan.mode`); assert.equal(plan.evidenceLevel, "DRY-RUN", `${label}.dryRunPlan.evidenceLevel`); assert.equal(plan.route, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1", `${label}.dryRunPlan.route`); assert.equal(plan.dryRunCallsLiveEndpoints, false, `${label}.dryRunPlan.dryRunCallsLiveEndpoints`); assert.equal(plan.liveWriteWillRun, false, `${label}.dryRunPlan.liveWriteWillRun`); if (Object.hasOwn(plan, "failureClassifications")) { assertStringArray(plan.failureClassifications, `${label}.dryRunPlan.failureClassifications`, { minLength: 5 }); for (const classification of [ "target_missing", "identity_not_distinct", "patch_panel_wiring_missing", "operation_failed", "evidence_missing" ]) { assert.ok( plan.failureClassifications.includes(classification), `${label}.dryRunPlan.failureClassifications missing ${classification}` ); } } assertStringArray(plan.liveWritePreconditions, `${label}.dryRunPlan.liveWritePreconditions`, { minLength: 4 }); assertStringArray(plan.evidenceFields, `${label}.dryRunPlan.evidenceFields`, { minLength: 4 }); assertArray(plan.endpointPlan, `${label}.dryRunPlan.endpointPlan`); assert.ok(plan.endpointPlan.length >= 8, `${label}.dryRunPlan.endpointPlan must enumerate precondition and live endpoints`); assert.ok( plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-patch-panel" && endpoint.path === "/signals/route" && endpoint.mutatesDevState === true), `${label}.dryRunPlan.endpointPlan must include the patch-panel route write endpoint` ); assert.ok( plan.endpointPlan.some((endpoint) => endpoint.serviceId === "hwlab-box-simu" && endpoint.path === "/ports/write" && endpoint.mutatesDevState === true), `${label}.dryRunPlan.endpointPlan must include the source box DO1 write endpoint` ); assert.ok( plan.endpointPlan.every((endpoint) => endpoint.calledInDryRun === false), `${label}.dryRunPlan.endpointPlan endpoints must not be called in dry-run` ); for (const requiredField of [ "operationId", "traceId", "auditId", "evidenceId", "routeResponse.propagatedBy=hwlab-patch-panel", "targetState.ports.DI1.propagatedBy=hwlab-patch-panel" ]) { assert.ok(plan.evidenceFields.includes(requiredField), `${label}.dryRunPlan.evidenceFields missing ${requiredField}`); } } 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 validatePreflightReport(relativePath, report) { const label = relativePath; assertObject(report, label); for (const field of [ "$schema", "$id", "reportVersion", "reportKind", "issue", "supports", "target", "generatedAt", "mode", "devOnly", "prodDisabled", "forbiddenActions", "validationCommands", "artifactIdentity", "registryCapabilities", "conclusion", "checks", "blockers" ]) { 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.reportKind, "dev-gate-preflight", `${label}.reportKind`); assert.equal(report.issue, requiredPreflightIssue, `${label}.issue`); assert.equal(report.mode, "read-only", `${label}.mode`); assert.equal(report.devOnly, true, `${label}.devOnly`); assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); assert.ok(preflightConclusions.has(report.conclusion), `${label}.conclusion must be ready or blocked`); assert.match(report.generatedAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, `${label}.generatedAt`); assertStringArray(report.supports, `${label}.supports`, { minLength: requiredPreflightSupports.length }); for (const supportedIssue of requiredPreflightSupports) { assert.ok(report.supports.includes(supportedIssue), `${label}.supports missing ${supportedIssue}`); } assertObject(report.target, `${label}.target`); assert.equal(report.target.ref, "origin/main", `${label}.target.ref`); assert.match(report.target.commitId, /^[a-f0-9]{40}$/, `${label}.target.commitId`); assert.equal(report.target.shortCommitId, report.target.commitId.slice(0, 7), `${label}.target.shortCommitId`); assertStringArray(report.forbiddenActions, `${label}.forbiddenActions`, { minLength: 1 }); for (const forbiddenAction of ["prod-deploy", "secret-material-read", "unidesk-runtime-substitute", "force-push"]) { assert.ok(report.forbiddenActions.includes(forbiddenAction), `${label}.forbiddenActions missing ${forbiddenAction}`); } assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: 2 }); for (const requiredCommand of requiredPreflightValidationCommands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } assertArtifactIdentity(report.artifactIdentity, `${label}.artifactIdentity`, report.target); assertRegistryCapabilities(report.registryCapabilities, `${label}.registryCapabilities`); assertArray(report.checks, `${label}.checks`); assert.ok(report.checks.length >= 1, `${label}.checks must not be empty`); assertUnique(report.checks.map((check) => check.id), `${label}.checks`); for (const [index, check] of report.checks.entries()) { const checkLabel = `${label}.checks[${index}]`; assertObject(check, checkLabel); for (const field of ["id", "category", "status", "summary", "evidence"]) { assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`); } assert.match(check.id, /^[a-z][a-z0-9-]*$/, `${checkLabel}.id`); assertString(check.category, `${checkLabel}.category`); assertStatus(check.status, `${checkLabel}.status`); assertString(check.summary, `${checkLabel}.summary`); assertArray(check.evidence, `${checkLabel}.evidence`); } assertArray(report.blockers, `${label}.blockers`); if (report.conclusion === "blocked") { assert.ok(report.blockers.length >= 1, `${label}.blockers required when conclusion is blocked`); } 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", "nextTask"]) { 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`); assertString(blocker.nextTask, `${blockerLabel}.nextTask`); } if (report.conclusion === "ready") { assert.equal(report.blockers.length, 0, `${label}.blockers must be empty when ready`); } if (Object.hasOwn(report, "notes")) { assertString(report.notes, `${label}.notes`); } } function assertEvidenceLevel(value, label) { assertString(value, label); assert.ok(evidenceLevels.has(value), `${label} must be one of ${Array.from(evidenceLevels).join(", ")}`); } function assertBoolean(value, label) { assert.equal(typeof value, "boolean", `${label} must be a boolean`); } async function readAggregatorSourceReports(report, label) { const sources = {}; for (const key of ["devEdgeHealth", "devM3Hardware", "devM4Agent", "devM5Gate"]) { const sourceReport = report.sourceReports[key]; const absolutePath = assertRepoRelativePath( sourceReport.path, `${label}.sourceReports.${key}.path` ); sources[key] = JSON.parse(await readFile(absolutePath, "utf8")); } return sources; } function observedLiveIdentifier(value) { return typeof value === "string" && value.trim().length > 0 && !["not_observed", "not_run", "unknown", "blocked", "missing"].includes(value); } function aggregatorLiveStateFromSources(sources) { const cloudApiDb = sources.devEdgeHealth.edgeHealth?.contracts?.deploy?.cloudApiDb ?? {}; const edgeDbLive = cloudApiDb.ready === true && (cloudApiDb.connected === true || cloudApiDb.liveConnected === true) && cloudApiDb.liveDbEvidence === true; const m4LiveDb = sources.devM4Agent.publicEntrypoints?.api?.live?.db ?? {}; const m4DbLive = m4LiveDb.ready === true && (m4LiveDb.connected === true || m4LiveDb.liveConnected === true) && m4LiveDb.liveDbEvidence === true; const dbLive = edgeDbLive || m4DbLive; const m3Operation = sources.devM3Hardware.liveOperation ?? {}; const m3Live = statusIsPass(m3Operation.status) && ["operationId", "traceId", "auditId", "evidenceId"].every((field) => observedLiveIdentifier(m3Operation[field]) ); const m4Live = statusIsPass(sources.devM4Agent.livePreflight?.status); const m5Live = dbLive && m3Live && m4Live && statusIsPass(sources.devM5Gate.devPreconditions?.status); return { dbLive, m3Live, m4Live, m5Live }; } function findById(items, id) { return items.find((item) => item.id === id); } function findByMilestone(items, milestone) { return items.find((item) => item.milestone === milestone || item.id === milestone); } function assertAggregatorBlockedDod(check, label, reason) { assertObject(check, label); assert.equal(check.status, "blocked", `${label}.status must stay blocked until ${reason}`); assert.equal(check.evidenceLevel, "BLOCKED", `${label}.evidenceLevel must stay BLOCKED until ${reason}`); } function assertAggregatorBlockedLayer(layer, label, reason) { assertObject(layer, label); assert.equal(layer.status, "blocked", `${label}.status must stay blocked until ${reason}`); assert.equal(layer.evidenceLevel, "BLOCKED", `${label}.evidenceLevel must stay BLOCKED until ${reason}`); } function assertAggregatorBlockedMilestone(milestone, label, reason) { assertObject(milestone, label); assert.equal(milestone.status, "blocked", `${label}.status must stay blocked until ${reason}`); assert.equal(milestone.liveEvidence, "missing_or_blocked", `${label}.liveEvidence must stay missing_or_blocked until ${reason}`); assert.notEqual(milestone.highestVisibleLevel, "DEV-LIVE", `${label}.highestVisibleLevel must not promote non-live evidence`); } function assertAggregatorBlockedClassification(classification, label, reason) { assertObject(classification, label); assert.equal(classification.status, "blocked", `${label}.status must stay blocked until ${reason}`); assert.equal(classification.currentLevel, "BLOCKED", `${label}.currentLevel must stay BLOCKED until ${reason}`); } function assertProtectedAggregatorDevLiveEvidence(item, label, liveState, assumedLevel = item.level) { if (!protectedAggregatorMilestones.has(item.milestone) || assumedLevel !== "DEV-LIVE") { return; } const requiredCategory = protectedAggregatorLiveCategories[item.milestone]; assert.equal( item.category, requiredCategory, `${label} ${item.milestone} DEV-LIVE evidence must come from ${requiredCategory}, not frontend, route, source, dry-run, or fixture evidence` ); const sourceLive = { M3: liveState.m3Live, M4: liveState.m4Live, M5: liveState.m5Live }[item.milestone]; assert.equal( sourceLive, true, `${label} ${item.milestone} DEV-LIVE evidence must be backed by its live source report` ); } function assertNoStaleDbRuntimeBlocker(report, label, liveState) { if (!liveState.dbLive) { return; } const staleDbScopes = new Set([ "cloud-api-db", "cloud-api-db-health-gate", "cloud-api-db-live", "db-live" ]); const staleBlocker = report.blockers?.find((blocker) => staleDbScopes.has(blocker.scope)); assert.equal( staleBlocker, undefined, `${label}.blockers must not keep stale DB live/runtime-env blocker ${staleBlocker?.scope ?? ""} when DB live evidence is pass` ); const dbCheck = findById(report.dod.checks, "cloud-api-db-ready"); assertObject(dbCheck, `${label}.dod.checks.cloud-api-db-ready`); assert.equal(dbCheck.status, "pass", `${label}.dod.checks.cloud-api-db-ready.status must be pass when DB live evidence is proven`); assert.equal(dbCheck.evidenceLevel, "DEV-LIVE", `${label}.dod.checks.cloud-api-db-ready.evidenceLevel must be DEV-LIVE when DB live evidence is proven`); } async function assertAggregatorNonPromotionBoundaries(report, label) { const sources = await readAggregatorSourceReports(report, label); const liveState = aggregatorLiveStateFromSources(sources); const anyProtectedLiveBlocked = !liveState.dbLive || !liveState.m3Live || !liveState.m4Live || !liveState.m5Live; assertNoStaleDbRuntimeBlocker(report, label, liveState); assertObject(report.latestFrontendDevFact, `${label}.latestFrontendDevFact`); assert.equal( report.latestFrontendDevFact.promotesM3M4M5, false, `${label}.latestFrontendDevFact.promotesM3M4M5 must be false` ); if (anyProtectedLiveBlocked) { assert.equal(report.overall.status, "blocked", `${label}.overall.status must remain blocked while DB/M3/M4/M5 live evidence is incomplete`); assert.equal(report.overall.green, false, `${label}.overall.green must remain false while DB/M3/M4/M5 live evidence is incomplete`); } if (!liveState.dbLive) { assertAggregatorBlockedDod( findById(report.dod.checks, "cloud-api-db-ready"), `${label}.dod.checks.cloud-api-db-ready`, "DB DEV-LIVE acceptance source reports prove ready=true, connected=true, and liveDbEvidence=true" ); if (report.currentDevLayering?.dbLive) { assertAggregatorBlockedLayer( report.currentDevLayering.dbLive, `${label}.currentDevLayering.dbLive`, "DB DEV-LIVE acceptance source reports prove ready=true, connected=true, and liveDbEvidence=true" ); } } if (!liveState.m3Live) { assertAggregatorBlockedDod( findById(report.dod.checks, "m3-hardware-trusted-loop"), `${label}.dod.checks.m3-hardware-trusted-loop`, "M3 liveOperation proves operationId, traceId, auditId, and evidenceId" ); assertAggregatorBlockedMilestone( findByMilestone(report.milestones, "M3"), `${label}.milestones.M3`, "M3 liveOperation proves operationId, traceId, auditId, and evidenceId" ); if (report.currentDevLayering?.m3HardwareTrustedLoop) { assertAggregatorBlockedLayer( report.currentDevLayering.m3HardwareTrustedLoop, `${label}.currentDevLayering.m3HardwareTrustedLoop`, "M3 liveOperation proves operationId, traceId, auditId, and evidenceId" ); } if (Array.isArray(report.milestoneLevelClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneLevelClassification, "M3"), `${label}.milestoneLevelClassification.M3`, "M3 liveOperation proves operationId, traceId, auditId, and evidenceId" ); } if (Array.isArray(report.milestoneBlockerClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneBlockerClassification, "M3"), `${label}.milestoneBlockerClassification.M3`, "M3 liveOperation proves operationId, traceId, auditId, and evidenceId" ); } } if (!liveState.m4Live) { assertAggregatorBlockedDod( findById(report.dod.checks, "m4-agent-loop-live"), `${label}.dod.checks.m4-agent-loop-live`, "M4 livePreflight source report passes" ); assertAggregatorBlockedMilestone( findByMilestone(report.milestones, "M4"), `${label}.milestones.M4`, "M4 livePreflight source report passes" ); if (report.currentDevLayering?.m4AgentLoop) { assertAggregatorBlockedLayer( report.currentDevLayering.m4AgentLoop, `${label}.currentDevLayering.m4AgentLoop`, "M4 livePreflight source report passes" ); } if (Array.isArray(report.milestoneLevelClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneLevelClassification, "M4"), `${label}.milestoneLevelClassification.M4`, "M4 livePreflight source report passes" ); } if (Array.isArray(report.milestoneBlockerClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneBlockerClassification, "M4"), `${label}.milestoneBlockerClassification.M4`, "M4 livePreflight source report passes" ); } } if (!liveState.m5Live) { assertAggregatorBlockedDod( findById(report.dod.checks, "m5-mvp-dev-live"), `${label}.dod.checks.m5-mvp-dev-live`, "M5 source report passes with DB, M3, and M4 live evidence" ); assertAggregatorBlockedMilestone( findByMilestone(report.milestones, "M5"), `${label}.milestones.M5`, "M5 source report passes with DB, M3, and M4 live evidence" ); if (Array.isArray(report.milestoneLevelClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneLevelClassification, "M5"), `${label}.milestoneLevelClassification.M5`, "M5 source report passes with DB, M3, and M4 live evidence" ); } if (Array.isArray(report.milestoneBlockerClassification)) { assertAggregatorBlockedClassification( findByMilestone(report.milestoneBlockerClassification, "M5"), `${label}.milestoneBlockerClassification.M5`, "M5 source report passes with DB, M3, and M4 live evidence" ); } } for (const [index, evidence] of report.evidence.entries()) { assertProtectedAggregatorDevLiveEvidence(evidence, `${label}.evidence[${index}]`, liveState); } for (const [index, evidence] of (report.levels["DEV-LIVE"] ?? []).entries()) { assertProtectedAggregatorDevLiveEvidence(evidence, `${label}.levels.DEV-LIVE[${index}]`, liveState, "DEV-LIVE"); } } async function validateAggregatorV2Report(relativePath, report) { const label = relativePath; assertObject(report, label); for (const field of [ "$schema", "$id", "reportVersion", "reportKind", "issue", "supports", "generatedAt", "generatedFromCommit", "environment", "endpoint", "devOnly", "prodDisabled", "safety", "sourceReports", "overall", "dod", "milestones", "evidence", "levels", "blockers", "nextSteps", "validationCommands" ]) { assert.ok(Object.hasOwn(report, field), `${label} missing ${field}`); } assertString(report.$schema, `${label}.$schema`); assertString(report.$id, `${label}.$id`); assert.equal(report.reportVersion, "v2", `${label}.reportVersion`); assert.equal(report.reportKind, "dev-m5-gate-aggregator", `${label}.reportKind`); assert.equal(report.issue, issueFamily.DEV_M5_GATE_AGGREGATOR_V2, `${label}.issue`); assertTimestamp(report.generatedAt, `${label}.generatedAt`); assert.match(report.generatedFromCommit, /^([a-f0-9]{7,40}|unknown)$/, `${label}.generatedFromCommit`); assert.equal(report.environment, "dev", `${label}.environment`); assert.equal(report.endpoint, "http://74.48.78.17:16667", `${label}.endpoint`); assert.equal(report.devOnly, true, `${label}.devOnly`); assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); assertStringArray(report.supports, `${label}.supports`, { minLength: 10 }); for (const requiredIssue of [ "pikasTech/HWLAB#7", "pikasTech/HWLAB#9", "pikasTech/HWLAB#33", "pikasTech/HWLAB#34", "pikasTech/HWLAB#35", "pikasTech/HWLAB#36", "pikasTech/HWLAB#37", "pikasTech/HWLAB#38", "pikasTech/HWLAB#39", "pikasTech/HWLAB#46" ]) { assert.ok(report.supports.includes(requiredIssue), `${label}.supports missing ${requiredIssue}`); } assertObject(report.safety, `${label}.safety`); for (const field of [ "reportOnly", "noDeploy", "noProd", "noSecretRead", "noRuntimeRestart", "noLiveProbe", "noHeavyE2E", "noUniDeskRuntimeSubstitute" ]) { assert.equal(report.safety[field], true, `${label}.safety.${field}`); } assertObject(report.sourceReports, `${label}.sourceReports`); for (const requiredReport of [ "devPreflight", "devDeploy", "devArtifacts", "devEdgeHealth", "devM3Hardware", "devM4Agent", "devM5Gate", "d601Observability" ]) { assert.ok(Object.hasOwn(report.sourceReports, requiredReport), `${label}.sourceReports missing ${requiredReport}`); const sourceReport = report.sourceReports[requiredReport]; assertObject(sourceReport, `${label}.sourceReports.${requiredReport}`); for (const field of ["path", "issue", "taskId", "status", "commitId"]) { assert.ok(Object.hasOwn(sourceReport, field), `${label}.sourceReports.${requiredReport} missing ${field}`); assertString(sourceReport[field], `${label}.sourceReports.${requiredReport}.${field}`); } assertRepoRelativePath(sourceReport.path, `${label}.sourceReports.${requiredReport}.path`); } assertObject(report.overall, `${label}.overall`); assert.ok(["green", "blocked"].includes(report.overall.status), `${label}.overall.status`); assertBoolean(report.overall.green, `${label}.overall.green`); assertString(report.overall.reason, `${label}.overall.reason`); assert.equal(report.overall.green, report.overall.status === "green", `${label}.overall.green must match status`); assertObject(report.dod, `${label}.dod`); assert.ok(["green", "blocked"].includes(report.dod.status), `${label}.dod.status`); assertBoolean(report.dod.green, `${label}.dod.green`); assertArray(report.dod.checks, `${label}.dod.checks`); assert.ok(report.dod.checks.length >= 7, `${label}.dod.checks must cover #9 DoD gates`); assertUnique(report.dod.checks.map((check) => check.id), `${label}.dod.checks`); for (const [index, check] of report.dod.checks.entries()) { const checkLabel = `${label}.dod.checks[${index}]`; assertObject(check, checkLabel); for (const field of ["id", "status", "evidenceLevel", "summary"]) { assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`); } assert.match(check.id, /^[a-z][a-z0-9-]*$/, `${checkLabel}.id`); assert.ok(["pass", "blocked"].includes(check.status), `${checkLabel}.status`); assertEvidenceLevel(check.evidenceLevel, `${checkLabel}.evidenceLevel`); assertString(check.summary, `${checkLabel}.summary`); } assertArray(report.milestones, `${label}.milestones`); assert.deepEqual( report.milestones.map((milestone) => milestone.id), requiredMilestoneIds, `${label}.milestones must cover M0-M5 in order` ); for (const [index, milestone] of report.milestones.entries()) { const milestoneLabel = `${label}.milestones[${index}]`; assertObject(milestone, milestoneLabel); for (const field of ["id", "status", "highestVisibleLevel", "liveEvidence", "evidenceCount", "blockerCount", "summary"]) { assert.ok(Object.hasOwn(milestone, field), `${milestoneLabel} missing ${field}`); } assertStatus(milestone.status, `${milestoneLabel}.status`); assertEvidenceLevel(milestone.highestVisibleLevel, `${milestoneLabel}.highestVisibleLevel`); assert.ok(["pass", "missing_or_blocked"].includes(milestone.liveEvidence), `${milestoneLabel}.liveEvidence`); assert.ok(Number.isInteger(milestone.evidenceCount), `${milestoneLabel}.evidenceCount`); assert.ok(Number.isInteger(milestone.blockerCount), `${milestoneLabel}.blockerCount`); assertString(milestone.summary, `${milestoneLabel}.summary`); } assertArray(report.evidence, `${label}.evidence`); assert.ok(report.evidence.length >= 10, `${label}.evidence must include M0-M5 entries`); for (const [index, evidence] of report.evidence.entries()) { const evidenceLabel = `${label}.evidence[${index}]`; assertObject(evidence, evidenceLabel); for (const field of ["milestone", "issue", "level", "status", "category", "summary"]) { assert.ok(Object.hasOwn(evidence, field), `${evidenceLabel} missing ${field}`); } assert.match(evidence.milestone, /^M[0-5]$/, `${evidenceLabel}.milestone`); assertString(evidence.issue, `${evidenceLabel}.issue`); assertEvidenceLevel(evidence.level, `${evidenceLabel}.level`); assertString(evidence.status, `${evidenceLabel}.status`); assertString(evidence.category, `${evidenceLabel}.category`); assertString(evidence.summary, `${evidenceLabel}.summary`); if (Object.hasOwn(evidence, "reportPath")) { assertRepoRelativePath(evidence.reportPath, `${evidenceLabel}.reportPath`); } if (Object.hasOwn(evidence, "commands")) { assertStringArray(evidence.commands, `${evidenceLabel}.commands`); } if (Object.hasOwn(evidence, "evidence")) { assertStringArray(evidence.evidence, `${evidenceLabel}.evidence`); } if (Object.hasOwn(evidence, "sources")) { assertStringArray(evidence.sources, `${evidenceLabel}.sources`); } } assertObject(report.levels, `${label}.levels`); assert.deepEqual(Object.keys(report.levels), Array.from(evidenceLevels), `${label}.levels keys`); for (const level of evidenceLevels) { assertArray(report.levels[level], `${label}.levels.${level}`); } assertArray(report.blockers, `${label}.blockers`); assert.ok(report.blockers.length >= 1, `${label}.blockers must not be empty while blocked`); for (const [index, blocker] of report.blockers.entries()) { const blockerLabel = `${label}.blockers[${index}]`; assertObject(blocker, blockerLabel); for (const field of ["id", "priority", "type", "scope", "status", "source", "sourceIssue", "summary", "unblockOrder", "unblocks", "rationale"]) { assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`); } assertString(blocker.id, `${blockerLabel}.id`); assert.match(blocker.priority, /^P[0-3]$/, `${blockerLabel}.priority`); assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type`); assertString(blocker.scope, `${blockerLabel}.scope`); assert.equal(blocker.status, "open", `${blockerLabel}.status`); assertRepoRelativePath(blocker.source, `${blockerLabel}.source`); assertString(blocker.sourceIssue, `${blockerLabel}.sourceIssue`); assertString(blocker.summary, `${blockerLabel}.summary`); assert.ok(Number.isInteger(blocker.unblockOrder), `${blockerLabel}.unblockOrder`); assertStringArray(blocker.unblocks, `${blockerLabel}.unblocks`, { minLength: 1 }); assertString(blocker.rationale, `${blockerLabel}.rationale`); if (Object.hasOwn(blocker, "nextTask")) { assertString(blocker.nextTask, `${blockerLabel}.nextTask`); } } assertArray(report.nextSteps, `${label}.nextSteps`); assert.ok(report.nextSteps.length >= 1, `${label}.nextSteps must contain blocker order`); assert.deepEqual( report.nextSteps.map((step) => step.blockerOrder).sort((a, b) => a - b), [...new Set(report.blockers.map((blocker) => blocker.unblockOrder))].sort((a, b) => a - b), `${label}.nextSteps must cover active blocker orders` ); assert.deepEqual( report.nextSteps.map((step) => step.order), [...report.nextSteps].map((step) => step.order).sort((a, b) => a - b), `${label}.nextSteps must be sorted by order` ); for (const [index, step] of report.nextSteps.entries()) { const stepLabel = `${label}.nextSteps[${index}]`; assertObject(step, stepLabel); for (const field of ["order", "priority", "scopes", "sourceIssues", "rationale", "action", "evidenceRequired"]) { assert.ok(Object.hasOwn(step, field), `${stepLabel} missing ${field}`); } assert.ok(Number.isInteger(step.order), `${stepLabel}.order`); if (Object.hasOwn(step, "blockerOrder")) { assert.ok(Number.isInteger(step.blockerOrder), `${stepLabel}.blockerOrder`); } assert.match(step.priority, /^P[0-3]$/, `${stepLabel}.priority`); assertStringArray(step.scopes, `${stepLabel}.scopes`, { minLength: 1 }); assertStringArray(step.sourceIssues, `${stepLabel}.sourceIssues`, { minLength: 1 }); assertString(step.rationale, `${stepLabel}.rationale`); assertString(step.action, `${stepLabel}.action`); assertString(step.evidenceRequired, `${stepLabel}.evidenceRequired`); } assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: requiredAggregatorV2Commands.length }); for (const requiredCommand of requiredAggregatorV2Commands) { assert.ok( report.validationCommands.includes(requiredCommand), `${label}.validationCommands missing ${requiredCommand}` ); } await assertAggregatorNonPromotionBoundaries(report, label); } async function main() { const reportFiles = await collectReportFiles(); assert.ok(reportFiles.length >= 1, "expected at least one dev-gate report JSON file"); for (const reportFile of reportFiles) { await validateReport(reportFile); } console.log(`validated ${reportFiles.length} dev-gate report JSON file(s)`); } await main();