import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { mkdtemp, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { promisify } from "node:util"; import { SERVICE_IDS } from "../internal/protocol/index.mjs"; const execFileAsync = promisify(execFile); const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".."); const V02_SERVICE_IDS = Object.freeze([ "hwlab-cloud-api", "hwlab-cloud-web", "hwlab-gateway", "hwlab-edge-proxy", "hwlab-agent-skills" ]); async function git(args) { const result = await execFileAsync("git", args, { cwd: repoRoot, timeout: 5000, maxBuffer: 1024 * 1024 }); return result.stdout.trim(); } function digestFor(index) { return `sha256:${String(index + 1).padStart(64, "0")}`; } test("refresh accepts an absolute publish report path", async () => { const commitId = await git(["rev-parse", "HEAD"]); const shortCommitId = await git(["rev-parse", "--short=7", "HEAD"]); const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-")); const reportPath = path.join(tempDir, "dev-artifacts.json"); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "g14-artifact-publish", commitId: shortCommitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: SERVICE_IDS.length, requiredServiceCount: SERVICE_IDS.length, disabledServiceCount: 0, publishedCount: SERVICE_IDS.length, publishPlan: { version: "v2", services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, required: true, digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}:${shortCommitId}`, imageTag: shortCommitId })) }, services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, status: "published", artifactRequired: true, image: `127.0.0.1:5000/hwlab/${serviceId}:${shortCommitId}`, imageTag: shortCommitId, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}`, buildCreatedAt: "2026-05-24T00:00:00.000Z", buildSource: `pikasTech/HWLAB@${commitId}`, componentCommitId: commitId, componentInputHash: `${String(index + 1).padStart(64, "a")}`, dockerfileHash: `${String(index + 1).padStart(64, "b")}`, buildArgsHash: `${String(index + 1).padStart(64, "c")}`, ciAffected: index === 0, ciReason: index === 0 ? ["component-path-changed"] : ["component-inputs-unchanged"] })) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--target-ref", "HEAD", "--publish-report", reportPath, "--no-write" ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "published"); assert.equal(payload.publishedCount, SERVICE_IDS.length); assert.deepEqual(payload.wrote, []); assert.equal(payload.services[0].componentCommitId, commitId); assert.equal(payload.services[0].ciAffected, true); assert.equal(payload.services[0].componentInputHash, `${String(1).padStart(64, "a")}`); }); test("refresh is read-only by default", async () => { const commitId = await git(["rev-parse", "HEAD"]); const shortCommitId = await git(["rev-parse", "--short=7", "HEAD"]); const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-default-readonly-")); const reportPath = path.join(tempDir, "dev-artifacts.json"); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "g14-artifact-publish", commitId: shortCommitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: SERVICE_IDS.length, requiredServiceCount: SERVICE_IDS.length, disabledServiceCount: 0, publishedCount: SERVICE_IDS.length, publishPlan: { version: "v2", services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, required: true, digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}:${shortCommitId}`, imageTag: shortCommitId })) }, services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, status: "published", artifactRequired: true, image: `127.0.0.1:5000/hwlab/${serviceId}:${shortCommitId}`, imageTag: shortCommitId, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}` })) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--target-ref", "HEAD", "--publish-report", reportPath ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "published"); assert.deepEqual(payload.wrote, []); assert.equal(payload.deployTruthPolicy, "deploy.json is human-authored runtime config; artifact promotion must not rewrite it"); }); test("refresh keeps reused service image tags from the publish report", async () => { const commitId = await git(["rev-parse", "HEAD"]); const shortCommitId = await git(["rev-parse", "--short=7", "HEAD"]); const reusedTag = "d42c77d"; const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-reused-")); const reportPath = path.join(tempDir, "dev-artifacts.json"); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "g14-artifact-publish", commitId: shortCommitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: SERVICE_IDS.length, requiredServiceCount: SERVICE_IDS.length, disabledServiceCount: 0, publishedCount: 1, reusedCount: SERVICE_IDS.length - 1, publishPlan: { version: "v2", services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, required: true, status: index === 0 ? "published" : "reused", digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}:${index === 0 ? shortCommitId : reusedTag}`, imageTag: index === 0 ? shortCommitId : reusedTag })) }, services: SERVICE_IDS.map((serviceId, index) => ({ serviceId, status: index === 0 ? "published" : "reused", artifactRequired: true, commitId: index === 0 ? shortCommitId : reusedTag, sourceCommitId: index === 0 ? commitId : reusedTag, image: `127.0.0.1:5000/hwlab/${serviceId}:${index === 0 ? shortCommitId : reusedTag}`, imageTag: index === 0 ? shortCommitId : reusedTag, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}`, buildCreatedAt: "2026-05-24T00:00:00.000Z", buildSource: index === 0 ? `pikasTech/HWLAB@${commitId}` : `pikasTech/HWLAB@${reusedTag}`, componentCommitId: index === 0 ? commitId : reusedTag, componentInputHash: `${String(index + 1).padStart(64, "d")}`, dockerfileHash: `${String(index + 1).padStart(64, "e")}`, buildArgsHash: `${String(index + 1).padStart(64, "f")}`, ciAffected: index === 0, ciReason: index === 0 ? ["component-path-changed"] : ["component-inputs-unchanged"], reusedFrom: index === 0 ? null : `catalog:${reusedTag}` })) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--target-ref", "HEAD", "--publish-report", reportPath, "--no-write" ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "published"); assert.equal(payload.publishedCount, 1); assert.equal(payload.reusedCount, SERVICE_IDS.length - 1); assert.equal(payload.services[0].imageTag, shortCommitId); assert.equal(payload.services[1].imageTag, reusedTag); assert.equal(payload.services[1].publishState, "reused"); assert.equal(payload.services[1].reusedFrom, `catalog:${reusedTag}`); }); test("v02 refresh accepts current runtime service full image tags", async () => { const commitId = await git(["rev-parse", "HEAD"]); const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v02-runtime-services-")); const reportPath = path.join(tempDir, "v02-artifacts.json"); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "v02-artifact-publish", commitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: V02_SERVICE_IDS.length, requiredServiceCount: V02_SERVICE_IDS.length, disabledServiceCount: 0, publishedCount: V02_SERVICE_IDS.length, reusedCount: 0, serviceInventory: { version: "v2", serviceCount: V02_SERVICE_IDS.length, requiredServiceCount: V02_SERVICE_IDS.length, disabledServiceCount: 0, requiredServiceIds: V02_SERVICE_IDS, disabledServiceIds: [], services: V02_SERVICE_IDS.map((serviceId) => ({ serviceId, publishEnabled: true, artifactRequired: true, artifactScope: "required", runtimeKind: "node-command", implementationState: "repo-entrypoint", sourceState: "source-present", entrypoint: `cmd/${serviceId}/main.ts`, disabledReason: null })) }, publishPlan: { version: "v2", services: V02_SERVICE_IDS.map((serviceId, index) => ({ serviceId, required: true, digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, imageTag: commitId })) }, services: V02_SERVICE_IDS.map((serviceId, index) => ({ serviceId, status: "published", artifactRequired: true, commitId, sourceCommitId: commitId, image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, imageTag: commitId, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}` })) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--lane", "v02", "--image-tag-mode", "full", "--target-ref", "HEAD", "--catalog-path", path.join(tempDir, "artifact-catalog.v02.json"), "--publish-report", reportPath, "--no-write" ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "published"); assert.equal(payload.publishedCount, V02_SERVICE_IDS.length); assert.equal(payload.reusedCount, 0); assert.deepEqual(payload.services.map((service) => service.serviceId), V02_SERVICE_IDS); for (const service of payload.services) { assert.equal(service.imageTag, commitId); assert.equal(service.runtimeMode, "service-image"); assert.equal(service.envReuse, false); } }); test("v02 refresh follows the current deploy endpoint instead of the retired 19667 freeze", async () => { const commitId = await git(["rev-parse", "HEAD"]); const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v02-https-endpoint-")); const deployPath = path.join(tempDir, "deploy.json"); const catalogPath = path.join(tempDir, "artifact-catalog.v02.json"); await writeFile(deployPath, `${JSON.stringify({ environment: "dev", namespace: "hwlab-dev", endpoint: "http://74.48.78.17:16667", profiles: { dev: { enabled: true }, prod: { enabled: false } }, lanes: { v02: { namespace: "hwlab-v02", endpoint: "https://hwlab.74-48-78-17.nip.io" } }, services: SERVICE_IDS.map((serviceId) => ({ serviceId })) }, null, 2)}\n`); await writeFile(catalogPath, `${JSON.stringify({ catalogVersion: "v1", kind: "hwlab-artifact-catalog", environment: "v02", profile: "v02", namespace: "hwlab-v02", endpoint: "http://74.48.78.17:19667", commitId, artifactState: "contract-skeleton", publish: { registryPrefix: "127.0.0.1:5000/hwlab" }, allowedProfiles: ["v02"], forbiddenProfiles: ["dev", "prod"], services: V02_SERVICE_IDS.map((serviceId) => ({ serviceId, profile: "v02", namespace: "hwlab-v02", commitId, sourceCommitId: commitId, image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, imageTag: commitId, digest: "not_published" })) }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--lane", "v02", "--image-tag-mode", "full", "--target-ref", "HEAD", "--deploy-json", deployPath, "--catalog-path", catalogPath, "--blocked", "--no-write" ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "blocked"); assert.equal(payload.endpoint, "https://hwlab.74-48-78-17.nip.io"); }); test("v02 refresh reports current runtime records as reused", async () => { const commitId = await git(["rev-parse", "HEAD"]); const previousCommitId = "1234567890abcdef1234567890abcdef12345678"; const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v02-reuse-")); const reportPath = path.join(tempDir, "v02-artifacts.json"); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "v02-artifact-publish", commitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: V02_SERVICE_IDS.length, requiredServiceCount: V02_SERVICE_IDS.length, disabledServiceCount: 0, publishedCount: 0, reusedCount: V02_SERVICE_IDS.length, serviceInventory: { version: "v2", serviceCount: V02_SERVICE_IDS.length, requiredServiceCount: V02_SERVICE_IDS.length, disabledServiceCount: 0, requiredServiceIds: V02_SERVICE_IDS, disabledServiceIds: [], services: V02_SERVICE_IDS.map((serviceId) => ({ serviceId, publishEnabled: true, artifactRequired: true, artifactScope: "required", runtimeKind: "node-command", implementationState: "repo-entrypoint", sourceState: "source-present", entrypoint: `cmd/${serviceId}/main.ts`, disabledReason: null })) }, publishPlan: { version: "v2", services: V02_SERVICE_IDS.map((serviceId, index) => ({ serviceId, required: true, status: "reused", digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}:${previousCommitId}`, imageTag: previousCommitId })) }, services: V02_SERVICE_IDS.map((serviceId, index) => ({ serviceId, status: "reused", artifactRequired: true, commitId: previousCommitId, sourceCommitId: previousCommitId, image: `127.0.0.1:5000/hwlab/${serviceId}:${previousCommitId}`, imageTag: previousCommitId, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}@${digestFor(index)}`, buildBackend: "reused-catalog", reusedFrom: `catalog:${previousCommitId}` })) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--lane", "v02", "--image-tag-mode", "full", "--target-ref", "HEAD", "--catalog-path", path.join(tempDir, "artifact-catalog.v02.json"), "--publish-report", reportPath, "--no-write" ], { cwd: repoRoot, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }); const payload = JSON.parse(result.stdout); assert.equal(payload.status, "published"); assert.equal(payload.publishedCount, 0); assert.equal(payload.reusedCount, V02_SERVICE_IDS.length); assert.deepEqual(payload.services.map((service) => service.serviceId), V02_SERVICE_IDS); for (const service of payload.services) { assert.equal(service.publishState, "reused"); assert.equal(service.buildBackend, "reused-catalog"); assert.equal(service.sourceCommitId, previousCommitId); assert.equal(service.imageTag, previousCommitId); assert.equal(service.runtimeMode, "service-image"); assert.equal(service.envReuse, false); } });