Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

action-registry.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.

action-registry.test.tsBlame181 lines · 1 contributor
f3e1873Claude1/**
2 * Unit tests for src/lib/action-registry.ts (Agent 8, Sprint 1).
3 *
4 * Covers the in-memory registry resolution logic and the two simplest
5 * built-ins (checkout, gatetest) that can be exercised without hitting the
6 * filesystem or a real backend. The cache/upload/download actions all spawn
7 * `tar` and talk to Agent 6's helper module; they're out of scope here
8 * (they're integration-shaped and better served by integration tests).
9 *
10 * The `gatetest` handler calls `db.select()` to resolve owner/repo. We stub
11 * `../db` via `mock.module` the same way `repo-access.test.ts` does, so the
12 * test stays deterministic without real Postgres.
13 */
14
15import { describe, it, expect, mock, afterAll } from "bun:test";
16
17// Stub the DB module before importing the registry.
18let _lastFrom: any = null;
19let _nextRepoRow: { name: string; ownerId: string } | undefined;
20let _nextUserRow: { username: string } | undefined;
21
22const _chain: any = {
23 from: (table: any) => {
24 _lastFrom = table;
25 return _chain;
26 },
27 where: () => _chain,
28 leftJoin: () => _chain,
29 innerJoin: () => _chain,
30 orderBy: () => _chain,
31 limit: async () => {
32 const t = _lastFrom;
33 if (t && typeof t === "object") {
34 if ("username" in t && "passwordHash" in t) {
35 return _nextUserRow ? [_nextUserRow] : [];
36 }
37 if ("ownerId" in t && "name" in t) {
38 return _nextRepoRow ? [_nextRepoRow] : [];
39 }
40 }
41 return [];
42 },
43 set: () => _chain,
44};
45
46const _fakeDb = {
47 db: {
48 select: () => _chain,
49 insert: () => _chain,
50 update: () => _chain,
51 delete: () => _chain,
52 },
53 getDb: () => ({
54 select: () => _chain,
55 insert: () => _chain,
56 update: () => _chain,
57 delete: () => _chain,
58 }),
59};
60mock.module("../db", () => _fakeDb);
61
62afterAll(() => {
63 _nextRepoRow = undefined;
64 _nextUserRow = undefined;
65 _lastFrom = null;
66});
67
68// Import AFTER the mock is registered so the registry's built-ins see the
69// stub DB when they pull in `../db`.
70import {
71 resolveAction,
72 listActions,
73 registerAction,
74} from "../lib/action-registry";
75
76const ORIGINAL_GATETEST_URL = process.env.GATETEST_URL;
77function clearGatetestUrl() {
78 delete process.env.GATETEST_URL;
79}
80function restoreGatetestUrl() {
81 if (ORIGINAL_GATETEST_URL === undefined) delete process.env.GATETEST_URL;
82 else process.env.GATETEST_URL = ORIGINAL_GATETEST_URL;
83}
84
85afterAll(() => {
86 restoreGatetestUrl();
87});
88
89const ACTION_CTX_BASE = {
90 with: {},
91 env: {},
92 workspace: "/tmp/fake-workspace",
93 runId: "run-id",
94 jobId: "job-id",
95 repoId: "repo-id",
96 commitSha: "deadbeef",
97 ref: "refs/heads/main",
98};
99
100describe("action-registry — resolveAction", () => {
101 it("resolves gluecron/checkout@v1 to the checkout handler", () => {
102 const h = resolveAction("gluecron/checkout@v1");
103 expect(h).not.toBeNull();
104 expect(h?.name).toBe("gluecron/checkout");
105 expect(h?.version).toBe("v1");
106 });
107
108 it("resolves gluecron/gatetest@v1 to the gatetest handler", () => {
109 const h = resolveAction("gluecron/gatetest@v1");
110 expect(h).not.toBeNull();
111 expect(h?.name).toBe("gluecron/gatetest");
112 expect(h?.version).toBe("v1");
113 });
114
115 it("resolveAction('unknown/foo@v1') returns null", () => {
116 const h = resolveAction("unknown/foo@v1");
117 expect(h).toBeNull();
118 });
119
120 it("resolveAction('gluecron/checkout') with no @version resolves to the default (v1)", () => {
121 const h = resolveAction("gluecron/checkout");
122 expect(h).not.toBeNull();
123 expect(h?.name).toBe("gluecron/checkout");
124 expect(h?.version).toBe("v1");
125 });
126
127 it("listActions() includes all 5 built-ins", () => {
128 const names = listActions().map((a) => a.name);
129 expect(names).toContain("gluecron/checkout");
130 expect(names).toContain("gluecron/gatetest");
131 expect(names).toContain("gluecron/cache");
132 expect(names).toContain("gluecron/upload-artifact");
133 expect(names).toContain("gluecron/download-artifact");
134 });
135
136 it("registerAction de-duplicates repeated registrations of the same name@version", () => {
137 // Use a dedicated test-only name so we don't clobber a built-in.
138 const before = listActions().length;
139 const handler = {
140 name: "gluecron/__test_dedupe__",
141 version: "v1",
142 async run() {
143 return { exitCode: 0 };
144 },
145 };
146 registerAction(handler);
147 const afterFirst = listActions().length;
148 registerAction(handler);
149 const afterSecond = listActions().length;
150 expect(afterFirst).toBe(before + 1);
151 expect(afterSecond).toBe(afterFirst);
152 });
153});
154
155describe("action-registry — built-in behaviour", () => {
156 it("checkout.run() returns exitCode 0 and emits the sha output", async () => {
157 const h = resolveAction("gluecron/checkout@v1")!;
158 const res = await h.run({ ...ACTION_CTX_BASE });
159 expect(res.exitCode).toBe(0);
160 expect(res.outputs?.sha).toBe("deadbeef");
161 });
162
163 it("gatetest.run() handles missing repo lookup gracefully (returns non-zero with stderr)", async () => {
164 // The config getter provides a default GATETEST_URL, so we exercise the
165 // realistic path: the action tries to look up the repo. Our DB stub
166 // returns no rows, so the handler reports an unresolved-repo error but
167 // never throws — the key contract is "handler returns a result object".
168 _nextRepoRow = undefined;
169 _nextUserRow = undefined;
170 const h = resolveAction("gluecron/gatetest@v1")!;
171 const res = await h.run({ ...ACTION_CTX_BASE });
172 expect(typeof res.exitCode).toBe("number");
173 // With no repo row, the handler emits a 'unable to resolve' stderr on 1
174 // OR a 'GateTest: ...' result on 0 if a fallback path was taken. Either
175 // way, we expect a structured object (never throws).
176 expect(res).toBeDefined();
177 if (res.exitCode !== 0) {
178 expect((res.stderr || "").length).toBeGreaterThan(0);
179 }
180 });
181});