1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import { expect, it } from "@effect/vitest";
3import { DEFAULT_SERVER_SETTINGS, EnvironmentId, ProjectId } from "@t3tools/contracts";
4import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
5import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
6import * as Deferred from "effect/Deferred";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as Path from "effect/Path";
11import { HttpServer } from "effect/http";
12import * as NetAddress from "effect/net/NetAddress";
13
14import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
15import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";
16import { SERVICE_LAUNCHER_PROTOCOL } from "./cloud/serviceProtocol.ts";
17import * as ServerConfig from "./config.ts";
18import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
19import * as Keybindings from "./keybindings.ts";
20import * as EffectWorker from "./orchestration-v2/EffectWorker.ts";
21import * as LegacyV1ThreadImporter from "./orchestration-v2/legacy/LegacyV1ThreadImporter.ts";
22import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
23import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
24import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts";
25import * as ProviderSessionManager from "./orchestration-v2/ProviderSessionManager.ts";
26import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts";
27import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts";
28import * as ExternalLauncher from "./process/externalLauncher.ts";
29import * as ProjectService from "./project/ProjectService.ts";
30import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
31import { ServerActivation } from "./serverActivation.ts";
32import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
33import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
34import * as ServerSettings from "./serverSettings.ts";
35import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
36import * as AnalyticsService from "./telemetry/AnalyticsService.ts";
37import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
38
39it.effect("parks automatic pull until activation without delaying command readiness", () =>
40 Effect.gen(function* () {
41 const activation = yield* Deferred.make<void>();
42 const prepared = yield* Deferred.make<void>();
43 const commitTrial = yield* Deferred.make<void>();
44 const statusCalled = yield* Deferred.make<string>();
45 const statusInterrupted = yield* Deferred.make<void>();
46 const cwd = "/auto-pull-project";
47 const updatedAt = "2026-01-01T00:00:00.000Z";
48 const snapshot = {
49 projects: [
50 {
51 id: ProjectId.make("auto-pull-project"),
52 title: "Auto pull project",
53 workspaceRoot: cwd,
54 repositoryIdentity: null,
55 defaultModelSelection: null,
56 scripts: [],
57 createdAt: updatedAt,
58 updatedAt,
59 deletedAt: null,
60 },
61 ],
62 updatedAt,
63 };
64 const recovery = {
65 terminalizedRuns: 0,
66 stoppedSessions: 0,
67 closedRequests: 0,
68 retiredEffects: 0,
69 requeuedEffects: 0,
70 };
71 const importSummary = { importedThreadCount: 0, importedMessageCount: 0 };
72 const dependencies: Layer.Layer<
73 Layer.Services<ReturnType<typeof ServerRuntimeStartup.layerWithOptions>>
74 > = Layer.mergeAll(
75 Layer.mock(ServerConfig.ServerConfig)({
76 ...(yield* ServerConfig.deriveServerPaths(cwd, undefined).pipe(Effect.provide(Path.layer))),
77 baseDir: cwd,
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: "desktop",
98 cwd,
99 host: "localhost",
100 port: 3773,
101 devUrl: undefined,
102 noBrowser: true,
103 startupPresentation: "browser",
104 autoBootstrapProjectFromCwd: false,
105 }),
106 Layer.mock(Keybindings.Keybindings)({ start: Effect.void }),
107 Layer.mock(LegacyV1ThreadImporter.LegacyV1ThreadImporter)({
108 pendingThreadCount: Effect.succeed(0),
109 reconcileShells: Effect.succeed(importSummary),
110 importPendingTranscripts: Effect.succeed(importSummary),
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) => Effect.succeed({ ...event, sequence: 1 }),
123 }),
124 Layer.mock(ServerSettings.ServerSettingsService)({
125 start: Effect.void,
126 getSettings: Effect.succeed({ ...DEFAULT_SERVER_SETTINGS, defaultAutoPull: true }),
127 }),
128 Layer.mock(ServerEnvironment.ServerEnvironment)({
129 getDescriptor: Effect.succeed({
130 environmentId: EnvironmentId.make("auto-pull-environment"),
131 label: "Test environment",
132 platform: { os: "darwin", arch: "arm64" },
133 serverVersion: "0.0.0-test",
134 capabilities: { repositoryIdentity: true },
135 }),
136 }),
137 Layer.mock(ProjectStore.ProjectStoreV2)({
138 listShells: () => Effect.succeed(snapshot.projects),
139 }),
140 Layer.mock(ProjectService.ProjectService)({ snapshot: Effect.succeed(snapshot) }),
141 Layer.mock(ThreadManagement.ThreadManagementService)({
142 countThreads: () => Effect.succeed(0),
143 }),
144 Layer.mock(ThreadLaunch.ThreadLaunchService)({}),
145 Layer.mock(ServiceLauncherClient.ServiceLauncherClient)({
146 managed: true,
147 protocol: SERVICE_LAUNCHER_PROTOCOL,
148 prepareTrial: () =>
149 Deferred.succeed(prepared, undefined).pipe(
150 Effect.andThen(Deferred.await(commitTrial)),
151 Effect.as(undefined),
152 ),
153 }),
154 Layer.mock(GitVcsDriver.GitVcsDriver)({
155 statusDetails: (root) =>
156 Deferred.succeed(statusCalled, root).pipe(
157 Effect.andThen(Effect.never),
158 Effect.onInterrupt(() => Deferred.succeed(statusInterrupted, undefined)),
159 ),
160 }),
161 Layer.mock(AnalyticsService.AnalyticsService)({ record: () => Effect.void }),
162 // No provider logins or Connect link, so startup records no owners.
163 FileSystem.layerNoop({}),
164 Layer.mock(ServerSecretStore.ServerSecretStore)({ get: () => Effect.succeedNone }),
165 NodeCrypto.layer,
166 Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
167 Layer.mock(ExternalLauncher.ExternalLauncher)({}),
168 Layer.mock(HttpServer.HttpServer)({
169 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
170 }),
171 Path.layer,
172 );
173
174 yield* Effect.gen(function* () {
175 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
176 yield* startup.markHttpListening;
177
178 yield* Effect.raceFirst(Deferred.await(prepared), Deferred.await(statusCalled));
179 expect(yield* Deferred.isDone(activation)).toBe(false);
180 expect(yield* Deferred.isDone(statusCalled)).toBe(false);
181 expect(yield* Deferred.isDone(prepared)).toBe(true);
182
183 yield* Deferred.succeed(commitTrial, undefined);
184 yield* Deferred.await(activation);
185 expect(yield* Deferred.await(statusCalled)).toBe(cwd);
186 yield* startup.awaitCommandReady;
187 }).pipe(
188 Effect.provide(
189 ServerRuntimeStartup.layerWithOptions({
190 activate: Deferred.succeed(activation, undefined).pipe(Effect.asVoid),
191 awaitAuxiliaryParked: Effect.void,
192 }).pipe(Layer.provide(dependencies)),
193 ),
194 Effect.provideService(ServerActivation, Deferred.await(activation)),
195 );
196 expect(yield* Deferred.isDone(statusInterrupted)).toBe(true);
197 }),
198);