Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-gatekeeper/__tests__/vendor.test.ts

Snapshot at 34e3175

Containing directory

29,692 bytes · typescriptView raw
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