1import {
2 BearerConnectionTarget,
3 PrimaryConnectionTarget,
4} from "@t3tools/client-runtime/connection";
5import type { EnvironmentCatalogState } from "@t3tools/client-runtime/state/connections";
6import type { EnvironmentShellState } from "@t3tools/client-runtime/state/shell";
7import { EnvironmentId } from "@t3tools/contracts";
8import * as Option from "effect/Option";
9import { Atom, AtomRegistry } from "effect/reactivity";
10import { describe, expect, it } from "vite-plus/test";
11
12import { createAllEnvironmentProjectSnapshotsReadyAtom } from "./shell";
13
14const LOCAL = EnvironmentId.make("local");
15const REMOTE = EnvironmentId.make("remote");
16
17function shellState(status: EnvironmentShellState["status"]): EnvironmentShellState {
18 return {
19 status,
20 snapshot:
21 status === "empty"
22 ? Option.none()
23 : Option.some({
24 schemaVersion: 1,
25 snapshotSequence: 1,
26 projects: [],
27 threads: [],
28 archivedThreads: [],
29 }),
30 error: Option.none(),
31 };
32}
33
34function catalogState(environmentIds: readonly EnvironmentId[]): EnvironmentCatalogState {
35 return {
36 isReady: true,
37 entries: new Map(
38 environmentIds.map((environmentId) => [
39 environmentId,
40 {
41 target:
42 environmentId === LOCAL
43 ? new PrimaryConnectionTarget({
44 environmentId,
45 label: environmentId,
46 httpBaseUrl: `https://${environmentId}.example.test`,
47 wsBaseUrl: `wss://${environmentId}.example.test`,
48 })
49 : new BearerConnectionTarget({
50 environmentId,
51 connectionId: environmentId,
52 label: environmentId,
53 }),
54 profile: Option.none(),
55 enabled: true,
56 },
57 ]),
58 ),
59 };
60}
61
62function makeHarness(requiresPrimaryEnvironment = true) {
63 const catalog = Atom.make<EnvironmentCatalogState>({ isReady: false, entries: new Map() });
64 const shells = Atom.family((_environmentId: EnvironmentId) =>
65 Atom.make<EnvironmentShellState>(shellState("empty")),
66 );
67 const ready = createAllEnvironmentProjectSnapshotsReadyAtom({
68 catalogValueAtom: catalog,
69 shellStateValueAtom: shells,
70 requiresPrimaryEnvironment,
71 });
72 const registry = AtomRegistry.make();
73 return { catalog, shells, ready, registry };
74}
75
76describe("project snapshot readiness", () => {
77 it("does not clear a saved scope while the ready catalog is still empty", () => {
78 const { catalog, ready, registry } = makeHarness();
79 expect(registry.get(ready)).toBe(false);
80 registry.set(catalog, catalogState([]));
81 expect(registry.get(ready)).toBe(false);
82 registry.dispose();
83 });
84
85 it("waits for primary discovery even if a persisted remote is already live", () => {
86 const { catalog, shells, ready, registry } = makeHarness();
87 registry.set(catalog, catalogState([REMOTE]));
88 registry.set(shells(REMOTE), shellState("live"));
89 expect(registry.get(ready)).toBe(false);
90
91 registry.set(catalog, catalogState([LOCAL, REMOTE]));
92 expect(registry.get(ready)).toBe(false);
93 registry.set(shells(LOCAL), shellState("live"));
94 expect(registry.get(ready)).toBe(true);
95 registry.dispose();
96 });
97
98 it("allows a hosted client to load projects without a primary environment", () => {
99 const { catalog, shells, ready, registry } = makeHarness(false);
100 registry.set(catalog, catalogState([REMOTE]));
101 expect(registry.get(ready)).toBe(false);
102 registry.set(shells(REMOTE), shellState("live"));
103 expect(registry.get(ready)).toBe(true);
104 registry.dispose();
105 });
106
107 it("waits for live snapshots through offline startup and reconnect", () => {
108 const { catalog, shells, ready, registry } = makeHarness();
109 registry.set(catalog, catalogState([LOCAL, REMOTE]));
110 registry.set(shells(LOCAL), shellState("live"));
111 expect(registry.get(ready)).toBe(false);
112
113 // An old cache and a reconnect in progress can both omit a real project.
114 registry.set(shells(REMOTE), shellState("cached"));
115 expect(registry.get(ready)).toBe(false);
116 registry.set(shells(REMOTE), shellState("synchronizing"));
117 expect(registry.get(ready)).toBe(false);
118 registry.set(shells(REMOTE), shellState("live"));
119 expect(registry.get(ready)).toBe(true);
120
121 registry.set(shells(REMOTE), shellState("cached"));
122 expect(registry.get(ready)).toBe(false);
123 registry.set(shells(REMOTE), shellState("live"));
124 expect(registry.get(ready)).toBe(true);
125 registry.dispose();
126 });
127
128 it("stops waiting for an environment only when it leaves the catalog", () => {
129 const { catalog, shells, ready, registry } = makeHarness();
130 registry.set(catalog, catalogState([LOCAL, REMOTE]));
131 registry.set(shells(LOCAL), shellState("live"));
132 expect(registry.get(ready)).toBe(false);
133 registry.set(catalog, catalogState([LOCAL]));
134 expect(registry.get(ready)).toBe(true);
135 registry.dispose();
136 });
137});