1import { assert, it } from "@effect/vitest";
2import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
3import {
4 DEFAULT_SERVER_SETTINGS,
5 ProjectId,
6 ProviderInstanceId,
7 ProviderDriverKind,
8 CommandId,
9 EnvironmentId,
10 type ServerLifecycleStreamEvent,
11} from "@t3tools/contracts";
12import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
13import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
14import { HttpServer } from "effect/http";
15import * as NetAddress from "effect/net/NetAddress";
16import * as Deferred from "effect/Deferred";
17import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";
19import * as Exit from "effect/Exit";
20import * as Fiber from "effect/Fiber";
21import * as Ref from "effect/Ref";
22import * as Layer from "effect/Layer";
23import * as Path from "effect/Path";
24
25import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
26
27import * as ServerConfig from "./config.ts";
28import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
29import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
30import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";
31import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
32import * as Keybindings from "./keybindings.ts";
33import * as EffectWorker from "./orchestration-v2/EffectWorker.ts";
34import * as LegacyV1ThreadImporter from "./orchestration-v2/legacy/LegacyV1ThreadImporter.ts";
35import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
36import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
37import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts";
38import * as ProviderSessionManager from "./orchestration-v2/ProviderSessionManager.ts";
39import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts";
40import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts";
41import * as ExternalLauncher from "./process/externalLauncher.ts";
42import * as ProjectService from "./project/ProjectService.ts";
43import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
44import { ServerActivation } from "./serverActivation.ts";
45import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
46import * as ServerSettings from "./serverSettings.ts";
47import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
48import * as AnalyticsService from "./telemetry/AnalyticsService.ts";
49
50it.effect.each([
51 "disabled-codex",
52 "disabled-default",
53 "launch-failure",
54 "project-failure",
55] as const)("keeps bootstrap launch failures separate from project failures: %s", (scenario) =>
56 Effect.scoped(
57 Effect.gen(function* () {
58 const projectId = ProjectId.make("bootstrap-project");
59 const instanceId = ProviderInstanceId.make(
60 scenario === "disabled-default" ? "custom" : "codex",
61 );
62 const events: ServerLifecycleStreamEvent[] = [];
63 const recovery = {
64 terminalizedRuns: 0,
65 stoppedSessions: 0,
66 closedRequests: 0,
67 retiredEffects: 0,
68 requeuedEffects: 0,
69 };
70 const imports = { importedThreadCount: 0, importedMessageCount: 0 };
71 const dependencies = Layer.mergeAll(
72 NodeCrypto.layer,
73 Path.layer,
74 Layer.mock(ServerConfig.ServerConfig)({
75 ...(yield* ServerConfig.deriveServerPaths("/bootstrap-project", undefined).pipe(
76 Effect.provide(Path.layer),
77 )),
78 baseDir: "/bootstrap-project",
79 logLevel: "Error",
80 traceMinLevel: "Info",
81 traceTimingEnabled: false,
82 traceBatchWindowMs: 200,
83 traceMaxBytes: 1024,
84 traceMaxFiles: 1,
85 otlpTracesUrl: undefined,
86 otlpMetricsUrl: undefined,
87 otlpLogsUrl: undefined,
88 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
89 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
90 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
91 otelEnvironment: OtelEnvironment.none,
92 staticDir: undefined,
93 devAllowedOrigins: [],
94 desktopBootstrapToken: undefined,
95 logWebSocketEvents: false,
96 tailscaleServeEnabled: false,
97 tailscaleServePort: 443,
98 mode: "web",
99 cwd: "/bootstrap-project",
100 host: "localhost",
101 port: 3773,
102 devUrl: undefined,
103 noBrowser: true,
104 startupPresentation: "browser",
105 autoBootstrapProjectFromCwd: true,
106 }),
107 Layer.mock(Keybindings.Keybindings)({ start: Effect.void }),
108 Layer.mock(LegacyV1ThreadImporter.LegacyV1ThreadImporter)({
109 pendingThreadCount: Effect.succeed(0),
110 reconcileShells: Effect.succeed(imports),
111 importPendingTranscripts: Effect.succeed(imports),
112 }),
113 Layer.mock(ProviderRuntimeRecovery.ProviderRuntimeRecoveryService)({
114 recover: Effect.succeed(recovery),
115 prepareForShutdown: Effect.void,
116 reconcile: () => Effect.succeed(recovery),
117 }),
118 Layer.mock(Orchestrator.OrchestratorV2)({ recoverDelegatedTasks: Effect.void }),
119 Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({ shutdown: Effect.void }),
120 Layer.mock(AgentAwarenessRelay.AgentAwarenessRelay)({ start: () => Effect.void }),
121 Layer.mock(EffectWorker.OrchestrationEffectWorkerV2)({ runOnce: () => Effect.never }),
122 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
123 publish: (event) =>
124 Effect.sync(() => {
125 const published = { ...event, sequence: events.length + 1 };
126 events.push(published);
127 return published;
128 }),
129 }),
130 Layer.mock(ServerSettings.ServerSettingsService)({
131 start: Effect.void,
132 getSettings: Effect.succeed({
133 ...DEFAULT_SERVER_SETTINGS,
134 defaultModelSelection:
135 scenario === "disabled-default" ? { instanceId, model: "custom-model" } : null,
136 providerInstances: {
137 [instanceId]: {
138 driver: ProviderDriverKind.make("codex"),
139 enabled: false,
140 config: {},
141 },
142 },
143 }),
144 }),
145 Layer.mock(ServerEnvironment.ServerEnvironment)({
146 getDescriptor: Effect.succeed({
147 environmentId: EnvironmentId.make("bootstrap-environment"),
148 label: "Test environment",
149 platform: { os: "linux", arch: "x64" },
150 serverVersion: "0.0.0-test",
151 capabilities: { repositoryIdentity: true },
152 }),
153 }),
154 Layer.mock(ProjectStore.ProjectStoreV2)({}),
155 Layer.mock(ProjectService.ProjectService)({
156 bootstrap: () =>
157 scenario === "project-failure"
158 ? Effect.die("project bootstrap failed")
159 : Effect.succeed({
160 created: true,
161 project: {
162 id: projectId,
163 title: "Project",
164 workspaceRoot: "/bootstrap-project",
165 repositoryIdentity: null,
166 faviconPath: null,
167 scripts: [],
168 createdAt: "2026-01-01T00:00:00Z",
169 updatedAt: "2026-01-01T00:00:00Z",
170 deletedAt: null,
171 defaultModelSelection:
172 scenario === "disabled-default"
173 ? { instanceId, model: "custom-model" }
174 : null,
175 },
176 }),
177 }),
178 Layer.mock(ThreadManagement.ThreadManagementService)({
179 countThreads: () => Effect.succeed(0),
180 listActiveShellIndex: () => Effect.succeed([]),
181 }),
182 Layer.mock(ThreadLaunch.ThreadLaunchService)({
183 launch: (input) => {
184 assert.equal(input.modelSelection.instanceId, instanceId);
185 return scenario === "launch-failure"
186 ? Effect.fail(
187 new ThreadLaunch.ThreadLaunchError({
188 operation: "create-thread",
189 commandId: CommandId.make("bootstrap-command"),
190 projectId,
191 cause: "test launch failure",
192 }),
193 )
194 : Effect.fail(
195 new ThreadManagement.ThreadManagementProviderInstanceDisabledError({
196 projectId,
197 instanceId,
198 }),
199 );
200 },
201 }),
202 Layer.mock(ServiceLauncherClient.ServiceLauncherClient)({
203 managed: false,
204 protocol: undefined,
205 prepareTrial: () => Effect.undefined,
206 }),
207 Layer.mock(GitVcsDriver.GitVcsDriver)({}),
208 Layer.mock(AnalyticsService.AnalyticsService)({}),
209 // No provider logins or Connect link, so startup records no owners.
210 FileSystem.layerNoop({}),
211 Layer.mock(ServerSecretStore.ServerSecretStore)({ get: () => Effect.succeedNone }),
212 Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
213 Layer.mock(ExternalLauncher.ExternalLauncher)({}),
214 Layer.mock(HttpServer.HttpServer)({
215 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
216 }),
217 );
218 yield* Effect.gen(function* () {
219 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
220 yield* startup.markHttpListening;
221 const ready = yield* Effect.exit(startup.awaitCommandReady);
222 assert.equal(Exit.isSuccess(ready), scenario !== "project-failure");
223 const welcome = events.find((event) => event.type === "welcome");
224 if (scenario === "project-failure") {
225 assert.isUndefined(welcome);
226 } else {
227 assert.isDefined(welcome);
228 if (welcome?.type === "welcome") {
229 assert.equal(welcome.payload.bootstrapProjectId, projectId);
230 assert.isUndefined(welcome.payload.bootstrapThreadId);
231 }
232 }
233 }).pipe(
234 Effect.provide(ServerRuntimeStartup.layerWithOptions().pipe(Layer.provide(dependencies))),
235 Effect.provideService(ServerActivation, Effect.never),
236 );
237 }),
238 ),
239);
240
241it.effect("interrupts the effect worker when awareness relay startup fails", () =>
242 Effect.scoped(
243 Effect.gen(function* () {
244 const workerInterrupted = yield* Ref.make(false);
245 const workerFiberRef = yield* Ref.make<Fiber.Fiber<void, never> | null>(null);
246
247 const exit = yield* ServerRuntimeStartup.startEffectWorkerWithRelay({
248 runWorker: Effect.never.pipe(Effect.ensuring(Ref.set(workerInterrupted, true))),
249 startRelay: Effect.yieldNow.pipe(
250 Effect.andThen(Effect.die("awareness relay startup failed")),
251 ),
252 workerFiberRef,
253 }).pipe(Effect.exit);
254
255 assert.isTrue(Exit.isFailure(exit));
256 assert.isTrue(yield* Ref.get(workerInterrupted));
257 assert.isNull(yield* Ref.get(workerFiberRef));
258 }),
259 ),
260);
261
262it.effect("queues commands until startup signals readiness", () =>
263 Effect.scoped(
264 Effect.gen(function* () {
265 const gate = yield* ServerRuntimeStartup.makeCommandGate;
266 const count = yield* Ref.make(0);
267 const queued = yield* gate
268 .enqueueCommand(Ref.updateAndGet(count, (value) => value + 1))
269 .pipe(Effect.forkScoped);
270
271 yield* Effect.yieldNow;
272 assert.equal(yield* Ref.get(count), 0);
273 yield* gate.signalCommandReady;
274 assert.equal(yield* Fiber.join(queued), 1);
275 }),
276 ),
277);
278
279it.effect("enqueueCommand fails queued work when readiness fails", () =>
280 Effect.scoped(
281 Effect.gen(function* () {
282 const commandGate = yield* ServerRuntimeStartup.makeCommandGate;
283 const failure = yield* Deferred.make<void, never>();
284
285 const queuedCommandFiber = yield* commandGate
286 .enqueueCommand(Deferred.await(failure).pipe(Effect.as("should-not-run")))
287 .pipe(Effect.forkScoped);
288
289 yield* commandGate.failCommandReady(
290 new ServerRuntimeStartup.ServerRuntimeStartupError({
291 mode: "web",
292 host: "127.0.0.1",
293 port: 3773,
294 cause: new Error("test startup failure"),
295 }),
296 );
297
298 const error = yield* Effect.flip(Fiber.join(queuedCommandFiber));
299 assert.equal(error.message, "Server runtime startup failed before command readiness.");
300 }),
301 ),
302);
303
304it.effect("automatic pull only updates enabled, behind, clean default-branch checkouts", () =>
305 Effect.gen(function* () {
306 const pulled: string[] = [];
307 const git = {
308 statusDetails: (cwd: string) =>
309 Effect.succeed({
310 isRepo: true,
311 isDefaultBranch: cwd !== "/feature",
312 hasUpstream: true,
313 hasWorkingTreeChanges: cwd === "/dirty",
314 aheadCount: cwd === "/ahead" ? 1 : 0,
315 behindCount: cwd === "/current" ? 0 : 1,
316 } as never),
317 pullCurrentBranch: (cwd: string) =>
318 Effect.sync(() => {
319 pulled.push(cwd);
320 return {
321 status: "pulled" as const,
322 refName: "main",
323 upstreamRef: "origin/main",
324 };
325 }),
326 } as unknown as GitVcsDriver.GitVcsDriver["Service"];
327 const project = (workspaceRoot: string) =>
328 ({ id: ProjectId.make(workspaceRoot), workspaceRoot }) as never;
329 const overrides = (entries: Record<string, boolean>) => ({
330 ...DEFAULT_SERVER_SETTINGS,
331 projectSettingsOverrides: Object.fromEntries(
332 Object.entries(entries).map(([root, defaultAutoPull]) => [
333 ProjectId.make(root),
334 { defaultAutoPull },
335 ]),
336 ),
337 });
338
339 yield* ServerRuntimeStartup.autoPullProjects(
340 [
341 project("/clean"),
342 project("/current"),
343 project("/dirty"),
344 project("/ahead"),
345 project("/feature"),
346 project("/disabled"),
347 ],
348 overrides({
349 "/clean": true,
350 "/current": true,
351 "/dirty": true,
352 "/ahead": true,
353 "/feature": true,
354 "/disabled": false,
355 }),
356 ).pipe(Effect.provideService(GitVcsDriver.GitVcsDriver, git));
357
358 assert.deepStrictEqual(pulled, ["/clean"]);
359
360 pulled.length = 0;
361 yield* ServerRuntimeStartup.autoPullProjects(
362 [project("/inherited"), project("/opted-out"), project("/dirty")],
363 { ...overrides({ "/opted-out": false }), defaultAutoPull: true },
364 ).pipe(Effect.provideService(GitVcsDriver.GitVcsDriver, git));
365 assert.deepStrictEqual(pulled, ["/inherited"]);
366 }),
367);