openclaw/test/scripts/prepare-git-hooks.test.ts
Peter Steinberger 7fbe426cda
test(core,plugins,scripts): remove low-value tests (batch d023) (#158911)
* test(media): deslop s155 tests

* test(googlechat): deslop s156 tests

* test(scripts): deslop s140 tests

* test(reef): deslop s158 tests

* test(infra): deslop s153 tests

* test(microsoft-foundry): deslop s163 tests

* test(ai): deslop s164 tests

* test(sessions): deslop s160 tests

* test(browser): deslop s165 tests

* test(config): deslop s162 tests

* test: preserve redaction and current release identity coverage

* test: retain helpers used by current main regressions

* test(googlechat): use current config type owner

* test(googlechat): remove unused config type import
2026-09-26 15:29:21 +00:00

241 lines
7.9 KiB
TypeScript

import { execFileSync, spawnSync as spawnGit, type SpawnSyncOptions } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { configurePrepareGitHooks } from "../../scripts/prepare-git-hooks.mjs";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
type SpawnResult = {
error?: NodeJS.ErrnoException;
status?: number | null;
stderr?: string;
stdout?: string;
};
function createSpawn(results: SpawnResult[]) {
return vi.fn((_bin: string, _args: string[]) => {
const result = results.shift();
if (!result) {
throw new Error("unexpected git invocation");
}
return result;
});
}
describe("configurePrepareGitHooks", () => {
it.each([0, 1, 2])("stays quiet when Git is unavailable at command %i", (command) => {
const warn = vi.fn();
const enoent = Object.assign(new Error("missing git"), { code: "ENOENT" });
const preceding: SpawnResult[] = [{ status: 0, stdout: "true\n" }, { status: 1 }];
expect(
configurePrepareGitHooks({
cwd: "C:\\repo",
existsSync: () => true,
spawnSync: createSpawn([...preceding.slice(0, command), { error: enoent }]),
warn,
}),
).toEqual({ configured: false, reason: "missing-git" });
expect(warn).not.toHaveBeenCalled();
});
it.each(["read", "write"])("warns without fallback when Git config %s fails", (operation) => {
const warn = vi.fn();
const spawnSync = createSpawn([
{ status: 0, stdout: "true\n" },
...(operation === "write" ? [{ status: 1 }] : []),
{ status: 128, stderr: "permission denied" },
]);
expect(
configurePrepareGitHooks({
cwd: "/repo",
existsSync: () => true,
spawnSync,
warn,
}),
).toEqual({ configured: false, reason: "config-failed" });
expect(warn).toHaveBeenCalledWith("[prepare] could not configure git hooks: permission denied");
expect(spawnSync).toHaveBeenCalledTimes(operation === "write" ? 3 : 2);
});
it("skips packaged installs without the source hook directory", () => {
const spawnSync = createSpawn([]);
expect(
configurePrepareGitHooks({
cwd: "/package",
existsSync: () => false,
spawnSync,
}),
).toEqual({ configured: false, reason: "missing-hooks-dir" });
expect(spawnSync).not.toHaveBeenCalled();
});
type OwnershipCase = {
name: string;
linked?: boolean;
worktreeConfig?: boolean;
local?: string;
global?: string;
overrides?: boolean;
reason: "configured" | "already-configured" | "config-failed";
expected: string | null;
};
const ownershipCases: OwnershipCase[] = [
{ name: "fresh primary", reason: "configured", expected: "git-hooks" },
{
name: "explicit local owner",
local: "local-hooks",
reason: "already-configured",
expected: "local-hooks",
},
{
name: "explicit global owner",
global: "global-hooks",
reason: "already-configured",
expected: "global-hooks",
},
{ name: "explicit empty value", local: "", reason: "already-configured", expected: "" },
{
name: "linked without private config",
linked: true,
reason: "config-failed",
expected: null,
},
{
name: "linked inherited owner",
linked: true,
local: "shared-hooks",
reason: "already-configured",
expected: "shared-hooks",
},
{
name: "linked private initialization",
linked: true,
worktreeConfig: true,
reason: "configured",
expected: "git-hooks",
},
{
name: "overrides masking shared config",
linked: true,
worktreeConfig: true,
local: "shared-hooks",
overrides: true,
reason: "already-configured",
expected: "linked-hooks",
},
];
it.each(ownershipCases)("preserves Git configuration ownership: $name", (scenario) => {
const root = tempDirs.make("prepare-hook-ownership-");
const primary = join(root, "primary");
const globalConfig = join(root, "global.config");
const template = join(root, "empty-template");
mkdirSync(template);
writeFileSync(globalConfig, "");
const env = {
PATH: process.env.PATH,
SystemRoot: process.env.SystemRoot,
GIT_CONFIG_NOSYSTEM: "1",
GIT_CONFIG_GLOBAL: globalConfig,
GIT_TEMPLATE_DIR: template,
GIT_TERMINAL_PROMPT: "0",
};
const git = (cwd: string, args: string[]) =>
execFileSync("git", ["-C", cwd, ...args], { env, encoding: "utf8", stdio: "pipe" }).trim();
git(root, ["init", "-q", "--initial-branch=main", primary]);
git(primary, [
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"-c",
"commit.gpgsign=false",
"commit",
"--allow-empty",
"-qm",
"fixture",
]);
const checkouts = [primary];
if (scenario.linked) {
for (const name of ["linked", "sibling"]) {
const checkout = join(root, name);
git(primary, ["worktree", "add", "--detach", "-q", checkout, "HEAD"]);
checkouts.push(checkout);
}
}
const target = scenario.linked ? join(root, "linked") : primary;
mkdirSync(join(target, "git-hooks"));
if (scenario.worktreeConfig) {
git(primary, ["config", "extensions.worktreeConfig", "true"]);
}
if (scenario.local !== undefined) {
git(primary, ["config", "--local", "core.hooksPath", scenario.local]);
}
if (scenario.global !== undefined) {
git(primary, ["config", "--file", globalConfig, "core.hooksPath", scenario.global]);
}
if (scenario.overrides) {
checkouts.forEach((checkout) =>
git(checkout, ["config", "--worktree", "core.hooksPath", `${basename(checkout)}-hooks`]),
);
}
const hookPath = (cwd: string) => {
const result = spawnGit("git", ["-C", cwd, "config", "--get", "core.hooksPath"], {
env,
encoding: "utf8",
});
expect([0, 1]).toContain(result.status);
return result.status === 0 ? result.stdout.replace(/\r?\n$/u, "") : null;
};
const effectiveBefore = checkouts.map(hookPath);
const unchangedFiles = [
globalConfig,
...(scenario.linked || scenario.reason !== "configured"
? [join(primary, ".git", "config")]
: []),
...checkouts
.filter(
(checkout) =>
!(checkout === target && scenario.worktreeConfig && scenario.reason === "configured"),
)
.map((checkout) =>
resolve(checkout, git(checkout, ["rev-parse", "--git-path", "config.worktree"])),
),
];
const contents = (file: string) => (existsSync(file) ? readFileSync(file, "utf8") : null);
const contentsBefore = unchangedFiles.map(contents);
const warn = vi.fn();
const result = configurePrepareGitHooks({
cwd: target,
spawnSync: (bin: string, args: string[], options: SpawnSyncOptions) => {
expect(bin).toBe("git");
expect(options.cwd).toBe(target);
return spawnGit(bin, args, { ...options, env });
},
warn,
});
// Private overrides can mask a shared write, so compare config bytes as well as effective values.
expect(unchangedFiles.map(contents)).toEqual(contentsBefore);
expect(result).toEqual({
configured: scenario.reason === "configured",
reason: scenario.reason,
});
expect(checkouts.map(hookPath)).toEqual(
checkouts.map((checkout, index) =>
checkout === target ? scenario.expected : effectiveBefore[index],
),
);
if (scenario.reason === "config-failed") {
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("[prepare] could not configure git hooks"),
);
} else {
expect(warn).not.toHaveBeenCalled();
}
});
});