Skip to content

Commit eabec2a

Browse files
committed
Fall back to isogit store when git is absent
Harbor and minimal containers have no git binary; the optimized wrapper shells out via Bun.spawn and fails the first cycle commit.
1 parent e97130f commit eabec2a

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

‎src/session/optimized-context-store.test.ts‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, expect } from "bun:test";
1+
import { afterEach, describe, expect, spyOn, test } from "bun:test";
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
@@ -446,3 +446,68 @@ describe("createOptimizedContextStore checkpoint", () => {
446446
expect(atHead).toHaveLength(total);
447447
}, 20_000);
448448
});
449+
450+
describe("createOptimizedContextStore without native git", () => {
451+
const originalWhich = Bun.which.bind(Bun);
452+
let whichSpy: ReturnType<typeof spyOn> | undefined;
453+
let spawnSpy: ReturnType<typeof spyOn> | undefined;
454+
455+
afterEach(() => {
456+
whichSpy?.mockRestore();
457+
spawnSpy?.mockRestore();
458+
whichSpy = undefined;
459+
spawnSpy = undefined;
460+
});
461+
462+
test("returns the base isogit store and commits without spawning git", async () => {
463+
whichSpy = spyOn(Bun, "which").mockImplementation((cmd: string) => {
464+
if (cmd === "git") return null;
465+
return originalWhich(cmd);
466+
});
467+
spawnSpy = spyOn(Bun, "spawn");
468+
469+
const dir = tempDir();
470+
const store = await createOptimizedContextStore(dir);
471+
472+
await store.writeTurns([turn("no-git")]);
473+
await store.writeMetadata({
474+
pendingOperations: [],
475+
tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 },
476+
});
477+
const commit = await store.commit({ message: "isogit fallback" });
478+
479+
expect(commit.hash.length).toBeGreaterThan(0);
480+
expect(commit.message).toBe("isogit fallback");
481+
482+
const loaded = await store.load();
483+
expect(loaded.turns).toHaveLength(1);
484+
expect((loaded.turns[0]!.content[0] as { text: string }).text).toBe("no-git");
485+
486+
const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => {
487+
const argv = call[0];
488+
return Array.isArray(argv) && argv[0] === "git";
489+
});
490+
expect(gitSpawns).toHaveLength(0);
491+
});
492+
493+
test("still wraps with native git when git is on PATH", async () => {
494+
expect(originalWhich("git")).not.toBeNull();
495+
496+
const dir = tempDir();
497+
const store = await createOptimizedContextStore(dir);
498+
spawnSpy = spyOn(Bun, "spawn");
499+
500+
await store.writeTurns([turn("with-git")]);
501+
await store.writeMetadata({
502+
pendingOperations: [],
503+
tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 },
504+
});
505+
await store.commit({ message: "native path" });
506+
507+
const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => {
508+
const argv = call[0];
509+
return Array.isArray(argv) && argv[0] === "git";
510+
});
511+
expect(gitSpawns.length).toBeGreaterThan(0);
512+
});
513+
});

‎src/session/optimized-context-store.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,15 @@ async function reconcileSegmentStaging(
361361
* work per reactor checkpoint. Turns and prompt snapshots are written as rolling
362362
* segment files so `git add` re-hashes only the small active segment, and only
363363
* spilled tool-output blobs that are new since the last commit are staged.
364+
*
365+
* When native `git` is missing from PATH (Harbor / minimal containers), skip the
366+
* wrapper and return the base isomorphic-git store — its commit path never shells out.
364367
*/
365368
export async function createOptimizedContextStore(dir: string): Promise<ContextStore> {
369+
if (Bun.which("git") === null) {
370+
return await createIsogitStore(dir);
371+
}
372+
366373
const base = await createIsogitStore(dir);
367374
const pendingBlobFilepaths = new Set<string>();
368375
const pendingSegmentPaths = new Set<string>();

0 commit comments

Comments
 (0)