Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/session/optimized-context-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, test, expect } from "bun:test";
import { afterEach, describe, expect, spyOn, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -446,3 +446,68 @@ describe("createOptimizedContextStore checkpoint", () => {
expect(atHead).toHaveLength(total);
}, 20_000);
});

describe("createOptimizedContextStore without native git", () => {
const originalWhich = Bun.which.bind(Bun);
let whichSpy: ReturnType<typeof spyOn> | undefined;
let spawnSpy: ReturnType<typeof spyOn> | undefined;

afterEach(() => {
whichSpy?.mockRestore();
spawnSpy?.mockRestore();
whichSpy = undefined;
spawnSpy = undefined;
});

test("returns the base isogit store and commits without spawning git", async () => {
whichSpy = spyOn(Bun, "which").mockImplementation((cmd: string) => {
if (cmd === "git") return null;
return originalWhich(cmd);
});
spawnSpy = spyOn(Bun, "spawn");

const dir = tempDir();
const store = await createOptimizedContextStore(dir);

await store.writeTurns([turn("no-git")]);
await store.writeMetadata({
pendingOperations: [],
tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 },
});
const commit = await store.commit({ message: "isogit fallback" });

expect(commit.hash.length).toBeGreaterThan(0);
expect(commit.message).toBe("isogit fallback");

const loaded = await store.load();
expect(loaded.turns).toHaveLength(1);
expect((loaded.turns[0]!.content[0] as { text: string }).text).toBe("no-git");

const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => {
const argv = call[0];
return Array.isArray(argv) && argv[0] === "git";
});
expect(gitSpawns).toHaveLength(0);
});

test("still wraps with native git when git is on PATH", async () => {
expect(originalWhich("git")).not.toBeNull();

const dir = tempDir();
const store = await createOptimizedContextStore(dir);
spawnSpy = spyOn(Bun, "spawn");

await store.writeTurns([turn("with-git")]);
await store.writeMetadata({
pendingOperations: [],
tokenUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thinking: 0 },
});
await store.commit({ message: "native path" });

const gitSpawns = spawnSpy.mock.calls.filter((call: unknown[]) => {
const argv = call[0];
return Array.isArray(argv) && argv[0] === "git";
});
expect(gitSpawns.length).toBeGreaterThan(0);
});
});
7 changes: 7 additions & 0 deletions src/session/optimized-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,15 @@ async function reconcileSegmentStaging(
* work per reactor checkpoint. Turns and prompt snapshots are written as rolling
* segment files so `git add` re-hashes only the small active segment, and only
* spilled tool-output blobs that are new since the last commit are staged.
*
* When native `git` is missing from PATH (Harbor / minimal containers), skip the
* wrapper and return the base isomorphic-git store — its commit path never shells out.
*/
export async function createOptimizedContextStore(dir: string): Promise<ContextStore> {
if (Bun.which("git") === null) {
return await createIsogitStore(dir);
}

const base = await createIsogitStore(dir);
const pendingBlobFilepaths = new Set<string>();
const pendingSegmentPaths = new Set<string>();
Expand Down
Loading