import { afterEach, expect, test } from "bun:test"; import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { publishPlatformInfraGiteaGitOps } from "./publish-platform-infra-gitea-gitops.mjs"; const roots: string[] = []; afterEach(() => { while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); }); test("independent desired publisher is idempotent and preserves unrelated GitOps files", () => { const fixture = createFixture(); checkout(fixture.sourceWork, fixture.commits.second); const first = publish(fixture, fixture.commits.second); expect(first).toMatchObject({ ok: true, status: "published", changed: true, pushAttempts: 1 }); expect(show(fixture.gitOpsBare, fixture.delivery.branch, "deploy/gitops/unidesk-host/todo-note.yaml")).toBe("existing todo\n"); expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.desiredManifestPath)).toContain("kind: PersistentVolumeClaim"); expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.bootstrapApplicationPath)).toContain("name: platform-infra-gitea-nc01"); const second = publish(fixture, fixture.commits.second); expect(second).toMatchObject({ ok: true, status: "idempotent", changed: false, pushAttempts: 0 }); }); test("CAS retries from a fresh head and preserves a concurrent todo-note update", () => { const fixture = createFixture(); checkout(fixture.sourceWork, fixture.commits.second); let raced = false; const result = publishPlatformInfraGiteaGitOps({ delivery: fixture.delivery, desiredManifest: fixture.desiredManifest, sourceRoot: fixture.sourceWork, sourceCommit: fixture.commits.second, worktree: fixture.publisherWorktree, beforePush: ({ attempt }: { attempt: number }) => { if (attempt !== 1 || raced) return; raced = true; pushConcurrentFile(fixture.root, fixture.gitOpsBare, fixture.delivery.branch); }, }); expect(result).toMatchObject({ ok: true, status: "published", changed: true, pushAttempts: 2 }); expect(show(fixture.gitOpsBare, fixture.delivery.branch, "deploy/gitops/unidesk-host/concurrent-todo.yaml")).toBe("concurrent todo\n"); expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second); }); test("out-of-order source is superseded and divergent history fails closed", () => { const fixture = createFixture(); checkout(fixture.sourceWork, fixture.commits.second); expect(publish(fixture, fixture.commits.second)).toMatchObject({ status: "published" }); checkout(fixture.sourceWork, fixture.commits.first); expect(publish(fixture, fixture.commits.first)).toMatchObject({ ok: true, status: "superseded", authoritativeSourceCommit: fixture.commits.second, changed: false, }); expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second); checkout(fixture.sourceWork, fixture.commits.diverged); expect(() => publish(fixture, fixture.commits.diverged)).toThrow(/diverges from authoritative GitOps source/); expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second); }); test("same source commit with conflicting stored materialization hashes fails closed", () => { const fixture = createFixture(); checkout(fixture.sourceWork, fixture.commits.second); expect(publish(fixture, fixture.commits.second)).toMatchObject({ status: "published" }); mutateGitOpsState(fixture.root, fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath, (state) => ({ ...state, desiredManifestSha256: "f".repeat(64), })); expect(() => publish(fixture, fixture.commits.second)).toThrow(/same source commit .* conflicting GitOps materialization state/); const state = JSON.parse(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)); expect(state.desiredManifestSha256).toBe("f".repeat(64)); }); function createFixture() { const root = mkdtempSync(join(tmpdir(), "unidesk-platform-infra-gitea-publish-")); roots.push(root); const sourceWork = join(root, "source-work"); const sourceBare = join(root, "source.git"); const gitOpsSeed = join(root, "gitops-seed"); const gitOpsBare = join(root, "gitops.git"); mkdirSync(sourceWork); git(["init", "-b", "master"], sourceWork); identity(sourceWork); write(sourceWork, "source.txt", "first\n"); git(["add", "source.txt"], sourceWork); git(["commit", "-m", "first"], sourceWork); const first = git(["rev-parse", "HEAD"], sourceWork).stdout.trim(); write(sourceWork, "source.txt", "second\n"); git(["commit", "-am", "second"], sourceWork); const second = git(["rev-parse", "HEAD"], sourceWork).stdout.trim(); git(["checkout", "-b", "diverged", first], sourceWork); write(sourceWork, "source.txt", "diverged\n"); git(["commit", "-am", "diverged"], sourceWork); const diverged = git(["rev-parse", "HEAD"], sourceWork).stdout.trim(); git(["checkout", "master"], sourceWork); git(["init", "--bare", sourceBare], root); git(["remote", "add", "origin", fileUrl(sourceBare)], sourceWork); git(["push", "origin", "master"], sourceWork); const prefix = "refs/unidesk/snapshots/test-unidesk-master"; for (const commit of [first, second, diverged]) git(["push", "origin", `${commit}:${prefix}/${commit}`], sourceWork); mkdirSync(gitOpsSeed); git(["init", "-b", "unidesk-host-gitops"], gitOpsSeed); identity(gitOpsSeed); write(gitOpsSeed, "deploy/gitops/unidesk-host/todo-note.yaml", "existing todo\n"); git(["add", "."], gitOpsSeed); git(["commit", "-m", "seed gitops"], gitOpsSeed); git(["init", "--bare", gitOpsBare], root); git(["remote", "add", "origin", fileUrl(gitOpsBare)], gitOpsSeed); git(["push", "origin", "unidesk-host-gitops"], gitOpsSeed); const delivery = { enabled: true, targetId: "NC01", readUrl: fileUrl(gitOpsBare), writeUrl: fileUrl(gitOpsBare), branch: "unidesk-host-gitops", sourceSnapshotPrefix: prefix, desiredManifestPath: "deploy/gitops/platform-infra/gitea-nc01/resources.yaml", bootstrapApplicationPath: "deploy/gitops/unidesk-host/platform-infra-gitea-nc01-application.yaml", releaseStatePath: "deploy/gitops-state/platform-infra/gitea-nc01.json", application: { name: "platform-infra-gitea-nc01", namespace: "argocd", project: "default", repoUrl: fileUrl(gitOpsBare), targetRevision: "unidesk-host-gitops", path: "deploy/gitops/platform-infra/gitea-nc01", destinationNamespace: "devops-infra", automated: true, }, author: { name: "UniDesk Test", email: "unidesk-test@example.invalid" }, cas: { maxAttempts: 3 }, }; return { root, sourceWork, gitOpsBare, publisherWorktree: join(root, "publisher-worktree"), commits: { first, second, diverged }, delivery, desiredManifest: `--- apiVersion: v1 kind: ServiceAccount metadata: name: bridge --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: inbox --- apiVersion: apps/v1 kind: Deployment metadata: name: bridge `, }; } function publish(fixture: ReturnType, sourceCommit: string) { return publishPlatformInfraGiteaGitOps({ delivery: fixture.delivery, desiredManifest: fixture.desiredManifest, sourceRoot: fixture.sourceWork, sourceCommit, worktree: fixture.publisherWorktree, }); } function pushConcurrentFile(root: string, remote: string, branch: string) { const worktree = join(root, "concurrent-gitops"); rmSync(worktree, { recursive: true, force: true }); git(["clone", "--branch", branch, fileUrl(remote), worktree], root); identity(worktree); write(worktree, "deploy/gitops/unidesk-host/concurrent-todo.yaml", "concurrent todo\n"); git(["add", "."], worktree); git(["commit", "-m", "concurrent todo update"], worktree); git(["push", "origin", branch], worktree); } function mutateGitOpsState(root: string, remote: string, branch: string, relativePath: string, mutate: (state: Record) => Record) { const worktree = join(root, "mutate-gitops-state"); rmSync(worktree, { recursive: true, force: true }); git(["clone", "--branch", branch, fileUrl(remote), worktree], root); identity(worktree); const path = join(worktree, relativePath); writeFileSync(path, `${JSON.stringify(mutate(JSON.parse(readFileSync(path, "utf8"))), null, 2)}\n`); git(["add", "--", relativePath], worktree); git(["commit", "-m", "inject conflicting materialization state"], worktree); git(["push", "origin", branch], worktree); } function checkout(worktree: string, commit: string) { git(["checkout", "--detach", commit], worktree); } function show(remote: string, branch: string, path: string): string { const result = git(["--git-dir", remote, "show", `${branch}:${path}`], dirname(remote)); return result.stdout; } function write(root: string, relativePath: string, content: string) { const path = join(root, relativePath); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, content); expect(readFileSync(path, "utf8")).toBe(content); } function identity(root: string) { git(["config", "user.name", "UniDesk Test"], root); git(["config", "user.email", "unidesk-test@example.invalid"], root); } function fileUrl(path: string): string { return `file://${path}`; } function git(args: string[], cwd: string) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`); return result; }