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.
125 lines
3.3 KiB
TypeScript
125 lines
3.3 KiB
TypeScript
import { Effect } from "effect";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
createBranch,
|
|
createIssue,
|
|
createPullRequest,
|
|
getIssue,
|
|
listIssues,
|
|
} from "./git-remote-runtime";
|
|
import type { GitRemoteConfig, GitRemoteTransport } from "./git-remote-runtime";
|
|
|
|
const config: GitRemoteConfig = {
|
|
baseUrl: "https://git.example.com",
|
|
owner: "puter",
|
|
repo: "zopu-code",
|
|
token: "secret",
|
|
};
|
|
|
|
const stubTransport = (
|
|
responder: (method: string, path: string, body: unknown) => unknown,
|
|
status = 200
|
|
): GitRemoteTransport => ({
|
|
request: ({ body, method, url }) =>
|
|
Effect.sync(() => {
|
|
const path = url.replace(
|
|
`${config.baseUrl}/api/v1/repos/puter/zopu-code`,
|
|
""
|
|
);
|
|
const result = responder(method, path, body);
|
|
return {
|
|
json: () => Promise.resolve(result),
|
|
status,
|
|
};
|
|
}),
|
|
});
|
|
|
|
const run = <A, E>(eff: Effect.Effect<A, E>) => Effect.runPromise(eff);
|
|
|
|
describe("GitRemoteRuntime", () => {
|
|
it("creates an issue and maps Gitea fields", async () => {
|
|
const transport = stubTransport(() => ({
|
|
body: "the body",
|
|
html_url: "https://git.example.com/puter/zopu-code/issues/1",
|
|
number: 1,
|
|
state: "open",
|
|
title: "Add status control",
|
|
}));
|
|
const issue = await run(
|
|
createIssue(transport, config, {
|
|
body: "the body",
|
|
title: "Add status control",
|
|
})
|
|
);
|
|
expect(issue.number).toBe(1);
|
|
expect(issue.url).toContain("/issues/1");
|
|
});
|
|
|
|
it("maps 401 to Unauthorized", async () => {
|
|
const transport = stubTransport(() => ({}), 401);
|
|
await expect(run(listIssues(transport, config))).rejects.toMatchObject({
|
|
reason: "Unauthorized",
|
|
});
|
|
});
|
|
|
|
it("maps 404 to NotFound", async () => {
|
|
const transport = stubTransport(() => ({}), 404);
|
|
await expect(run(getIssue(transport, config, 7))).rejects.toMatchObject({
|
|
reason: "NotFound",
|
|
});
|
|
});
|
|
|
|
it("creates a branch", async () => {
|
|
const transport = stubTransport((_m, _p, body) => ({
|
|
name: (body as { new_branch_name: string }).new_branch_name,
|
|
}));
|
|
const branch = await run(
|
|
createBranch(transport, config, { name: "work/1/fix" })
|
|
);
|
|
expect(branch.name).toBe("work/1/fix");
|
|
});
|
|
|
|
it("creates a pull request and reads nested ref fields", async () => {
|
|
const transport = stubTransport(() => ({
|
|
base: { ref: "main" },
|
|
head: { ref: "work/1/fix" },
|
|
html_url: "https://git.example.com/puter/zopu-code/pulls/2",
|
|
number: 2,
|
|
state: "open",
|
|
title: "Fix thing",
|
|
}));
|
|
const pr = await run(
|
|
createPullRequest(transport, config, {
|
|
baseBranch: "main",
|
|
body: "",
|
|
branch: "work/1/fix",
|
|
title: "Fix thing",
|
|
})
|
|
);
|
|
expect(pr.number).toBe(2);
|
|
expect(pr.branch).toBe("work/1/fix");
|
|
expect(pr.baseBranch).toBe("main");
|
|
});
|
|
|
|
it("maps merged PR state", async () => {
|
|
const transport = stubTransport(() => ({
|
|
base: { ref: "main" },
|
|
head: { ref: "work/1/fix" },
|
|
merged: true,
|
|
number: 2,
|
|
state: "closed",
|
|
title: "x",
|
|
}));
|
|
const pr = await run(
|
|
createPullRequest(transport, config, {
|
|
baseBranch: "main",
|
|
body: "",
|
|
branch: "work/1/fix",
|
|
title: "x",
|
|
})
|
|
);
|
|
expect(pr.state).toBe("merged");
|
|
});
|
|
});
|