test: isolate git commit resolution fallbacks

This commit is contained in:
Peter Steinberger
2026-03-08 17:05:53 +00:00
parent c70151e873
commit dd7470730d
2 changed files with 111 additions and 145 deletions

View File

@@ -41,12 +41,14 @@ async function makeFakeGitRepo(
describe("git commit resolution", () => {
const originalCwd = process.cwd();
afterEach(() => {
afterEach(async () => {
process.chdir(originalCwd);
vi.restoreAllMocks();
vi.doUnmock("node:fs");
vi.doUnmock("node:module");
vi.resetModules();
const { __testing } = await import("./git-commit.js");
__testing.clearCachedGitCommits();
});
it("resolves commit metadata from the caller module root instead of the caller cwd", async () => {
@@ -85,85 +87,64 @@ describe("git commit resolution", () => {
encoding: "utf-8",
}).trim();
vi.doMock("node:module", () => ({
createRequire: () => {
return (specifier: string) => {
if (specifier === "../build-info.json" || specifier === "./build-info.json") {
return { commit: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" };
}
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
};
},
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
const entryModuleUrl = pathToFileURL(path.join(originalCwd, "src", "entry.ts")).href;
expect(resolveCommitHash({ moduleUrl: entryModuleUrl, env: {} })).toBe(repoHead);
vi.doUnmock("node:module");
expect(
resolveCommitHash({
moduleUrl: entryModuleUrl,
env: {},
readers: {
readBuildInfoCommit: () => "deadbee",
},
}),
).toBe(repoHead);
});
it("caches build-info fallback results per resolved search directory", async () => {
const temp = await makeTempDir("git-commit-build-info-cache");
const moduleRequire = vi.fn((specifier: string) => {
if (specifier === "../build-info.json" || specifier === "./build-info.json") {
return { commit: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" };
}
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
});
vi.doMock("node:module", () => ({
createRequire: () => moduleRequire,
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
const readBuildInfoCommit = vi.fn(() => "deadbee");
expect(resolveCommitHash({ cwd: temp, env: {} })).toBe("deadbee");
const firstCallRequires = moduleRequire.mock.calls.length;
expect(resolveCommitHash({ cwd: temp, env: {}, readers: { readBuildInfoCommit } })).toBe(
"deadbee",
);
const firstCallRequires = readBuildInfoCommit.mock.calls.length;
expect(firstCallRequires).toBeGreaterThan(0);
expect(resolveCommitHash({ cwd: temp, env: {} })).toBe("deadbee");
expect(moduleRequire.mock.calls.length).toBe(firstCallRequires);
vi.doUnmock("node:module");
expect(resolveCommitHash({ cwd: temp, env: {}, readers: { readBuildInfoCommit } })).toBe(
"deadbee",
);
expect(readBuildInfoCommit.mock.calls.length).toBe(firstCallRequires);
});
it("caches package.json fallback results per resolved search directory", async () => {
const temp = await makeTempDir("git-commit-package-json-cache");
const moduleRequire = vi.fn((specifier: string) => {
if (specifier === "../build-info.json" || specifier === "./build-info.json") {
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
}
if (specifier === "../../package.json") {
return { gitHead: "badc0ffee0ddf00d" };
}
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
});
vi.doMock("node:module", () => ({
createRequire: () => moduleRequire,
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
const readPackageJsonCommit = vi.fn(() => "badc0ff");
expect(resolveCommitHash({ cwd: temp, env: {} })).toBe("badc0ff");
const firstCallRequires = moduleRequire.mock.calls.length;
expect(
resolveCommitHash({
cwd: temp,
env: {},
readers: {
readBuildInfoCommit: () => null,
readPackageJsonCommit,
},
}),
).toBe("badc0ff");
const firstCallRequires = readPackageJsonCommit.mock.calls.length;
expect(firstCallRequires).toBeGreaterThan(0);
expect(resolveCommitHash({ cwd: temp, env: {} })).toBe("badc0ff");
expect(moduleRequire.mock.calls.length).toBe(firstCallRequires);
vi.doUnmock("node:module");
expect(
resolveCommitHash({
cwd: temp,
env: {},
readers: {
readBuildInfoCommit: () => null,
readPackageJsonCommit,
},
}),
).toBe("badc0ff");
expect(readPackageJsonCommit.mock.calls.length).toBe(firstCallRequires);
});
it("treats invalid moduleUrl inputs as a fallback hint instead of throwing", async () => {
@@ -204,28 +185,19 @@ describe("git commit resolution", () => {
);
const moduleUrl = pathToFileURL(path.join(packageRoot, "dist", "entry.js")).href;
vi.doMock("node:module", () => ({
createRequire: () => {
return (specifier: string) => {
if (specifier === "../build-info.json" || specifier === "./build-info.json") {
return { commit: "feedfacefeedfacefeedfacefeedfacefeedface" };
}
if (specifier === "../../package.json") {
return { name: "openclaw", version: "2026.3.8", gitHead: "badc0ffee0ddf00d" };
}
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
};
},
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
expect(resolveCommitHash({ moduleUrl, cwd: packageRoot, env: {} })).toBe("feedfac");
vi.doUnmock("node:module");
expect(
resolveCommitHash({
moduleUrl,
cwd: packageRoot,
env: {},
readers: {
readBuildInfoCommit: () => "feedfac",
readPackageJsonCommit: () => "badc0ff",
},
}),
).toBe("feedfac");
});
it("caches git lookups per resolved search directory", async () => {
@@ -253,27 +225,14 @@ describe("git commit resolution", () => {
head: "not-a-commit\n",
});
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
const readFileSyncSpy = vi.fn(actualFs.readFileSync);
vi.doMock("node:fs", () => ({
...actualFs,
default: {
...actualFs,
readFileSync: readFileSyncSpy,
},
readFileSync: readFileSyncSpy,
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
const readGitCommit = vi.fn(() => null);
expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBeNull();
const firstCallReads = readFileSyncSpy.mock.calls.length;
expect(resolveCommitHash({ cwd: repoRoot, env: {}, readers: { readGitCommit } })).toBeNull();
const firstCallReads = readGitCommit.mock.calls.length;
expect(firstCallReads).toBeGreaterThan(0);
expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBeNull();
expect(readFileSyncSpy.mock.calls.length).toBe(firstCallReads);
vi.doUnmock("node:fs");
expect(resolveCommitHash({ cwd: repoRoot, env: {}, readers: { readGitCommit } })).toBeNull();
expect(readGitCommit.mock.calls.length).toBe(firstCallReads);
});
it("caches caught null fallback results per resolved search directory", async () => {
@@ -282,49 +241,39 @@ describe("git commit resolution", () => {
await makeFakeGitRepo(repoRoot, {
head: "0123456789abcdef0123456789abcdef01234567\n",
});
const headPath = path.join(repoRoot, ".git", "HEAD");
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
const readFileSyncSpy = vi.fn((filePath: string, ...args: unknown[]) => {
if (path.resolve(filePath) === path.resolve(headPath)) {
const error = Object.assign(new Error(`EACCES: permission denied, open '${filePath}'`), {
code: "EACCES",
});
throw error;
}
return Reflect.apply(actualFs.readFileSync, actualFs, [filePath, ...args]);
});
vi.doMock("node:fs", () => ({
...actualFs,
default: {
...actualFs,
readFileSync: readFileSyncSpy,
},
readFileSync: readFileSyncSpy,
}));
vi.doMock("node:module", () => ({
createRequire: () => {
return (specifier: string) => {
throw Object.assign(new Error(`Cannot find module ${specifier}`), {
code: "MODULE_NOT_FOUND",
});
};
},
}));
vi.resetModules();
const { resolveCommitHash } = await import("./git-commit.js");
const readGitCommit = vi.fn(() => {
const error = Object.assign(new Error(`EACCES: permission denied`), {
code: "EACCES",
});
throw error;
});
expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBeNull();
const headReadCount = () =>
readFileSyncSpy.mock.calls.filter(([filePath]) => path.resolve(filePath) === headPath).length;
const firstCallReads = headReadCount();
expect(
resolveCommitHash({
cwd: repoRoot,
env: {},
readers: {
readGitCommit,
readBuildInfoCommit: () => null,
readPackageJsonCommit: () => null,
},
}),
).toBeNull();
const firstCallReads = readGitCommit.mock.calls.length;
expect(firstCallReads).toBe(2);
expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBeNull();
expect(headReadCount()).toBe(firstCallReads);
vi.doUnmock("node:fs");
vi.doUnmock("node:module");
expect(
resolveCommitHash({
cwd: repoRoot,
env: {},
readers: {
readGitCommit,
readBuildInfoCommit: () => null,
readPackageJsonCommit: () => null,
},
}),
).toBeNull();
expect(readGitCommit.mock.calls.length).toBe(firstCallReads);
});
it("formats env-provided commit strings consistently", async () => {

View File

@@ -22,6 +22,12 @@ const formatCommit = (value?: string | null) => {
const cachedGitCommitBySearchDir = new Map<string, string | null>();
export type CommitMetadataReaders = {
readGitCommit?: (searchDir: string, packageRoot: string | null) => string | null | undefined;
readBuildInfoCommit?: () => string | null;
readPackageJsonCommit?: () => string | null;
};
function isMissingPathError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
@@ -61,6 +67,10 @@ const cacheGitCommit = (searchDir: string, commit: string | null) => {
return commit;
};
const clearCachedGitCommits = () => {
cachedGitCommitBySearchDir.clear();
};
const resolveGitLookupDepth = (searchDir: string, packageRoot: string | null) => {
if (!packageRoot) {
return undefined;
@@ -176,9 +186,12 @@ export const resolveCommitHash = (
cwd?: string;
env?: NodeJS.ProcessEnv;
moduleUrl?: string;
readers?: CommitMetadataReaders;
} = {},
) => {
const env = options.env ?? process.env;
const readers = options.readers ?? {};
const readGitCommit = readers.readGitCommit ?? readCommitFromGit;
const envCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
const normalized = formatCommit(envCommit);
if (normalized) {
@@ -193,24 +206,28 @@ export const resolveCommitHash = (
moduleUrl: options.moduleUrl,
});
try {
const gitCommit = readCommitFromGit(searchDir, packageRoot);
const gitCommit = readGitCommit(searchDir, packageRoot);
if (gitCommit !== undefined) {
return cacheGitCommit(searchDir, gitCommit);
}
} catch {
// Fall through to baked metadata for packaged installs that are not in a live checkout.
}
const buildInfoCommit = readCommitFromBuildInfo();
const buildInfoCommit = readers.readBuildInfoCommit?.() ?? readCommitFromBuildInfo();
if (buildInfoCommit) {
return cacheGitCommit(searchDir, buildInfoCommit);
}
const pkgCommit = readCommitFromPackageJson();
const pkgCommit = readers.readPackageJsonCommit?.() ?? readCommitFromPackageJson();
if (pkgCommit) {
return cacheGitCommit(searchDir, pkgCommit);
}
try {
return cacheGitCommit(searchDir, readCommitFromGit(searchDir, packageRoot) ?? null);
return cacheGitCommit(searchDir, readGitCommit(searchDir, packageRoot) ?? null);
} catch {
return cacheGitCommit(searchDir, null);
}
};
export const __testing = {
clearCachedGitCommits,
};