1import { EnvironmentId, ProjectId, type ServerConfig } from "@t3tools/contracts";
2import { Atom, AtomRegistry } from "effect/reactivity";
3import * as Option from "effect/Option";
4import { describe, expect, it } from "vite-plus/test";
5
6import { PrimaryConnectionTarget } from "../connection/model.ts";
7import { createEnvironmentProjectAtoms } from "./projectEntities.ts";
8import { v2Project, v2ShellSnapshot } from "./orchestrationV2TestFixtures.ts";
9
10const environmentId = EnvironmentId.make("local");
11const remoteEnvironmentId = EnvironmentId.make("remote");
12
13function makeHarness(withConfig = true) {
14 const snapshotAtom = Atom.family((_environmentId: EnvironmentId) =>
15 Atom.make({
16 ...v2ShellSnapshot,
17 projects: [
18 { ...v2Project, title: "No project" },
19 {
20 ...v2Project,
21 title: "No project",
22 id: ProjectId.make("ordinary"),
23 workspaceRoot: "/repo",
24 },
25 ],
26 }),
27 );
28 const configAtom = Atom.family((_environmentId: EnvironmentId) =>
29 Atom.make<(Pick<ServerConfig, "scratchWorkspaceRoot"> & { revision?: number }) | null>(null),
30 );
31 const catalogValueAtom = Atom.make({
32 isReady: true,
33 entries: new Map(
34 [environmentId, remoteEnvironmentId].map((id) => [
35 id,
36 {
37 target: new PrimaryConnectionTarget({
38 environmentId: id,
39 label: id,
40 httpBaseUrl: `https://${id}.example.test`,
41 wsBaseUrl: `wss://${id}.example.test`,
42 }),
43 profile: Option.none(),
44 enabled: true,
45 },
46 ]),
47 ),
48 });
49 const projects = createEnvironmentProjectAtoms({
50 catalogValueAtom,
51 snapshotAtom,
52 ...(withConfig ? { serverConfigValueAtom: configAtom } : {}),
53 });
54 return { registry: AtomRegistry.make(), projects, configAtom, snapshotAtom, catalogValueAtom };
55}
56
57describe("project scratch identity", () => {
58 it("waits for enabled project configs and ignores a disabled environment with no config", () => {
59 const { registry, projects, configAtom, catalogValueAtom } = makeHarness();
60 try {
61 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(false);
62 registry.set(configAtom(environmentId), { scratchWorkspaceRoot: v2Project.workspaceRoot });
63 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(false);
64 const catalog = registry.get(catalogValueAtom);
65 registry.set(catalogValueAtom, {
66 ...catalog,
67 entries: new Map(
68 [...catalog.entries].map(([id, entry]) => [
69 id,
70 { ...entry, enabled: id === environmentId },
71 ]),
72 ),
73 });
74 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(true);
75 registry.set(catalogValueAtom, catalog);
76 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(false);
77 registry.set(configAtom(remoteEnvironmentId), {});
78 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(true);
79 registry.set(catalogValueAtom, { ...catalog, isReady: false });
80 expect(registry.get(projects.projectConfigsReadyAtom)).toBe(false);
81 } finally {
82 registry.dispose();
83 }
84 });
85
86 it("updates a loaded project when config arrives, preserving unrelated projects and snapshots", () => {
87 const { registry, projects, configAtom, snapshotAtom } = makeHarness();
88 try {
89 const initial = registry.get(projects.projectsAtom);
90 const source = registry.get(snapshotAtom(environmentId));
91 registry.set(configAtom(environmentId), {
92 scratchWorkspaceRoot: `${v2Project.workspaceRoot}/`,
93 });
94 const next = registry.get(projects.projectsAtom);
95
96 expect(next[0]?.isScratch).toBe(true);
97 expect(next[0]).not.toBe(initial[0]);
98 expect(next.slice(1)).toEqual(initial.slice(1));
99 for (let index = 1; index < initial.length; index++) {
100 expect(next[index]).toBe(initial[index]);
101 }
102 expect(registry.get(snapshotAtom(environmentId))).toBe(source);
103 expect(v2Project).not.toHaveProperty("isScratch");
104
105 registry.set(configAtom(environmentId), {
106 scratchWorkspaceRoot: v2Project.workspaceRoot,
107 revision: 1,
108 });
109 expect(registry.get(projects.projectsAtom)).toBe(next);
110
111 registry.set(configAtom(environmentId), { scratchWorkspaceRoot: "/repo" });
112 const changedRoot = registry.get(projects.projectsAtom);
113 expect(changedRoot[0]).not.toHaveProperty("isScratch");
114 expect(changedRoot[1]?.isScratch).toBe(true);
115 expect(changedRoot[2]).toBe(initial[2]);
116
117 registry.set(configAtom(environmentId), null);
118 expect(registry.get(projects.projectsAtom).every((project) => !project.isScratch)).toBe(true);
119 } finally {
120 registry.dispose();
121 }
122 });
123
124 it("keeps callers without server configs working without marking a same-title project", () => {
125 const { registry, projects, configAtom } = makeHarness(false);
126 try {
127 const initial = registry.get(projects.projectsAtom);
128 registry.set(configAtom(environmentId), { scratchWorkspaceRoot: v2Project.workspaceRoot });
129 expect(registry.get(projects.projectsAtom)).toBe(initial);
130 expect(initial.every((project) => !project.isScratch)).toBe(true);
131 } finally {
132 registry.dispose();
133 }
134 });
135});