Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/__tests__/sidebar.test.ts
Snapshot at 34e3175
1import { env, RpcTarget } from "cloudflare:workers";
2import { describe, expect, it, vi } from "vitest";
3import { sidebarAccountDescription, type WorkerUpdateReceiver } from "../src/worker/sidebar.ts";
4import type { SidebarBatch } from "../src/ui-types.ts";
5import type { FixtureHarness } from "./worker.ts";
6import brand from "../../../brand.json";
7import { markIconUrl } from "../../forkhorse-dashboard/src/mark.ts";
8import { railIconUrl } from "../src/rail-icon.ts";
9
10declare global {
11 namespace Cloudflare {
12 interface Env {
13 FIXTURE: Service<FixtureHarness>;
14 }
15 }
16}
17
18class Receiver extends RpcTarget implements WorkerUpdateReceiver {
19 readonly batches: SidebarBatch[] = [];
20 async receive(batch: SidebarBatch): Promise<void> {
21 this.batches.push(batch);
22 }
23}
24
25async function refuses(operation: () => PromiseLike<unknown>, message = ""): Promise<void> {
26 try {
27 await operation();
28 } catch (error) {
29 expect(error instanceof Error ? error.message : String(error)).toContain(message);
30 return;
31 }
32 throw new Error("Expected RPC refusal.");
33}
34
35describe("Worker-native sidebar capability (local fixtures)", () => {
36 it("checks a configurator draft through native RPC without echoing its credential or granting a connection", async () => {
37 using draft = await env.FIXTURE.configurator();
38 const result = await draft.checkDraft({
39 coreOrigin: "https://core.fixture.idiot.tax",
40 project: "fixture",
41 agentCredential: "fixture-only-not-a-real-credential",
42 });
43 expect(result.connectionAvailable).toBe(false);
44 expect(JSON.stringify(result)).not.toContain("fixture-only-not-a-real-credential");
45 await refuses(
46 () =>
47 draft.checkDraft({
48 coreOrigin: "http://127.0.0.1",
49 project: "fixture",
50 agentCredential: "fixture-only-not-a-real-credential",
51 }),
52 "HTTPS",
53 );
54 });
55 it("runs under workerd and keeps the emoji avatar separate from the regular Horse rail icon", async () => {
56 expect(navigator.userAgent).toBe("Cloudflare-Workers");
57 expect(sidebarAccountDescription()).toMatchObject({
58 displayName: brand.name,
59 providesUi: { title: brand.name },
60 });
61 expect(sidebarAccountDescription().singleton).toBeUndefined();
62 const icon = { url: markIconUrl(brand.mark) };
63 const description = sidebarAccountDescription();
64 expect(description.avatar).toEqual(icon);
65 expect(description.providesUi!.icon).toEqual({ url: railIconUrl(brand.railIcon) });
66 expect(description.providesUi!.icon).not.toEqual(icon);
67 const svg = decodeURIComponent(description.providesUi!.icon!.url.split(",").slice(1).join(","));
68 expect(svg).toContain('viewBox="0 0 256 256" fill="currentColor"');
69 expect(svg.match(/<path /g)).toHaveLength(1);
70 const path = /<path d="([^"]+)"/.exec(svg)![1]!;
71 // Independently recorded from Phosphor 2.1.10's regular definition, not another weight.
72 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(path));
73 expect(Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join(""))
74 .toBe("0003357977cd1a2ac5cf9cf1606a93830a72be0069dcb9434428c18e3c8a1232");
75 });
76
77 it("authorizes before returning a capability", async () => {
78 expect(await env.FIXTURE.deniedSetup()).toMatchObject({ opened: false, reads: 0 });
79 });
80
81 it("releasing the UI capability stops its producer", async () => {
82 using session = await env.FIXTURE.session();
83 {
84 using ui = await session.open();
85 await ui.snapshot();
86 }
87 await vi.waitFor(async () => expect(await session.stopped()).toBe(1));
88 });
89
90 it("retains push callbacks across RPC and closes subscriptions", async () => {
91 using session = await env.FIXTURE.session();
92 using ui = await session.open();
93 expect((await ui.snapshot()).state.project.id).toBe("fixture");
94 const receiver = new Receiver();
95 using subscription = await ui.subscribe(receiver);
96 await vi.waitFor(() => expect(receiver.batches).toHaveLength(1));
97 await session.publish();
98 await vi.waitFor(() => expect(receiver.batches.at(-1)?.version).toBe(1));
99 expect(receiver.batches.at(-1)?.state.packets).toHaveLength(1);
100 await subscription.close();
101 await subscription.close();
102 await session.publish();
103 await vi.waitFor(async () => expect((await ui.snapshot()).version).toBe(2));
104 expect(receiver.batches).toHaveLength(2);
105 });
106
107 it("bounds pending polls, wakes on a batch, and rejects forged cursors", async () => {
108 using session = await env.FIXTURE.session();
109 using ui = await session.open();
110 const pending = ui.waitForChanges(0);
111 await refuses(() => ui.waitForChanges(0), "one pending poll");
112 await session.publish();
113 expect((await pending).version).toBe(1);
114 await refuses(() => ui.waitForChanges(-1), "Invalid UI version");
115 await refuses(() => ui.waitForChanges(99), "Invalid UI version");
116 });
117
118 it("revocation stops the producer and rejects already-waiting reads", async () => {
119 using session = await env.FIXTURE.session();
120 using ui = await session.open();
121 const pending = ui.waitForChanges(0);
122 const rejected = refuses(() => pending);
123 await session.revoke();
124 await rejected;
125 await refuses(() => ui.snapshot(), "unavailable");
126 expect(await session.stopped()).toBe(1);
127 });
128
129 it("rechecks authorization before a callback even without a revocation notification", async () => {
130 using session = await env.FIXTURE.session();
131 using ui = await session.open();
132 const receiver = new Receiver();
133 using _subscription = await ui.subscribe(receiver);
134 await vi.waitFor(() => expect(receiver.batches).toHaveLength(1));
135 await session.revoke(false);
136 await session.publish();
137 await vi.waitFor(async () => expect(await session.stopped()).toBe(1));
138 expect(receiver.batches).toHaveLength(1);
139 await refuses(() => ui.snapshot(), "unavailable");
140 });
141
142 it("closes instead of leaking a foreign project emitted by its producer", async () => {
143 using session = await env.FIXTURE.session();
144 using ui = await session.open();
145 await session.foreign();
146 await refuses(() => ui.snapshot(), "unavailable");
147 expect(await session.stopped()).toBe(1);
148 });
149});
150
151it("replaces and releases native subscriptions after reconnect without leaking callbacks", async () => {
152 using session = await env.FIXTURE.session();
153 using ui = await session.open();
154 const first = new Receiver();
155 using old = await ui.subscribe(first);
156 await vi.waitFor(() => expect(first.batches.length).toBe(1));
157 await old.close();
158 const second = new Receiver();
159 using replacement = await ui.subscribe(second);
160 await session.publish();
161 await vi.waitFor(() => expect(second.batches.at(-1)?.version).toBe(1));
162 expect(first.batches.length).toBe(1);
163 await replacement.close();
164 await session.publish();
165 await vi.waitFor(async () => expect((await ui.snapshot()).version).toBe(2));
166 expect(second.batches.at(-1)?.version).toBe(1);
167});
168