1import { describe, expect, it } from "@effect/vitest";
2import {
3 type AssetCreateUrlResult,
4 AssetWorkspaceAssetNotFoundError,
5 AssetWorkspaceAssetInspectionError,
6 AssetWorkspaceContextNotFoundError,
7 EnvironmentAuthorizationError,
8 EnvironmentId,
9 type ProjectCloneSnapshot,
10 ProjectId,
11 ThreadId,
12 WS_METHODS,
13} from "@t3tools/contracts";
14import { HttpClient, HttpClientResponse } from "effect/http";
15import * as HubConnection from "../connection/hub.ts";
16import * as Cause from "effect/Cause";
17import * as Effect from "effect/Effect";
18import * as Stream from "effect/Stream";
19import * as SubscriptionRef from "effect/SubscriptionRef";
20import * as Option from "effect/Option";
21import * as Layer from "effect/Layer";
22import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
23
24import * as EnvironmentRegistry from "../connection/registry.ts";
25import {
26 AVAILABLE_CONNECTION_STATE,
27 PrimaryConnectionTarget,
28 type PreparedConnection,
29 type SupervisorConnectionState,
30} from "../connection/model.ts";
31import * as EnvironmentSupervisor from "../connection/supervisor.ts";
32import type { RpcSession } from "../rpc/session.ts";
33import type { WsRpcProtocolClient } from "../rpc/protocol.ts";
34import { createOrchestrationEnvironmentAtoms } from "./orchestration.ts";
35import { v2Project, v2ThreadShell, v2Now } from "./orchestrationV2TestFixtures.ts";
36import { createProjectFaviconCache } from "../projectFaviconCache.ts";
37import {
38 createAssetEnvironmentAtoms,
39 createProjectFaviconUrlAtomFamily,
40 InvalidAssetCollectionKeyError,
41 parseAssetCollectionKey,
42 resolveAssetUrl,
43} from "./assets.ts";
44
45describe("asset addresses", () => {
46 it("resolves signed environment paths through the proxy while retaining external assets", () => {
47 const base = "https://hub.example/prefix/environments/one/";
48 expect(resolveAssetUrl(base, "/api/assets/a%2Fb?signature=signed")).toBe(
49 `${base}api/assets/a%2Fb?signature=signed`,
50 );
51 expect(resolveAssetUrl(base, "https://other.example/api/assets/signed")).toBe(
52 "https://other.example/api/assets/signed",
53 );
54 });
55});
56
57describe("asset collection keys", () => {
58 it("preserves malformed JSON and its native cause", () => {
59 const key = "not-json";
60 let error: unknown;
61
62 try {
63 parseAssetCollectionKey(key);
64 } catch (cause) {
65 error = cause;
66 }
67
68 expect(error).toBeInstanceOf(InvalidAssetCollectionKeyError);
69 expect(error).toMatchObject({ key, cause: expect.any(SyntaxError) });
70 });
71
72 it("rejects invalid asset collection shapes", () => {
73 const key = JSON.stringify(["environment-1", [{ _tag: "unknown" }]]);
74
75 expect(() => parseAssetCollectionKey(key)).toThrowError(InvalidAssetCollectionKeyError);
76 });
77});
78
79describe("createAssetEnvironmentAtoms", () => {
80 it.effect("issues NAS attachment URLs without waiting for a sleeping source session", () =>
81 Effect.gen(function* () {
82 const environmentId = EnvironmentId.make("sleeping-laptop");
83 const resource = {
84 _tag: "attachment" as const,
85 attachmentId: "stored-image",
86 fileName: "picture.png",
87 };
88 const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({
89 target: new PrimaryConnectionTarget({
90 environmentId,
91 label: "Laptop",
92 httpBaseUrl: "https://source.example.test",
93 wsBaseUrl: "wss://source.example.test",
94 }),
95 state: yield* SubscriptionRef.make<SupervisorConnectionState>({
96 ...AVAILABLE_CONNECTION_STATE,
97 phase: "offline",
98 }),
99 session: yield* SubscriptionRef.make(Option.none<RpcSession>()),
100 prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()),
101 connect: Effect.void,
102 disconnect: Effect.void,
103 retryNow: Effect.void,
104 });
105 const environments = EnvironmentRegistry.EnvironmentRegistry.of({
106 run: (_id, effect) =>
107 Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
108 followStream: (_id, stream) =>
109 Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
110 } as EnvironmentRegistry.EnvironmentRegistry["Service"]);
111 const hubState = yield* SubscriptionRef.make<HubConnection.HubConnectionState>({
112 ...HubConnection.EMPTY_HUB_CONNECTION_STATE,
113 inbox: {
114 environments: [
115 {
116 environmentId,
117 pendingThreadIds: [],
118 snapshot: {
119 schemaVersion: 2,
120 snapshotSequence: 17,
121 projects: [v2Project],
122 threads: [],
123 archivedThreads: [{ ...v2ThreadShell, archivedAt: v2Now }],
124 },
125 },
126 ],
127 },
128 connection: {
129 environmentId: EnvironmentId.make("nas"),
130 label: "Archive",
131 httpBaseUrl: "https://archive.example.test",
132 },
133 });
134 const hub = HubConnection.HubConnection.of({
135 state: hubState,
136 headers: Effect.succeed({ authorization: "Bearer ephemeral" }),
137 connect: () => Effect.void,
138 connectAuthenticated: () => Effect.void,
139 refresh: Effect.void,
140 refreshInbox: Effect.void,
141 organize: () => Effect.succeedNone,
142 disconnect: Effect.void,
143 });
144 const client = HttpClient.make((request) =>
145 Effect.sync(() => {
146 expect(request.method).toBe("POST");
147 expect(request.url).toBe("https://archive.example.test/api/archive/assets");
148 expect(request.headers.authorization).toBe("Bearer ephemeral");
149 if (request.body._tag !== "Uint8Array") throw new Error("Expected JSON body");
150 expect(JSON.parse(new TextDecoder().decode(request.body.body))).toEqual({
151 environmentId,
152 resource,
153 });
154 return HttpClientResponse.fromWeb(
155 request,
156 Response.json({
157 relativeUrl: "/api/archive/assets/signed/picture.png",
158 expiresAt: 4_000_000_000_000,
159 }),
160 );
161 }),
162 );
163 const runtime = Atom.runtime(
164 Layer.mergeAll(
165 Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environments),
166 Layer.succeed(HubConnection.HubConnection, hub),
167 Layer.succeed(HttpClient.HttpClient, client),
168 ),
169 );
170 const registry = AtomRegistry.make();
171 yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose()));
172 const query = createAssetEnvironmentAtoms(runtime).createUrl({
173 environmentId,
174 input: { resource },
175 });
176 const result = yield* AtomRegistry.getResult(registry, query, { suspendOnWaiting: true });
177 expect(result.relativeUrl).toBe(
178 "https://archive.example.test/api/archive/assets/signed/picture.png",
179 );
180 const archiveQuery = createOrchestrationEnvironmentAtoms(runtime).archivedShellSnapshot({
181 environmentId,
182 input: {},
183 });
184 const discovered = yield* AtomRegistry.getResult(registry, archiveQuery, {
185 suspendOnWaiting: true,
186 });
187 expect(discovered.threads.map((thread) => thread.id)).toEqual([v2ThreadShell.id]);
188 expect(discovered.projects[0]?.id).toBe(v2Project.id);
189 }).pipe(Effect.scoped),
190 );
191
192 it.effect.each([
193 { name: "missing video", path: "/tmp/clip.mp4", fallback: true },
194 { name: "literal filename characters", path: "/tmp/frame#one?two.png", fallback: true },
195 { name: "windows path", path: "C:\\Users\\demo\\clip.mp4", fallback: true },
196 { name: "inspection failure", path: "/tmp/clip.mp4", error: "inspection", fallback: true },
197 { name: "foreign thread", path: "/tmp/clip.mp4", error: "context", fallback: true },
198 { name: "remote success", path: "/tmp/clip.mp4", success: true },
199 { name: "relative path", path: "clip.mp4" },
200 { name: "no primary", path: "/tmp/clip.mp4", primary: "none" },
201 { name: "primary reconnect", path: "/tmp/frame.png", primary: "reconnecting" },
202 { name: "same environment", path: "/tmp/clip.mp4", primary: "same" },
203 { name: "non-media", path: "/tmp/report.html" },
204 { name: "authorization failure", path: "/tmp/clip.mp4", error: "auth" },
205 ])("uses the correct environment for $name", (scenario) =>
206 Effect.gen(function* () {
207 const remoteId = EnvironmentId.make("remote");
208 const localId = EnvironmentId.make("local");
209 const resource = {
210 _tag: "media-file" as const,
211 threadId: ThreadId.make("foreign-thread"),
212 path: scenario.path,
213 };
214 const error =
215 scenario.error === "auth"
216 ? new EnvironmentAuthorizationError({
217 message: "denied",
218 requiredScope: "orchestration:read",
219 })
220 : scenario.error === "inspection"
221 ? new AssetWorkspaceAssetInspectionError({ resource, cause: new Error("unreadable") })
222 : scenario.error === "context"
223 ? new AssetWorkspaceContextNotFoundError({ resource })
224 : new AssetWorkspaceAssetNotFoundError({ resource });
225 const calls: EnvironmentId[] = [];
226 const supervisors = new Map<
227 EnvironmentId,
228 EnvironmentSupervisor.EnvironmentSupervisor["Service"]
229 >();
230 for (const environmentId of [remoteId, localId]) {
231 const client = {
232 [WS_METHODS.assetsCreateUrl]: () => {
233 calls.push(environmentId);
234 return environmentId === remoteId && !scenario.success
235 ? Effect.fail(error)
236 : Effect.succeed({
237 relativeUrl: `/api/assets/${environmentId}/media`,
238 expiresAt: 999999,
239 });
240 },
241 } as unknown as WsRpcProtocolClient;
242 const session = { client } as RpcSession;
243 supervisors.set(
244 environmentId,
245 EnvironmentSupervisor.EnvironmentSupervisor.of({
246 target: new PrimaryConnectionTarget({
247 environmentId,
248 label: environmentId,
249 httpBaseUrl: `https://${environmentId}.test`,
250 wsBaseUrl: `wss://${environmentId}.test`,
251 }),
252 state: yield* SubscriptionRef.make<SupervisorConnectionState>({
253 ...AVAILABLE_CONNECTION_STATE,
254 phase: "connected" as const,
255 }),
256 session: yield* SubscriptionRef.make(Option.some(session)),
257 prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()),
258 connect: Effect.void,
259 disconnect: Effect.void,
260 retryNow: Effect.void,
261 }),
262 );
263 }
264 const environments = EnvironmentRegistry.EnvironmentRegistry.of({
265 run: (id, effect) =>
266 Effect.provideService(
267 effect,
268 EnvironmentSupervisor.EnvironmentSupervisor,
269 supervisors.get(id)!,
270 ),
271 followStream: (id, stream) =>
272 Stream.provideService(
273 stream,
274 EnvironmentSupervisor.EnvironmentSupervisor,
275 supervisors.get(id)!,
276 ),
277 } as EnvironmentRegistry.EnvironmentRegistry["Service"]);
278 const registry = AtomRegistry.make();
279 yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose()));
280 const localTarget = {
281 environmentId: scenario.primary === "same" ? remoteId : localId,
282 httpBaseUrl: "https://local.test",
283 };
284 const localEnvironment = Atom.make<typeof localTarget | null>(
285 scenario.primary === "none" || scenario.primary === "reconnecting" ? null : localTarget,
286 );
287 const assets = createAssetEnvironmentAtoms(
288 Atom.runtime(Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environments)),
289 localEnvironment,
290 );
291 const query = assets.createUrl({ environmentId: remoteId, input: { resource } });
292 const result = AtomRegistry.getResult(registry, query, { suspendOnWaiting: true });
293 if (scenario.fallback || scenario.success) {
294 expect((yield* result).relativeUrl).toBe(
295 scenario.fallback
296 ? "https://local.test/api/assets/local/media"
297 : "/api/assets/remote/media",
298 );
299 } else {
300 expect(yield* Effect.flip(result)).toEqual(error);
301 }
302 expect(calls).toEqual(scenario.fallback ? [remoteId, localId] : [remoteId]);
303 if (scenario.primary === "reconnecting") {
304 registry.set(localEnvironment, localTarget);
305 expect((yield* result).relativeUrl).toBe("https://local.test/api/assets/local/media");
306 expect(calls).toEqual([remoteId, remoteId, localId]);
307 }
308 }).pipe(Effect.scoped),
309 );
310
311 it("keys asset URL queries by environment and resource", () => {
312 const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime<
313 EnvironmentRegistry.EnvironmentRegistry,
314 never
315 >;
316 const assets = createAssetEnvironmentAtoms(runtime);
317 const environmentId = EnvironmentId.make("environment-1");
318 const originalTarget = {
319 environmentId,
320 input: {
321 resource: {
322 _tag: "project-favicon" as const,
323 cwd: "/repo/original",
324 },
325 },
326 };
327
328 expect(assets.createUrl(originalTarget)).toBe(
329 assets.createUrl({
330 environmentId,
331 input: {
332 resource: {
333 _tag: "project-favicon",
334 cwd: "/repo/original",
335 },
336 },
337 }),
338 );
339 expect(
340 assets.createUrl({
341 environmentId,
342 input: {
343 resource: {
344 _tag: "project-favicon",
345 cwd: "/repo/next",
346 },
347 },
348 }),
349 ).not.toBe(assets.createUrl(originalTarget));
350 expect(
351 assets.createUrl({
352 environmentId,
353 input: {
354 resource: {
355 _tag: "project-favicon",
356 cwd: "/repo/original",
357 path: "brand/icon.svg",
358 },
359 },
360 }),
361 ).not.toBe(assets.createUrl(originalTarget));
362 expect(
363 assets.createUrl({
364 environmentId: EnvironmentId.make("environment-2"),
365 input: originalTarget.input,
366 }),
367 ).not.toBe(assets.createUrl(originalTarget));
368 });
369
370 it("keys collections while preserving independent resource queries", () => {
371 const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime<
372 EnvironmentRegistry.EnvironmentRegistry,
373 never
374 >;
375 const assets = createAssetEnvironmentAtoms(runtime);
376 const environmentId = EnvironmentId.make("environment-1");
377 const resources = [
378 { _tag: "attachment" as const, attachmentId: "attachment-1" },
379 { _tag: "attachment" as const, attachmentId: "attachment-2" },
380 ];
381
382 expect(assets.createUrls({ environmentId, resources })).toBe(
383 assets.createUrls({
384 environmentId,
385 resources: resources.map((resource) => ({ ...resource })),
386 }),
387 );
388 expect(
389 assets.createUrls({
390 environmentId,
391 resources: [...resources].toReversed(),
392 }),
393 ).not.toBe(assets.createUrls({ environmentId, resources }));
394 });
395});
396
397describe("project favicon URL cache", () => {
398 it("renders a persisted thumbnail immediately in a fresh registry and refreshes it remotely", async () => {
399 const image = "data:image/png;base64,aWNvbg==";
400 const replacement = "data:image/png;base64,bmV3";
401 const records = new Map<string, unknown>();
402 const storage = {
403 list: async () => [...records.values()],
404 put: async (key: string, entry: unknown) => {
405 records.set(key, entry);
406 },
407 remove: async (key: string) => {
408 records.delete(key);
409 },
410 };
411 const target = { environmentId: EnvironmentId.make("remote"), cwd: "/workspace" };
412 const previousCache = createProjectFaviconCache({ storage, load: async () => image });
413 await previousCache.resolve(
414 target,
415 "https://remote.test/api/assets/old/v1-icon.png",
416 new AbortController().signal,
417 );
418 await previousCache.flush();
419 const cache = createProjectFaviconCache({ storage, load: async () => replacement });
420 await cache.hydrate();
421 const registry = AtomRegistry.make();
422 const result = Atom.make<AsyncResult.AsyncResult<AssetCreateUrlResult, unknown>>(
423 AsyncResult.initial(),
424 );
425 const connection = Atom.make<Option.Option<{ httpBaseUrl: string }>>(Option.none());
426 const favicon = createProjectFaviconUrlAtomFamily({
427 createUrl: () => result,
428 preparedConnection: () => connection,
429 imageCache: cache,
430 })(target);
431 const unmount = registry.mount(favicon);
432 try {
433 expect(registry.get(favicon)).toBe(image);
434 let unsubscribe = () => {};
435 const refreshed = new Promise<void>((resolve) => {
436 unsubscribe = registry.subscribe(favicon, (value) => {
437 if (value === replacement) resolve();
438 });
439 });
440 registry.set(connection, Option.some({ httpBaseUrl: "https://remote.test" }));
441 registry.set(
442 result,
443 AsyncResult.success({
444 relativeUrl: "/api/assets/new/v2-icon.png",
445 expiresAt: 4_000_000_000_000,
446 }),
447 );
448 expect(registry.get(favicon)).toBe(image);
449 await refreshed;
450 unsubscribe();
451 expect(registry.get(favicon)).toBe(replacement);
452 registry.set(connection, Option.none());
453 registry.set(result, AsyncResult.failure(Cause.die("offline")));
454 expect(registry.get(favicon)).toBe(replacement);
455 } finally {
456 unmount();
457 registry.dispose();
458 }
459 });
460
461 it("retains icons across outages and remounts, then accepts refreshed and missing icons", () => {
462 const registry = AtomRegistry.make();
463 const result = Atom.make<AsyncResult.AsyncResult<AssetCreateUrlResult, unknown>>(
464 AsyncResult.initial(),
465 );
466 const connection = Atom.make(Option.some({ httpBaseUrl: "https://remote.test" }));
467 const favicon = createProjectFaviconUrlAtomFamily({
468 createUrl: () => result,
469 preparedConnection: () => connection,
470 })({ environmentId: EnvironmentId.make("remote"), cwd: "/workspace" });
471 let unmount = registry.mount(favicon);
472 try {
473 expect(registry.get(favicon)).toBeNull();
474 registry.set(
475 result,
476 AsyncResult.success({
477 expiresAt: 4_000_000_000_000,
478 relativeUrl: "/api/assets/token-a/icon.svg",
479 }),
480 );
481 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-a/icon.svg");
482
483 registry.set(connection, Option.none());
484 registry.set(result, AsyncResult.failure(Cause.die("disconnected")));
485 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-a/icon.svg");
486 unmount();
487 unmount = registry.mount(favicon);
488 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-a/icon.svg");
489
490 registry.set(result, AsyncResult.initial());
491 registry.set(connection, Option.some({ httpBaseUrl: "https://reconnected.test" }));
492 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-a/icon.svg");
493 registry.set(
494 result,
495 AsyncResult.success({
496 expiresAt: 4_000_000_000_000,
497 relativeUrl: "/api/assets/token-b/icon.svg",
498 }),
499 );
500 expect(registry.get(favicon)).toBe("https://reconnected.test/api/assets/token-b/icon.svg");
501
502 registry.set(
503 result,
504 AsyncResult.success({
505 expiresAt: 4_000_000_000_000,
506 relativeUrl: "/api/assets/token-c/project-favicon-missing",
507 }),
508 );
509 expect(registry.get(favicon)).toBe(
510 "https://reconnected.test/api/assets/token-c/project-favicon-missing",
511 );
512 registry.set(connection, Option.none());
513 expect(registry.get(favicon)).toBe(
514 "https://reconnected.test/api/assets/token-c/project-favicon-missing",
515 );
516 } finally {
517 unmount();
518 registry.dispose();
519 }
520 });
521
522 it("does not reuse another environment, workspace, or selected icon's cached URL", () => {
523 const registry = AtomRegistry.make();
524 const result = Atom.make<AsyncResult.AsyncResult<AssetCreateUrlResult, unknown>>(
525 AsyncResult.success({
526 expiresAt: 4_000_000_000_000,
527 relativeUrl: "/api/assets/token/icon.svg",
528 }),
529 );
530 const favicon = createProjectFaviconUrlAtomFamily({
531 createUrl: () => result,
532 preparedConnection: () => Atom.make(Option.some({ httpBaseUrl: "https://remote.test" })),
533 });
534 const target = { environmentId: EnvironmentId.make("remote"), cwd: "/workspace" };
535 const unmount = registry.mount(favicon(target));
536 try {
537 expect(registry.get(favicon(target))).toBe("https://remote.test/api/assets/token/icon.svg");
538 registry.set(result, AsyncResult.failure(Cause.die("disconnected")));
539 expect(
540 registry.get(favicon({ ...target, environmentId: EnvironmentId.make("other") })),
541 ).toBeNull();
542 expect(registry.get(favicon({ ...target, cwd: "/other" }))).toBeNull();
543 expect(registry.get(favicon({ ...target, faviconPath: "brand.svg" }))).toBeNull();
544 expect(registry.get(favicon({ ...target, faviconPath: null }))).toBe(
545 "https://remote.test/api/assets/token/icon.svg",
546 );
547 } finally {
548 unmount();
549 registry.dispose();
550 }
551 });
552
553 const cloning: ProjectCloneSnapshot = {
554 projectId: ProjectId.make("project-cloning"),
555 remoteUrl: "git@github.com:octocat/app.git",
556 destinationPath: "/workspace",
557 repository: null,
558 phase: "running",
559 stage: "receiving",
560 percent: 10,
561 detail: null,
562 error: null,
563 startedAt: "2026-01-01T00:00:00.000Z",
564 endedAt: null,
565 sequence: 1,
566 };
567
568 function mountClonedProjectFavicon(initialClones: ReadonlyArray<ProjectCloneSnapshot>) {
569 const registry = AtomRegistry.make();
570 // Stands in for the server, which reports the icon missing until the clone lands.
571 const server = { lookups: 0, landed: false };
572 const result = Atom.make(() => {
573 server.lookups += 1;
574 return AsyncResult.success({
575 expiresAt: 4_000_000_000_000,
576 relativeUrl: server.landed
577 ? "/api/assets/token-b/v1-icon.svg"
578 : "/api/assets/token-a/project-favicon-missing",
579 });
580 });
581 const clones = Atom.make(initialClones);
582 const connection = Atom.make(Option.some({ httpBaseUrl: "https://remote.test" }));
583 const favicon = createProjectFaviconUrlAtomFamily({
584 createUrl: () => result,
585 preparedConnection: () => connection,
586 projectClones: () => clones,
587 })({ environmentId: EnvironmentId.make("remote"), cwd: "/workspace" });
588 const unmount = registry.mount(favicon);
589 return {
590 registry,
591 server,
592 clones,
593 favicon,
594 dispose: () => {
595 unmount();
596 registry.dispose();
597 },
598 };
599 }
600
601 it("asks for a cloned project's icon again once its clone lands", () => {
602 const { registry, server, clones, favicon, dispose } = mountClonedProjectFavicon([cloning]);
603 try {
604 expect(registry.get(favicon)).toBe(
605 "https://remote.test/api/assets/token-a/project-favicon-missing",
606 );
607 // Progress, and another folder's clone landing, do not ask again.
608 registry.set(clones, [
609 { ...cloning, percent: 80, sequence: 2 },
610 {
611 ...cloning,
612 projectId: ProjectId.make("project-other"),
613 destinationPath: "/other",
614 phase: "done",
615 sequence: 3,
616 },
617 ]);
618 expect(server.lookups).toBe(1);
619
620 server.landed = true;
621 registry.set(clones, [{ ...cloning, phase: "done", sequence: 4 }]);
622 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-b/v1-icon.svg");
623 expect(server.lookups).toBe(2);
624 } finally {
625 dispose();
626 }
627 });
628
629 it("asks again when the first clone list it sees already says done", () => {
630 const { registry, server, clones, favicon, dispose } = mountClonedProjectFavicon([]);
631 try {
632 expect(registry.get(favicon)).toBe(
633 "https://remote.test/api/assets/token-a/project-favicon-missing",
634 );
635 server.landed = true;
636 registry.set(clones, [{ ...cloning, phase: "done", sequence: 2 }]);
637 expect(registry.get(favicon)).toBe("https://remote.test/api/assets/token-b/v1-icon.svg");
638 } finally {
639 dispose();
640 }
641 });
642});