Merge pull request #422 from pikasTech/fix/live-builds-no-reports

fix: 收束 live 构建时间 metadata 来源
This commit is contained in:
Lyon
2026-05-24 12:30:24 +08:00
committed by GitHub
6 changed files with 222 additions and 134 deletions
+36 -40
View File
@@ -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 没有 buildCreatedAtmetadata 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 / buildCreatedAtdeploy/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 metadatalive 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
View File
@@ -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 });
}
});
+56 -4
View File
@@ -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,
+2 -3
View File
@@ -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;
+2
View File
@@ -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 正常", () => {