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"; import { writeStructuredFile } from "./src/structured-config.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")}`; } function envInputHashFor(index) { return String(index + 1).padStart(64, "a"); } 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: "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: "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.yaml 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: "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); } }); test("v03 refresh accepts runtime lane config and env-reuse image tags", async () => { const commitId = await git(["rev-parse", "HEAD"]); const tempDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-refresh-v03-runtime-services-")); const deployPath = path.join(tempDir, "deploy.yaml"); const catalogPath = path.join(tempDir, "artifact-catalog.v03.json"); const reportPath = path.join(tempDir, "v03-artifacts.json"); const serviceIds = [...V02_SERVICE_IDS, "hwlab-tasktree-api", "hwlab-tasktree-worker"]; await writeStructuredFile(tempDir, deployPath, { environment: "dev", namespace: "hwlab-dev", endpoint: "http://74.48.78.17:16667", profiles: { dev: { enabled: true }, prod: { enabled: false } }, lanes: { v03: { namespace: "hwlab-v03", endpoint: "https://hwlab-v03.74-48-78-17.nip.io", envReuseServices: serviceIds, serviceDeclarations: Object.fromEntries(serviceIds.map((serviceId) => [serviceId, { runtimeKind: "node-command", entrypoint: `cmd/${serviceId}/main.ts`, componentPaths: [`cmd/${serviceId}/`] }])), bootScripts: Object.fromEntries(serviceIds.map((serviceId) => [serviceId, `deploy/runtime/boot/${serviceId}.sh`])) } }, services: SERVICE_IDS.map((serviceId) => ({ serviceId })) }); await writeFile(catalogPath, `${JSON.stringify({ catalogVersion: "v1", kind: "hwlab-artifact-catalog", environment: "v03", profile: "v03", namespace: "hwlab-v03", endpoint: "https://hwlab-v03.74-48-78-17.nip.io", commitId, artifactState: "published", publish: { ciPublished: true, registryVerified: true, provenance: "previous-run" }, allowedProfiles: ["v03"], forbiddenProfiles: ["dev", "prod"], services: V02_SERVICE_IDS.map((serviceId, index) => ({ serviceId, commitId, sourceCommitId: commitId, image: `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`, imageTag: commitId, digest: digestFor(index), profile: "v03", namespace: "hwlab-v03" })) }, null, 2)}\n`); await writeFile(reportPath, `${JSON.stringify({ reportVersion: "v1", taskId: "v03-artifact-publish", commitId, artifactPublish: { status: "published", mode: "publish", sourceCommitId: commitId, registryPrefix: "127.0.0.1:5000/hwlab", serviceCount: serviceIds.length, requiredServiceCount: serviceIds.length, disabledServiceCount: 0, publishedCount: serviceIds.length, reusedCount: 0, serviceInventory: { version: "v2", serviceCount: serviceIds.length, requiredServiceCount: serviceIds.length, disabledServiceCount: 0, requiredServiceIds: serviceIds, disabledServiceIds: [], services: serviceIds.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: serviceIds.map((serviceId, index) => { const envTag = `env-${envInputHashFor(index).slice(0, 12)}`; return { serviceId, required: true, digest: digestFor(index), image: `127.0.0.1:5000/hwlab/${serviceId}-env:${envTag}`, imageTag: envTag }; }) }, services: serviceIds.map((serviceId, index) => { const environmentInputHash = envInputHashFor(index); const envTag = `env-${environmentInputHash.slice(0, 12)}`; const image = `127.0.0.1:5000/hwlab/${serviceId}-env:${envTag}`; return { serviceId, status: "published", artifactRequired: true, runtimeMode: "env-reuse-gitea-checkout", envReuse: true, commitId, sourceCommitId: commitId, image, imageTag: envTag, digest: digestFor(index), repositoryDigest: `127.0.0.1:5000/hwlab/${serviceId}-env@${digestFor(index)}`, environmentImage: image, environmentDigest: digestFor(index), environmentInputHash, codeInputHash: envInputHashFor(index + serviceIds.length), bootRepo: "git@github.com:pikasTech/HWLAB.git", bootCommit: commitId, bootSh: `deploy/runtime/boot/${serviceId}.sh` }; }) } }, null, 2)}\n`); const result = await execFileAsync(process.execPath, [ "scripts/refresh-artifact-catalog.mjs", "--lane", "v03", "--image-tag-mode", "full", "--target-ref", "HEAD", "--deploy-config", deployPath, "--catalog-path", catalogPath, "--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.endpoint, "https://hwlab-v03.74-48-78-17.nip.io"); assert.equal(payload.requiredServiceCount, serviceIds.length); assert.deepEqual(payload.services.map((service) => service.serviceId), serviceIds); assert.equal(payload.services.every((service) => service.runtimeMode === "env-reuse-gitea-checkout"), true); assert.equal(payload.services.every((service) => service.imageTag.startsWith("env-")), true); }); 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.yaml"); const catalogPath = path.join(tempDir, "artifact-catalog.v02.json"); await writeStructuredFile(tempDir, deployPath, { 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", envReuseServices: V02_SERVICE_IDS, serviceDeclarations: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, { runtimeKind: "node-command", entrypoint: `cmd/${serviceId}/main.ts`, componentPaths: [`cmd/${serviceId}/`] }])), bootScripts: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, `deploy/runtime/boot/${serviceId}.sh`])) } }, services: SERVICE_IDS.map((serviceId) => ({ serviceId })) }); 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-config", 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); } });