Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/__tests__/worker.ts
Snapshot at 34e3175
1import { WorkflowActions } from "../src/worker/workflow.ts";
2import { workflowSource } from "../src/worker/workflow-source.ts";
3import { WorkflowSession } from "../src/worker/workflow-session.ts";
4import { DurableObject, RpcTarget, RpcStub, WorkerEntrypoint } from "cloudflare:workers";
5import { createSidebarFrame, type SidebarUiImpl, type SidebarSource } from "../src/worker/sidebar.ts";
6import type { AgentForkSession } from "../src/fork-types.d.ts";
7import type { AgentAccount } from "../src/worker/account.ts";
8import type { ForkGatekeeper } from "../src/worker/fork.ts";
9import type { ProjectState } from "../../forkhorse-dashboard/src/contracts.ts";
10import { ConnectionDraftUiImpl } from "../src/worker/configurator.ts";
11import type { ApprovalQueue, GitCache, GitPullHints, GatekeeperUser, GatekeeperConnectCallback } from "@gadgets/workshop-shared/gatekeeper";
12
13let reconnectStage: string | undefined;
14export class FixtureConnectCallback extends WorkerEntrypoint implements GatekeeperConnectCallback {
15 async complete(user: Fetcher<GatekeeperUser>) {
16 await this.env.WORKSPACES.getByName("callback-vault").saveAccount(user);
17 return { targetOrigin: "https://os.fixture.idiot.tax", ticket: "fixture-handoff-ticket" };
18 }
19 async reconnectComplete(stageId: string) {
20 reconnectStage = stageId;
21 return { targetOrigin: "https://os.fixture.idiot.tax", ticket: "fixture-reconnect-ticket" };
22 }
23 async credentialsExpired(): Promise<void> {}
24 async credentialsRestored(): Promise<void> {}
25}
26export class FixtureWorkspace extends DurableObject {
27 #workflow() {
28 return new WorkflowActions(this.ctx.storage.kv, workflowSource({
29 project: "fixture",
30 identity: async () => ({ agent: "fixture-agent", work: [], forks: [] }),
31 request: async (path, init) => {
32 if (!init) return { project: { id: "fixture" }, packets: [{ id: "fixture-packet", mode: "standard" }], forks: [], claims: [], races: [], reviews: [] };
33 const calls = this.ctx.storage.kv.get<unknown[]>("fixtureCalls") ?? [];
34 calls.push({ path, method: init.method, body: init.body ? JSON.parse(String(init.body)) : null });
35 this.ctx.storage.kv.put("fixtureCalls", calls);
36 return { token: "fixture-token-must-not-escape" };
37 },
38 }));
39 }
40 async workflowSession(queue: RpcStub<ApprovalQueue>): Promise<WorkflowSession> {
41 return new WorkflowSession(this.#workflow(), queue.dup());
42 }
43 async applyWorkflow(id: number): Promise<void> { await this.#workflow().apply(id); }
44 async rejectWorkflow(id: number): Promise<void> { await this.#workflow().reject(id); }
45 async workflowCalls(): Promise<unknown[]> { return this.ctx.storage.kv.get<unknown[]>("fixtureCalls") ?? []; }
46 #fork() {
47 return this.ctx.facets.get<ForkGatekeeper>("fork", () => ({
48 class: this.ctx.storage.kv.get<DurableObjectClass<ForkGatekeeper>>("forkClass")!,
49 }));
50 }
51 async startSession(queue: RpcStub<ApprovalQueue>): Promise<AgentForkSession> {
52 return await this.#fork().startSession(queue as unknown as ApprovalQueue) as unknown as AgentForkSession;
53 }
54 async describeFork() { return this.#fork().describe(); }
55 async forkTypes() { return this.#fork().getTypeScriptTypes(); }
56 async autoApprovable() { return this.#fork().getAutoApprovableActions(); }
57 async applyAction(id: number, cache: RpcStub<GitCache>): Promise<void> { await this.#fork().applyAction(id, cache); }
58 async gitPull(oids: string[], cache: RpcStub<GitCache>, hints: GitPullHints): Promise<void> { await this.#fork().gitPull(oids, cache, hints); }
59 async rejectAction(id: number): Promise<void> { await this.#fork().rejectAction(id); }
60
61 async saveAccount(user: Fetcher<GatekeeperUser>): Promise<void> { this.ctx.storage.kv.put("account", user); }
62 async getAccount(): Promise<AgentAccount> {
63 const user = this.ctx.storage.kv.get<Fetcher<GatekeeperUser>>("account");
64 if (!user) throw new Error("No fixture account completed.");
65 return user as unknown as AgentAccount;
66 }
67 async bind(user: Fetcher<GatekeeperUser>, url: string) {
68 const resolved = await user.getGatekeeperClassFor(url);
69 this.ctx.storage.kv.put("forkClass", resolved.class);
70 }
71}
72
73// Isolated, labeled fixture producer. No HTTP, account credentials or external service bindings.
74export class FixtureSession extends RpcTarget implements SidebarSource {
75 #authorized = true;
76 #changed?: (state: ProjectState) => void;
77 #revoked?: () => void;
78 #stops = 0;
79 #state: ProjectState = {
80 project: { id: "fixture", repoName: "fixture", defaultBranch: "main" },
81 agents: [], forks: [], packets: [], claims: [], blockers: [], reviews: [],
82 ledger: [], reconcilerAlerts: [],
83 };
84 constructor(authorized: boolean) { super(); this.#authorized = authorized; }
85 async authorize(): Promise<void> {
86 if (!this.#authorized) throw new Error("Fixture access revoked.");
87 }
88 async read(): Promise<ProjectState> { return structuredClone(this.#state); }
89 watch(changed: (state: ProjectState) => void, revoked: () => void): () => void {
90 this.#changed = changed; this.#revoked = revoked;
91 return () => { this.#stops++; this.#changed = undefined; this.#revoked = undefined; };
92 }
93 async open(): Promise<SidebarUiImpl> {
94 const frame = await createSidebarFrame(this, "<!doctype html><p>Fixture sidebar</p>");
95 return frame.ui as unknown as SidebarUiImpl;
96 }
97 async publish(): Promise<void> {
98 this.#state.packets.push({ id: `fixture-${this.#state.packets.length}`, title: "Fixture packet", goal: "Fixture", paths: [], acceptance: [] });
99 this.#changed?.(structuredClone(this.#state));
100 }
101 async foreign(): Promise<void> {
102 const state = structuredClone(this.#state); state.project.id = "foreign";
103 this.#changed?.(state);
104 }
105 async revoke(notify = true): Promise<void> {
106 this.#authorized = false;
107 if (notify) this.#revoked?.();
108 }
109 async stopped(): Promise<number> { return this.#stops; }
110}
111
112// Runtime control: deliberately contains no sidebar, feed or validation implementation.
113class DiagnosticTarget extends RpcTarget {
114 constructor(private readonly disposed: () => void) { super(); }
115 async reject(): Promise<void> { throw new Error("Plain target diagnostic refusal."); }
116 async ping(): Promise<string> { return "ready"; }
117 [Symbol.dispose](): void { this.disposed(); }
118}
119
120let diagnosticDisposals = 0;
121export class FixtureHarness extends WorkerEntrypoint {
122 async begin(): Promise<{ url: string }> {
123 const exports = this.ctx.exports as unknown as {
124 FixtureConnectCallback(options: {}): Fetcher<GatekeeperConnectCallback>;
125 };
126 return this.ctx.exports.GatekeeperVendor({}).connectAccount(exports.FixtureConnectCallback({}));
127 }
128 async account(): Promise<AgentAccount> {
129 return await this.env.WORKSPACES.getByName("callback-vault").getAccount() as unknown as AgentAccount;
130 }
131 async reconnectStage(): Promise<string> {
132 if (!reconnectStage) throw new Error("No fixture reconnect staged.");
133 return reconnectStage;
134 }
135 async diagnosticTarget(): Promise<DiagnosticTarget> {
136 return new DiagnosticTarget(() => { diagnosticDisposals++; });
137 }
138 async diagnosticDisposals(): Promise<number> { return diagnosticDisposals; }
139 async diagnosticReject(): Promise<void> { throw new Error("Plain native RPC diagnostic refusal."); }
140 async session(authorized = true): Promise<FixtureSession> { return new FixtureSession(authorized); }
141 async configurator(): Promise<ConnectionDraftUiImpl> { return new ConnectionDraftUiImpl(); }
142 async deniedSetup(): Promise<{ opened: boolean; reads: number }> {
143 let reads = 0;
144 try {
145 const frame = await createSidebarFrame({
146 async authorize() { throw new Error("Fixture access denied."); },
147 async read() { reads++; return new FixtureSession(true).read(); },
148 watch() { throw new Error("Must not start a denied source."); },
149 }, "Fixture");
150 frame.ui[Symbol.dispose]();
151 return { opened: true, reads };
152 } catch { return { opened: false, reads }; }
153 }
154}
155export { default, GatekeeperVendor, UserAccount, AgentAccount, AgentVerifier, ForkGatekeeper } from "../src/worker/index.ts";
156