Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const createSignedCommit = vi.fn();

vi.mock("@posthog/git/signed-commit", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@posthog/git/signed-commit")>();
return {
...actual,
createSignedCommit: (...args: unknown[]) => createSignedCommit(...args),
};
});

// Importing the tool after the mock so its transitive `createSignedCommit`
// reference resolves to the mock above.
const { signedCommitTool } = await import("./signed-commit");

describe("signed-commit tool handler", () => {
const savedSandbox = process.env.IS_SANDBOX;

beforeEach(() => {
createSignedCommit.mockReset();
createSignedCommit.mockResolvedValue({
branch: "posthog-code/feature",
commits: [
{ sha: "deadbeef", url: "https://github.com/x/y/commit/deadbeef" },
],
});
});

afterEach(() => {
if (savedSandbox === undefined) {
delete process.env.IS_SANDBOX;
} else {
process.env.IS_SANDBOX = savedSandbox;
}
});

it("defaults to the session cwd when args.cwd is absent", async () => {
await signedCommitTool.handler(
{ cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" },
{ message: "chore: bump" },
);
const [ctx] = createSignedCommit.mock.calls[0];
expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/code");
});

it("uses an absolute args.cwd verbatim so a sibling clone is reachable", async () => {
await signedCommitTool.handler(
{ cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" },
{
message: "chore: bump",
cwd: "/tmp/workspace/repos/posthog/posthog",
},
);
const [ctx] = createSignedCommit.mock.calls[0];
expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/posthog");
});

it("resolves a relative args.cwd against the session cwd", async () => {
await signedCommitTool.handler(
{ cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" },
{ message: "chore: bump", cwd: "../posthog" },
);
const [ctx] = createSignedCommit.mock.calls[0];
expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/posthog");
});

it("does not forward cwd to createSignedCommit input", async () => {
await signedCommitTool.handler(
{ cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" },
{ message: "chore: bump", cwd: "/elsewhere" },
);
const [, input] = createSignedCommit.mock.calls[0];
expect(input).not.toHaveProperty("cwd");
expect(input).toEqual({ message: "chore: bump" });
});

it("returns the no-token error without invoking createSignedCommit", async () => {
const savedGh = process.env.GH_TOKEN;
const savedGithub = process.env.GITHUB_TOKEN;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
try {
const result = await signedCommitTool.handler(
{ cwd: "/tmp/workspace/repos/posthog/code" },
{ message: "chore: bump" },
);
expect(result.isError).toBe(true);
expect(createSignedCommit).not.toHaveBeenCalled();
} finally {
if (savedGh !== undefined) process.env.GH_TOKEN = savedGh;
if (savedGithub !== undefined) process.env.GITHUB_TOKEN = savedGithub;
}
});
});
12 changes: 8 additions & 4 deletions packages/agent/src/adapters/local-tools/tools/signed-commit.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from "node:path";
import { isCloudRun, resolveGithubToken } from "../../../utils/common";
import {
runSignedCommitTool,
Expand Down Expand Up @@ -33,9 +34,12 @@ export const signedCommitTool = defineLocalTool({
isError: true,
});
}
return runSignedCommitTool(
{ cwd: ctx.cwd, token, taskId: ctx.taskId },
args,
);
// Resolve an explicit `cwd` arg against the session cwd so the agent can
// commit from any clone reachable in the sandbox, not just the one the
// session was rooted at. Absolute paths fall through `path.resolve`
// unchanged; relative paths join the session cwd.
const { cwd: argCwd, ...input } = args;
const cwd = argCwd ? path.resolve(ctx.cwd, argCwd) : ctx.cwd;
return runSignedCommitTool({ cwd, token, taskId: ctx.taskId }, input);
},
});
8 changes: 8 additions & 0 deletions packages/agent/src/adapters/signed-commit-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export const signedCommitToolSchema = {
.describe(
"Files to stage before committing; defaults to already-staged files.",
),
cwd: z
.string()
.optional()
.describe(
"Path to the git checkout to commit from; defaults to the session's working directory. " +
"Pass this when committing to a clone outside the session cwd (e.g. a sibling repo cloned during the run). " +
"Relative paths resolve against the session cwd.",
),
};

export function formatSignedCommitResult(result: SignedCommitResult): string {
Expand Down
Loading