// Integration-style tests for src/stow.ts. These exercise the real `stow` binary and // real filesystem calls, so each test: // - runs against a throwaway $HOME (a fresh mkdtempSync dir), never the real one // - is skipped when `stow` isn't installed (confirmed missing on a bare host) — // run the full suite for real via `./docker/test.sh "npm test"` import { test, after } from "node:test"; import assert from "node:assert"; import { execFileSync } from "node:child_process"; import { existsSync, lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; function hasStow(): boolean { try { execFileSync("which", ["stow"], { stdio: "ignore" }); return true; } catch { return false; } } const skip = !hasStow(); // os.homedir() reads $HOME on POSIX, so setting this before importing stow.js // (whose module-level consts are computed from homedir() at import time) sandboxes // every path the module touches, with no changes to production code. const fakeHome = mkdtempSync(join(tmpdir(), "dotfiles-test-home-")); process.env.HOME = fakeHome; after(() => rmSync(fakeHome, { recursive: true, force: true })); const { backupConflicts, restoreLatestBackup, restow, stow, unstow, writeState, readState, } = skip ? ({} as any) : await import("../src/stow.js"); const dirs = ["hyprland", "waybar", "kitty", "hyprpaper"]; const hyprlandTarget = join(fakeHome, ".config", "hypr", "hyprland.lua"); const wallpaperTarget = join(fakeHome, ".config", "hypr", "wallpapers", "wallpaper.png"); test("seeds a pre-existing conflicting file", { skip }, () => { mkdirSync(join(fakeHome, ".config", "hypr"), { recursive: true }); writeFileSync(hyprlandTarget, "old config\n"); assert.strictEqual(readFileSync(hyprlandTarget, "utf8").trim(), "old config"); }); test("backupConflicts moves the pre-existing file out of the way", { skip }, () => { const backupDir = backupConflicts(dirs); assert.ok(backupDir, "expected a backup dir since a conflict existed"); assert.ok(!existsSync(hyprlandTarget), "conflicting file should have been moved out of the way"); }); test("stow symlinks the chosen packages into $HOME", { skip }, () => { stow(dirs); assert.ok(lstatSync(hyprlandTarget).isSymbolicLink(), "hyprland.lua should now be a symlink"); assert.ok(existsSync(wallpaperTarget), "wallpaper.png should be reachable through the stowed dir"); }); test("writeState/readState round-trip the chosen dirs", { skip }, () => { writeState({ dirs }); assert.deepStrictEqual(readState().dirs, dirs); }); test("restow keeps the symlinks intact", { skip }, () => { restow(dirs); assert.ok(lstatSync(hyprlandTarget).isSymbolicLink(), "restow should keep the symlink"); assert.ok(existsSync(wallpaperTarget), "restow should keep wallpaper reachable"); }); test("unstow removes the symlinks", { skip }, () => { unstow(dirs); assert.ok( !existsSync(hyprlandTarget) || !lstatSync(hyprlandTarget).isSymbolicLink(), "unstow should remove the symlink", ); assert.ok(!existsSync(wallpaperTarget), "unstow should remove the wallpaper symlink"); }); test("restoreLatestBackup brings back the original file content", { skip }, () => { const restored = restoreLatestBackup(); assert.ok(restored, "expected a backup to restore"); assert.strictEqual(readFileSync(hyprlandTarget, "utf8").trim(), "old config", "original content should be restored"); });