import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { mkdir, 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 { classifyGlobalChange, componentModelsForServices, createG14CiPlan, matchingPaths } from "./src/g14-ci-plan-lib.mjs"; const execFileAsync = promisify(execFile); test("G14 CI planner maps bridge changes to cloud-api only", async () => { const repo = await createFixtureRepo(); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge v2');\n"); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "change bridge"]); const plan = await createG14CiPlan({ repoRoot: repo, baseRef: "HEAD~1", targetRef: "HEAD" }); assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api"]); assert.equal(plan.reusedServices.includes("hwlab-cloud-web"), true); const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api"); assert.equal(cloudApi.affected, true); assert.deepEqual(cloudApi.reason, ["component-path-changed"]); }); test("G14 CI planner treats docs-only changes as no image build", async () => { const repo = await createFixtureRepo(); await mkdir(path.join(repo, "docs/reference"), { recursive: true }); await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs v2\n"); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "change docs"]); const plan = await createG14CiPlan({ repoRoot: repo, baseRef: "HEAD~1", targetRef: "HEAD" }); assert.deepEqual(plan.affectedServices, []); assert.equal(plan.imageBuildRequired, false); assert.equal(plan.changedPathSummary.docsOnly, true); }); test("component model supports CI.json overrides without replacing deploy.json", () => { const models = componentModelsForServices(["hwlab-cloud-api"], { g14CiPlan: { components: { "hwlab-cloud-api": { componentPaths: ["custom/api/"], sharedPaths: ["shared/runtime.mjs"], runtimeDeps: ["package.json"] } } } }); assert.deepEqual(models[0].componentPaths, ["custom/api/"]); assert.deepEqual(matchingPaths(["custom/api/main.mjs"], models[0].componentPaths), ["custom/api/main.mjs"]); }); test("planner remains compatible with deploy.k3s.serviceMappings shape", async () => { const repo = await createFixtureRepo({ deployServices: false, k3sServiceMappings: true }); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge v2');\n"); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "change bridge"]); const plan = await createG14CiPlan({ repoRoot: repo, baseRef: "HEAD~1", targetRef: "HEAD" }); assert.equal(plan.compatibility.currentRenderCompatible, true); assert.equal(plan.compatibility.serviceIdSource, "deploy.k3s.serviceMappings"); assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api"]); assert.equal(plan.inputs.serviceCount, 2); }); test("global change classifier distinguishes gitops-only and test-only", () => { assert.equal(classifyGlobalChange(["deploy/gitops/g14/runtime-dev/workloads.yaml"]).gitopsOnly, true); assert.equal(classifyGlobalChange(["cmd/hwlab-cloud-api/main.test.mjs"]).testOnly, true); }); async function createFixtureRepo(options = {}) { const deployServices = options.deployServices !== false; const k3sServiceMappings = options.k3sServiceMappings === true; const repo = await mkdtemp(path.join(os.tmpdir(), "hwlab-g14-ci-plan-")); await mkdir(path.join(repo, "cmd/hwlab-cloud-api"), { recursive: true }); await mkdir(path.join(repo, "cmd/hwlab-deepseek-responses-bridge"), { recursive: true }); await mkdir(path.join(repo, "web/hwlab-cloud-web"), { recursive: true }); await mkdir(path.join(repo, "deploy"), { recursive: true }); await writeFile(path.join(repo, "CI.json"), JSON.stringify({ version: 1, commands: [] }, null, 2)); await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings }), null, 2)); await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2)); await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2)); await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.mjs"), "console.log('api');\n"); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge');\n"); await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.mjs"), "console.log('test');\n"); await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "\n"); await git(repo, ["init"]); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "initial"]); return repo; } function createDeployFixture({ deployServices, k3sServiceMappings }) { const deploy = {}; if (deployServices) { deploy.services = [ { serviceId: "hwlab-cloud-api" }, { serviceId: "hwlab-cloud-web" } ]; } if (k3sServiceMappings) { deploy.k3s = { serviceMappings: [ { serviceId: "hwlab-cloud-api", name: "hwlab-cloud-api" }, { serviceId: "hwlab-cloud-web", name: "hwlab-cloud-web" } ] }; } return deploy; } async function git(cwd, args) { return execFileAsync("git", ["-c", "user.name=HWLAB Test", "-c", "user.email=hwlab-test@example.invalid", ...args], { cwd }); }