/** * editor.test.ts * -------------- * Unit tests for the `--edit` helper. The spawn path is not exercised * (would actually launch $EDITOR); instead we test: * - the pure argv-construction helper, and * - the two refusal paths ($EDITOR unset, file missing). * * Run via `bun test`. */ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { editConfig, editorCommand } from "../src/editor.ts"; import { CliError } from "../src/errors.ts"; describe("editorCommand", () => { test("builds 'sh -c \"$@\"' argv with -- placeholder and path", () => { const argv = editorCommand("vim", "/tmp/x.json"); expect(argv).toEqual(["sh", "-c", 'vim "$@"', "--", "/tmp/x.json"]); }); test("interpolates the editor verbatim so shell word-splits multi-word values", () => { const argv = editorCommand("code --wait", "/path with space.json"); expect(argv).toEqual([ "sh", "-c", 'code --wait "$@"', "--", "/path with space.json", ]); }); }); describe("editConfig", () => { let TMP: string; let savedEditor: string | undefined; beforeAll(() => { TMP = mkdtempSync(join(tmpdir(), "move-edit-test-")); }); afterAll(() => { rmSync(TMP, { recursive: true, force: true }); }); beforeEach(() => { savedEditor = process.env.EDITOR; }); afterEach(() => { if (savedEditor === undefined) delete process.env.EDITOR; else process.env.EDITOR = savedEditor; }); test("throws CliError when $EDITOR is unset", () => { delete process.env.EDITOR; expect(() => editConfig(join(TMP, "any.json"))).toThrow(CliError); }); test("throws CliError when $EDITOR is empty", () => { process.env.EDITOR = ""; expect(() => editConfig(join(TMP, "any.json"))).toThrow(CliError); }); test("throws CliError when the config file does not exist", () => { // Use a benign editor command that we never actually reach (the // existence check fires first). process.env.EDITOR = "true"; const missing = join(TMP, "no-such-file.json"); expect(() => editConfig(missing)).toThrow(/no config file at/); }); test("error message names the missing path", () => { process.env.EDITOR = "true"; const missing = join(TMP, "missing.json"); expect(() => editConfig(missing)).toThrow(new RegExp(missing.replace(/[.]/g, "\\."))); }); test("error message mentions 'reinstall' as a recovery hint", () => { process.env.EDITOR = "true"; expect(() => editConfig(join(TMP, "x.json"))).toThrow(/reinstall/); }); test("$EDITOR unset error explicitly mentions setting it", () => { delete process.env.EDITOR; expect(() => editConfig(join(TMP, "x.json"))).toThrow(/export EDITOR/); }); // Success path: $EDITOR set, file exists. The editor IS spawned and we // then call process.exit() — which kills the test process. So we don't // exercise this code path in unit tests; the manual smoke test in // dev-setup verifies end-to-end behavior instead. test("placeholder: success path is verified via manual `EDITOR=true move -e` run", () => { // Intentionally empty assertion. See comment above. expect(true).toBe(true); // Ensure the fixture path is referenced so this test isn't seen // as truly empty if the fixture system ever needs assertion. writeFileSync(join(TMP, "exists.json"), "{}"); }); });