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

packages/forkhorse-gatekeeper/__tests__/worker.ts

Snapshot at 34e3175

Containing directory

8,139 bytes · typescriptView raw
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