Blame · Line-by-line history
first-repo-scaffold.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 47b1dc7 | 1 | /** |
| 2 | * A new repository should arrive working, not empty. | |
| 3 | * | |
| 4 | * POST /new called bootstrapRepository(), which creates the DB side only — | |
| 5 | * gate settings, branch protection, labels, welcome issue. It created no git | |
| 6 | * content, so a new user landed on an EMPTY repository: no commit, no README, | |
| 7 | * no workflow, and therefore no CI run to be green. Every push-triggered | |
| 8 | * feature the product sells had nothing to act on, and the first thing a new | |
| 9 | * user saw was an empty state telling them to go and configure something. | |
| 10 | * | |
| 11 | * scaffoldFirstRepo seeds a README and a CI workflow, then calls the SAME | |
| 12 | * syncAndEnqueuePushWorkflows() seam the git post-receive hook calls. That | |
| 13 | * last step is the one that is easy to miss and impossible to notice: files | |
| 14 | * written through the plumbing API do not fire the git hook, so without it | |
| 15 | * the workflow sits on disk with no `workflows` row and no run — the repo | |
| 16 | * looks configured and does nothing. | |
| 17 | */ | |
| 18 | ||
| 19 | import { describe, expect, it } from "bun:test"; | |
| 20 | import { readFileSync } from "fs"; | |
| 21 | ||
| 22 | const SRC = readFileSync("src/lib/first-repo-scaffold.ts", "utf8"); | |
| 23 | const WEB = readFileSync("src/routes/web.tsx", "utf8"); | |
| 24 | ||
| 25 | describe("wiring", () => { | |
| 26 | it("POST /new scaffolds after bootstrapping", () => { | |
| 27 | const handler = WEB.slice( | |
| 28 | WEB.indexOf('web.post("/new"'), | |
| 29 | WEB.indexOf("// Daily brief") | |
| 30 | ); | |
| 31 | expect(handler.length).toBeGreaterThan(200); | |
| 32 | expect(handler).toContain("bootstrapRepository"); | |
| 33 | expect(handler).toContain("scaffoldFirstRepo"); | |
| 34 | // Bootstrap first: the scaffold's CI run is gated by settings that | |
| 35 | // bootstrapRepository creates. | |
| 36 | expect(handler.indexOf("bootstrapRepository")).toBeLessThan( | |
| 37 | handler.indexOf("scaffoldFirstRepo") | |
| 38 | ); | |
| 39 | }); | |
| 40 | ||
| 41 | it("a scaffold failure cannot break repo creation", () => { | |
| 42 | const handler = WEB.slice( | |
| 43 | WEB.indexOf('web.post("/new"'), | |
| 44 | WEB.indexOf("// Daily brief") | |
| 45 | ); | |
| 46 | const block = handler.slice(handler.indexOf("scaffoldFirstRepo") - 400); | |
| 47 | expect(block).toContain("catch"); | |
| 48 | }); | |
| 49 | ||
| 50 | it("uses the same sync seam as the post-receive hook", () => { | |
| 51 | // Writing via the plumbing API does not fire the git hook, so the | |
| 52 | // workflow would never be discovered without calling this directly. | |
| 53 | expect(SRC).toContain("syncAndEnqueuePushWorkflows"); | |
| 54 | const hook = readFileSync("src/hooks/post-receive.ts", "utf8"); | |
| 55 | expect(hook).toContain("syncAndEnqueuePushWorkflows"); | |
| 56 | }); | |
| 57 | }); | |
| 58 | ||
| 59 | describe("what gets seeded", () => { | |
| 60 | it("writes a README and a CI workflow at the discovered path", () => { | |
| 61 | expect(SRC).toContain('"README.md"'); | |
| 62 | // workflows.tsx documents .gluecron/workflows/ as the discovery dir; | |
| 63 | // a workflow written anywhere else is invisible. | |
| 64 | expect(SRC).toContain('".gluecron/workflows/ci.yml"'); | |
| 65 | }); | |
| 66 | ||
| 67 | it("the workflow triggers on push", () => { | |
| 68 | // syncAndEnqueuePushWorkflows only enqueues workflows whose `on:` | |
| 69 | // includes push — a workflow without it syncs but never runs. | |
| 70 | const wf = SRC.slice(SRC.indexOf("function ciWorkflow"), SRC.indexOf("function readme")); | |
| 71 | expect(wf).toMatch(/on:\s*\n\s*- push/); | |
| 72 | }); | |
| 73 | ||
| 74 | it("the workflow has no external dependencies", () => { | |
| 75 | // It runs on a bare runner. The point is proving the pipeline works end | |
| 76 | // to end, not guessing the user's stack — anything needing a package | |
| 77 | // manager would fail and hand a new user a red X as their first result. | |
| 78 | const wf = SRC.slice(SRC.indexOf("function ciWorkflow"), SRC.indexOf("function readme")); | |
| 79 | expect(wf).not.toMatch(/npm |yarn |pnpm |bun install|pip |cargo /); | |
| 80 | }); | |
| 81 | }); | |
| 82 | ||
| 83 | describe("fault isolation", () => { | |
| 84 | const ok = async () => ({ commitSha: "abc", blobSha: "b", parentSha: null }); | |
| 85 | const base = { | |
| 86 | owner: "alice", | |
| 87 | repoName: "demo", | |
| 88 | repositoryId: "r1", | |
| 89 | defaultBranch: "main", | |
| 90 | authorName: "alice", | |
| 91 | authorEmail: "a@b.c", | |
| 92 | userId: "u1", | |
| 93 | }; | |
| 94 | ||
| 95 | it("reports a write failure instead of throwing", async () => { | |
| 96 | const { scaffoldFirstRepo } = await import("../lib/first-repo-scaffold"); | |
| 97 | const r = await scaffoldFirstRepo(base, { | |
| 98 | createOrUpdateFileOnBranch: async () => ({ error: "write-failed" }), | |
| 99 | syncAndEnqueuePushWorkflows: async () => ({ synced: 0, enqueued: 0, errors: [] }), | |
| 100 | }); | |
| 101 | expect(r.readmeCommitted).toBe(false); | |
| 102 | expect(r.workflowCommitted).toBe(false); | |
| 103 | expect(r.errors.join(" ")).toContain("write-failed"); | |
| 104 | }); | |
| 105 | ||
| 106 | it("does not attempt workflow sync when the workflow never committed", async () => { | |
| 107 | let syncCalled = false; | |
| 108 | const { scaffoldFirstRepo } = await import("../lib/first-repo-scaffold"); | |
| 109 | const r = await scaffoldFirstRepo(base, { | |
| 110 | createOrUpdateFileOnBranch: async (i) => | |
| 111 | i.filePath === "README.md" ? await ok() : { error: "write-failed" }, | |
| 112 | syncAndEnqueuePushWorkflows: async () => { | |
| 113 | syncCalled = true; | |
| 114 | return { synced: 1, enqueued: 1, errors: [] }; | |
| 115 | }, | |
| 116 | }); | |
| 117 | expect(r.readmeCommitted).toBe(true); | |
| 118 | expect(r.workflowCommitted).toBe(false); | |
| 119 | // Syncing a commit with no workflow in it would report a phantom run. | |
| 120 | expect(syncCalled).toBe(false); | |
| 121 | expect(r.runsEnqueued).toBe(0); | |
| 122 | }); | |
| 123 | ||
| 124 | it("surfaces a sync failure without losing the commits", async () => { | |
| 125 | const { scaffoldFirstRepo } = await import("../lib/first-repo-scaffold"); | |
| 126 | const r = await scaffoldFirstRepo(base, { | |
| 127 | createOrUpdateFileOnBranch: ok, | |
| 128 | syncAndEnqueuePushWorkflows: async () => { | |
| 129 | throw new Error("sync exploded"); | |
| 130 | }, | |
| 131 | }); | |
| 132 | expect(r.readmeCommitted).toBe(true); | |
| 133 | expect(r.workflowCommitted).toBe(true); | |
| 134 | expect(r.errors.join(" ")).toContain("sync exploded"); | |
| 135 | }); | |
| 136 | ||
| 137 | it("reports the enqueued run on the happy path", async () => { | |
| 138 | const { scaffoldFirstRepo } = await import("../lib/first-repo-scaffold"); | |
| 139 | const r = await scaffoldFirstRepo(base, { | |
| 140 | createOrUpdateFileOnBranch: ok, | |
| 141 | syncAndEnqueuePushWorkflows: async () => ({ synced: 1, enqueued: 1, errors: [] }), | |
| 142 | }); | |
| 143 | expect(r.workflowsSynced).toBe(1); | |
| 144 | // This is the whole point: a green CI run with zero configuration. | |
| 145 | expect(r.runsEnqueued).toBe(1); | |
| 146 | expect(r.errors).toEqual([]); | |
| 147 | }); | |
| 148 | }); |