import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { classifyInventory } = require("../src/main/inventory-classifier.cjs"); const { deploymentIdentity, deploymentEvidenceHash, deploymentAuthorityKey } = require("../src/main/deployment-identity.cjs"); const { InventoryReviewService } = require("../src/main/inventory-review-service.cjs"); function workload(id, options = {}) { return { workloadId: id, serverId: options.serverId || "unraid", displayName: options.name || id, status: options.status || (options.link ? "linked" : "suggested"), link: options.link || null, compose: { project: options.project || id, workingDir: options.root || `/mnt/user/appdata/${id}`, configFiles: options.files || [`/mnt/user/appdata/${id}/compose.yml`], services: ["app"] }, containers: options.noContainers ? [] : [{ id: `container-${id}`, name: id, running: options.running !== false }], runtime: { running: options.running !== false, health: options.health || "healthy" }, metadata: { sourceRepository: options.remote ?? "git@gitea.test:Jens/Portfolio.git", liveRevision: options.sha || "a".repeat(40), branch: options.branch || "main" }, candidates: options.candidates || [{ repositoryFullName: "Jens/Portfolio", score: 100, exact: true }], remoteFolderCandidate: id, }; } test("deployment identity is canonical across SSH and HTTPS remotes", () => { const ssh = deploymentIdentity({ workload: workload("one") }); const https = deploymentIdentity({ workload: workload("two", { remote: "https://gitea.test/Jens/Portfolio.git" }) }); assert.equal(ssh.repository, https.repository); assert.equal(deploymentAuthorityKey(ssh), deploymentAuthorityKey({ ...https, serverId: ssh.serverId, environment: ssh.environment })); }); test("running duplicate is authoritative and historical folder is never linked", () => { const active = workload("portfolio-current", { link: { profileId: "profile", repositoryFullName: "Jens/Portfolio" } }); const historical = workload("portfolio-old", { running: false, root: "/mnt/user/appdata/portfolio/releases/old", link: { profileId: "profile-old", repositoryFullName: "Jens/Portfolio" } }); const result = classifyInventory([historical, active], [{ id: "profile", environment: "production" }, { id: "profile-old", environment: "production" }]); assert.equal(result.find((item) => item.workloadId === "portfolio-current").authoritative, true); assert.equal(result.find((item) => item.workloadId === "portfolio-old").classification.type, "duplicate"); assert.equal(result.find((item) => item.workloadId === "portfolio-old").link, null); }); for (const [label, item, expected] of [ ["backup Compose folder", workload("backup", { root: "/mnt/user/appdata/portfolio-backup", running: false }), "backup"], ["release directory", workload("release", { root: "/mnt/user/appdata/portfolio/releases/a1", running: false }), "release-folder"], ["staging workload", workload("stage", { root: "/mnt/user/appdata/portfolio-staging" }), "staging"], ["stopped legitimate app", workload("stopped", { running: false }), "stopped-application"], ["Compose without container", workload("historical", { noContainers: true, running: false }), "historical-compose"], ["container without repository", workload("orphan", { remote: "", candidates: [], status: "unmatched" }), "orphan-container"], ["system container", workload("infra", { name: "watchtower", remote: "", candidates: [] }), "system-container"], ["ambiguous exact matches", workload("ambiguous", { status: "ambiguous", candidates: [{ repositoryFullName: "Jens/A", score: 100, exact: true }, { repositoryFullName: "Jens/B", score: 100, exact: true }] }), "ambiguous"], ["profile whose server workload disappeared", { ...workload("stale", { running: false, noContainers: true, link: { profileId: "profile-stale", repositoryFullName: "Jens/Portfolio" } }), metadata: { sourceRepository: "git@gitea.test:Jens/Portfolio.git", branch: "main", staleLink: true } }, "stale-link"], ]) test(`inventory classifies ${label}`, () => { assert.equal(classifyInventory([item])[0].classification.type, expected); }); test("multi-instance environments remain separate authority groups", () => { const a = workload("instance-a", { link: { profileId: "a", repositoryFullName: "Jens/Portfolio" } }); const b = workload("instance-b", { link: { profileId: "b", repositoryFullName: "Jens/Portfolio" } }); const result = classifyInventory([a, b], [{ id: "a", environment: "production" }, { id: "b", environment: "staging" }]); assert.equal(result.filter((item) => item.classification.type === "duplicate").length, 0); }); test("stored review decision becomes stale when remote evidence changes", () => { const original = classifyInventory([workload("review")])[0]; const decision = { workloadId: original.workloadId, evidenceHash: original.evidenceHash, action: "ignore", reason: "Known external workload" }; const unchanged = classifyInventory([workload("review")], [], [decision])[0]; const changed = classifyInventory([workload("review", { remote: "git@gitea.test:Jens/Renamed.git" })], [], [decision])[0]; assert.equal(unchanged.reviewDecision.action, "ignore"); assert.equal(changed.reviewDecision, null); assert.equal(changed.reviewDecisionStale, true); }); test("review decisions drive classification and explicit authority", () => { const primary = classifyInventory([workload("primary")])[0]; const secondary = classifyInventory([workload("secondary")])[0]; const decisions = [ { workloadId: primary.workloadId, evidenceHash: primary.evidenceHash, action: "mark-historical", reason: "Retained rollback definition" }, { workloadId: secondary.workloadId, evidenceHash: secondary.evidenceHash, action: "select-authoritative", reason: "Verified production instance" }, ]; const result = classifyInventory([workload("primary"), workload("secondary")], [], decisions); assert.equal(result.find((item) => item.workloadId === "primary").classification.type, "duplicate"); assert.equal(result.find((item) => item.workloadId === "secondary").authoritative, true); }); test("ignore and monitor-only decisions stay evidence-bound", () => { const ignored = classifyInventory([workload("ignored")])[0]; const monitoredSource = workload("monitored", { remote: "git@gitea.test:Jens/Monitored.git", candidates: [{ repositoryFullName: "Jens/Monitored", score: 100, exact: true }] }); const monitored = classifyInventory([monitoredSource])[0]; const result = classifyInventory([workload("ignored"), monitoredSource], [], [ { workloadId: ignored.workloadId, evidenceHash: ignored.evidenceHash, action: "ignore", reason: "Managed by another platform" }, { workloadId: monitored.workloadId, evidenceHash: monitored.evidenceHash, action: "monitor-only", reason: "Visibility without deployment ownership" }, ]); assert.equal(result.find((item) => item.workloadId === "ignored").classification.type, "manually-excluded"); assert.equal(result.find((item) => item.workloadId === "monitored").classification.type, "monitor-only"); }); test("review service requires reason, exact plan and recovery snapshot", async () => { const decisions = []; const store = { getInventoryReviewDecisions: () => decisions, createRecoverySnapshot: async () => ({ filePath: "snapshot.json" }), saveInventoryReviewDecision: async (_server, decision) => { decisions.push(decision); return decision; } }; const service = new InventoryReviewService({ store }); const item = classifyInventory([workload("review")])[0]; assert.throws(() => service.preview({ serverId: "unraid", workload: item, action: "ignore", reason: "no" }), (error) => error.code === "INVENTORY_REVIEW_REASON_REQUIRED"); const plan = service.preview({ serverId: "unraid", workload: item, action: "ignore", reason: "Managed outside ForgeFlow" }); await assert.rejects(service.apply({ plan }), (error) => error.code === "INVENTORY_REVIEW_PLAN_REQUIRED"); const result = await service.apply({ plan, expectedPlanId: plan.id }); assert.equal(result.snapshot.filePath, "snapshot.json"); assert.equal(decisions[0].evidenceHash, item.evidenceHash); }); test("large inventory classification is deterministic and bounded", () => { const input = Array.from({ length: 1200 }, (_, index) => workload(`app-${index}`, { remote: `git@gitea.test:Jens/App-${index}.git`, candidates: [{ repositoryFullName: `Jens/App-${index}`, score: 100, exact: true }] })); const started = Date.now(); const result = classifyInventory(input); assert.equal(result.length, 1200); assert.ok(Date.now() - started < 2000); assert.equal(new Set(result.map((item) => item.evidenceHash)).size, 1200); }); test("evidence hash changes for runtime, Compose and candidate changes", () => { const identity = deploymentIdentity({ workload: workload("hash") }); const one = deploymentEvidenceHash(identity, { running: true, files: ["compose.yml"] }); const two = deploymentEvidenceHash(identity, { running: false, files: ["compose.yml"] }); assert.notEqual(one, two); });