mirror of https://github.com/garrytan/gstack.git
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
import { describe, test, expect, afterEach, beforeEach } from "bun:test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execSync } from "node:child_process";
|
|
import { makeTmpDir, rmTmpDir } from "../helpers.js";
|
|
import {
|
|
findGitRoot,
|
|
findLocalInstall,
|
|
isGitRepo,
|
|
isInstalled,
|
|
readVersion,
|
|
resolveProjectInstallPaths,
|
|
} from "../../src/lib/paths.js";
|
|
import type { InstallPaths } from "../../src/lib/paths.js";
|
|
|
|
describe("findGitRoot", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
test("returns null when not in a git repo", () => {
|
|
expect(findGitRoot(tmp)).toBeNull();
|
|
});
|
|
|
|
test("returns repo root from repo root", () => {
|
|
execSync("git init -q", { cwd: tmp });
|
|
const root = findGitRoot(tmp);
|
|
expect(root).toBeTruthy();
|
|
expect(fs.realpathSync(root!)).toBe(fs.realpathSync(tmp));
|
|
});
|
|
|
|
test("walks up from subdirectory", () => {
|
|
execSync("git init -q", { cwd: tmp });
|
|
const sub = path.join(tmp, "a", "b", "c");
|
|
fs.mkdirSync(sub, { recursive: true });
|
|
const root = findGitRoot(sub);
|
|
expect(root).toBeTruthy();
|
|
expect(fs.realpathSync(root!)).toBe(fs.realpathSync(tmp));
|
|
});
|
|
});
|
|
|
|
describe("isGitRepo", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
test("false for non-repo", () => {
|
|
expect(isGitRepo(tmp)).toBe(false);
|
|
});
|
|
|
|
test("true for repo", () => {
|
|
execSync("git init -q", { cwd: tmp });
|
|
expect(isGitRepo(tmp)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isInstalled", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
function makePaths(gstackDir: string): InstallPaths {
|
|
return {
|
|
home: tmp,
|
|
claudeDir: path.join(tmp, ".claude"),
|
|
claudeSkillsDir: path.join(tmp, ".claude", "skills"),
|
|
gstackDir,
|
|
gstackStateDir: path.join(tmp, ".gstack"),
|
|
claudeMd: path.join(tmp, ".claude", "CLAUDE.md"),
|
|
};
|
|
}
|
|
|
|
test("false when directory is missing", () => {
|
|
expect(isInstalled(makePaths(path.join(tmp, "nope")))).toBe(false);
|
|
});
|
|
|
|
test("true for real directory", () => {
|
|
const dir = path.join(tmp, "gstack");
|
|
fs.mkdirSync(dir);
|
|
expect(isInstalled(makePaths(dir))).toBe(true);
|
|
});
|
|
|
|
test("true for symlink to directory", () => {
|
|
const target = path.join(tmp, "target");
|
|
const link = path.join(tmp, "link");
|
|
fs.mkdirSync(target);
|
|
fs.symlinkSync(target, link);
|
|
expect(isInstalled(makePaths(link))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolveProjectInstallPaths", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
test("roots install inside the project, not home", () => {
|
|
const p = resolveProjectInstallPaths(tmp);
|
|
expect(p.gstackDir).toBe(path.join(tmp, ".claude", "skills", "gstack"));
|
|
expect(p.claudeMd).toBe(path.join(tmp, "CLAUDE.md"));
|
|
});
|
|
});
|
|
|
|
describe("findLocalInstall", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
test("returns null when no local install is present", () => {
|
|
expect(findLocalInstall(tmp)).toBeNull();
|
|
});
|
|
|
|
test("finds install at cwd", () => {
|
|
fs.mkdirSync(path.join(tmp, ".claude", "skills", "gstack"), { recursive: true });
|
|
const found = findLocalInstall(tmp);
|
|
expect(found).toBeTruthy();
|
|
expect(fs.realpathSync(found!.gstackDir)).toBe(
|
|
fs.realpathSync(path.join(tmp, ".claude", "skills", "gstack")),
|
|
);
|
|
});
|
|
|
|
test("walks up to find install in parent", () => {
|
|
fs.mkdirSync(path.join(tmp, ".claude", "skills", "gstack"), { recursive: true });
|
|
const sub = path.join(tmp, "a", "b");
|
|
fs.mkdirSync(sub, { recursive: true });
|
|
expect(findLocalInstall(sub)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("readVersion", () => {
|
|
let tmp: string;
|
|
beforeEach(() => (tmp = makeTmpDir()));
|
|
afterEach(() => rmTmpDir(tmp));
|
|
|
|
test("returns null when VERSION is missing", () => {
|
|
const paths: InstallPaths = {
|
|
home: tmp,
|
|
claudeDir: tmp,
|
|
claudeSkillsDir: tmp,
|
|
gstackDir: tmp,
|
|
gstackStateDir: tmp,
|
|
claudeMd: path.join(tmp, "CLAUDE.md"),
|
|
};
|
|
expect(readVersion(paths)).toBeNull();
|
|
});
|
|
|
|
test("reads and trims VERSION file", () => {
|
|
fs.writeFileSync(path.join(tmp, "VERSION"), "1.2.3.4\n");
|
|
const paths: InstallPaths = {
|
|
home: tmp,
|
|
claudeDir: tmp,
|
|
claudeSkillsDir: tmp,
|
|
gstackDir: tmp,
|
|
gstackStateDir: tmp,
|
|
claudeMd: path.join(tmp, "CLAUDE.md"),
|
|
};
|
|
expect(readVersion(paths)).toBe("1.2.3.4");
|
|
});
|
|
});
|