Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/__tests__/vendor.test.ts
Snapshot at 34e3175
1import { env, RpcTarget } from "cloudflare:workers";
2import { afterEach, beforeEach, expect, it, vi } from "vitest";
3import type { GatekeeperVendor } from "../src/worker/index.ts";
4import type { ApprovalQueue, GitCache, GatekeeperConnectCallback } from "@gadgets/workshop-shared/gatekeeper";
5import type { AgentForkSession } from "../src/fork-types.d.ts";
6import type { FixtureWorkspace } from "./worker.ts";
7import { encodePktLine, FLUSH_PKT } from "@gadgets/gatekeeper-kit/git-transport";
8import { fixtureState } from "../../forkhorse-dashboard/test/fixtures.ts";
9import brand from "../../../brand.json";
10import { markIconUrl } from "../../forkhorse-dashboard/src/mark.ts";
11
12declare global {
13 namespace Cloudflare { interface Env {
14 VENDOR: Service<GatekeeperVendor>; VENDOR_HTTP: Fetcher; CONNECT_CALLBACK: Service<GatekeeperConnectCallback>;
15 WORKSPACES: DurableObjectNamespace<FixtureWorkspace>;
16 } }
17}
18const ORIGIN = "https://core.fixture.idiot.tax";
19const REMOTE = "https://git.fixture.idiot.tax/fixture--alpha-1.git";
20const OLD = "a".repeat(40), NEXT = "b".repeat(40);
21const CREDENTIAL = "fixture-agent-credential-only";
22let head = OLD, role = "agent", agent = "alpha", scopes: string[], pushes: string[], unauthorized = false;
23let moveDuringPush = false, badGrant = false, redirectCore = false, redirectGit = false;
24let pulls: string[] = [];
25let workflowCalls: { path: string; method: string; body: unknown }[] = [];
26let pushFailure: "applied" | "absent" | null = null;
27let workflowMode: "standard" | "race" = "standard";
28let workflowFailure = false;
29let coreReads: string[] = [];
30let discoveredForks: { repoName: string; remote: string; head: string; createdAt: string }[] = [];
31let packetReply: unknown;
32let revokeAfterPackets = false;
33const state = () => { const value = fixtureState("fixture"); value.forks.push({ repoName: "fixture--alpha-1", agent: "alpha", remote: REMOTE, head, createdAt: "2026-01-01" }); value.packets.push({ id: "workflow-packet", mode: workflowMode, title: "Workflow fixture", goal: "Fixture", paths: [], acceptance: [] }); return { ...value, races: [] }; };
34function packets(lines: string[]): string {
35 const chunks = [...lines.map(encodePktLine), FLUSH_PKT];
36 return chunks.map(c => new TextDecoder().decode(c)).join("");
37}
38beforeEach(() => {
39 workflowCalls = []; workflowMode = "standard"; workflowFailure = false;
40 pushFailure = null; head = OLD; role = "agent"; agent = "alpha"; scopes = []; pushes = []; unauthorized = false; moveDuringPush = false; badGrant = false; redirectCore = false; redirectGit = false; pulls = [];
41 coreReads = []; packetReply = undefined; revokeAfterPackets = false;
42 discoveredForks = [{ repoName: "fixture--alpha-1", remote: REMOTE, head: OLD, createdAt: "2026-01-01T00:00:00Z" }];
43 vi.stubGlobal("fetch", async (input: string | URL | Request, init?: RequestInit) => {
44 const request = new Request(input, init);
45 const url = new URL(request.url);
46 if (url.origin === ORIGIN) {
47 expect(request.redirect).toBe("manual");
48 if (redirectCore) return new Response(null, { status: 302, headers: { location: "https://foreign.idiot.tax" } });
49 expect(request.headers.get("authorization")).toBe(`Bearer ${CREDENTIAL}`);
50 if (request.method === "GET") coreReads.push(url.pathname);
51 if (url.pathname.endsWith("/me")) return Response.json({ role, agent,
52 forks: discoveredForks.map(fork => ({ ...fork, head: fork.repoName === "fixture--alpha-1" ? head : fork.head })), work: [],
53 }, { status: unauthorized ? 401 : 200 });
54 if (url.pathname.endsWith("/state")) return Response.json(state());
55 if (url.pathname.endsWith("/packets") && request.method === "GET") {
56 if (revokeAfterPackets) unauthorized = true;
57 return Response.json(packetReply ?? { packets: state().packets });
58 }
59 if (url.pathname.endsWith("/reviews/workflow-review") && request.method === "GET") {
60 return Response.json({ review: { id: "workflow-review", receipt: null } });
61 }
62 if (url.pathname.endsWith("/join") || url.pathname.endsWith("/packets/workflow-packet/claim") || url.pathname.endsWith("/reviews")) {
63 const body = request.method === "DELETE" ? null : await request.json();
64 workflowCalls.push({ path: url.pathname, method: request.method, body });
65 if (workflowFailure) return new Response("fixture-secret-error", { status: 503 });
66 return Response.json({ token: "fixture-join-secret-never-return", repoName: "fixture--alpha-2", expiresAt: new Date(Date.now() + 60_000).toISOString() });
67 }
68 if (url.pathname.endsWith("/forks/fixture--alpha-1/token")) {
69 const requested = await request.json<{ scope: string; ttlSeconds: number }>();
70 scopes.push(requested.scope); expect(requested.ttlSeconds).toBe(60);
71 return Response.json({ repoName: "fixture--alpha-1", remote: badGrant ? "https://other.idiot.tax/repo" : REMOTE,
72 scope: requested.scope, token: "fixture-git-token?expires=fixture", expiresAt: new Date(Date.now() + 60_000).toISOString() }, { status: 201 });
73 }
74 }
75 if (request.url === `${REMOTE}/git-upload-pack`) {
76 expect(request.headers.get("git-protocol")).toBe("version=2");
77 expect(request.headers.get("user-agent")).toBe("git/2.0 (gatekeeper)");
78 expect(request.headers.get("authorization")).toBe(`Basic ${btoa("x:fixture-git-token")}`);
79 pulls.push(new TextDecoder().decode(await request.arrayBuffer()));
80 return new Response(packets(["packfile", "\x01fixture-pack"]));
81 }
82 if (request.url === `${REMOTE}/git-receive-pack`) {
83 expect(request.redirect).toBe("manual");
84 if (redirectGit) return new Response(null, { status: 307, headers: { location: "https://foreign.idiot.tax" } });
85 expect(request.headers.get("user-agent")).toBe("git/2.0 (gatekeeper)");
86 expect(request.headers.get("authorization")).toBe(`Basic ${btoa("x:fixture-git-token")}`);
87 const body = new TextDecoder().decode(await request.arrayBuffer()); pushes.push(body);
88 expect(body).toContain(`${OLD} ${NEXT} refs/heads/main`);
89 if (moveDuringPush) return new Response(packets(["unpack ok", "ng refs/heads/main stale old sha"]));
90 if (pushFailure === "absent") throw new Error("Fixture failed before push");
91 head = NEXT;
92 if (pushFailure === "applied") throw new Error("Fixture lost successful push reply");
93 return new Response(packets(["unpack ok", "ok refs/heads/main"]));
94 }
95 throw new Error("Unexpected offline fixture request.");
96 });
97});
98afterEach(() => { vi.unstubAllGlobals(); });
99
100async function refuse(operation: () => PromiseLike<unknown>, pattern: string) {
101 try { await operation(); } catch (error) { expect(String(error)).toContain(pattern); return; }
102 throw new Error("Expected refusal");
103}
104async function begin() { return (await env.FIXTURE.begin()).url; }
105async function submit(url: string, credential = CREDENTIAL) {
106 return env.VENDOR_HTTP.fetch(url, { method: "POST", headers: {
107 origin: "https://os.fixture.idiot.tax", "content-type": "application/x-www-form-urlencoded",
108 }, body: new URLSearchParams({ coreOrigin: ORIGIN, project: "fixture", agentCredential: credential }).toString() });
109}
110async function connect() {
111 const url = await begin();
112 const form = await env.VENDOR_HTTP.fetch(url);
113 expect(form.status).toBe(200); expect(form.headers.get("referrer-policy")).toBe("same-origin"); await form.text();
114 const response = await submit(url);
115 expect(response.status).toBe(200);
116 const html = await response.text();
117 expect(html).toContain("fixture-handoff-ticket"); expect(html).not.toContain(CREDENTIAL);
118 return { user: await env.FIXTURE.account(), url };
119}
120
121class Cache extends RpcTarget {
122 advertised: string[] = [];
123 builds = 0;
124 consumed = "";
125 async consumePack(pack: ReadableStream<Uint8Array>): Promise<string[]> {
126 this.consumed = new TextDecoder().decode(await new Response(pack).arrayBuffer()); return [OLD];
127 }
128 ancestor = true;
129 async advertiseCommit(sha: string) { this.advertised.push(sha); }
130 async isAncestor(): Promise<boolean> { return this.ancestor; }
131 async buildPack(): Promise<ReadableStream<Uint8Array>> {
132 this.builds++;
133 return new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("fixture-pack")); c.close(); } });
134 }
135}
136class Queue extends RpcTarget {
137 readonly cache = new Cache();
138 observations = 0;
139 submitted: { id: number; description: any }[] = [];
140 loseReply = false;
141 async authorizeObservation() { this.observations++; }
142 async getGitCache() { return this.cache; }
143 async submitAction(id: number, description: unknown) {
144 this.submitted.push({ id, description });
145 if (this.loseReply) throw new Error("Synthetic lost queue reply");
146 }
147}
148async function resource() {
149 const { user } = await connect();
150 const workspace = env.WORKSPACES.getByName(crypto.randomUUID());
151 await workspace.bind(user as unknown as Parameters<typeof workspace.bind>[0], `${ORIGIN}/projects/fixture/forks/fixture--alpha-1`);
152 const gatekeeper = workspace;
153 const queue = new Queue();
154 const session = await gatekeeper.startSession(queue as unknown as ApprovalQueue) as AgentForkSession;
155 return { user, gatekeeper, queue, session };
156}
157
158it("connects an enrolled agent, advertises only its owned resource and embeds the real sidebar", async () => {
159 const icon = { url: markIconUrl(brand.mark) };
160 expect(await env.VENDOR.describe()).toMatchObject({ displayName: brand.name, logo: icon, providesAuth: false, autoProvisionsAccount: false });
161 expect(await env.VENDOR.getSupportedResources()).toMatchObject([{ title: "Agent fork", icon }]);
162 const { user, url } = await connect();
163 expect(await user.describe()).toMatchObject({ displayName: `${brand.name} · alpha`, uniqueName: "alpha · fixture", providesUi: { title: brand.name } });
164 expect((await user.getSupportedResources())).toHaveLength(1);
165 expect(await user.getSupportedResources()).toMatchObject([{ icon }]);
166 expect((await submit(url)).status).toBe(400);
167 await refuse(() => user.getGatekeeperClassFor(`${ORIGIN}/projects/fixture/forks/fixture--foreign-1`), "does not own");
168 await refuse(() => user.getGatekeeperClassFor(`${ORIGIN}/projects/other/forks/fixture--alpha-1`), "outside");
169 const frame = await user.startAppUi!({ isAdmin: true });
170 expect(frame.iframeHtml).toContain("Ask an agent");
171 expect(frame.iframeHtml.match(/<div id="sidebar" data-core-origin="([^"]+)">/)?.[1]).toBe(ORIGIN);
172 expect(frame.iframeHtml).not.toContain(CREDENTIAL);
173 expect(frame.iframeHtml).not.toContain(`Bearer ${CREDENTIAL}`);
174 const batch = await (frame.ui as any).snapshot();
175 expect(batch.state.claims.filter((claim: { active: boolean }) => claim.active)).toHaveLength(3);
176 expect(batch.state.recentActivity).toHaveLength(3);
177 expect(batch.state.blockers[0].reason).toContain("overlaps");
178 expect(batch.state.reviews.map((review: { status: string }) => review.status)).toEqual(["pending", "stale", "pending"]);
179 frame.ui[Symbol.dispose]();
180 expect(scopes).toEqual([]);
181});
182
183it("refuses to disclose the sidebar or public-origin metadata after authorization is lost", async () => {
184 const { user } = await connect();
185 unauthorized = true;
186 await refuse(() => user.startAppUi!({ isAdmin: true }), "Sidebar session unavailable");
187});
188
189it("refuses owner credentials, changed agent identity, unsafe origins and cross-origin form posts", async () => {
190 role = "owner";
191 expect((await submit(await begin())).status).toBe(400);
192 role = "agent";
193 const { user } = await connect();
194 agent = "beta";
195 await refuse(() => user.describe(), "enrolled agent");
196 const url = await begin();
197 expect((await env.VENDOR_HTTP.fetch(url, { method: "POST", body: "", headers: {
198 origin: "https://foreign.idiot.tax", "content-type": "application/x-www-form-urlencoded",
199 } })).status).toBe(403);
200 const invalid = await env.VENDOR_HTTP.fetch(url, { method: "POST", headers: {
201 origin: "https://os.fixture.idiot.tax", "content-type": "application/x-www-form-urlencoded",
202 }, body: new URLSearchParams({ coreOrigin: "http://127.0.0.1", project: "fixture", agentCredential: CREDENTIAL }).toString() });
203 expect(invalid.status).toBe(400);
204});
205
206it("advertises heads and submits approval without minting or pushing until apply; retry is idempotent", async () => {
207 const { gatekeeper, queue, session } = await resource();
208 expect(await session.getHead()).toEqual({ repoName: "fixture--alpha-1", branch: "main", commitId: OLD });
209 expect(queue.observations).toBe(1); expect(queue.cache.advertised).toEqual([OLD]);
210 await session.push("main", NEXT);
211 expect(scopes).toEqual([]); expect(pushes).toEqual([]);
212 expect(queue.submitted[0].description).toMatchObject({ pushedCommits: [NEXT], awaitDecision: true, implementsRevert: false });
213 await gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache);
214 expect(head).toBe(NEXT); expect(scopes).toEqual(["write"]); expect(queue.cache.builds).toBe(1);
215 await gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache);
216 expect(pushes).toHaveLength(1);
217});
218
219it("recovers an ambiguous approval submission using the same action ID without an early write", async () => {
220 const { gatekeeper, queue, session } = await resource();
221 queue.loseReply = true;
222 await refuse(() => session.push("main", NEXT), "interrupted");
223 await refuse(() => session.push("main", "c".repeat(40)), "same branch and commit");
224 expect(scopes).toEqual([]); expect(pushes).toEqual([]);
225 queue.loseReply = false;
226 await session.push("main", NEXT);
227 expect(queue.submitted.map(action => action.id)).toEqual([1, 1]);
228 await gatekeeper.applyAction(1, queue.cache as unknown as GitCache);
229 await gatekeeper.applyAction(1, queue.cache as unknown as GitCache);
230 expect(pushes).toHaveLength(1);
231});
232
233it("can approve or reject an OS-queued action after its submission reply was lost", async () => {
234 const { gatekeeper, queue, session } = await resource();
235 queue.loseReply = true;
236 await refuse(() => session.push("main", NEXT), "interrupted");
237 await gatekeeper.applyAction(1, queue.cache as unknown as GitCache);
238 expect(head).toBe(NEXT);
239 await refuse(() => session.push("main", "c".repeat(40)), "interrupted");
240 await gatekeeper.rejectAction(2);
241 await refuse(() => gatekeeper.applyAction(2, queue.cache as unknown as GitCache), "not pending");
242 expect(pushes).toHaveLength(1);
243});
244
245it("rejects non-fast-forward pushes, unsupported branches and stale approved heads without a write token", async () => {
246 const { gatekeeper, queue, session } = await resource();
247 queue.cache.ancestor = false;
248 await refuse(() => session.push("main", NEXT), "fast-forward");
249 queue.cache.ancestor = true;
250 await refuse(() => session.push("other", NEXT), "default branch");
251 await session.push("main", NEXT);
252 head = "c".repeat(40);
253 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "Fork moved");
254 expect(scopes).toEqual([]); expect(pushes).toEqual([]);
255 await gatekeeper.rejectAction(queue.submitted[0].id);
256 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "not pending");
257});
258
259it("keeps the wire CAS and refuses token remote substitution", async () => {
260 const { gatekeeper, queue, session } = await resource();
261 await session.push("main", NEXT); moveDuringPush = true;
262 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "Git push failed");
263 expect(head).toBe(OLD);
264 badGrant = true; moveDuringPush = false;
265 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "Git push failed");
266 expect(pushes).toHaveLength(1);
267});
268
269it("rechecks revoked credentials and stops previously created fork capabilities", async () => {
270 const { user, queue, session, gatekeeper } = await resource();
271 await session.push("main", NEXT);
272 unauthorized = true;
273 await refuse(() => session.getHead(), "authorization");
274 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "authorization");
275 unauthorized = false;
276 await user.revoke();
277 await refuse(() => session.getHead(), "connection unavailable");
278 expect(scopes).toEqual([]);
279});
280
281it("stages reconnect credentials until the exact Workshop stage is committed", async () => {
282 const { user } = await connect();
283 const { url } = await user.reconnect();
284 expect((await submit(url)).status).toBe(200);
285 await refuse(() => user.commitReconnect("wrong-stage"), "stage unavailable");
286 await user.commitReconnect(await env.FIXTURE.reconnectStage());
287 expect((await user.describe()).uniqueName).toBe("alpha · fixture");
288});
289
290it("pulls through kit protocol v2 with a read token and refuses redirects", async () => {
291 const { gatekeeper, queue, session } = await resource();
292 await session.getHead();
293 await gatekeeper.gitPull([OLD], queue.cache as unknown as GitCache, { type: "commit", commitHistory: { kind: "depth", depth: 1 } });
294 expect(pulls).toHaveLength(1);
295 expect(pulls[0]).toContain(`want ${OLD}`);
296 expect(pulls[0]).not.toContain("have ");
297 expect(queue.cache.consumed).toContain("fixture-pack");
298 expect(scopes).toEqual(["read"]);
299 redirectCore = true;
300 await refuse(() => session.getHead(), "Core request failed");
301 redirectCore = false;
302 await session.push("main", NEXT);
303 redirectGit = true;
304 await refuse(() => gatekeeper.applyAction(queue.submitted[0].id, queue.cache as unknown as GitCache), "Git push failed");
305 expect(pushes).toEqual([]);
306});
307
308it("publishes Stable Code Mode types, exact resource addresses, and six distinct eligible kinds", async () => {
309 const { user, gatekeeper } = await resource();
310 expect(await gatekeeper.describeFork()).toMatchObject({
311 url: `${ORIGIN}/projects/fixture/forks/fixture--alpha-1`,
312 snippet: `Agent-owned Git fork. Attach this exact address: ${ORIGIN}/projects/fixture/forks/fixture--alpha-1`,
313 });
314 expect((await user.getSupportedResources())[0]!.description).toContain(`${ORIGIN}/projects/fixture/forks/<repoName>`);
315 const types = await gatekeeper.forkTypes();
316 for (const method of ["context", "join", "claim", "release", "requestReview", "readReview", "readReceipts"]) expect(types).toContain(`${method}(`);
317 expect(types).toContain("export type ContextResponse");
318 expect(types).toContain("After a write, end your turn. The OS posts 'approved and applied' when it's done. Don't poll context().");
319 expect(types).not.toMatch(/from ["']\./);
320 expect((await gatekeeper.autoApprovable()).map(kind => kind.tag)).toEqual(["claim", "race-entry", "join", "push", "request-review", "release"]);
321});
322
323it("runs production workflow calls through the account provider, with observations and no early writes", async () => {
324 const { session, gatekeeper, queue } = await resource();
325 expect(await session.context()).toMatchObject({ project: { id: "fixture" }, myWork: [] });
326 expect(await session.readReview("workflow-review")).toMatchObject({ review: { id: "workflow-review" } });
327 expect(await session.readReceipts("workflow-review")).toEqual({ receipt: null });
328 expect(queue.observations).toBe(3);
329 const operations = [
330 () => session.join(),
331 () => session.claim("workflow-packet", "fixture--alpha-1"),
332 () => session.requestReview("workflow-packet", "fixture--alpha-1", OLD),
333 () => session.release("workflow-packet"),
334 ];
335 for (const [index, operation] of operations.entries()) {
336 coreReads = [];
337 expect(await operation()).toBeUndefined();
338 expect(coreReads).not.toContain("/projects/fixture/state");
339 expect(coreReads.filter(path => path.endsWith("/packets"))).toHaveLength(index === 1 ? 1 : 0);
340 expect(workflowCalls).toHaveLength(index);
341 const action = queue.submitted[index]!;
342 expect(action.description).toMatchObject({ awaitDecision: true, autoApprovable: true });
343 if (index === 0) expect(action.description.description).toBe("Create one new fork of fixture for alpha.");
344 if (index === 2) expect(action.description.description).toBe(`Request review of commit ${OLD.slice(0, 7)} on fork fixture--alpha-1 for workflow-packet.`);
345 await gatekeeper.applyAction(action.id, queue.cache as unknown as GitCache);
346 await gatekeeper.applyAction(action.id, queue.cache as unknown as GitCache);
347 expect(workflowCalls).toHaveLength(index + 1);
348 }
349 expect(workflowCalls).toEqual([
350 { path: "/projects/fixture/join", method: "POST", body: { agent: "alpha", ttlSeconds: 60 } },
351 { path: "/projects/fixture/packets/workflow-packet/claim", method: "POST", body: { repoName: "fixture--alpha-1" } },
352 { path: "/projects/fixture/reviews", method: "POST", body: { packet: "workflow-packet", repoName: "fixture--alpha-1", sha: OLD } },
353 { path: "/projects/fixture/packets/workflow-packet/claim", method: "DELETE", body: null },
354 ]);
355 expect(JSON.stringify(queue.submitted)).not.toContain("fixture-join-secret");
356});
357
358for (const mode of ["standard", "race"] as const) {
359 it(`queues a ${mode} claim using the latest authenticated fork without reading /state`, async () => {
360 const { session, queue, gatekeeper } = await resource();
361 workflowMode = mode;
362 // /state deliberately has only the original fork; /me is authoritative.
363 discoveredForks.unshift({ repoName: "fixture--alpha-latest", remote: "https://git.fixture.idiot.tax/fixture--alpha-latest.git", head: OLD, createdAt: "2026-03-01T00:00:00Z" });
364 discoveredForks.push({ repoName: "fixture--alpha-middle", remote: "https://git.fixture.idiot.tax/fixture--alpha-middle.git", head: OLD, createdAt: "2026-02-01T00:00:00Z" });
365 coreReads = [];
366 await session.claim("workflow-packet");
367 expect(coreReads).not.toContain("/projects/fixture/state");
368 expect(coreReads.filter(path => path.endsWith("/packets"))).toEqual(["/projects/fixture/packets"]);
369 expect(workflowCalls).toEqual([]);
370 expect(queue.submitted[0]!.description).toMatchObject({
371 title: mode === "race" ? "Enter race workflow-packet" : "Claim workflow-packet",
372 description: mode === "race" ? "Enter alpha in race workflow-packet on fork fixture--alpha-latest." : "Claim packet workflow-packet on fork fixture--alpha-latest.",
373 actionKind: { tag: mode === "race" ? "race-entry" : "claim" },
374 awaitDecision: true, autoApprovable: true, implementsRevert: false,
375 });
376 discoveredForks.push({ repoName: "fixture--alpha-after", remote: "https://git.fixture.idiot.tax/fixture--alpha-after.git", head: OLD, createdAt: "2026-04-01T00:00:00Z" });
377 await session.claim("workflow-packet");
378 expect(queue.submitted).toHaveLength(1);
379 await gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache);
380 expect(workflowCalls).toEqual([{ path: "/projects/fixture/packets/workflow-packet/claim", method: "POST", body: { repoName: "fixture--alpha-latest" } }]);
381 });
382}
383
384it("refuses unavailable packet metadata and authorization lost during the narrow read before queuing", async () => {
385 const { session, queue } = await resource();
386 for (const [reply, message] of [
387 [{ packets: [] }, "Packet is unavailable"],
388 [{ packets: null }, "Invalid packet response"],
389 [{ packets: [{ id: "workflow-packet", mode: "unknown" }] }, "Invalid packet response"],
390 ] as const) {
391 packetReply = reply;
392 await refuse(() => session.claim("workflow-packet"), message);
393 }
394 packetReply = undefined;
395 revokeAfterPackets = true;
396 await refuse(() => session.claim("workflow-packet"), "authorization");
397 expect(queue.submitted).toEqual([]);
398 expect(workflowCalls).toEqual([]);
399 expect(scopes).toEqual([]);
400});
401
402it("keeps the selected fork and approval ID after a lost claim submission reply", async () => {
403 const { session, queue } = await resource();
404 queue.loseReply = true;
405 await refuse(() => session.claim("workflow-packet"), "submission interrupted");
406 discoveredForks.push({ repoName: "fixture--alpha-later", remote: "https://git.fixture.idiot.tax/fixture--alpha-later.git", head: OLD, createdAt: "2026-02-01T00:00:00Z" });
407 queue.loseReply = false;
408 coreReads = [];
409 await session.claim("workflow-packet");
410 expect(queue.submitted.map(action => action.id)).toEqual([1, 1]);
411 expect(queue.submitted[1]!.description).toEqual(queue.submitted[0]!.description);
412 expect(coreReads).not.toContain("/projects/fixture/state");
413 expect(coreReads).not.toContain("/projects/fixture/packets");
414 expect(workflowCalls).toEqual([]);
415});
416
417for (const result of ["applied", "absent"] as const) {
418 it(`still reads recovery evidence before a workflow write after an uncertain ${result} push`, async () => {
419 const { session, queue, gatekeeper } = await resource();
420 await session.push("main", NEXT);
421 pushFailure = result;
422 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "Git push failed");
423 coreReads = [];
424 if (result === "applied") {
425 await session.join();
426 expect(queue.submitted).toHaveLength(2);
427 } else {
428 await refuse(() => session.join(), "pending push");
429 expect(queue.submitted).toHaveLength(1);
430 }
431 expect(coreReads).toContain("/projects/fixture/state");
432 expect(pushes).toHaveLength(1);
433 expect(workflowCalls).toEqual([]);
434 });
435}
436
437it("classifies race entry from core and prevents push/workflow queue conflicts", async () => {
438 const { session, gatekeeper, queue } = await resource();
439 workflowMode = "race";
440 await session.claim("workflow-packet", "fixture--alpha-1");
441 expect(queue.submitted[0]!.description.actionKind.tag).toBe("race-entry");
442 expect(queue.submitted[0]!.description.description).toBe("Enter alpha in race workflow-packet on fork fixture--alpha-1.");
443 await refuse(() => session.push("main", NEXT), "pending workflow");
444 await gatekeeper.rejectAction(queue.submitted[0]!.id);
445 await session.push("main", NEXT);
446 expect(queue.submitted[1]!.description.actionKind.tag).toBe("push");
447 await refuse(() => session.join(), "pending push");
448 await gatekeeper.rejectAction(queue.submitted[1]!.id);
449 workflowMode = "standard";
450 await session.claim("workflow-packet");
451 expect(queue.submitted[2]!.description.actionKind.tag).toBe("claim");
452 workflowMode = "race";
453 await refuse(() => gatekeeper.applyAction(queue.submitted[2]!.id, queue.cache as unknown as GitCache), "kind changed");
454 expect(workflowCalls).toEqual([]);
455});
456
457it("bounds absent-join retries and keeps provider errors private", async () => {
458 const { session, gatekeeper, queue, user } = await resource();
459 await session.join();
460 workflowFailure = true;
461 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "outcome uncertain");
462 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "outcome uncertain");
463 workflowFailure = false;
464 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "outcome uncertain");
465 expect(workflowCalls).toHaveLength(2);
466 await user.revoke();
467 await refuse(() => session.context(), "connection unavailable");
468});
469
470it("uses the injected Stable provider bearer for every workflow HTTP call instead of the draft key", async () => {
471 const { CoreClient } = await import("../src/worker/core-client.ts");
472 const getBearer = vi.fn(async () => `Bearer ${CREDENTIAL}` as const);
473 const client = new CoreClient({ coreOrigin: ORIGIN, project: "fixture", agentCredential: "fixture-unused-draft-credential" }, "alpha", () => {}, { getBearer });
474 await client.context();
475 await client.executeWorkflow({ kind: "join" });
476 await client.executeWorkflow({ kind: "claim", packet: "workflow-packet" });
477 await client.executeWorkflow({ kind: "requestReview", packet: "workflow-packet", repoName: "fixture--alpha-1", sha: OLD });
478 await client.executeWorkflow({ kind: "release", packet: "workflow-packet" });
479 await client.readReview("workflow-review");
480 await client.readReceipts("workflow-review");
481 expect(getBearer).toHaveBeenCalledTimes(14);
482 for (const args of getBearer.mock.calls) expect(args).toEqual([{ origin: ORIGIN, project: "fixture" }]);
483});
484
485
486it("reconciles a successful push with a lost reply from the exact head", async () => {
487 const { session, gatekeeper, queue } = await resource();
488 await session.push("main", NEXT);
489 pushFailure = "applied";
490 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "Git push failed");
491 expect((await session.getHead()).commitId).toBe(NEXT);
492 await gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache);
493 expect(pushes).toHaveLength(1);
494 await session.join();
495 expect(queue.submitted).toHaveLength(2);
496});
497
498it("allows one push retry at the original head and fences further or divergent attempts", async () => {
499 const { session, gatekeeper, queue } = await resource();
500 await session.push("main", NEXT);
501 pushFailure = "absent";
502 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "Git push failed");
503 await session.push("main", NEXT);
504 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "Git push failed");
505 pushFailure = null;
506 await refuse(() => gatekeeper.applyAction(queue.submitted[0]!.id, queue.cache as unknown as GitCache), "outcome uncertain");
507 expect(pushes).toHaveLength(2);
508 head = "c".repeat(40);
509 await refuse(() => session.push("main", NEXT), "outcome uncertain");
510 head = NEXT;
511 await session.getHead();
512 await session.join();
513});
514