Files
zopu-code/packages/primitives/src/git-local-runtime.test.ts
-Puter e744ac4687 Add Zopu dev bootstrap: git runtimes, Codex agent-os, dev agent
A vertical slice to bootstrap the product loop on the canonical zopu-code repo.

Primitives (packages/primitives, all tested + lint/type clean):
- GitRemoteRuntime: Gitea REST client (createIssue, createBranch,
  createPullRequest, listIssues, getIssue) as an Effect context service with
  a fetch-backed transport. Live-verified against puter/zopu-code.
- GitLocalRuntime: clone, addWorktree, commit, push, currentBranch,
  setRemoteUrl over a pluggable Shell (host subprocess now, AgentOS VM exec
  later). Tested against real temp git repos.
- agent-os: Codex support via @agentos-software/codex-cli — codexSoftware
  bundle (codex+git), makeCodexAgentOsConfig(), codexSessionEnv() for the
  OpenAI base URL/key.

Env:
- GITEA_URL/GITEA_TOKEN wired into convex.ts and convex.config.ts.
- .env.example documents the self-hosted git section.

Agents (packages/agents):
- zopu-dev: development agent that creates issues on the canonical repo and
  starts autonomous work runs. git-remote tools (create/list/branch/PR) wired
  to GitRemoteRuntime; start_workflow tool enqueues a work run.
- flue run zopu-dev verified live (lists/creates issues on real Gitea).

Backend (packages/backend):
- workflows.startIssueWork mutation: fetches a Gitea issue server-side and
  admits a queued work run. The Codex VM spawn body is the next step.
2026-07-25 03:18:23 +05:30

117 lines
3.5 KiB
TypeScript

import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
// oxlint-disable-next-line unicorn/import-style -- named import avoids shadowing local `path` vars
import { join } from "node:path";
import { Effect } from "effect";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
addWorktree,
commit,
currentBranch,
nodeShell,
push,
} from "./git-local-runtime";
const run = <A, E>(eff: Effect.Effect<A, E>) => Effect.runPromise(eff);
const execPlain = async (cwd: string, cmd: string) => {
const res = await run(nodeShell.exec("sh", ["-c", cmd], { cwd }));
return res.stdout.trim();
};
let root = "";
let origin = "";
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "git-local-"));
origin = join(root, "origin.git");
await execPlain(root, `git init --bare -b main ${origin}`);
const seed = join(root, "seed");
await execPlain(root, `git clone ${origin} ${seed}`);
await execPlain(
seed,
"git config user.email t@t.t && git config user.name t"
);
await writeFile(join(seed, "README.md"), "# hi\n");
await execPlain(
seed,
"git add -A && git commit -m init && git push -u origin main"
);
});
afterEach(async () => {
await execPlain(root, "chmod -R u+w . 2>/dev/null; true");
});
describe("GitLocalRuntime", () => {
it("creates a worktree on a new branch", async () => {
const cloneDir = join(root, "work");
await run(nodeShell.exec("git", ["clone", origin, cloneDir]));
const worktree = join(root, "wt-1");
const result = await run(
addWorktree(nodeShell, {
branch: "work/1/fix",
path: worktree,
repositoryPath: cloneDir,
})
);
expect(result).toBe(worktree);
expect(await execPlain(worktree, "git rev-parse --abbrev-ref HEAD")).toBe(
"work/1/fix"
);
});
it("commits staged changes and returns the new sha", async () => {
const cloneDir = join(root, "work2");
await run(nodeShell.exec("git", ["clone", origin, cloneDir]));
await run(
nodeShell.exec(
"git",
["worktree", "add", "-b", "work/2", join(root, "wt-2")],
{ cwd: cloneDir }
)
);
const worktree = join(root, "wt-2");
await writeFile(join(worktree, "a.txt"), "a");
const sha = await run(
commit(nodeShell, { message: "add a", repositoryPath: worktree })
);
expect(sha).toMatch(/^[0-9a-f]{7,}/u);
});
it("reads the current branch", async () => {
const cloneDir = join(root, "work3");
await run(nodeShell.exec("git", ["clone", origin, cloneDir]));
expect(await run(currentBranch(nodeShell, cloneDir))).toBe("main");
});
it("pushes a branch to origin", async () => {
const cloneDir = join(root, "work4");
await run(nodeShell.exec("git", ["clone", origin, cloneDir]));
await run(
nodeShell.exec(
"git",
["worktree", "add", "-b", "work/4", join(root, "wt-4")],
{ cwd: cloneDir }
)
);
const worktree = join(root, "wt-4");
await writeFile(join(worktree, "b.txt"), "b");
await run(
commit(nodeShell, { message: "add b", repositoryPath: worktree })
);
await run(push(nodeShell, { refspec: "work/4", repositoryPath: worktree }));
const branches = await execPlain(origin, "git branch --list");
expect(branches).toContain("work/4");
});
it("maps a non-repo path to NotARepository", async () => {
const empty = await mkdtemp(join(tmpdir(), "empty-"));
await expect(run(currentBranch(nodeShell, empty))).rejects.toMatchObject({
reason: "NotARepository",
});
});
});