Skip to content
Source
Sign inSign up

serverRuntimeStartup.test.ts

357 lines13.6 KB

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