import { describe, expect, it } from "vitest"; import { runSetup, type ClientConfigurationResult, type SetupClient, type SetupHost, } from "../src/application/Setup.js"; import type { DoctorCheck } from "../src/application/Doctor.js"; class FakeSetupHost implements SetupHost { readonly platform = "darwin" as const; readonly nodeVersion = "22.4.0"; version: string | undefined = "14.5"; brew = false; hopper: string | undefined; homebrewInstallSucceeds = true; hopperInstallSucceeds = true; skill: "installed" | "unchanged" | "failed" = "installed"; clients: readonly SetupClient[] = []; clientResults = new Map(); homebrewInstalls = 0; hopperInstalls = 0; configurations = 0; configuredHopperPaths: string[] = []; macosVersion = (): Promise => Promise.resolve(this.version); hasHomebrew = (): Promise => Promise.resolve(this.brew); installHomebrew = (): Promise => { this.homebrewInstalls += 1; if (this.homebrewInstallSucceeds) this.brew = true; return Promise.resolve(this.homebrewInstallSucceeds); }; hopperPath = (): Promise => Promise.resolve(this.hopper); installHopper = (): Promise => { this.hopperInstalls += 1; if (this.hopperInstallSucceeds) this.hopper = "/manual/Hopper"; return Promise.resolve(this.hopperInstallSucceeds); }; detectedClients = (): Promise => Promise.resolve(this.clients); configureClient = ( client: SetupClient, hopperPath: string, ): Promise => { this.configurations += 1; this.configuredHopperPaths.push(hopperPath); return Promise.resolve( this.clientResults.get(client.name) ?? { status: "configured", backupPath: `${client.configPath}.backup`, }, ); }; installSkill = (): Promise<"installed" | "unchanged" | "failed"> => Promise.resolve(this.skill); doctor = (): Promise<{ healthy: boolean; hopperPath?: string; checks: readonly DoctorCheck[]; }> => Promise.resolve({ healthy: this.brew && this.hopper !== undefined, ...(this.hopper === undefined ? {} : { hopperPath: this.hopper }), checks: [], }); } describe("setup workflow", () => { it("installs Homebrew and Hopper on a clean approved machine", async () => { const host = new FakeSetupHost(); const result = await runSetup(true, host); expect(result.status).toBe("ready"); expect(result.actions).toEqual([ "installed_homebrew", "installed_hopper", "installed_skill", ]); }); it("requires --yes before installing Homebrew", async () => { const host = new FakeSetupHost(); const result = await runSetup(false, host); expect(result.status).toBe("needs_human"); expect(host.homebrewInstalls).toBe(0); expect(result.remediation).toContain("--yes"); }); it("requires --yes before installing Hopper", async () => { const host = new FakeSetupHost(); host.brew = true; const result = await runSetup(false, host); expect(result.status).toBe("needs_human"); expect(host.hopperInstalls).toBe(0); expect(result.remediation).toContain("--yes"); }); it("accepts existing Homebrew and manually installed Hopper", async () => { const host = new FakeSetupHost(); host.brew = true; host.hopper = "/Applications/Manual Hopper"; expect((await runSetup(true, host)).actions).toEqual(["installed_skill"]); expect(host.hopperInstalls).toBe(0); expect(host.configuredHopperPaths).toEqual([]); }); it.each(["homebrew", "hopper"] as const)( "returns needs_human when %s installation is interrupted", async (stage) => { const host = new FakeSetupHost(); if (stage === "homebrew") host.homebrewInstallSucceeds = false; else { host.brew = true; host.hopperInstallSucceeds = false; } const result = await runSetup(true, host); expect(result.status).toBe("needs_human"); expect(result.remediation).toContain("interrupted"); }, ); it("configures every detected client and reports backup paths", async () => { const host = new FakeSetupHost(); host.brew = true; host.hopper = "/Hopper"; host.clients = [ { name: "claude", configPath: "/claude.json" }, { name: "cursor", configPath: "/cursor.json" }, ]; const result = await runSetup(true, host); expect(Object.keys(result.clients)).toEqual(["claude", "cursor"]); expect(host.configurations).toBe(2); expect(host.configuredHopperPaths).toEqual(["/Hopper", "/Hopper"]); }); it.each(["backup", "write", "readback"] as const)( "stops with precise remediation after a %s failure", async (reason) => { const host = new FakeSetupHost(); host.brew = true; host.hopper = "/Hopper"; host.clients = [{ name: "cursor", configPath: "/cursor.json" }]; host.clientResults.set("cursor", { status: "failed", reason }); const result = await runSetup(true, host); expect(result.status).toBe("needs_human"); expect(result.remediation).toContain(reason); }, ); it("is idempotent on repeated setup", async () => { const host = new FakeSetupHost(); host.brew = true; host.hopper = "/Hopper"; host.skill = "unchanged"; host.clients = [{ name: "cursor", configPath: "/cursor.json" }]; host.clientResults.set("cursor", { status: "unchanged" }); const first = await runSetup(true, host); const second = await runSetup(true, host); expect(first.actions).toEqual([]); expect(second.actions).toEqual([]); expect(host.homebrewInstalls).toBe(0); expect(host.hopperInstalls).toBe(0); }); it.each(["11.7", undefined])( "rejects unsupported macOS %s before mutation", async (version) => { const host = new FakeSetupHost(); host.version = version; expect((await runSetup(true, host)).status).toBe("needs_human"); expect(host.homebrewInstalls).toBe(0); }, ); });