import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { registerDeploymentIpc } = require("../src/main/ipc/deployment-handlers.cjs"); const { UnraidDeploymentService } = require("../src/main/unraid-deployment-service.cjs"); const REPOSITORY = { fullName: "Jens/ForgeFlow", owner: { login: "Jens" }, localPath: "C:/Projects/ForgeFlow" }; const WORKLOAD = { workloadId: "workload-1", classification: { type: "ambiguous" } }; // Every collaborator answers, so a channel can only fail on a dependency the // module references but never receives. function harness(overrides = {}) { const calls = []; const record = (name, result) => async (...args) => { calls.push({ name, args }); return typeof result === "function" ? result(...args) : result; }; const handlers = new Map(); const profile = overrides.profile || { id: "profile-1", provider: "ssh-unraid", branch: "main", name: "Production" }; const dependencies = { register: (channel, handler) => handlers.set(channel, handler), store: { data: { servers: [{ id: "server-1", name: "Unraid" }], operations: [] }, getDeploymentProfile: () => profile, getPublicState: () => ({ ok: true }), saveDeploymentProfile: record("store.saveDeploymentProfile", profile), deleteDeploymentProfile: record("store.deleteDeploymentProfile", []), addOperation: record("store.addOperation", null), }, resolveRepository: record("resolveRepository", REPOSITORY), unraid: { preflight: record("unraid.preflight", { ok: true }), repairWriteAccess: record("unraid.repairWriteAccess", { changed: true, after: {}, before: {} }), deploy: record("unraid.deploy", { id: "operation-1" }), rollback: record("unraid.rollback", { id: "operation-2" }), linkServerWorkload: record("unraid.linkServerWorkload", { linked: true }), configureServerGitAccess: record("unraid.configureServerGitAccess", { keyFingerprint: "a", hostFingerprint: "b" }), verifyServerGitProfile: record("unraid.verifyServerGitProfile", { readiness: "ready", ready: true, checkedAt: "now" }), discoverServerWorkloads: record("unraid.discoverServerWorkloads", { serverId: "server-1", workloads: [] }), planServerInventoryReconciliation: record("unraid.planServerInventoryReconciliation", { plan: { id: "plan-1", summary: {} } }), reconcileServerInventory: record("unraid.reconcileServerInventory", { adopted: 0, refreshed: 0, retired: 0 }), scanServerInventory: record("unraid.scanServerInventory", { workloads: [WORKLOAD] }), refreshProfileState: record("unraid.refreshProfileState", { liveSha: null }), applyDockerManMetadata: record("unraid.applyDockerManMetadata", { applied: true }), refreshOperation: record("unraid.refreshOperation", null), reconcileRecordedOperations: record("unraid.reconcileRecordedOperations", []), }, deployments: { deploy: record("deployments.deploy", { id: "operation-3" }), rollback: record("deployments.rollback", { id: "operation-4" }), checkHealth: record("deployments.checkHealth", { healthy: true }), refreshProfileState: record("deployments.refreshProfileState", { liveSha: null }), }, evaluateDeploymentPolicy: () => ({ note: "", overridden: false, reason: "", violations: [] }), audit: { append: record("audit.append", null) }, deployKeys: { inventory: record("deployKeys.inventory", { keys: [] }), planRotation: record("deployKeys.planRotation", { id: "rotation-1" }), rotate: record("deployKeys.rotate", { rotated: true }), planRevocation: record("deployKeys.planRevocation", { id: "revocation-1" }), revoke: record("deployKeys.revoke", { revoked: true }), restore: record("deployKeys.restore", { restored: true }), }, repositories: { refresh: record("repositories.refresh", [REPOSITORY]) }, inventoryReviews: { preview: (...args) => { calls.push({ name: "inventoryReviews.preview", args }); return { id: "review-1" }; }, apply: record("inventoryReviews.apply", { applied: true }), }, diagnostics: { info: record("diagnostics.info"), warning: record("diagnostics.warning"), error: record("diagnostics.error"), debug: record("diagnostics.debug") }, git: {}, gitea: { getBranch: record("gitea.getBranch", { commit: { id: "c".repeat(40) } }) }, ssh: {}, preflight: { runDeployment: record("preflight.runDeployment", { ok: "actions" }) }, ...overrides.dependencies, }; registerDeploymentIpc(dependencies); return { handlers, calls, names: () => calls.map((item) => item.name) }; } const PAYLOAD = { repository: REPOSITORY, fullName: REPOSITORY.fullName, profileId: "profile-1", sha: "a".repeat(40), serverId: "server-1", workloadId: WORKLOAD.workloadId, planId: "plan-1", action: "manual-link", url: "https://app.example/health", profile: { name: "Production" }, targetSha: "b".repeat(40), }; // Both provider paths have to run: a dependency that only the Gitea Actions // branch reads stays invisible while every channel is exercised as SSH/Unraid. for (const provider of ["ssh-unraid", "gitea-actions"]) { test(`every deployment IPC channel runs with the dependencies it is given (${provider})`, async () => { const { handlers } = harness({ profile: { id: "profile-1", provider, branch: "main", name: "Production" } }); assert.ok(handlers.size >= 20, "expected the complete deployment channel surface"); const failures = []; for (const [channel, handler] of handlers) { try { await handler({ ...PAYLOAD }); } catch (error) { // A refusal is a decision the handler made; a missing dependency is not. if (error instanceof ReferenceError || error instanceof TypeError) { failures.push(`${channel}: ${error.name}: ${error.message}`); } } } assert.deepEqual(failures, []); }); } test("deployment preflight routes by provider", async () => { const actions = harness({ profile: { id: "profile-1", provider: "gitea-actions" } }); assert.deepEqual(await actions.handlers.get("deployment:preflight")({ ...PAYLOAD }), { ok: "actions" }); assert.ok(actions.names().includes("preflight.runDeployment")); const unraid = harness(); assert.deepEqual(await unraid.handlers.get("deployment:preflight")({ ...PAYLOAD }), { ok: true }); assert.ok(unraid.names().includes("unraid.preflight")); assert.ok(!unraid.names().includes("preflight.runDeployment")); }); test("write-access repair is refused for anything but an SSH/Unraid profile", async () => { const actions = harness({ profile: { id: "profile-1", provider: "gitea-actions" } }); await assert.rejects( () => actions.handlers.get("deployment:repair-write-access")({ ...PAYLOAD }), /available only for SSH \/ Unraid/, ); }); test("a stale workload blocks an inventory review instead of guessing", async () => { const { handlers } = harness({ dependencies: { unraid: { scanServerInventory: async () => ({ workloads: [] }) } }, }); for (const channel of ["deployment:plan-inventory-review", "deployment:apply-inventory-review"]) { await assert.rejects(() => handlers.get(channel)({ ...PAYLOAD }), (error) => { assert.equal(error.code, "INVENTORY_REVIEW_WORKLOAD_STALE"); return true; }); } }); test("write-access repair builds a repair script that preserves runtime paths", () => { const service = new UnraidDeploymentService({}); const profile = { id: "profile-3", provider: "ssh-unraid", remoteFolder: "portfolio", composeFiles: ["docker-compose.yml"], preservePaths: ["data/uploads"], }; const server = { id: "server-1", basePath: "/mnt/user/appdata" }; const script = service.permissionRepairScript(profile, server, "/mnt/user/appdata/portfolio"); assert.equal(typeof script, "string"); assert.match(script, /data\/uploads/); assert.match(script, /node_modules/); assert.match(script, /ForgeFlow repaired project write access/); });