435 lines
16 KiB
TypeScript
435 lines
16 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudApiServer } from "./server.ts";
|
|
|
|
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",
|
|
status: "ok",
|
|
revision: "webabcdef123456",
|
|
commit: { id: "webabcdef123456", source: "test" },
|
|
image: {
|
|
reference: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webabcd",
|
|
tag: "webabcd",
|
|
digest: "sha256:" + "1".repeat(64)
|
|
},
|
|
build: {
|
|
createdAt: "2026-05-23T02:10:00.000Z",
|
|
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT"
|
|
}
|
|
}],
|
|
["/hwlab-agent-mgr/health/live", {
|
|
serviceId: "hwlab-agent-mgr",
|
|
status: "ok",
|
|
revision: "48dfbf9",
|
|
commit: { id: "48dfbf9", source: "test" },
|
|
image: {
|
|
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",
|
|
image: {
|
|
reference: "postgres:16",
|
|
tag: "16"
|
|
}
|
|
}]
|
|
]);
|
|
const liveBuildMetadata = {
|
|
deployManifest: {
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webcat",
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health/live",
|
|
replicas: 1
|
|
},
|
|
{
|
|
serviceId: "hwlab-agent-mgr",
|
|
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",
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health/live",
|
|
replicas: 0
|
|
},
|
|
{
|
|
serviceId: "hwlab-edge-proxy",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-edge-proxy:edgecat",
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health",
|
|
replicas: 1
|
|
},
|
|
{
|
|
serviceId: "hwlab-extra-lab",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health/live",
|
|
replicas: 1
|
|
}
|
|
]
|
|
},
|
|
artifactCatalog: {
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-agent-mgr",
|
|
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",
|
|
healthPath: "/health/live",
|
|
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",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
|
|
imageTag: "extracat",
|
|
digest: "sha256:" + "4".repeat(64),
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health/live",
|
|
buildCreatedAt: "2026-05-23T03:00:00.000Z",
|
|
buildSource: "artifact-catalog-test"
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: "",
|
|
HWLAB_COMMIT_ID: "apiabcdef123456",
|
|
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-api:apiabcd",
|
|
HWLAB_IMAGE_TAG: "apiabcd",
|
|
HWLAB_BUILD_CREATED_AT: "2026-05-23T01:00:00.000Z",
|
|
HOSTNAME: "hwlab-cloud-api-test-pod",
|
|
POD_NAMESPACE: "hwlab-dev",
|
|
HWLAB_RUNTIME_CONTAINER: "hwlab-cloud-api",
|
|
HWLAB_CLOUD_WEB_SERVICE_URL: "http://live.test/hwlab-cloud-web",
|
|
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr",
|
|
HWLAB_AGENT_WORKER_URL: "http://live.test/missing-agent-worker",
|
|
HWLAB_GATEWAY_URL: "http://live.test/missing-gateway",
|
|
HWLAB_GATEWAY_SIMU_URL: "http://live.test/missing-gateway-simu",
|
|
HWLAB_BOX_SIMU_URL: "http://live.test/missing-box-simu",
|
|
HWLAB_PATCH_PANEL_URL: "http://live.test/missing-patch-panel",
|
|
HWLAB_ROUTER_URL: "http://live.test/missing-router",
|
|
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/hwlab-agent-skills",
|
|
HWLAB_EXTRA_LAB_URL: "http://live.test/missing-extra-lab"
|
|
},
|
|
liveBuildMetadata,
|
|
fetchImpl: async (url) => {
|
|
const path = new URL(url).pathname;
|
|
const payload = healthByPath.get(path);
|
|
if (!payload) {
|
|
return {
|
|
ok: false,
|
|
status: 503,
|
|
async text() {
|
|
return JSON.stringify({ error: "offline" });
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify(payload);
|
|
}
|
|
};
|
|
}
|
|
});
|
|
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();
|
|
assert.equal(payload.contractVersion, "live-builds-v1");
|
|
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, /匹配的 deploy metadata/u);
|
|
const api = payload.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
|
assert.equal(api.runtime.pod.name, "hwlab-cloud-api-test-pod");
|
|
assert.equal(api.runtime.unideskRoute, "G14:k3s:hwlab-dev:pod:hwlab-cloud-api-test-pod:hwlab-cloud-api");
|
|
assert.equal(payload.counts.total, payload.services.length);
|
|
assert.equal(payload.counts.external, 2);
|
|
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: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);
|
|
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"));
|
|
assert.ok(payload.services.some((service) => service.serviceId === "hwlab-device-pod"));
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
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",
|
|
sourceCommitId: "48dfbf9abcdef",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
|
imageTag: "48dfbf9",
|
|
digest: "sha256:" + "6".repeat(64),
|
|
buildCreatedAt: "2099-01-01T00:00:00.000Z",
|
|
buildSource: "old-report-must-not-be-read"
|
|
}
|
|
]
|
|
}
|
|
}, 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()));
|
|
});
|
|
}
|
|
};
|
|
|
|
try {
|
|
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 rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api v02 live builds omit removed simulator and tunnel services from default inventory", async () => {
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: "",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
liveBuildMetadata: {
|
|
deployManifest: {
|
|
services: [
|
|
{ serviceId: "hwlab-cloud-api", profile: "v02", replicas: 1 },
|
|
{ serviceId: "hwlab-gateway-simu", profile: "v02", replicas: 1 },
|
|
{ serviceId: "hwlab-box-simu", profile: "v02", replicas: 1 },
|
|
{ serviceId: "hwlab-patch-panel", profile: "v02", replicas: 1 },
|
|
{ serviceId: "hwlab-router", profile: "v02", replicas: 1 },
|
|
{ serviceId: "hwlab-tunnel-client", profile: "v02", replicas: 1 }
|
|
]
|
|
},
|
|
artifactCatalog: {
|
|
services: [
|
|
{ serviceId: "hwlab-gateway-simu" },
|
|
{ serviceId: "hwlab-cloud-web" }
|
|
]
|
|
}
|
|
},
|
|
fetchImpl: async () => ({
|
|
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);
|
|
const payload = await response.json();
|
|
const serviceIds = payload.services.map((service) => service.serviceId);
|
|
assert.equal(serviceIds.includes("hwlab-cloud-api"), true);
|
|
assert.equal(serviceIds.includes("hwlab-cloud-web"), true);
|
|
assert.equal(serviceIds.includes("hwlab-gateway-simu"), false);
|
|
assert.equal(serviceIds.includes("hwlab-box-simu"), false);
|
|
assert.equal(serviceIds.includes("hwlab-patch-panel"), false);
|
|
assert.equal(serviceIds.includes("hwlab-router"), false);
|
|
assert.equal(serviceIds.includes("hwlab-tunnel-client"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|