t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Git/GitService.local.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

2634 lines
94 KiB
TypeScript

/**
* THE PRIMARY SUITE — full local dev-mode coverage of git-service.
*
* `Test.make({ providers: Cloudflare.providers(), dev: true })` runs the
* whole service on the local RPC-sidecar topology (`alchemy dev`'s process
* shape): a local workerd Worker hosting the GitRepo/GitRegistry Durable
* Objects and the R2 simulator. No cloud API is touched — the worker serves
* from `http://localhost:<port>`.
*
* Coverage:
* (a) every REST endpooint of `GitApi` via the typed `HttpApiClient`
* (repos create/get/update/list/delete, refs list/get/update/remove
* with CAS, objects commit/log/tree/blob, tokens create/list/revoke,
* plus the raw blob/file streaming routes) including the typed
* 401/403/404/409/422 error taxonomy;
* (b) the real `git` CLI over the smart-HTTP wire endpoints — empty clone,
* first push, clone-back + `git fsck --strict`, incremental fetch,
* force-push CAS, branch + annotated-tag lifecycle, readOnly rejection,
* and the wire auth matrix.
*/
import * as Alchemy from "@/index.ts";
import * as Cloudflare from "@/Cloudflare";
import * as Test from "@/Test/Alchemy";
import { expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { MinimumLogLevel } from "effect/References";
import * as Result from "effect/Result";
import * as Schedule from "effect/Schedule";
import * as Stream from "effect/Stream";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import * as HttpApiClient from "effect/http-api/HttpApiClient";
import * as ChildProcess from "effect/process/ChildProcess";
import type { Oid } from "@/Git/Api.ts";
import {
encodeCommit,
hashObject,
ObjectType,
parseCommit,
utf8Decode,
utf8Encode,
} from "@/Git/Protocol/ObjectCodec.ts";
import ProtectedGitHost from "./fixtures/protected-stack.ts";
import TestGitHost, {
TEST_SECRET,
TEST_SECRET_DEV,
TestApi,
} from "./fixtures/stack.ts";
import { verifyPackResponse } from "./harness/pack.ts";
// `dev: true` runs local providers behind the RPC sidecar proxy by default,
// matching the process topology of the real `alchemy dev` command.
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: Cloudflare.providers(),
dev: true,
});
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
// The local suite must not touch the cloud, so it composes its own Stack over
// `Alchemy.localState()` — the same user pattern as production, just with a
// local state store: deploy the fixture's building-block assembly worker.
// Importing `./fixtures/stack.ts` above installed the TEST_SECRET into
// the deployer env before this plan resolves
// the suite fixture's `AuthenticatedTest`.
const LocalStack = Alchemy.Stack(
"GitServiceLocalStack",
{ providers: Cloudflare.providers(), state: Alchemy.localState() },
Effect.gen(function* () {
const host = yield* TestGitHost;
return { url: host.url.as<string>() };
}),
);
// ── readiness ───────────────────────────────────────────────────────────────
class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
status: number;
}> {}
const boundedReadiness = Schedule.max([
Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("2 seconds"),
]),
Schedule.recurs(20),
]);
/** Wait until the local worker answers the admin repo listing with a 200. */
const awaitReady = (url: string) =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
yield* client
.get(`${url}/api/v1/repos`, {
headers: { authorization: `Bearer ${TEST_SECRET}` },
})
.pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.void
: Effect.fail(new WorkerNotReady({ status: res.status })),
),
// retries transport errors (connection refused during boot) too
Effect.retry({ schedule: boundedReadiness }),
);
}).pipe(Effect.orDie);
const stack = beforeAll(
deploy(LocalStack).pipe(
Effect.tap(({ url }) =>
Effect.gen(function* () {
// proof the run is local: no cloud URL, a localhost port
expect(url).toMatch(/^http:\/\/localhost:\d+$/);
yield* awaitReady(url);
}),
),
logLevel,
),
);
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(LocalStack));
// ── typed REST client ───────────────────────────────────────────────────────
const makeClient = (url: string, token: string) =>
HttpApiClient.make(TestApi, {
baseUrl: url,
transformClient: HttpClient.mapRequest((request) =>
request.pipe(HttpClientRequest.bearerToken(token)),
),
});
/** A client that sends NO Authorization header — the anonymous caller. */
const makeAnonymousClient = (url: string) =>
HttpApiClient.make(TestApi, { baseUrl: url });
const asOid = (oid: string): Oid => oid as Oid;
/** Assert an effect fails with the given tagged error. */
const expectTag = <A, E extends { readonly _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
tag: string,
) =>
Effect.gen(function* () {
const result = yield* Effect.result(effect);
expect(Result.isFailure(result)).toBe(true);
if (Result.isFailure(result)) {
expect(result.failure._tag).toBe(tag);
return result.failure;
}
return yield* Effect.die("unreachable");
});
// ── git CLI plumbing ────────────────────────────────────────────────────────
class GitError extends Data.TaggedError("GitError")<{
readonly args: ReadonlyArray<string>;
readonly exitCode: number;
readonly stdout: string;
readonly stderr: string;
}> {
override get message() {
return `git ${this.args.join(" ")} → exit ${this.exitCode}\n${this.stderr.trim()}\n${this.stdout.trim()}`;
}
}
/** Run `git <args>` in `cwd`, capturing exit code and output. Bounded. */
const git = Effect.fn(function* (cwd: string, ...args: Array<string>) {
const handle = yield* ChildProcess.make("git", args, {
cwd,
env: {
GIT_TERMINAL_PROMPT: "0",
GIT_CONFIG_NOSYSTEM: "1",
GIT_CONFIG_GLOBAL: "/dev/null",
GIT_AUTHOR_NAME: "Test User",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test User",
GIT_COMMITTER_EMAIL: "test@example.com",
},
extendEnv: true,
});
const [exitCode, stdout, stderr] = yield* Effect.all(
[
handle.exitCode,
Stream.mkString(Stream.decodeText(handle.stdout)),
Stream.mkString(Stream.decodeText(handle.stderr)),
],
{ concurrency: 3 },
);
return { args, exitCode, stdout: stdout.trim(), stderr };
}, Effect.timeout("60 seconds"));
/** Run git and require exit 0, failing with the captured output otherwise. */
const mustGit = Effect.fn(function* (cwd: string, ...args: Array<string>) {
const result = yield* git(cwd, ...args);
if (result.exitCode !== 0) {
return yield* new GitError(result);
}
return result;
});
/** Run git and require a non-zero exit (an expected rejection). */
const mustFailGit = Effect.fn(function* (cwd: string, ...args: Array<string>) {
const result = yield* git(cwd, ...args);
if (result.exitCode === 0) {
return yield* new GitError(result);
}
return result;
});
/** `http://x:<token>@localhost:<port>/<owner>/<repo>.git` */
const authRemote = (url: string, token: string, owner: string, repo: string) =>
Effect.sync(() => {
const parsed = new URL(url);
return `${parsed.protocol}//x:${token}@${parsed.host}/${owner}/${repo}.git`;
});
/** `git rev-parse <rev>` → the 40-hex oid. */
const revParse = (cwd: string, rev: string) =>
Effect.map(mustGit(cwd, "rev-parse", rev), (result) => result.stdout);
const tempDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.makeTempDirectory({ prefix: "git-service-local-" });
});
/** Create a repo via the admin REST client, returning its bootstrap token. */
/** Poll marker: repo still visible while its async purge drains. */
class StillDeleting extends Data.TaggedError("StillDeleting")<{}> {}
const createRepo = Effect.fn(function* (
url: string,
owner: string,
name: string,
) {
const client = yield* makeClient(url, TEST_SECRET);
// Retry-safe: a previous (failed, runner-retried) attempt may have left
// the repo behind — delete it and wait out the async purge before
// creating, so the create below never 409s on our own leftovers.
yield* client.repos.delete({ params: { owner, repo: name } }).pipe(
Effect.flatMap(() =>
client.repos.get({ params: { owner, repo: name } }).pipe(
Effect.flatMap(() => Effect.fail(new StillDeleting())),
Effect.catchTag("RepoNotFound", () => Effect.void),
Effect.retry({
while: (error) => error instanceof StillDeleting,
schedule: Schedule.spaced("250 millis"),
times: 40,
}),
),
),
Effect.catchTag("RepoNotFound", () => Effect.void),
);
const created = yield* client.repos.create({ payload: { owner, name } });
return {
client,
created,
token: TEST_SECRET,
remote: yield* authRemote(url, TEST_SECRET, owner, name),
};
});
// ═══════════════════════════════════════════════════════════════════════════
// (a) REST management plane
// ═══════════════════════════════════════════════════════════════════════════
test(
"repos: create, duplicate 409, get, list, update, typed auth failures",
Effect.gen(function* () {
const { url } = yield* stack;
const admin = yield* makeClient(url, TEST_SECRET);
const created = yield* admin.repos.create({
payload: { owner: "acme", name: "rest-repos", description: "d0" },
});
expect(created.repo.owner).toBe("acme");
expect(created.repo.name).toBe("rest-repos");
expect(created.repo.status).toBe("ready");
expect(created.repo.defaultBranch).toBe("main");
expect(created.repo.readOnly).toBe(false);
expect(created.remote).toContain("/acme/rest-repos.git");
// duplicate → typed 409
yield* expectTag(
admin.repos.create({ payload: { owner: "acme", name: "rest-repos" } }),
"RepoAlreadyExists",
);
const fetched = yield* admin.repos.get({
params: { owner: "acme", repo: "rest-repos" },
});
expect(fetched.repoId).toBe(created.repo.repoId);
expect(fetched.description).toBe("d0");
// list-all is admin-only and contains the repo
const listed = yield* admin.repos.list({ query: {} });
expect(
listed.items.some((r) => r.owner === "acme" && r.name === "rest-repos"),
).toBe(true);
// PATCH description + readOnly round-trip
const patched = yield* admin.repos.update({
params: { owner: "acme", repo: "rest-repos" },
payload: { description: "d1", readOnly: true },
});
expect(patched.description).toBe("d1");
expect(patched.readOnly).toBe(true);
yield* admin.repos.update({
params: { owner: "acme", repo: "rest-repos" },
payload: { readOnly: false },
});
// missing repo → typed 404
yield* expectTag(
admin.repos.get({ params: { owner: "acme", repo: "does-not-exist" } }),
"RepoNotFound",
);
// no credentials → typed 401 from the fixture's middleware
const anonymous = yield* HttpApiClient.make(TestApi, { baseUrl: url });
yield* expectTag(
anonymous.repos.get({ params: { owner: "acme", repo: "rest-repos" } }),
"Unauthorized",
);
// garbage token → typed 401 (the middleware rejects it before any route)
const garbage = yield* makeClient(url, "gs_garbage-token");
yield* expectTag(
garbage.repos.get({ params: { owner: "acme", repo: "rest-repos" } }),
"Unauthorized",
);
// Anonymous callers may read one public repository at a time;
// listing is not that, so it 401s like any other anonymous call.
yield* expectTag(anonymous.repos.list({ query: {} }), "Unauthorized");
// The engine lists everything the Registry holds; `public: true`
// narrows it, so a private repo is not in that view.
const publicListing = yield* admin.repos.list({
query: { public: true },
});
expect(
publicListing.items.some(
(row) => row.owner === "acme" && row.name === "rest-repos",
),
).toBe(false);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"refs + objects: CAS writes, typed conflicts, commit/log/tree/blob reads, raw routes",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin, remote } = yield* createRepo(
url,
"acme",
"rest-refs",
);
const params = { owner: "acme", repo: "rest-refs" };
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
// refs on a fresh repo are empty
const empty = yield* admin.refs.list({ params, query: {} });
expect(empty.refs).toEqual([]);
// seed real objects over the wire (2 commits)
const tmp = yield* tempDir;
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
remote,
"work",
);
const work = path.join(tmp, "work");
yield* fs.writeFileString(path.join(work, "hello.txt"), "hello local\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1: seed");
yield* fs.writeFileString(
path.join(work, "hello.txt"),
"hello local, v2\n",
);
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c2: bump");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
const parent = (yield* mustGit(work, "rev-parse", "HEAD~1")).stdout;
// refs.list / refs.get agree with the CLI
const refs = yield* admin.refs.list({
params,
query: { prefix: "refs/heads/" },
});
expect(refs.refs.map((r) => r.name)).toEqual(["refs/heads/main"]);
expect(refs.refs[0]!.oid).toBe(head);
const mainRef = yield* admin.refs.get({
params,
query: { name: "refs/heads/main" },
});
expect(mainRef.oid).toBe(head);
yield* expectTag(
admin.refs.get({ params, query: { name: "refs/heads/nope" } }),
"RefNotFound",
);
// CAS create (expectedOid: null = must-not-exist)
const branch = yield* admin.refs.update({
params,
query: { name: "refs/heads/rest-branch" },
payload: { newOid: asOid(parent), expectedOid: null },
});
expect(branch.oid).toBe(parent);
// must-not-exist against an existing ref → typed 409
yield* expectTag(
admin.refs.update({
params,
query: { name: "refs/heads/rest-branch" },
payload: { newOid: asOid(head), expectedOid: null },
}),
"RefConflict",
);
// stale expectedOid → typed 409
yield* expectTag(
admin.refs.update({
params,
query: { name: "refs/heads/rest-branch" },
payload: { newOid: asOid(head), expectedOid: asOid(head) },
}),
"RefConflict",
);
// correct CAS moves the ref
const moved = yield* admin.refs.update({
params,
query: { name: "refs/heads/rest-branch" },
payload: { newOid: asOid(head), expectedOid: asOid(parent) },
});
expect(moved.oid).toBe(head);
// pointing a ref at an object we do not have → typed 404
yield* expectTag(
admin.refs.update({
params,
query: { name: "refs/heads/rest-branch" },
payload: { newOid: asOid("f".repeat(40)) },
}),
"ObjectNotFound",
);
// remove with a stale CAS → 409; with the right one → gone
yield* expectTag(
admin.refs.remove({
params,
query: { name: "refs/heads/rest-branch" },
payload: { expectedOid: asOid(parent) },
}),
"RefConflict",
);
yield* admin.refs.remove({
params,
query: { name: "refs/heads/rest-branch" },
payload: { expectedOid: asOid(head) },
});
yield* expectTag(
admin.refs.get({ params, query: { name: "refs/heads/rest-branch" } }),
"RefNotFound",
);
// readOnly gates REST ref writes with the typed 403
yield* admin.repos.update({ params, payload: { readOnly: true } });
yield* expectTag(
admin.refs.update({
params,
query: { name: "refs/heads/blocked" },
payload: { newOid: asOid(head) },
}),
"ReadOnlyRepo",
);
yield* admin.repos.update({ params, payload: { readOnly: false } });
// objects: commit → tree → blob agree with the CLI byte-for-byte
const commit = yield* admin.objects.commit({
params: { ...params, oid: asOid(head) },
});
expect(commit.message).toContain("c2: bump");
expect(commit.parents).toEqual([parent]);
expect(commit.author.email).toBe("test@example.com");
const tree = yield* admin.objects.tree({
params: { ...params, oid: commit.tree },
});
const helloEntry = tree.entries.find((e) => e.name === "hello.txt")!;
expect(helloEntry.type).toBe("blob");
expect(helloEntry.mode).toBe("100644");
const blob = yield* admin.objects.blob({
params: { ...params, oid: helloEntry.oid },
});
expect(blob.encoding).toBe("base64");
const blobText = yield* Effect.sync(() =>
Buffer.from(blob.content, "base64").toString("utf8"),
);
expect(blobText).toBe("hello local, v2\n");
// wrong-kind reads → typed 422
yield* expectTag(
admin.objects.tree({ params: { ...params, oid: helloEntry.oid } }),
"WrongObjectType",
);
yield* expectTag(
admin.objects.blob({ params: { ...params, oid: asOid(head) } }),
"WrongObjectType",
);
yield* expectTag(
admin.objects.commit({
params: { ...params, oid: asOid("e".repeat(40)) },
}),
"ObjectNotFound",
);
// log walks the first-parent chain
const log = yield* admin.objects.log({
params,
query: { ref: "refs/heads/main", limit: 10 },
});
expect(log.items.map((c) => c.oid)).toEqual([head, parent]);
// raw streaming routes (outside HttpApi schema-land)
const http = yield* HttpClient.HttpClient;
const rawBlob = yield* http.get(
`${url}/api/v1/repos/acme/rest-refs/blobs/${helloEntry.oid}/raw`,
{ headers: { authorization: `Bearer ${TEST_SECRET}` } },
);
expect(rawBlob.status).toBe(200);
expect(yield* rawBlob.text).toBe("hello local, v2\n");
const rawFile = yield* http.get(
`${url}/api/v1/repos/acme/rest-refs/file?ref=refs/heads/main&path=hello.txt`,
{ headers: { authorization: `Bearer ${TEST_SECRET}` } },
);
expect(rawFile.status).toBe(200);
expect(yield* rawFile.text).toBe("hello local, v2\n");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"repos.delete: 204 now, async purge to a typed 404",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin } = yield* createRepo(url, "acme", "rest-delete");
const params = { owner: "acme", repo: "rest-delete" };
yield* admin.repos.delete({ params });
// status flips to 'deleting' and the alarm-driven purge ends in a 404
const gone = yield* admin.repos.get({ params }).pipe(
Effect.map((repo) => ({ deleted: false, status: repo.status })),
Effect.catchTag("RepoNotFound", () =>
Effect.succeed({ deleted: true as const, status: undefined }),
),
Effect.repeat({
schedule: Schedule.spaced("1 second"),
until: (state) => state.deleted,
times: 30,
}),
);
expect(gone.deleted).toBe(true);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (b) real git CLI over the local wire
// ═══════════════════════════════════════════════════════════════════════════
test(
"git: empty clone, first push (exec bit + subdir), REST agreement",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin, remote } = yield* createRepo(
url,
"acme",
"wire-basic",
);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
// empty-repo clone succeeds (unborn HEAD advertisement)
const clone = yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
remote,
"work",
);
expect(`${clone.stdout}\n${clone.stderr}`).toContain("empty repository");
const work = path.join(tmp, "work");
// three commits: plain file, subdirectory, executable
yield* fs.writeFileString(path.join(work, "hello.txt"), "hello wire\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1: hello");
yield* fs.makeDirectory(path.join(work, "sub"), { recursive: true });
yield* fs.writeFileString(path.join(work, "sub", "nested.txt"), "nested\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c2: subdir");
yield* fs.writeFileString(
path.join(work, "run.sh"),
"#!/bin/sh\necho ok\n",
);
yield* fs.chmod(path.join(work, "run.sh"), 0o755);
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c3: executable");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
// REST agrees with the CLI
const params = { owner: "acme", repo: "wire-basic" };
const refs = yield* admin.refs.list({ params, query: {} });
expect(refs.refs.find((r) => r.name === "refs/heads/main")?.oid).toBe(head);
const commit = yield* admin.objects.commit({
params: { ...params, oid: asOid(head) },
});
expect(commit.message).toContain("c3: executable");
const tree = yield* admin.objects.tree({
params: { ...params, oid: commit.tree },
});
expect(tree.entries.find((e) => e.name === "run.sh")?.mode).toBe("100755");
expect(tree.entries.find((e) => e.name === "sub")?.type).toBe("tree");
// ls-remote sees exactly what REST sees
const lsRemote = (yield* mustGit(work, "ls-remote", "origin")).stdout;
expect(lsRemote).toContain(`${head}\trefs/heads/main`);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"git: clone-back + fsck --strict, incremental fetch fast-forwards",
Effect.gen(function* () {
const { url } = yield* stack;
const { remote } = yield* createRepo(url, "acme", "wire-fetch");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
// seed from clone A
yield* mustGit(tmp, "-c", "init.defaultBranch=main", "clone", remote, "a");
const a = path.join(tmp, "a");
yield* fs.writeFileString(path.join(a, "file.txt"), "v1\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c1");
yield* fs.writeFileString(path.join(a, "file.txt"), "v2\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c2");
yield* mustGit(a, "push", "origin", "main");
// clone-back is fsck-clean and log-identical — transitively proves
// advertisement, negotiation, pack emission, and object round-tripping
yield* mustGit(tmp, "clone", remote, "b");
const b = path.join(tmp, "b");
yield* mustGit(b, "fsck", "--strict");
const logA = (yield* mustGit(a, "log", "--format=%H %s")).stdout;
const logB = (yield* mustGit(b, "log", "--format=%H %s")).stdout;
expect(logB).toBe(logA);
// two more commits in A → B fetches incrementally (the haves/ACK path)
yield* fs.writeFileString(path.join(a, "file.txt"), "v3\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c3");
yield* fs.writeFileString(path.join(a, "extra.txt"), "extra\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c4");
yield* mustGit(a, "push", "origin", "main");
const headA = (yield* mustGit(a, "rev-parse", "HEAD")).stdout;
yield* mustGit(b, "fetch", "origin");
yield* mustGit(b, "merge", "--ff-only", "origin/main");
expect((yield* mustGit(b, "rev-parse", "HEAD")).stdout).toBe(headA);
expect(yield* fs.readFileString(path.join(b, "file.txt"))).toBe("v3\n");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"git: force-push CAS, branch + annotated tag lifecycle",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin, remote } = yield* createRepo(
url,
"acme",
"wire-refs",
);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
const params = { owner: "acme", repo: "wire-refs" };
yield* mustGit(tmp, "-c", "init.defaultBranch=main", "clone", remote, "a");
const a = path.join(tmp, "a");
yield* fs.writeFileString(path.join(a, "f.txt"), "one\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c1");
yield* fs.writeFileString(path.join(a, "f.txt"), "two\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c2");
yield* mustGit(a, "push", "origin", "main");
// B clones now, then A advances the remote — B's next push is stale
yield* mustGit(tmp, "clone", remote, "b");
const b = path.join(tmp, "b");
yield* fs.writeFileString(path.join(a, "f.txt"), "three\n");
yield* mustGit(a, "add", "-A");
yield* mustGit(a, "commit", "-m", "c3");
yield* mustGit(a, "push", "origin", "main");
// B pushes its own commit against the stale old-oid → server-side CAS ng
yield* fs.writeFileString(path.join(b, "g.txt"), "divergent\n");
yield* mustGit(b, "add", "-A");
yield* mustGit(b, "commit", "-m", "divergent");
const rejected = yield* mustFailGit(b, "push", "origin", "main");
expect(`${rejected.stdout}\n${rejected.stderr}`).toContain("rejected");
// fetch + force overwrites; A fetches the forced history cleanly
yield* mustGit(b, "fetch", "origin");
yield* mustGit(b, "push", "--force", "origin", "main");
const headB = (yield* mustGit(b, "rev-parse", "HEAD")).stdout;
yield* mustGit(a, "fetch", "origin");
expect((yield* mustGit(a, "rev-parse", "origin/main")).stdout).toBe(headB);
// annotated tag round-trips as a tag object with a peeled advertisement
yield* mustGit(b, "tag", "-a", "v1", "-m", "release v1");
yield* mustGit(b, "push", "origin", "v1");
const tagOid = (yield* mustGit(b, "rev-parse", "v1")).stdout;
const tagRef = yield* admin.refs.get({
params,
query: { name: "refs/tags/v1" },
});
expect(tagRef.oid).toBe(tagOid);
expect(tagRef.peeled).toBe(headB); // peeled to the commit it annotates
const lsRemote = (yield* mustGit(b, "ls-remote", "--tags", "origin"))
.stdout;
expect(lsRemote).toContain(`${tagOid}\trefs/tags/v1`);
expect(lsRemote).toContain(`${headB}\trefs/tags/v1^{}`);
// branch push + delete-refs for branch and tag
yield* mustGit(b, "push", "origin", "main:feature");
const feature = yield* admin.refs.get({
params,
query: { name: "refs/heads/feature" },
});
expect(feature.oid).toBe(headB);
yield* mustGit(b, "push", "origin", "--delete", "feature");
yield* mustGit(b, "push", "origin", "--delete", "v1");
yield* expectTag(
admin.refs.get({ params, query: { name: "refs/heads/feature" } }),
"RefNotFound",
);
yield* expectTag(
admin.refs.get({ params, query: { name: "refs/tags/v1" } }),
"RefNotFound",
);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"git: readOnly repos reject pushes in-band, then accept after unflag",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin, remote } = yield* createRepo(
url,
"acme",
"wire-readonly",
);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
const params = { owner: "acme", repo: "wire-readonly" };
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
remote,
"work",
);
const work = path.join(tmp, "work");
yield* fs.writeFileString(path.join(work, "f.txt"), "one\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1");
yield* mustGit(work, "push", "origin", "main");
yield* admin.repos.update({ params, payload: { readOnly: true } });
yield* fs.writeFileString(path.join(work, "f.txt"), "two\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c2");
const rejected = yield* mustFailGit(work, "push", "origin", "main");
expect(`${rejected.stdout}\n${rejected.stderr}`).toContain("rejected");
yield* admin.repos.update({ params, payload: { readOnly: false } });
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
const mainRef = yield* admin.refs.get({
params,
query: { name: "refs/heads/main" },
});
expect(mainRef.oid).toBe(head);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"git: wire auth matrix — garbage token 401s, read token clones but cannot push",
Effect.gen(function* () {
const { url } = yield* stack;
const { client: admin, remote } = yield* createRepo(
url,
"acme",
"wire-auth",
);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
const params = { owner: "acme", repo: "wire-auth" };
// seed one commit with the bootstrap write token
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
remote,
"seed",
);
const seed = path.join(tmp, "seed");
yield* fs.writeFileString(path.join(seed, "f.txt"), "seed\n");
yield* mustGit(seed, "add", "-A");
yield* mustGit(seed, "commit", "-m", "c1");
yield* mustGit(seed, "push", "origin", "main");
// garbage token → 401 before any pack bytes flow. git surfaces the 401
// as "Authentication failed" (it never echoes the literal status code).
const badRemote = yield* authRemote(url, "gs_garbage", "acme", "wire-auth");
const unauthorized = yield* mustFailGit(tmp, "clone", badRemote, "nope");
expect(unauthorized.stderr).toContain("Authentication failed");
// a read-scope token clones (token in the remote URL) but cannot push
// the write path still works for the bootstrap token
yield* fs.writeFileString(path.join(seed, "f.txt"), "seed2\n");
yield* mustGit(seed, "add", "-A");
yield* mustGit(seed, "commit", "-m", "c2");
yield* mustGit(seed, "push", "origin", "main");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// v2 storage plane: compaction (DESIGN.md §12.1)
// ═══════════════════════════════════════════════════════════════════════════
test(
"compaction: objects move into an R2 pack and clones stay byte-identical",
Effect.gen(function* () {
const { url } = yield* stack;
const repo = yield* createRepo(url, "acme", "compaction");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
// A history with enough distinct blobs that a compaction run has real
// work to do, plus one binary file whose bytes must survive exactly.
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
repo.remote,
"work",
);
const work = path.join(tmp, "work");
const binary = new Uint8Array(4096);
for (let i = 0; i < binary.length; i++) binary[i] = (i * 31 + 7) & 0xff;
yield* fs.writeFile(path.join(work, "blob.bin"), binary);
for (let i = 0; i < 12; i++) {
yield* fs.writeFileString(path.join(work, `f${i}.txt`), `content ${i}\n`);
}
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "seed");
yield* mustGit(work, "push", "origin", "main");
// Compaction is normally armed by size thresholds; force a run now so
// the test does not depend on repo size.
yield* repo.client.repos.compact({
params: { owner: "acme", repo: "compaction" },
});
const stats = yield* repo.client.repos
.get({ params: { owner: "acme", repo: "compaction" } })
.pipe(
Effect.map((found) => found.objects),
Effect.repeat({
schedule: Schedule.spaced("500 millis"),
until: (objects: { loose: number; packed: number }) =>
objects.packed > 0 && objects.loose === 0,
times: 40,
}),
);
expect(stats.packed).toBeGreaterThan(0);
expect(stats.loose).toBe(0);
// Only blobs move to R2 (Compact.ts BLOB_TYPE): commits/trees/tags stay
// row-resident so the fetch closure never walks pack windows.
expect(stats.resident).toBeGreaterThan(0);
// Everything now reads through R2 ranged GETs: a fresh clone must be
// fsck-clean and byte-identical, and REST blob reads must still work.
yield* mustGit(tmp, "clone", repo.remote, "verify");
const verify = path.join(tmp, "verify");
yield* mustGit(verify, "fsck", "--strict");
const round = yield* fs.readFile(path.join(verify, "blob.bin"));
expect(Array.from(round)).toEqual(Array.from(binary));
for (let i = 0; i < 12; i++) {
const text = yield* fs.readFileString(path.join(verify, `f${i}.txt`));
expect(text).toBe(`content ${i}\n`);
}
// ...and an incremental push on top of a fully packed repo still works.
yield* fs.writeFileString(path.join(work, "after.txt"), "after\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "after-compaction");
yield* mustGit(work, "push", "origin", "main");
yield* mustGit(verify, "pull", "origin", "main");
expect(yield* fs.readFileString(path.join(verify, "after.txt"))).toBe(
"after\n",
);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"clone bundles: a full clone is served from the R2 bundle, byte-identically",
Effect.gen(function* () {
const { url } = yield* stack;
const repo = yield* createRepo(url, "acme", "bundles");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
repo.remote,
"work",
);
const work = path.join(tmp, "work");
yield* fs.writeFileString(path.join(work, "a.txt"), "one\n");
yield* fs.makeDirectory(path.join(work, "sub"), { recursive: true });
yield* fs.writeFileString(path.join(work, "sub", "b.txt"), "two\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
// The bundle is cut by the post-push alarm; wait for it to land, then
// clone — that clone is served straight from the R2 bundle bytes.
yield* Effect.sleep("4 seconds");
yield* mustGit(tmp, "clone", repo.remote, "fromBundle");
const fromBundle = path.join(tmp, "fromBundle");
yield* mustGit(fromBundle, "fsck", "--strict");
expect((yield* mustGit(fromBundle, "rev-parse", "HEAD")).stdout).toBe(head);
expect(yield* fs.readFileString(path.join(fromBundle, "a.txt"))).toBe(
"one\n",
);
expect(
yield* fs.readFileString(path.join(fromBundle, "sub", "b.txt")),
).toBe("two\n");
// A push after the bundle was cut invalidates it: the next clone must
// still see the NEW commit (served dynamically, or from a fresh bundle).
yield* fs.writeFileString(path.join(work, "c.txt"), "three\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c2");
yield* mustGit(work, "push", "origin", "main");
const head2 = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
yield* mustGit(tmp, "clone", repo.remote, "afterPush");
const afterPush = path.join(tmp, "afterPush");
yield* mustGit(afterPush, "fsck", "--strict");
expect((yield* mustGit(afterPush, "rev-parse", "HEAD")).stdout).toBe(head2);
// A single-branch clone is covered by the same (superset) bundle.
yield* Effect.sleep("4 seconds");
yield* mustGit(
tmp,
"clone",
"--single-branch",
"--branch",
"main",
repo.remote,
"single",
);
yield* mustGit(path.join(tmp, "single"), "fsck", "--strict");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"clone bundles: the wire response proves the R2 fast path served it",
Effect.gen(function* () {
const { url } = yield* stack;
const repo = yield* createRepo(url, "acme", "bundle-wire");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
repo.remote,
"work",
);
const work = path.join(tmp, "work");
yield* fs.writeFileString(path.join(work, "f.txt"), "hello\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
yield* Effect.sleep("4 seconds");
// Hand-built v0 upload-pack request: one `want`, flush, `done` — the
// shape a fresh clone sends. No capabilities, so the response is
// `NAK` followed by the raw pack.
const pkt = (line: string) =>
`${(line.length + 4).toString(16).padStart(4, "0")}${line}`;
const body = `${pkt(`want ${head}\n`)}0000${pkt("done\n")}`;
const client = yield* HttpClient.HttpClient;
const response = yield* client.execute(
HttpClientRequest.post(
`${url}/acme/bundle-wire.git/git-upload-pack`,
).pipe(
HttpClientRequest.setHeaders({
authorization: `Bearer ${repo.token}`,
"content-type": "application/x-git-upload-pack-request",
}),
HttpClientRequest.bodyText(body),
),
);
expect(response.status).toBe(200);
// The header is only set on the bundle path (DESIGN.md §12.2).
expect(response.headers["x-git-bundle"]).toBeDefined();
const bytes = yield* response.arrayBuffer.pipe(
Effect.map((buffer) => new Uint8Array(buffer)),
);
const text = new TextDecoder().decode(bytes.subarray(0, 8));
expect(text).toBe("0008NAK\n");
expect(new TextDecoder().decode(bytes.subarray(8, 12))).toBe("PACK");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"clone bundles: sideband clones are served from the pre-framed twin, raw from the bundle — both whole packs",
Effect.gen(function* () {
const { url } = yield* stack;
const repo = yield* createRepo(url, "acme", "bundle-framed");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
repo.remote,
"work",
);
const work = path.join(tmp, "work");
for (let i = 0; i < 20; i++) {
yield* fs.writeFileString(
path.join(work, `f${i}.txt`),
`${"line\n".repeat(200)}${i}\n`,
);
}
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
const pkt = (line: string) =>
`${(line.length + 4).toString(16).padStart(4, "0")}${line}`;
const client = yield* HttpClient.HttpClient;
const fetchPack = (caps: string) =>
client
.execute(
HttpClientRequest.post(
`${url}/acme/bundle-framed.git/git-upload-pack`,
).pipe(
HttpClientRequest.setHeaders({
authorization: `Bearer ${repo.token}`,
"content-type": "application/x-git-upload-pack-request",
}),
HttpClientRequest.bodyText(
`${pkt(`want ${head}${caps}\n`)}0000${pkt("done\n")}`,
),
),
)
.pipe(
Effect.flatMap((response) =>
response.arrayBuffer.pipe(
Effect.map((buffer) => ({
status: response.status,
via: response.headers["x-git-served-by"],
bytes: new Uint8Array(buffer),
})),
),
),
);
// The bundle job runs after the push; wait for it to cover HEAD.
const sideband = yield* fetchPack(" side-band-64k").pipe(
Effect.repeat({
schedule: Schedule.spaced("500 millis"),
until: (r) => r.via !== undefined,
times: 30,
}),
);
expect(sideband.status).toBe(200);
// Access was decided in front of the route, so a credentialed clone
// takes the same DO-less path as an anonymous one.
expect(sideband.via).toBe("head-snapshot:bundle+framed");
const framed = verifyPackResponse(sideband.bytes, true);
expect(framed.error).toBeUndefined();
expect(framed.objects).toBeGreaterThan(20);
const raw = yield* fetchPack("");
expect(raw.via).toBe("head-snapshot:bundle");
const plain = verifyPackResponse(raw.bytes, false);
expect(plain.error).toBeUndefined();
expect(plain.objects).toBe(framed.objects);
// And real git agrees.
yield* mustGit(tmp, "clone", repo.remote, "again");
yield* mustGit(path.join(tmp, "again"), "fsck", "--strict");
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 90_000,
},
);
test(
"clone bundles: a stale bundle falls back cleanly to the dynamic path",
Effect.gen(function* () {
const { url } = yield* stack;
const repo = yield* createRepo(url, "acme", "bundle-splice");
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
yield* mustGit(
tmp,
"-c",
"init.defaultBranch=main",
"clone",
repo.remote,
"work",
);
const work = path.join(tmp, "work");
yield* fs.writeFileString(path.join(work, "f.txt"), "hello\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c1");
yield* mustGit(work, "push", "origin", "main");
// Let the bundle land for THIS ref snapshot...
yield* Effect.sleep("4 seconds");
// ...then move main past it, so every clone wants an oid the bundle
// does not carry.
yield* fs.writeFileString(path.join(work, "g.txt"), "world\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c2");
yield* fs.writeFileString(path.join(work, "h.txt"), "again\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-m", "c3");
yield* mustGit(work, "push", "origin", "main");
const head = (yield* mustGit(work, "rev-parse", "HEAD")).stdout;
// Raw wire request, immediately — before the bundle job re-cuts, so
// the bundle on disk is definitively stale.
const pkt = (line: string) =>
`${(line.length + 4).toString(16).padStart(4, "0")}${line}`;
const client = yield* HttpClient.HttpClient;
const response = yield* client.execute(
HttpClientRequest.post(
`${url}/acme/bundle-splice.git/git-upload-pack`,
).pipe(
HttpClientRequest.setHeaders({
authorization: `Bearer ${repo.token}`,
"content-type": "application/x-git-upload-pack-request",
}),
HttpClientRequest.bodyText(
`${pkt(`want ${head}\n`)}0000${pkt("done\n")}`,
),
),
);
expect(response.status).toBe(200);
// A stale bundle is NOT used: serving it would need a true delta, and
// `computeClosure` deliberately does not subtract boundary trees (see
// Closure.ts "accepted fat"), so the walk is the honest answer here.
expect(response.headers["x-git-bundle"]).toBeUndefined();
const bytes = yield* response.arrayBuffer.pipe(
Effect.map((buffer) => new Uint8Array(buffer)),
);
expect(new TextDecoder().decode(bytes.subarray(0, 8))).toBe("0008NAK\n");
expect(new TextDecoder().decode(bytes.subarray(8, 12))).toBe("PACK");
// And the clone is still correct end to end through that path.
yield* mustGit(tmp, "clone", repo.remote, "spliced");
const spliced = path.join(tmp, "spliced");
yield* mustGit(spliced, "fsck", "--strict");
expect((yield* mustGit(spliced, "rev-parse", "HEAD")).stdout).toBe(head);
const logWork = (yield* mustGit(work, "log", "--format=%H %s")).stdout;
const logSpliced = (yield* mustGit(spliced, "log", "--format=%H %s"))
.stdout;
expect(logSpliced).toBe(logWork);
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 180_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (j) public repos — anonymous read access (GitHub model)
// ═══════════════════════════════════════════════════════════════════════════
test(
"public repos: anonymous REST reads + tokenless clone; writes still need a token",
Effect.gen(function* () {
const { url } = yield* stack;
const admin = yield* makeClient(url, TEST_SECRET);
const anonymous = yield* makeAnonymousClient(url);
const tmp = yield* tempDir;
const path = yield* Path.Path;
// A public repo with one commit pushed by its owner.
const repo = yield* createRepo(url, "acme", "town-square");
yield* admin.repos.update({
params: { owner: "acme", repo: "town-square" },
payload: { public: true },
});
const fs = yield* FileSystem.FileSystem;
const pub = path.join(tmp, "pub");
yield* fs.makeDirectory(pub, { recursive: true });
yield* mustGit(pub, "init", "-q", "-b", "main");
yield* fs.writeFileString(
path.join(pub, "greeting.txt"),
"hello anonymous\n",
);
yield* mustGit(pub, "add", "-A");
yield* mustGit(pub, "commit", "-qm", "public commit");
yield* mustGit(pub, "push", "-q", repo.remote, "main");
// Anonymous REST: repo meta, refs, log, tree, blob — no token anywhere.
const meta = yield* anonymous.repos.get({
params: { owner: "acme", repo: "town-square" },
});
expect(meta.public).toBe(true);
const refs = yield* anonymous.refs.list({
params: { owner: "acme", repo: "town-square" },
query: {},
});
expect(refs.head).toBe("refs/heads/main");
const head = refs.refs.find((ref) => ref.name === "refs/heads/main")!;
const commit = yield* anonymous.objects.commit({
params: { owner: "acme", repo: "town-square", oid: head.oid },
});
expect(commit.message).toContain("public commit");
const log = yield* anonymous.objects.log({
params: { owner: "acme", repo: "town-square" },
query: {},
});
expect(log.items.length).toBe(1);
// The public-only listing shows it.
const listing = yield* admin.repos.list({ query: { public: true } });
expect(listing.items.some((row) => row.name === "town-square")).toBe(true);
// Tokenless clone over the wire.
const parsed = new URL(url);
const anonymousRemote = `${parsed.protocol}//${parsed.host}/acme/town-square.git`;
yield* mustGit(tmp, `clone`, `-q`, anonymousRemote, `anon-clone`);
const cloned = yield* mustGit(
path.join(tmp, "anon-clone"),
`show`,
`HEAD:greeting.txt`,
);
expect(cloned.stdout.trim()).toBe("hello anonymous");
// Writes stay locked: anonymous push is rejected (401 → git fails).
const anonWork = path.join(tmp, "anon-clone");
yield* fs.writeFileString(
path.join(anonWork, "greeting.txt"),
"hello anonymous\nmore\n",
);
yield* mustGit(anonWork, "add", "-A");
yield* mustGit(anonWork, "commit", "-qm", "anon write");
const push = yield* Effect.result(
mustGit(anonWork, "push", "-q", "origin", "main"),
);
expect(Result.isFailure(push)).toBe(true);
// Anonymous token management is rejected too.
yield* expectTag(
anonymous.repos.update({
params: { owner: "acme", repo: "town-square" },
payload: { description: "anon" },
}),
"Unauthorized",
);
// Flipping back to private locks anonymous readers out again.
yield* admin.repos.update({
params: { owner: "acme", repo: "town-square" },
payload: { public: false },
});
yield* expectTag(
anonymous.repos.get({ params: { owner: "acme", repo: "town-square" } }),
"Unauthorized",
);
const privateListing = yield* admin.repos.list({
query: { public: true },
});
expect(privateListing.items.some((row) => row.name === "town-square")).toBe(
false,
);
const privateClone = yield* Effect.result(
mustGit(tmp, `clone`, `-q`, anonymousRemote, `anon-clone-private`),
);
expect(Result.isFailure(privateClone)).toBe(true);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (k) diffs & compare — server-side changed-file lists
// ═══════════════════════════════════════════════════════════════════════════
test(
"diff: adds/mods/deletes/mode change across two commits; root commit vs empty tree",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const { client, token } = yield* createRepo(url, "acme", "diffs");
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
// commit 1: a.txt, dir/b.txt, script.sh (0644)
yield* fs.writeFileString(`${work}/a.txt`, "one\n");
yield* fs.makeDirectory(`${work}/dir`);
yield* fs.writeFileString(`${work}/dir/b.txt`, "bee\n");
yield* fs.writeFileString(`${work}/script.sh`, "#!/bin/sh\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "c1");
const c1 = yield* revParse(work, "HEAD");
// commit 2: modify a.txt, delete dir/b.txt, add c.txt, chmod +x script.sh
yield* fs.writeFileString(`${work}/a.txt`, "one\ntwo\n");
yield* mustGit(work, "rm", "-q", "dir/b.txt");
yield* fs.writeFileString(`${work}/c.txt`, "sea\n");
yield* mustGit(work, "update-index", "--chmod=+x", "script.sh");
yield* mustGit(work, "add", "a.txt", "c.txt");
yield* mustGit(work, "commit", "-qm", "c2");
const c2 = yield* revParse(work, "HEAD");
const remote = yield* authRemote(url, token, "acme", "diffs");
yield* mustGit(work, "push", "-q", remote, "main");
// c2 vs c1
const d2 = yield* client.objects.diff({
params: { owner: "acme", repo: "diffs", oid: asOid(c2) },
});
expect(d2.parent).toBe(c1);
expect(d2.truncated).toBe(false);
const byPath = new Map(d2.files.map((f) => [f.path, f]));
expect(byPath.get("a.txt")?.status).toBe("modified");
expect(byPath.get("a.txt")?.oldOid).not.toBe(byPath.get("a.txt")?.newOid);
expect(byPath.get("dir/b.txt")?.status).toBe("removed");
expect(byPath.get("dir/b.txt")?.oldOid).toBeDefined();
expect(byPath.get("dir/b.txt")?.newOid).toBeUndefined();
expect(byPath.get("c.txt")?.status).toBe("added");
// mode-only change: same oid, different modes, still "modified"
const script = byPath.get("script.sh")!;
expect(script.status).toBe("modified");
expect(script.oldMode).toBe("100644");
expect(script.newMode).toBe("100755");
expect(script.oldOid).toBe(script.newOid);
expect(d2.files.length).toBe(4);
// sizes present for text blobs
expect(byPath.get("a.txt")?.newSize).toBe(8);
// root commit: parent null, everything added
const d1 = yield* client.objects.diff({
params: { owner: "acme", repo: "diffs", oid: asOid(c1) },
});
expect(d1.parent).toBeNull();
expect(d1.files.every((f) => f.status === "added")).toBe(true);
expect(d1.files.map((f) => f.path).sort()).toEqual([
"a.txt",
"dir/b.txt",
"script.sh",
]);
// wrong object type: a tree oid on the diff endpoint → 422
const treeOid = (yield* client.objects.commit({
params: { owner: "acme", repo: "diffs", oid: asOid(c2) },
})).tree;
yield* expectTag(
client.objects.diff({
params: { owner: "acme", repo: "diffs", oid: treeOid },
}),
"WrongObjectType",
);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"diff: compare merge base across branches, ahead/behind, three-dot files, ref names",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const { client, token } = yield* createRepo(url, "acme", "compare");
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${work}/base.txt`, "base\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "base");
const fork = yield* revParse(work, "HEAD");
// feature: 1 commit touching feature.txt
yield* mustGit(work, "checkout", "-qb", "feature");
yield* fs.writeFileString(`${work}/feature.txt`, "feat\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "feat 1");
const featTip = yield* revParse(work, "HEAD");
// main: 2 more commits touching main-only files
yield* mustGit(work, "checkout", "-q", "main");
yield* fs.writeFileString(`${work}/m1.txt`, "m1\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "m1");
yield* fs.writeFileString(`${work}/m2.txt`, "m2\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "m2");
const remote = yield* authRemote(url, token, "acme", "compare");
yield* mustGit(work, "push", "-q", remote, "main", "feature");
const cmp = yield* client.objects.compare({
params: { owner: "acme", repo: "compare" },
query: { base: "main", head: "feature" }, // short ref names
});
expect(cmp.mergeBase).toBe(fork);
expect(cmp.head).toBe(featTip);
expect(cmp.aheadBy).toBe(1);
expect(cmp.behindBy).toBe(2);
expect(cmp.commits.length).toBe(1);
expect(cmp.commits[0]!.message).toContain("feat 1");
// three-dot: only the feature-side change, none of main's m1/m2
expect(cmp.files.map((f) => f.path)).toEqual(["feature.txt"]);
expect(cmp.files[0]!.status).toBe("added");
expect(cmp.filesTruncated).toBe(false);
// reversed: head is behind → empty files, counts swap
const rev = yield* client.objects.compare({
params: { owner: "acme", repo: "compare" },
query: { base: "feature", head: fork }, // oid accepted too
});
expect(rev.mergeBase).toBe(fork);
expect(rev.aheadBy).toBe(0);
expect(rev.files.length).toBe(0);
// unknown ref → RefNotFound (404)
yield* expectTag(
client.objects.compare({
params: { owner: "acme", repo: "compare" },
query: { base: "main", head: "no-such-branch" },
}),
"RefNotFound",
);
// disjoint history → NoMergeBase (422)
yield* mustGit(work, "checkout", "-q", "--orphan", "island");
yield* mustGit(work, "rm", "-rfq", "--cached", ".");
yield* fs.writeFileString(`${work}/island.txt`, "alone\n");
yield* mustGit(work, "add", "island.txt");
yield* mustGit(work, "commit", "-qm", "island");
yield* mustGit(work, "push", "-q", remote, "island");
yield* expectTag(
client.objects.compare({
params: { owner: "acme", repo: "compare" },
query: { base: "main", head: "island" },
}),
"NoMergeBase",
);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"diff: anonymous read on public repos; 401 on private",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const anonymous = yield* makeAnonymousClient(url);
// private repo → anonymous is 401 on both endpoints
const priv = yield* createRepo(url, "acme", "diff-private");
const pw = yield* tempDir;
yield* mustGit(pw, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${pw}/f.txt`, "x\n");
yield* mustGit(pw, "add", ".");
yield* mustGit(pw, "commit", "-qm", "c");
const privTip = yield* revParse(pw, "HEAD");
yield* mustGit(
pw,
"push",
"-q",
yield* authRemote(url, priv.token, "acme", "diff-private"),
"main",
);
yield* expectTag(
anonymous.objects.diff({
params: { owner: "acme", repo: "diff-private", oid: asOid(privTip) },
}),
"Unauthorized",
);
yield* expectTag(
anonymous.objects.compare({
params: { owner: "acme", repo: "diff-private" },
query: { base: "main", head: "main" },
}),
"Unauthorized",
);
// public repo → anonymous reads succeed
const pub = yield* createRepo(url, "acme", "diff-public");
yield* pub.client.repos.update({
params: { owner: "acme", repo: "diff-public" },
payload: { public: true },
});
const ww = yield* tempDir;
yield* mustGit(ww, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${ww}/hello.txt`, "hi\n");
yield* mustGit(ww, "add", ".");
yield* mustGit(ww, "commit", "-qm", "c1");
yield* fs.writeFileString(`${ww}/hello.txt`, "hi there\n");
yield* mustGit(ww, "add", ".");
yield* mustGit(ww, "commit", "-qm", "c2");
const pubTip = yield* revParse(ww, "HEAD");
yield* mustGit(
ww,
"push",
"-q",
yield* authRemote(url, pub.token, "acme", "diff-public"),
"main",
);
const d = yield* anonymous.objects.diff({
params: { owner: "acme", repo: "diff-public", oid: asOid(pubTip) },
});
expect(d.files).toEqual([
expect.objectContaining({ path: "hello.txt", status: "modified" }),
]);
const c = yield* anonymous.objects.compare({
params: { owner: "acme", repo: "diff-public" },
query: { base: d.parent!, head: "main" },
});
expect(c.aheadBy).toBe(1);
expect(c.behindBy).toBe(0);
expect(c.mergeBase).toBe(d.parent);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (l) pull requests — lifecycle, live compare fields, merges
// ═══════════════════════════════════════════════════════════════════════════
test(
"pulls: open + numbering, typed create failures, list pagination, live detail, close/reopen/edit",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const { client, token } = yield* createRepo(url, "acme", "pulls-basic");
const params = { owner: "acme", repo: "pulls-basic" };
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${work}/README.md`, "base\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "base");
const mainTip = yield* revParse(work, "HEAD");
yield* mustGit(work, "checkout", "-qb", "feature");
yield* fs.writeFileString(`${work}/feature.txt`, "feat\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "feat");
yield* mustGit(work, "checkout", "-qb", "feature-2", "main");
yield* fs.writeFileString(`${work}/f2.txt`, "f2\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "f2");
const remote = yield* authRemote(url, token, "acme", "pulls-basic");
yield* mustGit(work, "push", "-q", remote, "main", "feature", "feature-2");
// open #1 (short names normalize to refs/heads/…)
const pr1 = yield* client.pulls.create({
params,
payload: {
title: "add feature",
body: "the body",
base: "main",
head: "feature",
},
});
expect(pr1.number).toBe(1);
expect(pr1.state).toBe("open");
expect(pr1.baseRef).toBe("refs/heads/main");
expect(pr1.headRef).toBe("refs/heads/feature");
expect(pr1.mergeCommit).toBeNull();
// typed create failures
yield* expectTag(
client.pulls.create({
params,
payload: { title: "same", base: "main", head: "main" },
}),
"ValidationError",
);
yield* expectTag(
client.pulls.create({
params,
payload: { title: "missing", base: "main", head: "no-such-branch" },
}),
"BranchMissing",
);
yield* expectTag(
client.pulls.create({
params,
payload: {
title: "tags are not branches",
base: "main",
head: "refs/tags/v1",
},
}),
"ValidationError",
);
const dup = yield* expectTag(
client.pulls.create({
params,
payload: { title: "dup", base: "main", head: "feature" },
}),
"PullExists",
);
expect((dup as { number?: number }).number).toBe(1);
// #2 gets the next dense number
const pr2 = yield* client.pulls.create({
params,
payload: { title: "second", base: "main", head: "feature-2" },
});
expect(pr2.number).toBe(2);
// list: newest first, keyset pagination
const open = yield* client.pulls.list({ params, query: {} });
expect(open.items.map((p) => p.number)).toEqual([2, 1]);
expect(open.hasMore).toBe(false);
const page1 = yield* client.pulls.list({ params, query: { limit: 1 } });
expect(page1.items.map((p) => p.number)).toEqual([2]);
expect(page1.hasMore).toBe(true);
expect(page1.nextCursor).toBe("2");
const page2 = yield* client.pulls.list({
params,
query: { limit: 1, cursor: page1.nextCursor! },
});
expect(page2.items.map((p) => p.number)).toEqual([1]);
expect(page2.hasMore).toBe(false);
const closedList = yield* client.pulls.list({
params,
query: { state: "closed" },
});
expect(closedList.items).toEqual([]);
// live detail: feature is 1 ahead of main and fast-forwardable
const detail = yield* client.pulls.get({
params: { ...params, number: 1 },
});
expect(detail.baseOid).toBe(mainTip);
expect(detail.mergeBase).toBe(mainTip);
expect(detail.aheadBy).toBe(1);
expect(detail.behindBy).toBe(0);
expect(detail.mergeable).toBe(true);
expect(detail.mergeableReason).toBe("ff");
// unknown number → typed 404
yield* expectTag(
client.pulls.get({ params: { ...params, number: 999 } }),
"PullNotFound",
);
// close → detail still computed (both refs exist) → reopen → edit
const closed = yield* client.pulls.update({
params: { ...params, number: 1 },
payload: { state: "closed" },
});
expect(closed.state).toBe("closed");
const closedDetail = yield* client.pulls.get({
params: { ...params, number: 1 },
});
expect(closedDetail.mergeable).toBe(true);
const reopened = yield* client.pulls.update({
params: { ...params, number: 1 },
payload: { state: "open" },
});
expect(reopened.state).toBe("open");
const edited = yield* client.pulls.update({
params: { ...params, number: 1 },
payload: { title: "renamed", body: null },
});
expect(edited.title).toBe("renamed");
expect(edited.body).toBeNull();
expect(edited.updatedAt).toBeGreaterThanOrEqual(edited.createdAt);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"pulls: fast-forward merge moves the base ref; merged is terminal; up-to-date is 422; encodeCommit round-trips real git bytes",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const { client, token } = yield* createRepo(url, "acme", "pulls-ff");
const params = { owner: "acme", repo: "pulls-ff" };
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${work}/README.md`, "hello\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "base");
yield* mustGit(work, "checkout", "-qb", "feature");
yield* fs.writeFileString(`${work}/feature.txt`, "feat\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "feat");
const featureTip = yield* revParse(work, "HEAD");
yield* mustGit(work, "checkout", "-q", "main");
const remote = yield* authRemote(url, token, "acme", "pulls-ff");
yield* mustGit(work, "push", "-q", remote, "main", "feature");
// pin encodeCommit against real git-produced bytes: `git cat-file
// commit` output re-hashed must reproduce the oid (byte-exactness),
// and parse→encode must reproduce those bytes.
const catFile = (yield* mustGit(work, "cat-file", "commit", featureTip))
.stdout;
const rawCommit = `${catFile}\n`; // mustGit trims the trailing LF
expect(yield* hashObject(ObjectType.commit, utf8Encode(rawCommit))).toBe(
featureTip,
);
const parsed = yield* parseCommit(utf8Encode(rawCommit));
const reEncoded = yield* encodeCommit({
tree: parsed.tree,
parents: parsed.parents,
author: parsed.author,
committer: parsed.committer,
message: parsed.message,
});
expect(utf8Decode(reEncoded)).toBe(rawCommit);
expect(yield* hashObject(ObjectType.commit, reEncoded)).toBe(featureTip);
// open + FF merge
const pr = yield* client.pulls.create({
params,
payload: { title: "ff me", base: "main", head: "feature" },
});
const merged = yield* client.pulls.merge({
params: { ...params, number: pr.number },
payload: {},
});
expect(merged.method).toBe("ff");
expect(merged.oid).toBe(featureTip);
expect(merged.pull.state).toBe("merged");
expect(merged.pull.mergeCommit).toBe(featureTip);
expect(merged.pull.mergedAt).not.toBeNull();
// the base ref moved to the head tip
const mainRef = yield* client.refs.get({
params,
query: { name: "refs/heads/main" },
});
expect(mainRef.oid).toBe(featureTip);
// the clone can FF-pull the merged base
yield* mustGit(work, "pull", "-q", remote, "main");
expect(yield* revParse(work, "HEAD")).toBe(featureTip);
// merged is terminal: live fields null, re-merge and reopen are 409s
const detail = yield* client.pulls.get({
params: { ...params, number: pr.number },
});
expect(detail.state).toBe("merged");
expect(detail.baseOid).toBeNull();
expect(detail.headOid).toBeNull();
expect(detail.mergeable).toBeNull();
yield* expectTag(
client.pulls.merge({
params: { ...params, number: pr.number },
payload: {},
}),
"PullStateConflict",
);
yield* expectTag(
client.pulls.update({
params: { ...params, number: pr.number },
payload: { state: "open" },
}),
"PullStateConflict",
);
yield* expectTag(
client.pulls.merge({ params: { ...params, number: 42 }, payload: {} }),
"PullNotFound",
);
// up-to-date: a PR whose head is already reachable from base
yield* mustGit(
work,
"push",
"-q",
remote,
`${featureTip}:refs/heads/already-in`,
);
const upToDate = yield* client.pulls.create({
params,
payload: { title: "nothing to do", base: "main", head: "already-in" },
});
const upToDateDetail = yield* client.pulls.get({
params: { ...params, number: upToDate.number },
});
expect(upToDateDetail.mergeable).toBe(false);
expect(upToDateDetail.mergeableReason).toBe("up-to-date");
yield* expectTag(
client.pulls.merge({
params: { ...params, number: upToDate.number },
payload: {},
}),
"NothingToMerge",
);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"pulls: trivial merge commit joins disjoint changes (fsck-clean); conflicts are typed 409; stale expectedHeadOid is RefConflict",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { client, token } = yield* createRepo(url, "acme", "pulls-merge");
const params = { owner: "acme", repo: "pulls-merge" };
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${work}/README.md`, "base\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "base");
const baseCommit = yield* revParse(work, "HEAD");
// topic-a: adds feat-a.txt
yield* mustGit(work, "checkout", "-qb", "topic-a");
yield* fs.writeFileString(`${work}/feat-a.txt`, "a\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "feat a");
const topicATip = yield* revParse(work, "HEAD");
// main advances with a disjoint file
yield* mustGit(work, "checkout", "-q", "main");
yield* fs.writeFileString(`${work}/b.txt`, "b\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "main b");
const mainTip = yield* revParse(work, "HEAD");
const remote = yield* authRemote(url, token, "acme", "pulls-merge");
yield* mustGit(work, "push", "-q", remote, "main", "topic-a");
const pr = yield* client.pulls.create({
params,
payload: { title: "merge topic-a", base: "main", head: "topic-a" },
});
const detail = yield* client.pulls.get({
params: { ...params, number: pr.number },
});
expect(detail.mergeBase).toBe(baseCommit);
expect(detail.mergeable).toBe(true);
expect(detail.mergeableReason).toBe("merge-commit");
// stale expectedHeadOid (a race guard) → typed 409, PR stays open
yield* expectTag(
client.pulls.merge({
params: { ...params, number: pr.number },
payload: { expectedHeadOid: asOid(baseCommit) },
}),
"RefConflict",
);
const merged = yield* client.pulls.merge({
params: { ...params, number: pr.number },
payload: { expectedHeadOid: asOid(topicATip) },
});
expect(merged.method).toBe("merge-commit");
expect(merged.pull.state).toBe("merged");
expect(merged.pull.mergeCommit).toBe(merged.oid);
const mainRef = yield* client.refs.get({
params,
query: { name: "refs/heads/main" },
});
expect(mainRef.oid).toBe(merged.oid);
// clone back: fsck-clean, both sides present, exactly 2 parents in order
const tmp = yield* tempDir;
yield* mustGit(tmp, "clone", "-q", remote, "fresh");
const fresh = path.join(tmp, "fresh");
yield* mustGit(fresh, "fsck", "--strict");
expect(yield* revParse(fresh, "HEAD")).toBe(merged.oid);
expect(yield* fs.readFileString(path.join(fresh, "feat-a.txt"))).toBe(
"a\n",
);
expect(yield* fs.readFileString(path.join(fresh, "b.txt"))).toBe("b\n");
expect(yield* fs.readFileString(path.join(fresh, "README.md"))).toBe(
"base\n",
);
const parents = (yield* mustGit(fresh, "log", "--format=%P", "-1")).stdout;
expect(parents).toBe(`${mainTip} ${topicATip}`);
// conflict: both sides edit README.md relative to the merge base
yield* mustGit(work, "checkout", "-qb", "topic-b", baseCommit);
yield* fs.writeFileString(`${work}/README.md`, "topic-b version\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "topic-b readme");
yield* mustGit(work, "checkout", "-q", "main");
yield* mustGit(work, "pull", "-q", remote, "main");
yield* fs.writeFileString(`${work}/README.md`, "main version\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "main readme");
yield* mustGit(work, "push", "-q", remote, "main", "topic-b");
const mainBefore = yield* revParse(work, "HEAD");
const conflictPr = yield* client.pulls.create({
params,
payload: { title: "conflicting", base: "main", head: "topic-b" },
});
const conflictDetail = yield* client.pulls.get({
params: { ...params, number: conflictPr.number },
});
expect(conflictDetail.mergeable).toBe(false);
expect(conflictDetail.mergeableReason).toBe("conflict");
const conflict = yield* expectTag(
client.pulls.merge({
params: { ...params, number: conflictPr.number },
payload: {},
}),
"MergeConflict",
);
expect((conflict as { paths?: ReadonlyArray<string> }).paths).toEqual([
"README.md",
]);
// the PR stays open and the base ref did not move
const still = yield* client.pulls.get({
params: { ...params, number: conflictPr.number },
});
expect(still.state).toBe("open");
const mainAfter = yield* client.refs.get({
params,
query: { name: "refs/heads/main" },
});
expect(mainAfter.oid).toBe(mainBefore);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"pulls: deleted head branch degrades to nulls and BranchMissing; anonymous reads on public; writes need a write token",
Effect.gen(function* () {
const { url } = yield* stack;
const fs = yield* FileSystem.FileSystem;
const anonymous = yield* makeAnonymousClient(url);
const { client, token } = yield* createRepo(url, "acme", "pulls-auth");
const params = { owner: "acme", repo: "pulls-auth" };
const work = yield* tempDir;
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(`${work}/README.md`, "hi\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "base");
yield* mustGit(work, "checkout", "-qb", "topic-c");
yield* fs.writeFileString(`${work}/c.txt`, "c\n");
yield* mustGit(work, "add", ".");
yield* mustGit(work, "commit", "-qm", "c");
const remote = yield* authRemote(url, token, "acme", "pulls-auth");
yield* mustGit(work, "push", "-q", remote, "main", "topic-c");
const pr = yield* client.pulls.create({
params,
payload: { title: "from topic-c", base: "main", head: "topic-c" },
});
// delete the head branch: the PR row survives, live fields go null
yield* client.refs.remove({
params,
query: { name: "refs/heads/topic-c" },
payload: {},
});
const detail = yield* client.pulls.get({
params: { ...params, number: pr.number },
});
expect(detail.state).toBe("open");
expect(detail.headOid).toBeNull();
expect(detail.baseOid).not.toBeNull();
expect(detail.mergeBase).toBeNull();
expect(detail.aheadBy).toBeNull();
expect(detail.mergeable).toBeNull();
const missing = yield* expectTag(
client.pulls.merge({
params: { ...params, number: pr.number },
payload: {},
}),
"BranchMissing",
);
expect((missing as { ref?: string }).ref).toBe("refs/heads/topic-c");
// private repo: anonymous reads are 401
yield* expectTag(
anonymous.pulls.list({ params, query: {} }),
"Unauthorized",
);
// public repo: anonymous reads work, writes never do
yield* client.repos.update({ params, payload: { public: true } });
const publicList = yield* anonymous.pulls.list({ params, query: {} });
expect(publicList.items.map((p) => p.number)).toEqual([pr.number]);
const publicDetail = yield* anonymous.pulls.get({
params: { ...params, number: pr.number },
});
expect(publicDetail.title).toBe("from topic-c");
yield* expectTag(
anonymous.pulls.create({
params,
payload: { title: "nope", base: "main", head: "topic-c" },
}),
"Unauthorized",
);
yield* expectTag(
anonymous.pulls.merge({
params: { ...params, number: pr.number },
payload: {},
}),
"Unauthorized",
);
yield* expectTag(
anonymous.pulls.update({
params: { ...params, number: pr.number },
payload: { state: "closed" },
}),
"Unauthorized",
);
// a read-scoped token reads but cannot write → typed 403
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (k) GitHub REST v3 compatibility facade (/api/v3 — gh api / Octokit)
// ═══════════════════════════════════════════════════════════════════════════
/** Raw fetch against the facade with GitHub's `token` auth scheme. */
const ghFetch = Effect.fn(function* (
url: string,
path: string,
options?: {
readonly method?: string;
readonly token?: string | undefined;
readonly body?: unknown;
},
) {
const client = yield* HttpClient.HttpClient;
let request = HttpClientRequest.make((options?.method ?? "GET") as "GET")(
`${url}/api/v3${path}`,
);
if (options?.token !== undefined) {
request = HttpClientRequest.setHeader(
request,
"authorization",
`token ${options.token}`,
);
}
if (options?.body !== undefined) {
request = HttpClientRequest.bodyJsonUnsafe(options.body)(request);
}
const response = yield* client.execute(request);
const text = yield* response.text;
return {
status: response.status,
headers: response.headers,
json: text.length === 0 ? undefined : (JSON.parse(text) as any),
};
});
test(
"ghapi: /user auth probe, repo + branches + commits + contents in GitHub shapes",
Effect.gen(function* () {
const { url } = yield* stack;
const admin = yield* makeClient(url, TEST_SECRET);
const repo = yield* createRepo(url, "acme", "gh-compat");
yield* admin.repos.update({
params: { owner: "acme", repo: "gh-compat" },
payload: { public: true },
});
// seed two commits on main
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
const work = path.join(tmp, "ghc");
yield* fs.makeDirectory(work, { recursive: true });
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(path.join(work, "hello.txt"), "one\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-qm", "c1");
yield* fs.writeFileString(path.join(work, "hello.txt"), "two\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-qm", "c2");
yield* mustGit(work, "push", "-q", repo.remote, "main");
// /user: gh's probe — the fixture overrides the engine's route to
// answer from the caller its middleware resolved (`token` scheme);
// anonymous is the middleware's 401.
const user = yield* ghFetch(url, "/user", { token: TEST_SECRET });
expect(user.status).toBe(200);
expect(user.json.login).toBe("Suite");
const anonUser = yield* ghFetch(url, "/user");
expect(anonUser.status).toBe(401);
// repo shape (anonymous — the repo is public)
const repoJson = yield* ghFetch(url, "/repos/acme/gh-compat");
expect(repoJson.status).toBe(200);
expect(repoJson.json.full_name).toBe("acme/gh-compat");
expect(repoJson.json.private).toBe(false);
expect(repoJson.json.default_branch).toBe("main");
expect(repoJson.json.clone_url).toContain("/acme/gh-compat.git");
expect(typeof repoJson.json.id).toBe("number");
// branches
const branches = yield* ghFetch(url, "/repos/acme/gh-compat/branches");
expect(branches.status).toBe(200);
expect(branches.json[0].name).toBe("main");
expect(branches.json[0].commit.sha).toMatch(/^[0-9a-f]{40}$/);
// commits list + Link pagination
const commits = yield* ghFetch(
url,
"/repos/acme/gh-compat/commits?per_page=1",
);
expect(commits.status).toBe(200);
expect(commits.json.length).toBe(1);
expect(commits.json[0].commit.message).toContain("c2");
expect(commits.headers.link).toContain('rel="next"');
// follow the Link cursor like gh --paginate does
const nextUrl = /<([^>]+)>/.exec(commits.headers.link ?? "")![1]!;
const pathPart = nextUrl.slice(nextUrl.indexOf("/api/v3") + 7);
const page2 = yield* ghFetch(url, pathPart);
expect(page2.json[0].commit.message).toContain("c1");
// single commit incl. files
const head = commits.json[0].sha as string;
const one = yield* ghFetch(url, `/repos/acme/gh-compat/commits/${head}`);
expect(one.status).toBe(200);
expect(one.json.files[0].filename).toBe("hello.txt");
expect(one.json.files[0].status).toBe("modified");
// contents (base64)
const contents = yield* ghFetch(
url,
"/repos/acme/gh-compat/contents/hello.txt",
);
expect(contents.status).toBe(200);
expect(contents.json.encoding).toBe("base64");
expect(atob(contents.json.content as string)).toBe("two\n");
// unknown repo → GitHub-shaped 404
const missing = yield* ghFetch(url, "/repos/acme/nope");
expect(missing.status).toBe(404);
expect(missing.json.message).toBe("Not Found");
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
test(
"ghapi: pull lifecycle through the facade — create, list states, merge, files",
Effect.gen(function* () {
const { url } = yield* stack;
const admin = yield* makeClient(url, TEST_SECRET);
const repo = yield* createRepo(url, "acme", "gh-pulls");
yield* admin.repos.update({
params: { owner: "acme", repo: "gh-pulls" },
payload: { public: true },
});
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tmp = yield* tempDir;
const work = path.join(tmp, "ghp");
yield* fs.makeDirectory(work, { recursive: true });
yield* mustGit(work, "init", "-q", "-b", "main");
yield* fs.writeFileString(path.join(work, "base.txt"), "base\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-qm", "base");
yield* mustGit(work, "checkout", "-qb", "topic");
yield* fs.writeFileString(path.join(work, "topic.txt"), "topic\n");
yield* mustGit(work, "add", "-A");
yield* mustGit(work, "commit", "-qm", "topic work");
yield* mustGit(work, "checkout", "-q", "main");
yield* mustGit(work, "push", "-q", repo.remote, "main", "topic");
// POST /pulls with GitHub's field names (head/base short names)
const created = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls", {
method: "POST",
token: TEST_SECRET,
body: {
title: "Topic work",
head: "topic",
base: "main",
body: "pr body",
},
});
expect(created.status).toBe(201);
expect(created.json.number).toBe(1);
expect(created.json.state).toBe("open");
expect(created.json.merged).toBe(false);
expect(created.json.head.ref).toBe("topic");
expect(created.json.base.ref).toBe("main");
// anonymous write → 401
const anonCreate = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls", {
method: "POST",
body: { title: "x", head: "topic", base: "main" },
});
expect(anonCreate.status).toBe(401);
// GET single: mergeable + shas live
const single = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1");
expect(single.json.mergeable).toBe(true);
expect(single.json.head.sha).toMatch(/^[0-9a-f]{40}$/);
// files (three-dot)
const files = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1/files");
expect(files.json.map((f: any) => f.filename)).toEqual(["topic.txt"]);
expect(files.json[0].status).toBe("added");
// unsupported merge method → 405
const squash = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1/merge", {
method: "PUT",
token: TEST_SECRET,
body: { merge_method: "squash" },
});
expect(squash.status).toBe(405);
// PUT merge (fast-forward)
const merged = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1/merge", {
method: "PUT",
token: TEST_SECRET,
body: {},
});
expect(merged.status).toBe(200);
expect(merged.json.merged).toBe(true);
expect(merged.json.sha).toMatch(/^[0-9a-f]{40}$/);
// GitHub semantics: merged PR is state=closed + merged=true
const after = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1");
expect(after.json.state).toBe("closed");
expect(after.json.merged).toBe(true);
expect(after.json.merge_commit_sha).toBe(merged.json.sha);
// list state filters: open empty; closed includes the merged PR
const open = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls?state=open");
expect(open.json.length).toBe(0);
const closed = yield* ghFetch(
url,
"/repos/acme/gh-pulls/pulls?state=closed",
);
expect(closed.json.length).toBe(1);
expect(closed.json[0].number).toBe(1);
// merged PR files fall back to the merge commit's diff
const mergedFiles = yield* ghFetch(
url,
"/repos/acme/gh-pulls/pulls/1/files",
);
expect(mergedFiles.json.map((f: any) => f.filename)).toEqual(["topic.txt"]);
// PATCH validation
const badState = yield* ghFetch(url, "/repos/acme/gh-pulls/pulls/1", {
method: "PATCH",
token: TEST_SECRET,
body: { state: "merged" },
});
expect(badState.status).toBe(422);
}),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 120_000,
},
);
// ═══════════════════════════════════════════════════════════════════════════
// (c) application ref authorization
// ═══════════════════════════════════════════════════════════════════════════
// A second assembly, with custom native HTTP handlers sharing
// one rule: `refs/heads/main` moves only for the repository's owner (the
// fixture's `protectMain`). Its own entry module: a Worker's generated
// entry serves its main module's DEFAULT export, so one Worker class per
// entry module. Deployed inside the test so the shared fixture stays one
// worker for the e2e suites.
const ProtectedLocalStack = Alchemy.Stack(
"GitServiceProtectedLocalStack",
{ providers: Cloudflare.providers(), state: Alchemy.localState() },
Effect.gen(function* () {
const host = yield* ProtectedGitHost;
return { url: host.url.as<string>() };
}),
);
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(ProtectedLocalStack));
test(
"policies: an application handler protects main — per-ref rule over the parsed updates",
Effect.gen(function* () {
const { url } = yield* deploy(ProtectedLocalStack);
expect(url).toMatch(/^http:\/\/localhost:\d+$/);
yield* awaitReady(url);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { created } = yield* createRepo(url, "e2e", "protected");
// A second user, not the owner, does the pushing.
const token = TEST_SECRET_DEV;
const remote = yield* authRemote(url, token, "e2e", "protected");
const tmp = yield* tempDir;
yield* mustGit(tmp, "-c", "init.defaultBranch=main", "clone", remote, "w");
const w = path.join(tmp, "w");
yield* fs.writeFileString(path.join(w, "file.txt"), "v1\n");
yield* mustGit(w, "add", "-A");
yield* mustGit(w, "commit", "-m", "c1");
// The middleware lets the user push; the policy lets any OTHER branch
// through...
yield* mustGit(w, "push", "origin", "HEAD:refs/heads/feature");
// ...but a direct push to main is refused PER-REF, after the pack is
// parsed, with the reason the policy gave.
const denied = yield* mustFailGit(
w,
"push",
"origin",
"HEAD:refs/heads/main",
);
expect(denied.stderr).toContain("not permitted");
// The owner passes the same policy.
const adminRemote = yield* authRemote(url, TEST_SECRET, "e2e", "protected");
yield* mustGit(w, "push", adminRemote, "HEAD:refs/heads/main");
// The push advertisement is a write probe: anonymous gets the 401
// (with `WWW-Authenticate`) that makes git ask for credentials.
const client = yield* HttpClient.HttpClient;
const probe = yield* client.get(
`${url}/e2e/protected/info/refs?service=git-receive-pack`,
);
expect(probe.status).toBe(401);
expect(probe.headers["www-authenticate"]).toContain("Basic");
// The REST ref writes run the same policy: the user is refused on main
// (a typed 403) and may still create a feature branch.
const head = yield* revParse(w, "HEAD");
const tokenClient = yield* makeClient(url, token);
yield* expectTag(
tokenClient.refs.update({
params: { owner: "e2e", repo: "protected" },
query: { name: "refs/heads/main" },
payload: { newOid: asOid(head) },
}),
"PushDenied",
);
const moved = yield* tokenClient.refs.update({
params: { owner: "e2e", repo: "protected" },
query: { name: "refs/heads/feature2" },
payload: { newOid: asOid(head), expectedOid: null },
});
expect(moved.oid).toBe(head);
// A policy can inspect the newly uploaded commit without publishing it.
yield* fs.writeFileString(path.join(w, "new.txt"), "not published yet\n");
yield* mustGit(w, "add", "-A");
yield* mustGit(w, "commit", "-m", "[reject-content]");
const rejectedOid = yield* revParse(w, "HEAD");
const rejectedContent = yield* mustFailGit(
w,
"push",
adminRemote,
"HEAD:refs/heads/main",
);
expect(rejectedContent.stderr).toContain(
"commit rejected by content policy",
);
const ownerClient = yield* makeClient(url, TEST_SECRET);
yield* expectTag(
ownerClient.objects.commit({
params: { owner: "e2e", repo: "protected", oid: asOid(rejectedOid) },
}),
"ObjectNotFound",
);
expect(
(yield* ownerClient.refs.get({
params: { owner: "e2e", repo: "protected" },
query: { name: "refs/heads/main" },
})).oid,
).toBe(head);
// Retry with an acceptable commit: aborted staging must not poison ingestion.
yield* mustGit(w, "commit", "--amend", "-m", "accepted content");
yield* mustGit(w, "push", adminRemote, "HEAD:refs/heads/main");
}).pipe(logLevel),
{
tags: [
"provider:cloudflare",
"provider:cloudflare:r2",
"provider:cloudflare:worker",
"local",
],
timeout: 240_000,
},
);