Merge pull request #422 from pikasTech/fix/live-builds-no-reports
fix: 收束 live 构建时间 metadata 来源
This commit is contained in:
+36
-40
@@ -429,14 +429,13 @@ async function buildLiveBuildsPayload(options = {}) {
|
||||
contractVersion: "live-builds-v1",
|
||||
observedAt: new Date().toISOString(),
|
||||
source: {
|
||||
kind: "live-health+artifact-catalog",
|
||||
kind: "live-health+deploy-artifact-catalog",
|
||||
route: "/v1/live-builds",
|
||||
healthPath: "/health/live",
|
||||
metadataPaths: LIVE_BUILD_METADATA_PATHS,
|
||||
desiredStateSource: metadata.deployManifest.status === "ok" ? LIVE_BUILD_METADATA_PATHS.deployManifest : "unavailable",
|
||||
artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactCatalog : "unavailable",
|
||||
artifactReportSource: "disabled:repository-report-files-removed",
|
||||
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."
|
||||
note: "Each row prefers current live service health build metadata and falls back only to deploy/artifact catalog metadata when the live tag/revision matches; observedAt is not used as build time."
|
||||
},
|
||||
latest,
|
||||
counts: {
|
||||
@@ -604,18 +603,23 @@ function liveBuildRecordFromHealth(service, result, metadataRecords = []) {
|
||||
|
||||
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})`
|
||||
? "构建时间不可用:当前 live 镜像未提供 buildCreatedAt"
|
||||
: 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 未提供可匹配的构建时间";
|
||||
? `构建时间不可用:live tag 与 ${controlledMetadataLabel(metadataRecord)} 不匹配`
|
||||
: "构建时间不可用:当前 live 镜像未提供 buildCreatedAt";
|
||||
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",
|
||||
metadataSource: matched.record ? "live-health:matched-controlled-metadata-without-build-time" : metadataRecord ? "live-health:controlled-metadata-mismatch" : "live-health:build-created-at-missing",
|
||||
unavailableReason,
|
||||
metadataMismatch: mismatch
|
||||
metadataMismatch: mismatch,
|
||||
unavailableDetail: matched.record
|
||||
? `live tag ${liveIdentity.imageTag} / revision ${liveIdentity.revision} 已匹配 ${controlledMetadataLabel(matched.record)},但该 metadata 没有 buildCreatedAt(metadata tag ${matched.record.image.tag},metadata revision ${matched.record.revision},来源 ${matched.record.build.metadataSource})`
|
||||
: metadataRecord
|
||||
? `未使用可能过期的构建时间:live tag ${liveIdentity.imageTag},live revision ${liveIdentity.revision},metadata tag ${metadataRecord.image.tag},metadata revision ${metadataRecord.revision},来源 ${metadataRecord.build.metadataSource}`
|
||||
: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,deploy/artifact catalog 也没有可匹配的 buildCreatedAt"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -626,7 +630,8 @@ function liveBuildHealthPayloadRecord(service, result, {
|
||||
createdAt,
|
||||
metadataSource,
|
||||
unavailableReason,
|
||||
metadataMismatch = null
|
||||
metadataMismatch = null,
|
||||
unavailableDetail = null
|
||||
}) {
|
||||
return {
|
||||
serviceId: service.serviceId,
|
||||
@@ -641,6 +646,7 @@ function liveBuildHealthPayloadRecord(service, result, {
|
||||
source: payload.build?.source ?? payload.commit?.source ?? "health-payload",
|
||||
metadataSource,
|
||||
unavailableReason,
|
||||
unavailableDetail,
|
||||
metadataMismatch
|
||||
},
|
||||
image: {
|
||||
@@ -668,7 +674,7 @@ function metadataRecordWithHealthGap(metadataRecord, { service, healthUrl, httpS
|
||||
...metadataRecord.build,
|
||||
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}`
|
||||
unavailableReason: `构建时间不可用:当前 live health 不可用,无法确认 ${service.serviceId} live tag/revision 是否匹配 deploy/artifact catalog metadata,未使用可能过期的构建时间(metadata tag ${metadataTag},metadata revision ${metadataRevision});${reason}`
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -687,7 +693,7 @@ function mergeLiveHealthOntoMetadataRecord(metadataRecord, { match, liveIdentity
|
||||
metadataSource: metadataRecord.build.metadataSource,
|
||||
unavailableReason: null,
|
||||
liveMetadataMatch: match,
|
||||
liveHealthMissingReason: `health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 repo-owned artifact metadata(live tag ${liveIdentity.imageTag})`
|
||||
liveHealthMissingReason: `health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 ${controlledMetadataLabel(metadataRecord)}(live tag ${liveIdentity.imageTag})`
|
||||
},
|
||||
image: {
|
||||
reference: imageReference !== "unknown" ? imageReference : metadataRecord.image.reference,
|
||||
@@ -787,21 +793,14 @@ function liveBuildIdentityEquivalent(left, right) {
|
||||
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;
|
||||
: service.deploy;
|
||||
if (!source) return null;
|
||||
const createdAt = normalizeIsoTimestamp(
|
||||
sourceKind === "deploy" ? source.env?.HWLAB_BUILD_CREATED_AT : source.buildCreatedAt
|
||||
@@ -812,15 +811,13 @@ function liveBuildRecordFromMetadataSource(service, sourceKind) {
|
||||
: 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";
|
||||
: 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";
|
||||
? source.env?.HWLAB_BUILD_SOURCE ?? "deploy-desired-state-env"
|
||||
: source.buildSource ?? "artifact-catalog-metadata";
|
||||
const imageTag = sourceKind === "deploy"
|
||||
? source.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown"
|
||||
: source.imageTag ?? imageTagFromReference(imageReference) ?? "unknown";
|
||||
@@ -841,7 +838,7 @@ function liveBuildRecordFromMetadataSource(service, sourceKind) {
|
||||
metadataSource: artifactMetadataSource(sourceKind),
|
||||
unavailableReason: createdAt
|
||||
? null
|
||||
: `构建时间不可用:repo-owned ${artifactMetadataSource(sourceKind)} 缺少构建时间`
|
||||
: `构建时间不可用:${controlledMetadataLabel({ build: { metadataSource: artifactMetadataSource(sourceKind) } })} 缺少 buildCreatedAt`
|
||||
},
|
||||
image: {
|
||||
reference: imageReference,
|
||||
@@ -850,7 +847,7 @@ function liveBuildRecordFromMetadataSource(service, sourceKind) {
|
||||
},
|
||||
commit: {
|
||||
id: commitId,
|
||||
source: sourceKind === "artifact" ? "artifact-catalog" : sourceKind === "report" ? "artifact-report" : "deploy-desired-state"
|
||||
source: sourceKind === "artifact" ? "artifact-catalog" : "deploy-desired-state"
|
||||
},
|
||||
revision
|
||||
};
|
||||
@@ -868,10 +865,16 @@ function desiredStateSummary(service) {
|
||||
|
||||
function artifactMetadataSource(sourceKind) {
|
||||
if (sourceKind === "artifact") return "artifact-catalog:buildCreatedAt";
|
||||
if (sourceKind === "report") return "artifact-report:buildCreatedAt";
|
||||
return "deploy-env:HWLAB_BUILD_CREATED_AT";
|
||||
}
|
||||
|
||||
function controlledMetadataLabel(record) {
|
||||
const source = String(record?.build?.metadataSource ?? "");
|
||||
if (source.includes("artifact-catalog")) return "catalog metadata";
|
||||
if (source.includes("deploy-env")) return "deploy metadata";
|
||||
return "controlled metadata";
|
||||
}
|
||||
|
||||
function unavailableLiveBuildRecord(service, { healthUrl = null, httpStatus = null, reasonCode, reason }) {
|
||||
return {
|
||||
serviceId: service.serviceId,
|
||||
@@ -962,19 +965,17 @@ function normalizeHealthBuildTime(payload) {
|
||||
async function loadLiveBuildMetadata(options = {}) {
|
||||
if (options.liveBuildMetadata) return normalizeLiveBuildMetadata(options.liveBuildMetadata);
|
||||
const root = options.repoRoot ?? process.env.HWLAB_REPO_ROOT ?? repoRoot;
|
||||
const [deployManifest, artifactCatalog, artifactReport] = await Promise.all([
|
||||
const [deployManifest, artifactCatalog] = await Promise.all([
|
||||
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.deployManifest),
|
||||
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactCatalog),
|
||||
Promise.resolve({ status: "disabled", payload: null, error: "repository report files removed" })
|
||||
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactCatalog)
|
||||
]);
|
||||
return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog, artifactReport });
|
||||
return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog });
|
||||
}
|
||||
|
||||
function normalizeLiveBuildMetadata(metadata) {
|
||||
return {
|
||||
deployManifest: normalizeMetadataFile(metadata.deployManifest),
|
||||
artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog),
|
||||
artifactReport: normalizeMetadataFile(metadata.artifactReport)
|
||||
artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -993,7 +994,7 @@ async function readJsonMetadata(root, relativePath) {
|
||||
}
|
||||
|
||||
function normalizeMetadataFile(file) {
|
||||
if (file?.status === "ok" || file?.status === "unavailable" || file?.status === "disabled") {
|
||||
if (file?.status === "ok" || file?.status === "unavailable") {
|
||||
return {
|
||||
status: file.status,
|
||||
path: file.path ?? null,
|
||||
@@ -1018,10 +1019,6 @@ function liveBuildServiceInventory(metadata) {
|
||||
(Array.isArray(metadata.artifactCatalog.payload?.services) ? metadata.artifactCatalog.payload.services : [])
|
||||
.map((service) => [service.serviceId, service])
|
||||
);
|
||||
const reportByServiceId = new Map(
|
||||
(Array.isArray(metadata.artifactReport.payload?.artifactPublish?.services) ? metadata.artifactReport.payload.artifactPublish.services : [])
|
||||
.map((service) => [service.serviceId, service])
|
||||
);
|
||||
const serviceIds = uniqueStrings([
|
||||
...SERVICE_IDS,
|
||||
...deployServices.map((service) => service.serviceId),
|
||||
@@ -1041,8 +1038,7 @@ function liveBuildServiceInventory(metadata) {
|
||||
healthPath,
|
||||
activation: deploy?.replicas === 0 ? "zero-replica" : defaults.activation,
|
||||
deploy,
|
||||
artifact: artifactByServiceId.get(serviceId) ?? null,
|
||||
report: reportByServiceId.get(serviceId) ?? null
|
||||
artifact: artifactByServiceId.get(serviceId) ?? null
|
||||
});
|
||||
}),
|
||||
...LIVE_BUILD_EXTERNAL_COMPONENTS
|
||||
|
||||
+121
-87
@@ -177,7 +177,7 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api aggregates live HWLAB build times from health and repo-owned artifact metadata", async () => {
|
||||
test("cloud api aggregates live HWLAB build times from health and controlled deploy/catalog metadata", async () => {
|
||||
const healthByPath = new Map([
|
||||
["/hwlab-cloud-web/health/live", {
|
||||
serviceId: "hwlab-cloud-web",
|
||||
@@ -324,11 +324,6 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art
|
||||
buildSource: "artifact-catalog-test"
|
||||
}
|
||||
]
|
||||
},
|
||||
artifactReport: {
|
||||
artifactPublish: {
|
||||
services: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -389,7 +384,7 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art
|
||||
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.match(payload.latest.build.liveHealthMissingReason, /匹配的 deploy metadata/u);
|
||||
assert.equal(payload.counts.total, 15);
|
||||
assert.equal(payload.counts.external, 2);
|
||||
assert.equal(payload.counts.withBuildTime, 3);
|
||||
@@ -397,10 +392,10 @@ test("cloud api aggregates live HWLAB build times from health and repo-owned art
|
||||
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);
|
||||
assert.equal(manager.build.metadataSource, "live-health:controlled-metadata-mismatch");
|
||||
assert.equal(manager.build.unavailableReason, "构建时间不可用:live tag 与 catalog metadata 不匹配");
|
||||
assert.match(manager.build.unavailableDetail, /live tag 48dfbf9/u);
|
||||
assert.match(manager.build.unavailableDetail, /metadata tag 3df89fe/u);
|
||||
const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab");
|
||||
assert.equal(extra.build.createdAt, null);
|
||||
assert.match(extra.build.unavailableReason, /当前 live health 不可用/u);
|
||||
@@ -417,96 +412,135 @@ 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: {
|
||||
test("cloud api ignores old repository reports and keeps missing buildCreatedAt unavailable", async () => {
|
||||
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "hwlab-live-builds-"));
|
||||
const deployDir = path.join(tempRoot, "deploy");
|
||||
const oldReportDir = path.join(tempRoot, "reports", "dev-gate");
|
||||
await mkdir(deployDir, { recursive: true });
|
||||
await mkdir(oldReportDir, { recursive: true });
|
||||
await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({
|
||||
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
|
||||
}
|
||||
]
|
||||
}, null, 2)}\n`);
|
||||
await writeFile(path.join(deployDir, "artifact-catalog.dev.json"), `${JSON.stringify({
|
||||
services: [
|
||||
{
|
||||
serviceId: "hwlab-agent-mgr",
|
||||
commitId: "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: null,
|
||||
buildSource: null
|
||||
}
|
||||
]
|
||||
}, null, 2)}\n`);
|
||||
await writeFile(path.join(oldReportDir, "dev-artifacts.json"), `${JSON.stringify({
|
||||
artifactPublish: {
|
||||
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",
|
||||
sourceCommitId: "48dfbf9abcdef",
|
||||
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
|
||||
digest: "sha256:" + "6".repeat(64),
|
||||
buildCreatedAt: "2099-01-01T00:00:00.000Z",
|
||||
buildSource: "old-report-must-not-be-read"
|
||||
}
|
||||
]
|
||||
},
|
||||
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"
|
||||
}
|
||||
}, null, 2)}\n`);
|
||||
|
||||
const observe = async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: "",
|
||||
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr"
|
||||
},
|
||||
repoRoot: tempRoot,
|
||||
fetchImpl: async (url) => {
|
||||
if (new URL(url).pathname === "/hwlab-agent-mgr/health/live") {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
async text() {
|
||||
return JSON.stringify({ error: "offline" });
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
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);
|
||||
return response.json();
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
const withOldReport = await observe();
|
||||
await rm(path.join(tempRoot, "reports"), { recursive: true, force: true });
|
||||
const withoutOldReport = await observe();
|
||||
const summarize = (payload) => {
|
||||
const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr");
|
||||
return {
|
||||
latest: payload.latest,
|
||||
source: payload.source,
|
||||
createdAt: manager.build.createdAt,
|
||||
metadataSource: manager.build.metadataSource,
|
||||
unavailableReason: manager.build.unavailableReason,
|
||||
unavailableDetail: manager.build.unavailableDetail,
|
||||
liveMetadataMatch: manager.build.liveMetadataMatch,
|
||||
counts: payload.counts
|
||||
};
|
||||
};
|
||||
|
||||
assert.deepEqual(summarize(withOldReport), summarize(withoutOldReport));
|
||||
const manager = withOldReport.services.find((service) => service.serviceId === "hwlab-agent-mgr");
|
||||
assert.equal(withOldReport.latest, null);
|
||||
assert.equal(manager.build.createdAt, null);
|
||||
assert.equal(manager.build.metadataSource, "live-health:matched-controlled-metadata-without-build-time");
|
||||
assert.equal(manager.build.unavailableReason, "构建时间不可用:当前 live 镜像未提供 buildCreatedAt");
|
||||
assert.match(manager.build.unavailableDetail, /catalog metadata/u);
|
||||
assert.equal(Object.hasOwn(withOldReport.source, "artifactReportSource"), false);
|
||||
assert.equal(JSON.stringify(withOldReport).includes("2099-01-01T00:00:00.000Z"), false);
|
||||
assert.equal(JSON.stringify(withOldReport).includes("artifact-report"), false);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1071,7 +1071,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
|
||||
const server = useLiveUrl
|
||||
? null
|
||||
: await startStaticWebServer({ rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot });
|
||||
: await startStaticWebServer({ rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot, liveBuildsFixture: true });
|
||||
const url = useLiveUrl ? args.url : server.url;
|
||||
let browser;
|
||||
try {
|
||||
@@ -3198,7 +3198,7 @@ async function inspectLiveDom(url, options = {}) {
|
||||
/hwlab-agent-worker/u.test(dom.liveBuildDetailWhenOpen.text) &&
|
||||
/构建时间不可用/u.test(dom.liveBuildDetailWhenOpen.text) &&
|
||||
/外部镜像或非 HWLAB 构建产物/u.test(dom.liveBuildDetailWhenOpen.text) &&
|
||||
/来源 health metadata/u.test(dom.liveBuildDetailWhenOpen.text) &&
|
||||
/来源 live health/u.test(dom.liveBuildDetailWhenOpen.text) &&
|
||||
dom.firstViewportForbiddenPresent.length === 0 &&
|
||||
dom.labelsPresent &&
|
||||
dom.navLabelsPresent &&
|
||||
@@ -5394,6 +5394,10 @@ async function startStaticWebServer(options = {}) {
|
||||
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url, options })) {
|
||||
return;
|
||||
}
|
||||
if (options.liveBuildsFixture && request.method === "GET" && url.pathname === "/v1/live-builds") {
|
||||
jsonResponse(response, 200, liveBuildsFixturePayload());
|
||||
return;
|
||||
}
|
||||
let pathname = decodeURIComponent(url.pathname);
|
||||
if (pathname === "/" || pathname === "/workbench" || pathname === "/gate" || pathname === "/diagnostics/gate" || pathname === "/help") {
|
||||
pathname = "/index.html";
|
||||
@@ -5783,7 +5787,7 @@ function liveBuildsFixturePayload() {
|
||||
contractVersion: "live-builds-v1",
|
||||
observedAt: "2026-05-23T00:30:00.000Z",
|
||||
source: {
|
||||
kind: "live-health+artifact-catalog",
|
||||
kind: "live-health+deploy-artifact-catalog",
|
||||
route: "/v1/live-builds",
|
||||
healthPath: "/health/live"
|
||||
},
|
||||
@@ -5795,7 +5799,7 @@ function liveBuildsFixturePayload() {
|
||||
build: {
|
||||
createdAt: "2026-05-23T00:20:00.000Z",
|
||||
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT",
|
||||
liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用 repo-owned artifact metadata"
|
||||
liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 catalog metadata"
|
||||
},
|
||||
image: {
|
||||
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:f09ad05",
|
||||
@@ -7494,6 +7498,52 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
return element ? element.scrollWidth <= element.clientWidth + 2 : false;
|
||||
})()
|
||||
};
|
||||
const inspectLiveBuildLayout = async () => {
|
||||
const details = document.querySelector("#live-build-summary");
|
||||
const label = document.querySelector("#live-build-latest");
|
||||
const list = document.querySelector("#live-build-list");
|
||||
if (!details || !label || !list) {
|
||||
return { ok: false, exists: false, summaryOverflowX: null, listOverflowX: null, maxRowOverflowX: null, rows: 0 };
|
||||
}
|
||||
const wasOpen = details.open;
|
||||
const summaryOverflowX = Math.max(0, label.scrollWidth - label.clientWidth);
|
||||
details.open = true;
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
const rows = [...list.querySelectorAll(".live-build-row")];
|
||||
const rowOverflows = rows.map((row) => Math.max(0, row.scrollWidth - row.clientWidth));
|
||||
const maxRowOverflowX = Math.max(0, ...rowOverflows);
|
||||
const listOverflowX = Math.max(0, list.scrollWidth - list.clientWidth);
|
||||
const scrollContained = list.clientHeight <= Math.ceil(window.innerHeight * 0.42) + 4;
|
||||
const text = list.textContent?.replace(/\s+/gu, " ").trim() ?? "";
|
||||
const expectedRows = rows.length >= 4;
|
||||
const expectedText = /构建时间不可用/u.test(text) && /外部镜像或非 HWLAB 构建产物/u.test(text);
|
||||
details.open = wasOpen;
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
return {
|
||||
ok: summaryOverflowX <= 1 && listOverflowX <= 1 && maxRowOverflowX <= 1 && scrollContained && expectedRows && expectedText,
|
||||
exists: true,
|
||||
openStateRestored: details.open === wasOpen,
|
||||
summaryOverflowX,
|
||||
listOverflowX,
|
||||
maxRowOverflowX,
|
||||
rowOverflows,
|
||||
rows: rows.length,
|
||||
expectedRows,
|
||||
expectedText,
|
||||
scrollContained,
|
||||
textSample: text.slice(0, 240)
|
||||
};
|
||||
};
|
||||
const liveBuildLayout = await inspectLiveBuildLayout();
|
||||
if (!liveBuildLayout.ok) {
|
||||
failures.push({
|
||||
failureType: "overflow",
|
||||
selector: "#live-build-summary",
|
||||
viewport,
|
||||
summary: "Live build summary/details overflow or cannot be expanded within the viewport.",
|
||||
liveBuildLayout
|
||||
});
|
||||
}
|
||||
const hardwareTabRows = (() => {
|
||||
const tabs = [...document.querySelectorAll("[data-hardware-tab]")];
|
||||
return new Set(tabs.map((tab) => Math.round(tab.getBoundingClientRect().top))).size;
|
||||
@@ -7816,6 +7866,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
rightWidthTarget &&
|
||||
keyTargetsReachable &&
|
||||
resourceExplorerRemovalGuard &&
|
||||
liveBuildLayout.ok &&
|
||||
sidePanelOverflowUsable;
|
||||
const passWithWiring =
|
||||
pass &&
|
||||
@@ -7842,6 +7893,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
|
||||
overlapChecks,
|
||||
noHorizontalOverflow,
|
||||
horizontalOverflowFree: Object.values(noHorizontalOverflow).every(Boolean),
|
||||
liveBuildLayout,
|
||||
hardwareTabRows,
|
||||
rightWidthTarget,
|
||||
noIncoherentOverlap,
|
||||
|
||||
@@ -1381,9 +1381,8 @@ function liveBuildReasonText(service) {
|
||||
function liveBuildSourceLabel(source) {
|
||||
const text = String(source ?? "").trim();
|
||||
if (text.includes("artifact-catalog")) return "artifact catalog";
|
||||
if (text.includes("artifact-report")) return "artifact report";
|
||||
if (text.includes("runtime-env") || text.includes("health")) return "health metadata";
|
||||
if (text.includes("deploy")) return "desired-state";
|
||||
if (text.includes("deploy")) return "deploy metadata";
|
||||
if (text.includes("runtime-env") || text.includes("health")) return "live health";
|
||||
if (text.includes("external")) return "external image";
|
||||
if (!text || text === "unavailable") return "不可用";
|
||||
return text;
|
||||
|
||||
@@ -939,6 +939,8 @@ assert.match(app, /revision \$\{shortToken\(service\.revision\)\}/);
|
||||
assert.match(app, /liveBuildSourceLabel/);
|
||||
assert.match(app, /liveBuildDesiredStateText/);
|
||||
assert.match(app, /外部镜像或非 HWLAB 构建产物/);
|
||||
assert.doesNotMatch(app, /artifact report|artifact-report/);
|
||||
assert.match(app, /live health/);
|
||||
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);
|
||||
|
||||
@@ -200,6 +200,7 @@ test("cloud web live build surface renders Beijing summary, details, missing tim
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const html = readFileSync(new URL("../index.html", import.meta.url), "utf8");
|
||||
const app = readFileSync(new URL("../app.mjs", import.meta.url), "utf8");
|
||||
const styles = readFileSync(new URL("../styles.css", import.meta.url), "utf8");
|
||||
|
||||
assert.match(html, /id="live-build-summary"/u);
|
||||
assert.match(html, /id="live-build-latest"/u);
|
||||
@@ -209,6 +210,10 @@ test("cloud web live build surface renders Beijing summary, details, missing tim
|
||||
assert.match(app, /liveBuildSourceLabel/u);
|
||||
assert.match(app, /liveBuildDesiredStateText/u);
|
||||
assert.match(app, /外部镜像或非 HWLAB 构建产物/u);
|
||||
assert.doesNotMatch(app, /artifact report|artifact-report/u);
|
||||
assert.match(styles, /\.live-build-summary-label\s*\{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow-wrap:\s*anywhere;/su);
|
||||
assert.match(styles, /\.live-build-list\s*\{[^}]*max-height:\s*min\(280px,\s*42dvh\);[^}]*min-width:\s*0;[^}]*overflow:\s*auto;/su);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*\{[\s\S]*?grid-template-columns:\s*1fr;/u);
|
||||
});
|
||||
|
||||
test("classifies fully ready workbench runtime as API 正常", () => {
|
||||
|
||||
Reference in New Issue
Block a user