/** * Tests for workspace migration `053-release-notes-acp-codex`. * * Pins the four idempotency paths covered by the in-file HTML marker logic * the migration uses to guard against duplicate appends (crash between * `appendFileSync` and the runner's checkpoint promotion, or hand-edits to * UPDATES.md after a partial prior run): * * (a) Empty workspace — UPDATES.md is created with the marker + body. * (b) Existing UPDATES.md without the marker — append with one blank line * between prior content and the new note. * (c) Existing UPDATES.md with the marker already present — byte-identical * re-run (asserted twice). * (d) Existing UPDATES.md ending with `\n` vs `\n\n` — both produce exactly * one blank line between old and new content (no triple-newline, no * missing separator). */ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "bun:test"; import { releaseNotesAcpCodexMigration } from "../workspace/migrations/053-release-notes-acp-codex.js"; const MIGRATION_ID = "053-release-notes-acp-codex"; const MARKER = ``; let testRoot: string; let workspaceDir: string; beforeAll(() => { testRoot = mkdtempSync(join(tmpdir(), "migration-053-test-")); }); afterAll(() => { rmSync(testRoot, { recursive: true, force: true }); }); beforeEach(() => { workspaceDir = mkdtempSync(join(testRoot, "ws-")); }); afterEach(() => { rmSync(workspaceDir, { recursive: true, force: true }); }); function updatesPath(): string { return join(workspaceDir, "UPDATES.md"); } describe("workspace migration 053-release-notes-acp-codex", () => { test("has the correct id and description", () => { expect(releaseNotesAcpCodexMigration.id).toBe(MIGRATION_ID); expect(releaseNotesAcpCodexMigration.description).toContain("ACP"); }); // ─── (a) Empty workspace ────────────────────────────────────────── test("creates UPDATES.md with marker and key copy when file is absent", () => { expect(existsSync(updatesPath())).toBe(false); releaseNotesAcpCodexMigration.run(workspaceDir); expect(existsSync(updatesPath())).toBe(true); const content = readFileSync(updatesPath(), "utf-8"); expect(content).toContain(MARKER); expect(content).toContain("ACP"); expect(content).toContain("`claude`"); expect(content).toContain("`codex`"); expect(content).toContain("acp_steer"); expect(content).toContain("acp.enabled"); expect(content).toContain("@zed-industries/codex-acp"); expect(content).toContain("@agentclientprotocol/claude-agent-acp"); // First-time write has no leading separator — starts directly with the marker. expect(content.startsWith(MARKER)).toBe(true); }); // ─── (b) Existing UPDATES.md without the marker ─────────────────── test("appends to existing UPDATES.md when marker is absent, preserving prior content with one blank line between blocks", () => { const priorContent = "## Earlier note\n\nSomething the assistant wrote before.\n"; writeFileSync(updatesPath(), priorContent, "utf-8"); releaseNotesAcpCodexMigration.run(workspaceDir); const content = readFileSync(updatesPath(), "utf-8"); // Prior content preserved. expect(content.startsWith(priorContent)).toBe(true); // Marker present once. expect(content.split(MARKER).length - 1).toBe(1); // Exactly one blank line between old and new content: prior ends with // `\n`, so we expect a single `\n` separator added, producing `\n\n` // (one blank line) immediately before the marker. expect(content).toBe( `${priorContent}\n${content.slice(priorContent.length + 1)}`, ); // The appended block starts at the marker. expect(content.slice(priorContent.length)).toMatch( /^\n