1/**
2 * The Repository block's seams, as the docs show them: a route of your
3 * own over `Git.GitRepo`, and a decorated namespace that logs commits.
4 * Compile-checked against the real types; run against a fake stub.
5 */
6import * as Git from "@/Git/index.ts";
7import * as HttpApi from "effect/http-api/HttpApi";
8import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
9import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
10import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
11import { describe, expect, it } from "alchemy-test";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import { RuntimeContext } from "@/RuntimeContext.ts";
15
16// ── a route of your own ──────────────────────────────────────────────────────
17
18export const Tip = HttpApiEndpoint.get(
19 "tip",
20 "/api/v1/repos/:owner/:repo/tip",
21 {
22 params: Git.RepoPath,
23 success: Git.Ref,
24 error: [Git.RepoNotFound, Git.RefNotFound],
25 },
26);
27
28class TipApi extends HttpApi.make("tip").add(
29 HttpApiGroup.make("tip").add(Tip),
30) {}
31
32export const TipLive = HttpApiBuilder.group(TipApi, "tip", (h) =>
33 Effect.gen(function* () {
34 const registry = yield* Git.RegistryStore;
35 const repos = yield* Git.RepoStore;
36 return h.handle(
37 "tip",
38 Effect.fn(function* ({ params }) {
39 const entry = yield* registry
40 .resolve(params.owner, params.repo)
41 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
42 if (entry === undefined) return yield* new Git.RepoNotFound(params);
43 const repo = repos.getByName(entry.repoId);
44 const meta = yield* repo
45 .getRepoMeta()
46 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
47 const ref = yield* repo
48 .getRef(`refs/heads/${meta.defaultBranch}`)
49 .pipe(Effect.catchTag("StoreError", (error) => Effect.die(error)));
50 return new Git.Ref({
51 name: ref.name,
52 oid: ref.oid as Git.Oid,
53 ...(ref.peeled === null ? {} : { peeled: ref.peeled as Git.Oid }),
54 });
55 }),
56 );
57 }),
58);
59
60// ── wrap it ──────────────────────────────────────────────────────────────────
61
62const seen: Array<{ repoId: string; refs: ReadonlyArray<string> }> = [];
63
64const afterPush = (repoId: string, result: Git.CommitPushResult) =>
65 Effect.sync(() => {
66 seen.push({ repoId, refs: result.results.map((r) => r.ref) });
67 });
68
69export const ReposWithLogging = Layer.effect(
70 Git.RepoStore,
71 Effect.map(Git.RepoStore, (repos) => ({
72 getByName: (repoId) => {
73 const stub = repos.getByName(repoId);
74 return new Proxy(stub, {
75 get: (target, key, receiver) =>
76 key === "commitPush"
77 ? (input: Git.CommitPushInput) =>
78 target
79 .commitPush(input)
80 .pipe(Effect.tap((result) => afterPush(repoId, result)))
81 : Reflect.get(target, key, receiver),
82 });
83 },
84 })),
85);
86
87describe("Repository seams", { tags: ["unit", "local"] }, () => {
88 it.effect("a decorated namespace observes every commitPush", () =>
89 Effect.gen(function* () {
90 const fakeStub = {
91 commitPush: () =>
92 Effect.succeed({
93 unpack: "ok",
94 results: [{ ref: "refs/heads/main", status: "ok" }],
95 }),
96 readMeta: () => Effect.succeed(undefined),
97 };
98 const FakeRepos = Layer.succeed(Git.RepoStore, {
99 getByName: () => fakeStub as never,
100 });
101
102 const repos = yield* Git.RepoStore.pipe(
103 Effect.provide(ReposWithLogging.pipe(Layer.provide(FakeRepos))),
104 );
105 const stub = repos.getByName("01ARZ3NDEKTSV4RRFFQ69G5FAV");
106 const result = yield* stub.commitPush({} as Git.CommitPushInput);
107 expect(result.unpack).toBe("ok");
108 // other methods pass through the proxy untouched
109 expect(yield* stub.readMeta()).toBeUndefined();
110 expect(seen).toEqual([
111 { repoId: "01ARZ3NDEKTSV4RRFFQ69G5FAV", refs: ["refs/heads/main"] },
112 ]);
113 }).pipe(Effect.provide(RuntimeContext.phantom)),
114 );
115});