438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildCommanderReadinessSummary,
|
|
commitsMatch,
|
|
evaluateArtifactRuntimeReadiness,
|
|
normalizeCommit
|
|
} from "./src/artifact-runtime-readiness-guard.mjs";
|
|
|
|
const targetSha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
const staleSha = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
const digest = "sha256:1111111111111111111111111111111111111111111111111111111111111111";
|
|
|
|
function artifactReport(sourceCommitId = targetSha, overrides = {}) {
|
|
return {
|
|
commitId: sourceCommitId.slice(0, 7),
|
|
artifactPublish: {
|
|
status: "published",
|
|
sourceCommitId,
|
|
serviceCount: 2,
|
|
requiredServiceCount: 2,
|
|
publishedCount: 2,
|
|
registryPrefix: "127.0.0.1:5000/hwlab",
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
artifactRequired: true,
|
|
status: "published",
|
|
imageTag: sourceCommitId.slice(0, 7),
|
|
digest
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
artifactRequired: true,
|
|
status: "published",
|
|
imageTag: sourceCommitId.slice(0, 7),
|
|
digest,
|
|
runtimeKind: "cloud-web",
|
|
distFreshness: { status: "pass" }
|
|
}
|
|
],
|
|
...overrides
|
|
}
|
|
};
|
|
}
|
|
|
|
function artifactCatalog(commitId = targetSha, {
|
|
ciPublished = true,
|
|
registryVerified = true,
|
|
serviceDigest = digest,
|
|
artifactState = "published"
|
|
} = {}) {
|
|
return {
|
|
artifactState,
|
|
commitId,
|
|
publish: {
|
|
ciPublished,
|
|
registryVerified
|
|
},
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
commitId,
|
|
artifactRequired: true,
|
|
publishState: serviceDigest === "not_published" ? "skeleton-only" : "published",
|
|
imageTag: commitId.slice(0, 7),
|
|
digest: serviceDigest
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
commitId,
|
|
artifactRequired: true,
|
|
publishState: serviceDigest === "not_published" ? "skeleton-only" : "published",
|
|
imageTag: commitId.slice(0, 7),
|
|
digest: serviceDigest
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function desiredStatePlan(commitId = targetSha, state = "already_promoted", status = "pass") {
|
|
return {
|
|
status,
|
|
summary: {
|
|
desiredCommitId: commitId,
|
|
desiredImageTag: commitId.slice(0, 7),
|
|
diagnostics: status === "blocked" ? 1 : 0,
|
|
blockers: status === "blocked" ? 1 : 0
|
|
},
|
|
target: {
|
|
convergence: {
|
|
state,
|
|
comparableFields: 8,
|
|
matchingTargetFields: state === "already_promoted" ? 8 : 0,
|
|
pendingTargetFields: state === "already_promoted" ? 0 : 8
|
|
}
|
|
},
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
catalog: { commitId }
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function liveRuntime(commitId = targetSha, { cloudWebCommitId = commitId, apiCommitId = commitId } = {}) {
|
|
return {
|
|
mode: "live-read-only-http",
|
|
api: {
|
|
status: "observed",
|
|
source: "health-live",
|
|
evidenceSource: "live-http",
|
|
endpoint: "http://74.48.78.17:16667/health/live",
|
|
serviceId: "hwlab-cloud-api",
|
|
environment: "dev",
|
|
healthStatus: "ok",
|
|
commitId: apiCommitId,
|
|
shortCommitId: apiCommitId.slice(0, 7),
|
|
commitSource: "runtime-env",
|
|
imageTag: apiCommitId.slice(0, 7),
|
|
observedAt: "2026-05-22T00:00:00.000Z"
|
|
},
|
|
cloudWeb: {
|
|
status: "observed",
|
|
source: "health-live",
|
|
evidenceSource: "live-http",
|
|
endpoint: "http://74.48.78.17:16666/health/live",
|
|
serviceId: "hwlab-cloud-web",
|
|
environment: "dev",
|
|
healthStatus: "ok",
|
|
artifactKind: "cloud-web",
|
|
commitId: cloudWebCommitId,
|
|
shortCommitId: cloudWebCommitId.slice(0, 7),
|
|
commitSource: "runtime-env",
|
|
imageTag: cloudWebCommitId.slice(0, 7),
|
|
observedAt: "2026-05-22T00:00:00.000Z"
|
|
}
|
|
};
|
|
}
|
|
|
|
function readiness(overrides = {}) {
|
|
return evaluateArtifactRuntimeReadiness({
|
|
artifactReport: artifactReport(overrides.artifactSource ?? targetSha, overrides.artifactReportOverrides),
|
|
artifactCatalog: artifactCatalog(overrides.catalogCommit ?? targetSha, overrides.catalogOptions ?? {}),
|
|
desiredStatePlan: overrides.desiredPlan ?? desiredStatePlan(overrides.desiredCommit ?? targetSha, overrides.desiredState ?? "already_promoted", overrides.desiredStatus ?? "pass"),
|
|
runtimeReport: null,
|
|
liveRuntime: overrides.live ?? liveRuntime(targetSha),
|
|
targetRef: "origin/main",
|
|
targetCommitId: overrides.targetCommit ?? targetSha
|
|
});
|
|
}
|
|
|
|
test("commit comparison accepts full and short lowercase SHA forms only", () => {
|
|
assert.equal(normalizeCommit("ABCDEF1"), "abcdef1");
|
|
assert.equal(normalizeCommit("not-a-sha"), "unknown");
|
|
assert.equal(commitsMatch(targetSha, targetSha.slice(0, 7)), true);
|
|
assert.equal(commitsMatch(targetSha, "unknown"), false);
|
|
});
|
|
|
|
test("readiness passes only when artifact, desired-state, API runtime, and Cloud Web runtime match target", () => {
|
|
const result = readiness();
|
|
assert.equal(result.ready, true);
|
|
assert.equal(result.canInferFromSourceLocalStatic, false);
|
|
assert.deepEqual(result.blockers, []);
|
|
});
|
|
|
|
test("stale local artifact report cannot imply latest-main readiness", () => {
|
|
const result = readiness({ targetCommit: staleSha });
|
|
assert.equal(result.ready, false);
|
|
assert.equal(result.canInferFromSourceLocalStatic, false);
|
|
assert.deepEqual(
|
|
result.blockers.map((item) => item.id),
|
|
[
|
|
"artifact-report-target-match",
|
|
"artifact-catalog-target-match",
|
|
"desired-state-target-match",
|
|
"api-runtime-target-match",
|
|
"cloud-web-runtime-target-match"
|
|
]
|
|
);
|
|
});
|
|
|
|
test("desired-state promotion pending blocks before apply", () => {
|
|
const result = readiness({
|
|
desiredCommit: staleSha,
|
|
desiredState: "promotion_pending",
|
|
desiredStatus: "planned"
|
|
});
|
|
assert.equal(result.ready, false);
|
|
assert.equal(result.desiredState.targetConvergence.state, "promotion_pending");
|
|
assert.ok(result.blockers.some((item) => item.id === "desired-state-target-match"));
|
|
});
|
|
|
|
test("unpublished catalog digests keep readiness blocked", () => {
|
|
const result = readiness({
|
|
catalogOptions: {
|
|
artifactState: "contract-skeleton",
|
|
ciPublished: false,
|
|
registryVerified: false,
|
|
serviceDigest: "not_published"
|
|
}
|
|
});
|
|
assert.equal(result.ready, false);
|
|
assert.equal(result.catalog.ciPublished, false);
|
|
assert.equal(result.catalog.registryVerified, false);
|
|
assert.deepEqual(
|
|
result.blockers.map((item) => item.id),
|
|
["artifact-catalog-published"]
|
|
);
|
|
});
|
|
|
|
test("Cloud Web served runtime identity must match artifact and target", () => {
|
|
const result = readiness({
|
|
live: liveRuntime(targetSha, { cloudWebCommitId: staleSha })
|
|
});
|
|
assert.equal(result.ready, false);
|
|
assert.ok(result.blockers.some((item) => item.id === "cloud-web-runtime-target-match"));
|
|
assert.ok(result.blockers.some((item) => item.id === "cloud-web-runtime-artifact-match"));
|
|
assert.equal(result.runtime.cloudWeb.effective.shortCommitId, staleSha.slice(0, 7));
|
|
});
|
|
|
|
test("Cloud Web publish report must retain build freshness evidence", () => {
|
|
const result = readiness({
|
|
artifactReportOverrides: {
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
artifactRequired: true,
|
|
status: "published",
|
|
imageTag: targetSha.slice(0, 7),
|
|
digest
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
artifactRequired: true,
|
|
status: "published",
|
|
imageTag: targetSha.slice(0, 7),
|
|
digest,
|
|
runtimeKind: "cloud-web"
|
|
}
|
|
]
|
|
}
|
|
});
|
|
assert.equal(result.ready, false);
|
|
assert.equal(result.artifact.cloudWeb.distFreshnessStatus, "missing");
|
|
assert.ok(result.blockers.some((item) => item.id === "cloud-web-published-build-freshness"));
|
|
});
|
|
|
|
test("compact commander summary exposes canPublish/canApply and dry-run devLiveClaim false", () => {
|
|
const result = readiness();
|
|
const summary = buildCommanderReadinessSummary({
|
|
readiness: result,
|
|
targetRef: "origin/main",
|
|
generatedFromCommit: targetSha,
|
|
explicitLive: false,
|
|
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
|
codeAgentTimeoutReadiness: {
|
|
status: "pass",
|
|
frontendTimeoutMs: 1800000,
|
|
backendTimeoutMs: 1200000,
|
|
requiredFrontendTimeoutMs: 35000,
|
|
requiredBackendTimeoutMs: 1200000,
|
|
transportWaitsForBackend: true,
|
|
summary: "ready"
|
|
},
|
|
runtimeReport: {
|
|
runtimeIdentity: {
|
|
ready: true,
|
|
runtime: { durable: true, ready: true },
|
|
readiness: { ready: true, durability: { ready: true } }
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(summary.kind, "hwlab-dev-rollout-readiness");
|
|
assert.equal(summary.canPublish, true);
|
|
assert.equal(summary.canApply, true);
|
|
assert.equal(summary.devLiveClaim, false);
|
|
assert.deepEqual(summary.blockedReasons, []);
|
|
assert.equal(summary.currentSourceCommit.shortCommitId, targetSha.slice(0, 7));
|
|
});
|
|
|
|
test("compact summary blocks publish on stale Cloud Web source dist", () => {
|
|
const result = readiness();
|
|
const summary = buildCommanderReadinessSummary({
|
|
readiness: result,
|
|
targetRef: "origin/main",
|
|
generatedFromCommit: targetSha,
|
|
explicitLive: false,
|
|
cloudWebDistFreshness: { status: "blocked", mismatches: ["index.html"] },
|
|
codeAgentTimeoutReadiness: {
|
|
status: "pass",
|
|
frontendTimeoutMs: 1800000,
|
|
backendTimeoutMs: 1200000,
|
|
requiredFrontendTimeoutMs: 35000,
|
|
requiredBackendTimeoutMs: 1200000,
|
|
transportWaitsForBackend: true,
|
|
summary: "ready"
|
|
},
|
|
runtimeReport: {
|
|
runtimeIdentity: {
|
|
ready: true,
|
|
runtime: { durable: true, ready: true },
|
|
readiness: { ready: true, durability: { ready: true } }
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(summary.canPublish, false);
|
|
assert.equal(summary.canApply, false);
|
|
assert.equal(summary.cloudWebBuildFreshness.status, "blocked");
|
|
assert.ok(summary.blockedReasons.some((item) => item.id === "cloud-web-build-freshness"));
|
|
assert.ok(summary.nextCommands.includes("bun run --cwd web/hwlab-cloud-web build"));
|
|
});
|
|
|
|
test("compact summary separates Code Agent timeout and durable runtime blockers from apply readiness", () => {
|
|
const result = readiness();
|
|
const summary = buildCommanderReadinessSummary({
|
|
readiness: result,
|
|
targetRef: "origin/main",
|
|
generatedFromCommit: targetSha,
|
|
explicitLive: false,
|
|
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
|
codeAgentTimeoutReadiness: {
|
|
status: "blocked",
|
|
frontendTimeoutMs: 4500,
|
|
backendTimeoutMs: 1200000,
|
|
requiredFrontendTimeoutMs: 35000,
|
|
requiredBackendTimeoutMs: 1200000,
|
|
transportWaitsForBackend: false,
|
|
summary: "Code Agent browser timeout is too short."
|
|
},
|
|
runtimeReport: {
|
|
runtimeIdentity: {
|
|
ready: false,
|
|
runtime: {
|
|
durable: false,
|
|
ready: false,
|
|
blocker: "runtime_durable_adapter_query_blocked",
|
|
connection: { queryResult: "query_blocked" }
|
|
},
|
|
readiness: {
|
|
ready: false,
|
|
durability: {
|
|
ready: false,
|
|
blocker: "runtime_durable_adapter_query_blocked",
|
|
blockedLayer: "durability_query",
|
|
queryResult: "query_blocked"
|
|
}
|
|
},
|
|
blockerCodes: ["runtime_durable_adapter_query_blocked"]
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(summary.canPublish, false);
|
|
assert.equal(summary.canApply, false);
|
|
assert.equal(summary.codeAgentTimeoutReadiness.status, "blocked");
|
|
assert.equal(summary.runtimeDurableBlocker.classification, "runtime_durable_adapter_query_blocked");
|
|
assert.ok(summary.blockedReasons.some((item) => item.id === "code-agent-timeout-readiness"));
|
|
assert.ok(summary.blockedReasons.some((item) => item.id === "runtime-durable:runtime_durable_adapter_query_blocked"));
|
|
});
|
|
|
|
test("compact summary blocks publish when Code Agent transport timeout does not exceed backend timeout", () => {
|
|
const result = readiness();
|
|
const summary = buildCommanderReadinessSummary({
|
|
readiness: result,
|
|
targetRef: "origin/main",
|
|
generatedFromCommit: targetSha,
|
|
explicitLive: false,
|
|
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
|
codeAgentTimeoutReadiness: {
|
|
status: "blocked",
|
|
frontendTimeoutMs: 1200000,
|
|
backendTimeoutMs: 1200000,
|
|
requiredFrontendTimeoutMs: 35000,
|
|
requiredBackendTimeoutMs: 1200000,
|
|
transportWaitsForBackend: false,
|
|
summary: "Code Agent timeout layering is unsafe."
|
|
},
|
|
runtimeReport: {
|
|
runtimeIdentity: {
|
|
ready: true,
|
|
runtime: { durable: true, ready: true },
|
|
readiness: { ready: true, durability: { ready: true } }
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(summary.canPublish, false);
|
|
assert.equal(summary.canApply, false);
|
|
assert.equal(summary.codeAgentTimeoutReadiness.transportWaitsForBackend, false);
|
|
assert.ok(summary.blockedReasons.some((item) => item.id === "code-agent-timeout-readiness"));
|
|
});
|
|
|
|
test("compact summary keeps provider-secret blockers out of publish but blocks apply", () => {
|
|
const result = readiness();
|
|
const summary = buildCommanderReadinessSummary({
|
|
readiness: result,
|
|
targetRef: "origin/main",
|
|
generatedFromCommit: targetSha,
|
|
explicitLive: false,
|
|
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
|
codeAgentTimeoutReadiness: {
|
|
status: "pass",
|
|
frontendTimeoutMs: 1800000,
|
|
backendTimeoutMs: 1200000,
|
|
requiredFrontendTimeoutMs: 35000,
|
|
requiredBackendTimeoutMs: 1200000,
|
|
transportWaitsForBackend: true,
|
|
summary: "ready"
|
|
},
|
|
edgeReport: {
|
|
blockers: [
|
|
{
|
|
scope: "code-agent-provider-secret",
|
|
classification: "provider_secret_missing",
|
|
summary: "Code Agent provider Secret is missing."
|
|
}
|
|
]
|
|
},
|
|
runtimeReport: {
|
|
runtimeIdentity: {
|
|
ready: true,
|
|
runtime: { durable: true, ready: true },
|
|
readiness: { ready: true, durability: { ready: true } }
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(summary.canPublish, true);
|
|
assert.equal(summary.canApply, false);
|
|
assert.equal(summary.codeAgentProviderReadiness.status, "blocked");
|
|
assert.ok(summary.blockedReasons.some((item) => item.id === "code-agent-provider:code-agent-provider-secret"));
|
|
});
|