From 0c481c6dc508644d975b8934a0cb4a0fe7c02d8a Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sun, 24 May 2026 00:26:58 +0000 Subject: [PATCH] fix: keep live build metadata fresh --- internal/cloud/server.mjs | 252 ++++++++++++++++++++++---- internal/cloud/server.test.mjs | 181 ++++++++++++++++-- web/hwlab-cloud-web/scripts/check.mjs | 6 + 3 files changed, 381 insertions(+), 58 deletions(-) diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index 7f375c1b..45288033 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -421,7 +421,7 @@ async function buildLiveBuildsPayload(options = {}) { desiredStateSource: metadata.deployManifest.status === "ok" ? LIVE_BUILD_METADATA_PATHS.deployManifest : "unavailable", artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactCatalog : "unavailable", artifactReportSource: metadata.artifactReport.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactReport : "unavailable", - note: "Each row prefers current live service health build metadata and falls back only to repo-owned deploy/artifact metadata; observedAt is not used as build time." + note: "Each row prefers current live service health build metadata and falls back to repo-owned deploy/artifact metadata only when the live tag/revision matches; observedAt is not used as build time." }, latest, counts: { @@ -445,7 +445,8 @@ async function observeLiveBuildService(service, options) { }); } - const metadataRecord = liveBuildRecordFromMetadata(service); + const metadataRecords = liveBuildRecordsFromMetadata(service); + const metadataRecord = metadataRecords[0] ?? null; if (service.serviceId === CLOUD_API_SERVICE_ID) { const health = await buildHealthPayload(options); return liveBuildRecordFromHealth(service, { @@ -453,22 +454,29 @@ async function observeLiveBuildService(service, options) { status: 200, url: "/health/live", payload: health - }, metadataRecord); + }, metadataRecords); } const baseUrl = String(options.env?.[service.urlEnv] || service.defaultUrl || "").replace(/\/+$/u, ""); if (!baseUrl) { - return metadataRecord ?? unavailableLiveBuildRecord(service, { - reasonCode: "service_url_unavailable", - reason: `${service.serviceId} 没有配置 live health URL` - }); + return metadataRecord + ? metadataRecordWithHealthGap(metadataRecord, { + service, + healthUrl: null, + httpStatus: null, + reason: `${service.serviceId} 没有配置 live health URL` + }) + : unavailableLiveBuildRecord(service, { + reasonCode: "service_url_unavailable", + reason: `${service.serviceId} 没有配置 live health URL` + }); } const healthPath = service.healthPath ?? "/health/live"; return liveBuildRecordFromHealth( service, await fetchServiceHealth(`${baseUrl}${healthPath}`, options.fetchImpl ?? fetch), - metadataRecord + metadataRecords ); } @@ -515,11 +523,14 @@ async function fetchServiceHealth(url, fetchImpl) { } } -function liveBuildRecordFromHealth(service, result, metadataRecord = null) { +function liveBuildRecordFromHealth(service, result, metadataRecords = []) { + const candidates = Array.isArray(metadataRecords) ? metadataRecords : [metadataRecords].filter(Boolean); + const metadataRecord = candidates[0] ?? null; if (!result.ok) { const reason = healthUnavailableReason(service, result.reason ?? `${service.serviceId} health unavailable`); return metadataRecord ? metadataRecordWithHealthGap(metadataRecord, { + service, healthUrl: result.url, httpStatus: result.status, reason @@ -550,9 +561,24 @@ function liveBuildRecordFromHealth(service, result, metadataRecord = null) { const imageReference = payload.image?.reference ?? payload.image ?? "unknown"; const commitId = payload.commit?.id ?? payload.revision ?? "unknown"; + const liveIdentity = liveBuildIdentityFromHealth({ payload, imageReference, commitId }); const createdAt = normalizeHealthBuildTime(payload); - if (!createdAt && metadataRecord?.build?.createdAt) { - return mergeLiveHealthOntoMetadataRecord(metadataRecord, { + if (createdAt) { + return liveBuildHealthPayloadRecord(service, result, { + payload, + imageReference, + commitId, + createdAt, + metadataSource: payload.build?.metadataSource ?? "health:build.createdAt", + unavailableReason: null + }); + } + + const matched = selectMatchingMetadataRecord(liveIdentity, candidates); + if (matched.record?.build?.createdAt) { + return mergeLiveHealthOntoMetadataRecord(matched.record, { + match: matched.match, + liveIdentity, service, result, payload, @@ -560,6 +586,33 @@ function liveBuildRecordFromHealth(service, result, metadataRecord = null) { commitId }); } + + const mismatch = matched.match ?? liveBuildMetadataMatch(liveIdentity, metadataRecord); + const unavailableReason = matched.record + ? `构建时间不可用:当前 live tag/revision 已匹配 repo-owned metadata,但该 metadata 没有构建时间(live tag ${liveIdentity.imageTag},live revision ${liveIdentity.revision},metadata tag ${matched.record.image.tag},metadata revision ${matched.record.revision},来源 ${matched.record.build.metadataSource})` + : metadataRecord + ? `构建时间不可用:当前 live tag/revision 与 repo-owned artifact metadata 不匹配,未使用旧构建时间(live tag ${liveIdentity.imageTag},live revision ${liveIdentity.revision},metadata tag ${metadataRecord.image.tag},metadata revision ${metadataRecord.revision})` + : "构建时间不可用:health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,且 repo-owned artifact/catalog/deployment env 未提供可匹配的构建时间"; + return liveBuildHealthPayloadRecord(service, result, { + payload, + imageReference, + commitId, + createdAt: null, + metadataSource: matched.record ? "live-health:matched-metadata-without-build-time" : metadataRecord ? "live-health:metadata-mismatch" : "unavailable", + unavailableReason, + metadataMismatch: mismatch + }); +} + +function liveBuildHealthPayloadRecord(service, result, { + payload, + imageReference, + commitId, + createdAt, + metadataSource, + unavailableReason, + metadataMismatch = null +}) { return { serviceId: service.serviceId, name: service.serviceName, @@ -571,8 +624,9 @@ function liveBuildRecordFromHealth(service, result, metadataRecord = null) { build: { createdAt, source: payload.build?.source ?? payload.commit?.source ?? "health-payload", - metadataSource: payload.build?.metadataSource ?? (createdAt ? "health:build.createdAt" : "unavailable"), - unavailableReason: createdAt ? null : "构建时间不可用:health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt" + metadataSource, + unavailableReason, + metadataMismatch }, image: { reference: imageReference, @@ -587,19 +641,24 @@ function liveBuildRecordFromHealth(service, result, metadataRecord = null) { }; } -function metadataRecordWithHealthGap(metadataRecord, { healthUrl, httpStatus, reason }) { +function metadataRecordWithHealthGap(metadataRecord, { service, healthUrl, httpStatus, reason }) { + const metadataTag = metadataRecord.image?.tag ?? "unknown"; + const metadataRevision = metadataRecord.revision ?? metadataRecord.commit?.id ?? "unknown"; return { ...metadataRecord, healthUrl, httpStatus, + status: "build_time_unavailable", build: { ...metadataRecord.build, - liveHealthMissingReason: `live health 不可用,已使用 repo-owned artifact metadata;${reason}` + createdAt: null, + metadataSource: "live-health-unavailable", + unavailableReason: `构建时间不可用:当前 live health 不可用,无法确认 ${service.serviceId} live tag/revision 是否匹配 repo-owned artifact metadata,未使用可能过期的构建时间(metadata tag ${metadataTag},metadata revision ${metadataRevision});${reason}` } }; } -function mergeLiveHealthOntoMetadataRecord(metadataRecord, { service, result, payload, imageReference, commitId }) { +function mergeLiveHealthOntoMetadataRecord(metadataRecord, { match, liveIdentity, service, result, payload, imageReference, commitId }) { return { ...metadataRecord, serviceId: service.serviceId, @@ -612,7 +671,8 @@ function mergeLiveHealthOntoMetadataRecord(metadataRecord, { service, result, pa ...metadataRecord.build, metadataSource: metadataRecord.build.metadataSource, unavailableReason: null, - liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用 repo-owned artifact metadata" + liveMetadataMatch: match, + liveHealthMissingReason: `health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 repo-owned artifact metadata(live tag ${liveIdentity.imageTag})` }, image: { reference: imageReference !== "unknown" ? imageReference : metadataRecord.image.reference, @@ -627,43 +687,157 @@ function mergeLiveHealthOntoMetadataRecord(metadataRecord, { service, result, pa }; } -function liveBuildRecordFromMetadata(service) { - const createdAt = normalizeIsoTimestamp( - service.artifact?.buildCreatedAt ?? - service.report?.buildCreatedAt ?? - service.deploy?.env?.HWLAB_BUILD_CREATED_AT ?? - null - ); - if (!createdAt) return null; +function liveBuildIdentityFromHealth({ payload, imageReference, commitId }) { + return { + imageReference, + imageTag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown", + commitId, + revision: payload.revision ?? commitId, + digest: payload.image?.digest ?? "unknown" + }; +} - const imageReference = service.artifact?.image ?? service.deploy?.image ?? "unknown"; - const commitId = service.artifact?.commitId ?? service.report?.sourceCommitId ?? service.deploy?.env?.HWLAB_COMMIT_ID ?? imageTagFromReference(imageReference) ?? "unknown"; - const digest = service.artifact?.digest ?? service.report?.digest ?? service.deploy?.env?.HWLAB_IMAGE_DIGEST ?? "unknown"; - const source = service.artifact?.buildSource ?? service.report?.buildSource ?? service.deploy?.env?.HWLAB_BUILD_SOURCE ?? "repo-owned-artifact-metadata"; +function selectMatchingMetadataRecord(liveIdentity, metadataRecords) { + let firstMatched = null; + for (const record of metadataRecords) { + const match = liveBuildMetadataMatch(liveIdentity, record); + if (!match.matched) continue; + if (record.build?.createdAt) return { record, match }; + firstMatched ??= { record, match }; + } + if (firstMatched) return firstMatched; + return { + record: null, + match: metadataRecords.length ? liveBuildMetadataMatch(liveIdentity, metadataRecords[0]) : null + }; +} + +function liveBuildMetadataMatch(liveIdentity, metadataRecord) { + if (!metadataRecord) { + return { + matched: false, + matchedValues: [], + live: liveBuildIdentitySummary(liveIdentity), + metadata: null + }; + } + const liveValues = uniqueStrings([ + liveIdentity.imageTag, + imageTagFromReference(liveIdentity.imageReference), + liveIdentity.revision, + liveIdentity.commitId, + liveIdentity.digest + ]).filter((value) => value !== "unknown"); + const metadataValues = uniqueStrings([ + metadataRecord.image?.tag, + imageTagFromReference(metadataRecord.image?.reference), + metadataRecord.revision, + metadataRecord.commit?.id, + metadataRecord.image?.digest + ]).filter((value) => value !== "unknown" && value !== "not_published"); + const matchedValues = liveValues.filter((liveValue) => + metadataValues.some((metadataValue) => liveBuildIdentityEquivalent(liveValue, metadataValue)) + ); + return { + matched: matchedValues.length > 0, + matchedValues, + live: liveBuildIdentitySummary(liveIdentity), + metadata: { + imageTag: metadataRecord.image?.tag ?? "unknown", + revision: metadataRecord.revision ?? "unknown", + commitId: metadataRecord.commit?.id ?? "unknown", + digest: metadataRecord.image?.digest ?? "unknown" + } + }; +} + +function liveBuildIdentitySummary(identity) { + return { + imageTag: identity?.imageTag ?? "unknown", + revision: identity?.revision ?? "unknown", + commitId: identity?.commitId ?? "unknown", + digest: identity?.digest ?? "unknown" + }; +} + +function liveBuildIdentityEquivalent(left, right) { + const a = String(left ?? "").trim(); + const b = String(right ?? "").trim(); + if (!a || !b || a === "unknown" || b === "unknown") return false; + if (a === b) return true; + if (a.startsWith("sha256:") || b.startsWith("sha256:")) return false; + return a.length >= 7 && b.length >= 7 && (a.startsWith(b) || b.startsWith(a)); +} + +function liveBuildRecordsFromMetadata(service) { + return [ + liveBuildRecordFromMetadataSource(service, "artifact"), + liveBuildRecordFromMetadataSource(service, "report"), + liveBuildRecordFromMetadataSource(service, "deploy") + ].filter(Boolean); +} + +function liveBuildRecordFromMetadata(service) { + return liveBuildRecordsFromMetadata(service)[0] ?? null; +} + +function liveBuildRecordFromMetadataSource(service, sourceKind) { + const source = sourceKind === "artifact" + ? service.artifact + : sourceKind === "report" + ? service.report + : service.deploy; + if (!source) return null; + const createdAt = normalizeIsoTimestamp( + sourceKind === "deploy" ? source.env?.HWLAB_BUILD_CREATED_AT : source.buildCreatedAt + ); + + const imageReference = sourceKind === "deploy" + ? source.env?.HWLAB_IMAGE ?? source.image ?? "unknown" + : source.image ?? service.deploy?.image ?? "unknown"; + const commitId = sourceKind === "artifact" + ? source.commitId ?? imageTagFromReference(imageReference) ?? "unknown" + : sourceKind === "report" + ? source.sourceCommitId ?? source.commitId ?? imageTagFromReference(imageReference) ?? "unknown" + : source.env?.HWLAB_COMMIT_ID ?? imageTagFromReference(imageReference) ?? "unknown"; + const digest = sourceKind === "deploy" + ? source.env?.HWLAB_IMAGE_DIGEST ?? "unknown" + : source.digest ?? "unknown"; + const buildSource = sourceKind === "deploy" + ? source.env?.HWLAB_BUILD_SOURCE ?? "repo-owned-deployment-env" + : source.buildSource ?? "repo-owned-artifact-metadata"; + const imageTag = sourceKind === "deploy" + ? source.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown" + : source.imageTag ?? imageTagFromReference(imageReference) ?? "unknown"; + const revision = sourceKind === "deploy" + ? source.env?.HWLAB_REVISION ?? commitId + : commitId; return { serviceId: service.serviceId, name: service.serviceName, kind: "hwlab", - status: "ok", + status: createdAt ? "ok" : "build_time_unavailable", healthUrl: null, httpStatus: null, desiredState: desiredStateSummary(service), build: { createdAt, - source, - metadataSource: artifactMetadataSource(service), - unavailableReason: null + source: buildSource, + metadataSource: artifactMetadataSource(sourceKind), + unavailableReason: createdAt + ? null + : `构建时间不可用:repo-owned ${artifactMetadataSource(sourceKind)} 缺少构建时间` }, image: { reference: imageReference, - tag: service.artifact?.imageTag ?? service.deploy?.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown", + tag: imageTag, digest }, commit: { id: commitId, - source: service.artifact ? "artifact-catalog" : service.report ? "artifact-report" : "deploy-desired-state" + source: sourceKind === "artifact" ? "artifact-catalog" : sourceKind === "report" ? "artifact-report" : "deploy-desired-state" }, - revision: service.deploy?.env?.HWLAB_REVISION ?? commitId + revision }; } @@ -677,9 +851,9 @@ function desiredStateSummary(service) { }; } -function artifactMetadataSource(service) { - if (service.artifact?.buildCreatedAt) return "artifact-catalog:buildCreatedAt"; - if (service.report?.buildCreatedAt) return "artifact-report:buildCreatedAt"; +function artifactMetadataSource(sourceKind) { + if (sourceKind === "artifact") return "artifact-catalog:buildCreatedAt"; + if (sourceKind === "report") return "artifact-report:buildCreatedAt"; return "deploy-env:HWLAB_BUILD_CREATED_AT"; } diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index 0d0fef4d..b18c3ff9 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -196,14 +196,25 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art ["/hwlab-agent-mgr/health/live", { serviceId: "hwlab-agent-mgr", status: "ok", - revision: "mgrabcdef123456", - commit: { id: "mgrabcdef123456", source: "test" }, + revision: "48dfbf9", + commit: { id: "48dfbf9", source: "test" }, image: { - reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:mgrabcd", - tag: "mgrabcd", + reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + tag: "48dfbf9", digest: "sha256:" + "2".repeat(64) } }], + ["/hwlab-agent-skills/health/live", { + serviceId: "hwlab-agent-skills", + status: "ok", + revision: "skillsabcdef123456", + commit: { id: "skillsabcdef123456", source: "test" }, + image: { + reference: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd", + tag: "skillsabcd", + digest: "sha256:" + "5".repeat(64) + } + }], ["/external/health", { serviceId: "postgres", status: "ok", @@ -226,12 +237,27 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art }, { serviceId: "hwlab-agent-mgr", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:mgrcat", + image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe", namespace: "hwlab-dev", profile: "dev", healthPath: "/health/live", replicas: 1 }, + { + serviceId: "hwlab-agent-skills", + image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd", + namespace: "hwlab-dev", + profile: "dev", + healthPath: "/health/live", + replicas: 1, + env: { + HWLAB_COMMIT_ID: "skillsabcdef123456", + HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd", + HWLAB_IMAGE_TAG: "skillsabcd", + HWLAB_BUILD_CREATED_AT: "2026-05-23T05:30:00.000Z", + HWLAB_BUILD_SOURCE: "deploy-env-test" + } + }, { serviceId: "hwlab-agent-worker", image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:workcat", @@ -262,9 +288,9 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art services: [ { serviceId: "hwlab-agent-mgr", - commitId: "mgrcatabcdef123456", - image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:mgrcat", - imageTag: "mgrcat", + commitId: "3df89fe", + image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe", + imageTag: "3df89fe", digest: "sha256:" + "3".repeat(64), namespace: "hwlab-dev", profile: "dev", @@ -272,6 +298,18 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art buildCreatedAt: "2026-05-23T04:20:00.000Z", buildSource: "artifact-catalog-test" }, + { + serviceId: "hwlab-agent-skills", + commitId: "skillsabcdef123456", + image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd", + imageTag: "skillsabcd", + digest: "sha256:" + "5".repeat(64), + namespace: "hwlab-dev", + profile: "dev", + healthPath: "/health/live", + buildCreatedAt: null, + buildSource: null + }, { serviceId: "hwlab-extra-lab", commitId: "extracatabcdef123456", @@ -311,7 +349,7 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art HWLAB_TUNNEL_CLIENT_URL: "http://live.test/missing-tunnel-client", HWLAB_EDGE_PROXY_URL: "http://live.test/external", HWLAB_CLI_URL: "http://live.test/missing-cli", - HWLAB_AGENT_SKILLS_URL: "http://live.test/missing-agent-skills", + HWLAB_AGENT_SKILLS_URL: "http://live.test/hwlab-agent-skills", HWLAB_EXTRA_LAB_URL: "http://live.test/missing-extra-lab" }, liveBuildMetadata, @@ -344,19 +382,31 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art assert.equal(response.status, 200); const payload = await response.json(); assert.equal(payload.contractVersion, "live-builds-v1"); - assert.equal(payload.latest.serviceId, "hwlab-agent-mgr"); - assert.equal(payload.latest.build.createdAt, "2026-05-23T04:20:00.000Z"); - assert.equal(payload.latest.image.tag, "mgrabcd"); - assert.equal(payload.latest.commit.id, "mgrabcdef123456"); - assert.equal(payload.latest.build.metadataSource, "artifact-catalog:buildCreatedAt"); - assert.match(payload.latest.build.liveHealthMissingReason, /health payload 缺少 build\.createdAt/u); + assert.equal(payload.latest.serviceId, "hwlab-agent-skills"); + assert.equal(payload.latest.build.createdAt, "2026-05-23T05:30:00.000Z"); + assert.equal(payload.latest.image.tag, "skillsabcd"); + assert.equal(payload.latest.commit.id, "skillsabcdef123456"); + assert.equal(payload.latest.build.metadataSource, "deploy-env:HWLAB_BUILD_CREATED_AT"); + assert.equal(payload.latest.build.liveMetadataMatch.metadata.imageTag, "skillsabcd"); + assert.match(payload.latest.build.liveHealthMissingReason, /匹配的 repo-owned artifact metadata/u); assert.equal(payload.counts.total, 15); assert.equal(payload.counts.external, 2); - assert.equal(payload.counts.withBuildTime, 4); + assert.equal(payload.counts.withBuildTime, 3); + const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr"); + assert.equal(manager.build.createdAt, null); + assert.equal(manager.image.tag, "48dfbf9"); + assert.equal(manager.revision, "48dfbf9"); + assert.equal(manager.build.metadataSource, "live-health:metadata-mismatch"); + assert.match(manager.build.unavailableReason, /构建时间不可用:当前 live tag\/revision 与 repo-owned artifact metadata 不匹配/u); + assert.match(manager.build.unavailableReason, /live tag 48dfbf9/u); + assert.match(manager.build.unavailableReason, /metadata tag 3df89fe/u); const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab"); - assert.equal(extra.build.createdAt, "2026-05-23T03:00:00.000Z"); - assert.match(extra.build.liveHealthMissingReason, /live health 不可用/u); - assert.ok(payload.services.some((service) => service.serviceId === "hwlab-agent-worker" && /构建时间不可用:hwlab-agent-worker 当前 hwlab-dev desired replicas=0/u.test(service.build.unavailableReason))); + assert.equal(extra.build.createdAt, null); + assert.match(extra.build.unavailableReason, /当前 live health 不可用/u); + const worker = payload.services.find((service) => service.serviceId === "hwlab-agent-worker"); + assert.equal(worker.build.createdAt, null); + assert.match(worker.build.unavailableReason, /构建时间不可用:当前 live health 不可用/u); + assert.match(worker.build.unavailableReason, /hwlab-agent-worker 当前 hwlab-dev desired replicas=0/u); assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-edge-proxy" && service.healthUrl.endsWith("/health"))); assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-frpc" && service.image.tag === "v0.68.1")); } finally { @@ -366,6 +416,99 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art } }); +test("cloud api uses matching report metadata when catalog has the live tag but no build time", async () => { + const liveBuildMetadata = { + deployManifest: { + services: [ + { + serviceId: "hwlab-agent-mgr", + image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + namespace: "hwlab-dev", + profile: "dev", + healthPath: "/health/live", + replicas: 1 + } + ] + }, + artifactCatalog: { + services: [ + { + serviceId: "hwlab-agent-mgr", + commitId: "48dfbf9", + image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + imageTag: "48dfbf9", + digest: "not_published", + namespace: "hwlab-dev", + profile: "dev", + healthPath: "/health/live", + buildCreatedAt: null, + buildSource: null + } + ] + }, + artifactReport: { + artifactPublish: { + services: [ + { + serviceId: "hwlab-agent-mgr", + sourceCommitId: "48dfbf9abcdef", + image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + imageTag: "48dfbf9", + digest: "sha256:" + "6".repeat(64), + namespace: "hwlab-dev", + profile: "dev", + healthPath: "/health/live", + buildCreatedAt: "2026-05-24T06:00:00.000Z", + buildSource: "artifact-report-test" + } + ] + } + } + }; + + const server = createCloudApiServer({ + env: { + PATH: "", + HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr" + }, + liveBuildMetadata, + fetchImpl: async () => ({ + ok: true, + status: 200, + async text() { + return JSON.stringify({ + serviceId: "hwlab-agent-mgr", + status: "ok", + revision: "48dfbf9abcdef", + commit: { id: "48dfbf9abcdef", source: "test" }, + image: { + reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9", + tag: "48dfbf9", + digest: "sha256:" + "6".repeat(64) + } + }); + } + }) + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const response = await fetch(`http://127.0.0.1:${port}/v1/live-builds`); + assert.equal(response.status, 200); + const payload = await response.json(); + const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr"); + assert.equal(manager.build.createdAt, "2026-05-24T06:00:00.000Z"); + assert.equal(manager.build.metadataSource, "artifact-report:buildCreatedAt"); + assert.equal(manager.build.liveMetadataMatch.metadata.imageTag, "48dfbf9"); + assert.equal(payload.latest.serviceId, "hwlab-agent-mgr"); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("cloud api health reports DB env presence and live connection classification without leaking values", async () => { const originalUrl = process.env.HWLAB_CLOUD_DB_URL; const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE; diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index de50ed84..1c8f6d38 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -915,9 +915,15 @@ assert.match(app, /formatBeijingTime/); assert.match(app, /Asia\/Shanghai/); assert.match(app, /最新镜像构建时间/); assert.match(app, /构建时间不可用/); +assert.match(app, /revision \$\{shortToken\(latest\.revision\)\}/); +assert.match(app, /revision \$\{shortToken\(service\.revision\)\}/); assert.match(app, /liveBuildSourceLabel/); assert.match(app, /liveBuildDesiredStateText/); assert.match(app, /外部镜像或非 HWLAB 构建产物/); +assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow-wrap:\s*anywhere;/s); +assert.match(styles, /\.live-build-list\s*{[^}]*max-height:\s*min\(280px,\s*42dvh\);[^}]*min-width:\s*0;[^}]*overflow:\s*auto;/s); +assert.match(styles, /\.live-build-row-head\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*0\.8fr\)\s*minmax\(0,\s*1\.2fr\);/s); +assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*{[\s\S]*?grid-template-columns:\s*1fr;/); assert.match(styles, /\.live-build-meta span\s*{[^}]*overflow-wrap:\s*anywhere;/s); assert.match(app, /fetchJson\("\/health\/live"\)/); assert.match(app, /callRpc\("system\.health"\)/);