Skip to content
Source
Sign inSign up

shell.test.ts

183 lines6.4 KB

1import {
2 AVAILABLE_CONNECTION_STATE,
3 BearerConnectionTarget,
4 PrimaryConnectionTarget,
5} from "@t3tools/client-runtime/connection";
6import type { EnvironmentCatalogState } from "@t3tools/client-runtime/state/connections";
7import { EMPTY_SHELL_STATE, type EnvironmentShellState } from "@t3tools/client-runtime/state/shell";
8import { EnvironmentId } from "@t3tools/contracts";
9import * as Option from "effect/Option";
10import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
11import { describe, expect, it, vi } from "vite-plus/test";
12​
13import {
14 createAllEnvironmentProjectSnapshotsReadyAtom,
15 expectsPrimaryEnvironment,
16 environmentShell,
17} from "./shell";
18import { environmentCatalog } from "../connection/catalog";
19import { defaultProjectFallbackReadyAtom } from "./projects";
20​
21const LOCAL = EnvironmentId.make("local");
22const REMOTE = EnvironmentId.make("remote");
23​
24function shellState(status: EnvironmentShellState["status"]): EnvironmentShellState {
25 return {
26 ...EMPTY_SHELL_STATE,
27 status,
28 snapshot:
29 status === "empty"
30 ? Option.none()
31 : Option.some({
32 schemaVersion: 1,
33 snapshotSequence: 1,
34 projects: [],
35 threads: [],
36 archivedThreads: [],
37 }),
38 error: Option.none(),
39 };
41​
42function catalogState(environmentIds: readonly EnvironmentId[]): EnvironmentCatalogState {
43 return {
44 isReady: true,
45 entries: new Map(
46 environmentIds.map((environmentId) => [
47 environmentId,
48 {
49 target:
50 environmentId === LOCAL
51 ? new PrimaryConnectionTarget({
52 environmentId,
53 label: environmentId,
54 httpBaseUrl: `https://${environmentId}.example.test`,
55 wsBaseUrl: `wss://${environmentId}.example.test`,
56 })
57 : new BearerConnectionTarget({
58 environmentId,
59 connectionId: environmentId,
60 label: environmentId,
61 }),
62 profile: Option.none(),
63 enabled: true,
64 },
65 ]),
66 ),
67 };
69​
70function makeHarness(requiresPrimaryEnvironment = true) {
71 const catalog = Atom.make<EnvironmentCatalogState>({ isReady: false, entries: new Map() });
72 const shells = Atom.family((_environmentId: EnvironmentId) =>
73 Atom.make<EnvironmentShellState>(shellState("empty")),
74 );
75 const ready = createAllEnvironmentProjectSnapshotsReadyAtom({
76 catalogValueAtom: catalog,
77 shellStateValueAtom: shells,
78 requiresPrimaryEnvironment,
79 });
80 const registry = AtomRegistry.make();
81 return { catalog, shells, ready, registry };
83​
84describe("project snapshot readiness", () => {
85 it("does not clear a saved scope while the ready catalog is still empty", () => {
86 const { catalog, ready, registry } = makeHarness();
87 expect(registry.get(ready)).toBe(false);
88 registry.set(catalog, catalogState([]));
89 expect(registry.get(ready)).toBe(false);
90 registry.dispose();
91 });
92​
93 it("waits for primary discovery even if a persisted remote is already live", () => {
94 const { catalog, shells, ready, registry } = makeHarness();
95 registry.set(catalog, catalogState([REMOTE]));
96 registry.set(shells(REMOTE), shellState("live"));
97 expect(registry.get(ready)).toBe(false);
98​
99 registry.set(catalog, catalogState([LOCAL, REMOTE]));
100 expect(registry.get(ready)).toBe(false);
101 registry.set(shells(LOCAL), shellState("live"));
102 expect(registry.get(ready)).toBe(true);
103 registry.dispose();
104 });
105​
106 it("allows a hosted client to load projects without a primary environment", () => {
107 const { catalog, shells, ready, registry } = makeHarness(false);
108 registry.set(catalog, catalogState([REMOTE]));
109 expect(registry.get(ready)).toBe(false);
110 registry.set(shells(REMOTE), shellState("live"));
111 expect(registry.get(ready)).toBe(true);
112 registry.dispose();
113 });
114​
115 it("waits for live snapshots through offline startup and reconnect", () => {
116 const { catalog, shells, ready, registry } = makeHarness();
117 registry.set(catalog, catalogState([LOCAL, REMOTE]));
118 registry.set(shells(LOCAL), shellState("live"));
119 expect(registry.get(ready)).toBe(false);
120​
121 registry.set(shells(REMOTE), shellState("cached"));
122 expect(registry.get(ready)).toBe(false);
123 registry.set(shells(REMOTE), shellState("synchronizing"));
124 expect(registry.get(ready)).toBe(false);
125 registry.set(shells(REMOTE), shellState("live"));
126 expect(registry.get(ready)).toBe(true);
127​
128 registry.set(shells(REMOTE), shellState("cached"));
129 expect(registry.get(ready)).toBe(false);
130 registry.set(shells(REMOTE), shellState("live"));
131 expect(registry.get(ready)).toBe(true);
132 registry.dispose();
133 });
134​
135 it("stops waiting for an environment only when it leaves the catalog", () => {
136 const { catalog, shells, ready, registry } = makeHarness();
137 registry.set(catalog, catalogState([LOCAL, REMOTE]));
138 registry.set(shells(LOCAL), shellState("live"));
139 expect(registry.get(ready)).toBe(false);
140 registry.set(catalog, catalogState([LOCAL]));
141 expect(registry.get(ready)).toBe(true);
142 registry.dispose();
143 });
144});
145​
146it("expects a primary environment only on desktops with the local environment enabled", () => {
147 const desktop = (enabled: boolean) => {
148 vi.stubGlobal("window", { desktopBridge: { getLocalEnvironmentEnabled: () => enabled } });
149 try {
150 return expectsPrimaryEnvironment();
151 } finally {
152 vi.unstubAllGlobals();
153 }
154 };
155 expect(desktop(true)).toBe(true);
156 expect(desktop(false)).toBe(false);
157});
158​
159it.each([undefined, "", "Server update required"])(
160 "settles fallback for an unsupported remembered environment: %s",
161 (unsupportedReason) => {
162 const catalog = catalogState([LOCAL, REMOTE]);
163 const entries = new Map(catalog.entries);
164 entries.set(REMOTE, {
165 ...entries.get(REMOTE)!,
166 ...(unsupportedReason === undefined ? {} : { unsupportedReason }),
167 });
168 const registry = AtomRegistry.make({
169 initialValues: [
170 [environmentCatalog.catalogValueAtom, { ...catalog, entries }],
171 [environmentShell.statusAtom(REMOTE), "empty"],
172 [environmentCatalog.stateAtom(REMOTE), AsyncResult.success(AVAILABLE_CONNECTION_STATE)],
173 ],
174 });
175 try {
176 expect(registry.get(defaultProjectFallbackReadyAtom("remote:project"))).toBe(
177 unsupportedReason !== undefined,
178 );
179 } finally {
180 registry.dispose();
181 }
182 },
183);