| 1 | import { |
| 2 | BearerConnectionTarget, |
| 3 | PrimaryConnectionTarget, |
| 4 | } from "@t3tools/client-runtime/connection"; |
| 5 | import type { EnvironmentCatalogState } from "@t3tools/client-runtime/state/connections"; |
| 6 | import type { EnvironmentShellState } from "@t3tools/client-runtime/state/shell"; |
| 7 | import { EnvironmentId } from "@t3tools/contracts"; |
| 8 | import * as Option from "effect/Option"; |
| 9 | import { Atom, AtomRegistry } from "effect/reactivity"; |
| 10 | import { describe, expect, it } from "vite-plus/test"; |
| 11 | |
| 12 | import { createAllEnvironmentProjectSnapshotsReadyAtom } from "./shell"; |
| 13 | |
| 14 | const LOCAL = EnvironmentId.make("local"); |
| 15 | const REMOTE = EnvironmentId.make("remote"); |
| 16 | |
| 17 | function 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 | |
| 34 | function 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 | |
| 62 | function 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 | |
| 76 | describe("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 | }); |