Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/worktree-session.test.mjs
T

298 lines
10 KiB
JavaScript

// @vitest-environment node
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, it } from "vitest";
import {
assertCleanupEligibility,
branchSlug,
defaultWorktreePath,
editEnvironment,
finishSession,
normalMergeContainsHead,
parseWorktreeList,
pullEnvironment,
resolveAgeKeyFile,
startSession,
validateDecryptedEnv,
validateSessionBranch,
writeEnvAtomically,
} from "./worktree-session.mjs";
const fixtures = [];
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hivegrid-worktree-session-"));
fixtures.push(root);
return root;
}
afterEach(() => {
while (fixtures.length) {
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
}
});
describe("worktree session policy", () => {
it("parses branch and detached worktree records", () => {
assert.deepEqual(
parseWorktreeList(`worktree /repo
HEAD aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
branch refs/heads/main
worktree /repo-feature
HEAD bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
branch refs/heads/feat/example
worktree /repo-detached
HEAD cccccccccccccccccccccccccccccccccccccccc
detached
`),
[
{
path: "/repo",
head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
branch: "main",
detached: false,
},
{
path: "/repo-feature",
head: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
branch: "feat/example",
detached: false,
},
{
path: "/repo-detached",
head: "cccccccccccccccccccccccccccccccccccccccc",
branch: null,
detached: true,
},
]
);
});
it("derives a stable sibling worktree path and rejects main", () => {
assert.equal(branchSlug("fix/KWP details"), "fix-KWP-details");
assert.equal(defaultWorktreePath("/workspace/HiveGrid-Cloud", "fix/KWP-details"), "/workspace/HiveGrid-Cloud-fix-KWP-details");
assert.throws(() => validateSessionBranch("main"), /must never be main/);
assert.throws(() => validateSessionBranch(" fix/example"), /trimmed session branch/);
});
it("rejects a relative explicit worktree path before fetching or creating anything", () => {
assert.throws(
() =>
startSession({
branch: "fix/example",
requestedPath: "relative/path",
cwd: fixture(),
}),
/absolute path/
);
});
it("prefers an explicit age key and otherwise uses the canonical repo path", () => {
const homeDir = fixture();
const explicit = path.join(homeDir, "explicit.txt");
const canonical = path.join(homeDir, ".config", "sops", "age", "keys.txt");
fs.mkdirSync(path.dirname(canonical), { recursive: true });
fs.writeFileSync(explicit, "key");
fs.writeFileSync(canonical, "key");
assert.equal(resolveAgeKeyFile({ env: { SOPS_AGE_KEY_FILE: explicit }, homeDir }), explicit);
assert.equal(resolveAgeKeyFile({ env: {}, homeDir }), canonical);
});
it("writes decrypted env atomically with mode 600", () => {
const root = fixture();
fs.writeFileSync(path.join(root, ".env"), "DATABASE_URL=old\n");
const envPath = writeEnvAtomically(root, "DATABASE_URL=postgres://local/hivegrid\nOTHER=value\n");
assert.equal(fs.readFileSync(envPath, "utf8"), "DATABASE_URL=postgres://local/hivegrid\nOTHER=value\n");
assert.equal(fs.statSync(envPath).mode & 0o777, 0o600);
assert.deepEqual(
fs.readdirSync(root).filter((name) => name.startsWith(".env.tmp-")),
[]
);
});
it("does not replace an existing env when decryption fails", () => {
const root = fixture();
const keyPath = path.join(root, ".config", "sops", "age", "keys.txt");
fs.mkdirSync(path.join(root, "secrets"), { recursive: true });
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
fs.writeFileSync(path.join(root, "secrets", "dev.sops.env"), "encrypted");
fs.writeFileSync(path.join(root, ".env"), "DATABASE_URL=keep\n");
fs.writeFileSync(keyPath, "key");
assert.throws(
() =>
pullEnvironment(root, {
env: {},
homeDir: root,
commandRunner() {
throw new Error("decrypt failed");
},
}),
/decrypt failed/
);
assert.equal(fs.readFileSync(path.join(root, ".env"), "utf8"), "DATABASE_URL=keep\n");
});
it("passes the canonical age key to sops without exposing env values", () => {
const root = fixture();
const keyPath = path.join(root, ".config", "sops", "age", "keys.txt");
fs.mkdirSync(path.join(root, "secrets"), { recursive: true });
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
fs.writeFileSync(path.join(root, "secrets", "dev.sops.env"), "encrypted");
fs.writeFileSync(keyPath, "key");
const envPath = pullEnvironment(root, {
env: {},
homeDir: root,
commandRunner(command, args, options) {
assert.equal(command, "sops");
assert.deepEqual(args, ["-d", path.join(root, "secrets", "dev.sops.env")]);
assert.equal(options.env.SOPS_AGE_KEY_FILE, keyPath);
return { stdout: "DATABASE_URL=postgres://local/hivegrid\n" };
},
});
assert.equal(fs.statSync(envPath).mode & 0o777, 0o600);
});
it("passes the canonical age key to interactive sops editing", () => {
const root = fixture();
const keyPath = path.join(root, ".config", "sops", "age", "keys.txt");
fs.mkdirSync(path.join(root, "secrets"), { recursive: true });
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
fs.writeFileSync(path.join(root, "secrets", "dev.sops.env"), "encrypted");
fs.writeFileSync(keyPath, "key");
editEnvironment(root, {
env: {},
homeDir: root,
commandRunner(command, args, options) {
assert.equal(command, "sops");
assert.deepEqual(args, [path.join(root, "secrets", "dev.sops.env")]);
assert.equal(options.env.SOPS_AGE_KEY_FILE, keyPath);
assert.equal(options.inherit, true);
return { status: 0 };
},
});
});
it("rejects empty or incomplete decrypted environments", () => {
assert.throws(() => validateDecryptedEnv(""), /empty environment/);
assert.throws(() => validateDecryptedEnv("OTHER=value\n"), /missing a non-empty DATABASE_URL/);
});
it("recognizes only a merge commit whose non-first parent is the feature head", () => {
const featureHead = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const mergeLog =
`cccccccccccccccccccccccccccccccccccccccc aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ${featureHead}\n` +
"dddddddddddddddddddddddddddddddddddddddd cccccccccccccccccccccccccccccccccccccccc";
assert.equal(normalMergeContainsHead(mergeLog, featureHead), true);
assert.equal(normalMergeContainsHead(mergeLog, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), false);
});
it("allows cleanup only for a clean, normally merged, unchanged branch", () => {
assert.doesNotThrow(() =>
assertCleanupEligibility({
branch: "fix/example",
status: "",
localHead: "feature-head",
remoteHead: "feature-head",
isAncestor: true,
hasNormalMergeCommit: true,
})
);
assert.throws(
() =>
assertCleanupEligibility({
branch: "fix/example",
status: " M file.ts",
localHead: "feature-head",
isAncestor: true,
hasNormalMergeCommit: true,
}),
/not clean/
);
assert.throws(
() =>
assertCleanupEligibility({
branch: "fix/example",
status: "",
localHead: "feature-head",
isAncestor: false,
hasNormalMergeCommit: false,
}),
/unmerged or squashed/
);
assert.throws(
() =>
assertCleanupEligibility({
branch: "fix/example",
status: "",
localHead: "feature-head",
isAncestor: true,
hasNormalMergeCommit: false,
}),
/not attached to main by a normal merge commit/
);
assert.throws(
() =>
assertCleanupEligibility({
branch: "fix/example",
status: "",
localHead: "feature-head",
remoteHead: "new-remote-head",
isAncestor: true,
hasNormalMergeCommit: true,
}),
/Remote branch .* moved/
);
});
it("removes a clean local worktree and branch after a real normal merge commit", async () => {
const root = fixture();
const origin = path.join(root, "origin.git");
const primary = path.join(root, "primary");
const session = path.join(root, "session");
const git = (cwd, ...args) =>
execFileSync("git", ["-C", cwd, ...args], {
encoding: "utf8",
stdio: "pipe",
});
execFileSync("git", ["init", "--bare", "--initial-branch=main", origin], { stdio: "pipe" });
execFileSync("git", ["clone", origin, primary], { stdio: "pipe" });
git(primary, "config", "user.name", "Session Policy Test");
git(primary, "config", "user.email", "session-policy@example.test");
fs.writeFileSync(path.join(primary, "README.md"), "main\n");
git(primary, "add", "README.md");
git(primary, "commit", "-m", "initial");
git(primary, "push", "-u", "origin", "main");
git(primary, "worktree", "add", "-b", "fix/example", session, "origin/main");
fs.writeFileSync(path.join(session, "feature.txt"), "feature\n");
git(session, "add", "feature.txt");
git(session, "commit", "-m", "feature");
git(primary, "merge", "--no-ff", "fix/example", "-m", "Merge fix/example");
git(primary, "push", "origin", "main");
const registeredSessionPath = fs.realpathSync(session);
const result = await finishSession({ branch: "fix/example", cwd: session });
assert.equal(result.removedWorktree, registeredSessionPath);
assert.equal(result.removedRemoteBranch, false);
assert.equal(fs.existsSync(session), false);
assert.notEqual(spawnSync("git", ["-C", primary, "show-ref", "--verify", "--quiet", "refs/heads/fix/example"]).status, 0);
});
});