Merge pull request #408 from pikasTech/fix/live-build-metadata-freshness-359
fix: keep live build metadata fresh
This commit is contained in:
+213
-39
@@ -423,7 +423,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: {
|
||||
@@ -447,7 +447,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, {
|
||||
@@ -455,22 +456,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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -517,11 +525,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
|
||||
@@ -552,9 +563,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,
|
||||
@@ -562,6 +588,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,
|
||||
@@ -573,8 +626,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,
|
||||
@@ -589,19 +643,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,
|
||||
@@ -614,7 +673,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,
|
||||
@@ -629,43 +689,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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -679,9 +853,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";
|
||||
}
|
||||
|
||||
|
||||
+162
-19
@@ -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;
|
||||
|
||||
@@ -924,9 +924,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"\)/);
|
||||
|
||||
Reference in New Issue
Block a user