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 Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import { HttpServer } from "effect/http";
11import * as NetAddress from "effect/net/NetAddress";
12
13import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
14import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";
15import * as ServerConfig from "./config.ts";
16import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
17import * as Keybindings from "./keybindings.ts";
18import * as EffectWorker from "./orchestration-v2/EffectWorker.ts";
19import * as LegacyV1ThreadImporter from "./orchestration-v2/legacy/LegacyV1ThreadImporter.ts";
20import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
21import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
22import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts";
23import * as ProviderSessionManager from "./orchestration-v2/ProviderSessionManager.ts";
24import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts";
25import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts";
26import * as ExternalLauncher from "./process/externalLauncher.ts";
27import * as ProjectService from "./project/ProjectService.ts";
28import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
29import { ServerActivation } from "./serverActivation.ts";
30import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
31import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts";
32import * as ServerSettings from "./serverSettings.ts";
33import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
34
35it.effect("parks automatic pull until activation without delaying command readiness", () =>
36 Effect.scoped(
37 Effect.gen(function* () {
38 const activation = yield* Deferred.make<void>();
39 const prepared = yield* Deferred.make<void>();
40 const commitTrial = yield* Deferred.make<void>();
41 const statusCalled = yield* Deferred.make<string>();
42 const statusInterrupted = yield* Deferred.make<void>();
43 const cwd = "/auto-pull-project";
44 const updatedAt = "2026-01-01T00:00:00.000Z";
45 const snapshot = {
46 projects: [
47 {
48 id: ProjectId.make("auto-pull-project"),
49 title: "Auto pull project",
50 workspaceRoot: cwd,
51 repositoryIdentity: null,
52 defaultModelSelection: null,
53 scripts: [],
54 createdAt: updatedAt,
55 updatedAt,
56 deletedAt: null,
57 },
58 ],
59 updatedAt,
60 };
61 const recovery = {
62 terminalizedRuns: 0,
63 stoppedSessions: 0,
64 closedRequests: 0,
65 retiredEffects: 0,
66 requeuedEffects: 0,
67 };
68 const importSummary = { importedThreadCount: 0, importedMessageCount: 0 };
69 const dependencies: Layer.Layer<
70 Layer.Services<ReturnType<typeof ServerRuntimeStartup.layerWithOptions>>
71 > = Layer.mergeAll(
72 Layer.mock(ServerConfig.ServerConfig)({
73 ...(yield* ServerConfig.deriveServerPaths(cwd, undefined).pipe(
74 Effect.provide(Path.layer),
75 )),
76 baseDir: cwd,
77 logLevel: "Error",
78 traceMinLevel: "Info",
79 traceTimingEnabled: false,
80 traceBatchWindowMs: 200,
81 traceMaxBytes: 1024,
82 traceMaxFiles: 1,
83 otlpTracesUrl: undefined,
84 otlpMetricsUrl: undefined,
85 otlpLogsUrl: undefined,
86 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
87 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
88 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
89 otelEnvironment: OtelEnvironment.none,
90 staticDir: undefined,
91 devAllowedOrigins: [],
92 desktopBootstrapToken: undefined,
93 logWebSocketEvents: false,
94 tailscaleServeEnabled: false,
95 tailscaleServePort: 443,
96 mode: "desktop",
97 cwd,
98 host: "localhost",
99 port: 3773,
100 devUrl: undefined,
101 noBrowser: true,
102 startupPresentation: "browser",
103 autoBootstrapProjectFromCwd: false,
104 }),
105 Layer.mock(Keybindings.Keybindings)({ start: Effect.void }),
106 Layer.mock(LegacyV1ThreadImporter.LegacyV1ThreadImporter)({
107 pendingThreadCount: Effect.succeed(0),
108 reconcileShells: Effect.succeed(importSummary),
109 importPendingTranscripts: Effect.succeed(importSummary),
110 }),
111 Layer.mock(ProviderRuntimeRecovery.ProviderRuntimeRecoveryService)({
112 recover: Effect.succeed(recovery),
113 prepareForShutdown: Effect.void,
114 reconcile: () => Effect.succeed(recovery),
115 }),
116 Layer.mock(Orchestrator.OrchestratorV2)({ recoverDelegatedTasks: Effect.void }),
117 Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({ shutdown: Effect.void }),
118 Layer.mock(AgentAwarenessRelay.AgentAwarenessRelay)({ start: () => Effect.void }),
119 Layer.mock(EffectWorker.OrchestrationEffectWorkerV2)({ runOnce: Effect.never }),
120 Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
121 publish: (event) => Effect.succeed({ ...event, sequence: 1 }),
122 }),
123 Layer.mock(ServerSettings.ServerSettingsService)({
124 start: Effect.void,
125 getSettings: Effect.succeed({ ...DEFAULT_SERVER_SETTINGS, defaultAutoPull: true }),
126 }),
127 Layer.mock(ServerEnvironment.ServerEnvironment)({
128 getDescriptor: Effect.succeed({
129 environmentId: EnvironmentId.make("auto-pull-environment"),
130 label: "Test environment",
131 platform: { os: "darwin", arch: "arm64" },
132 serverVersion: "0.0.0-test",
133 capabilities: { repositoryIdentity: true },
134 }),
135 }),
136 Layer.mock(ProjectStore.ProjectStoreV2)({
137 listShells: () => Effect.succeed(snapshot.projects),
138 }),
139 Layer.mock(ProjectService.ProjectService)({ snapshot: Effect.succeed(snapshot) }),
140 Layer.mock(ThreadManagement.ThreadManagementService)({
141 getShellSnapshot: () =>
142 Effect.succeed({
143 schemaVersion: 1,
144 snapshotSequence: 0,
145 threads: [],
146 archivedThreads: [],
147 }),
148 }),
149 Layer.mock(ThreadLaunch.ThreadLaunchService)({}),
150 Layer.mock(ServiceLauncherClient.ServiceLauncherClient)({
151 managed: true,
152 prepareTrial: Deferred.succeed(prepared, undefined).pipe(
153 Effect.andThen(Deferred.await(commitTrial)),
154 Effect.as(undefined),
155 ),
156 }),
157 Layer.mock(GitVcsDriver.GitVcsDriver)({
158 statusDetails: (root) =>
159 Deferred.succeed(statusCalled, root).pipe(
160 Effect.andThen(Effect.never),
161 Effect.onInterrupt(() => Deferred.succeed(statusInterrupted, undefined)),
162 ),
163 }),
164 NodeCrypto.layer,
165 Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
166 Layer.mock(ExternalLauncher.ExternalLauncher)({}),
167 Layer.mock(HttpServer.HttpServer)({
168 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
169 }),
170 Path.layer,
171 );
172
173 yield* Effect.gen(function* () {
174 const startup = yield* ServerRuntimeStartup.ServerRuntimeStartup;
175 yield* startup.markHttpListening;
176
177 // A reverted, awaited pull reaches statusDetails instead of prepareTrial.
178 // Race the two receipts so that regression fails without a timeout.
179 yield* Effect.raceFirst(Deferred.await(prepared), Deferred.await(statusCalled));
180 expect(yield* Deferred.isDone(activation)).toBe(false);
181 expect(yield* Deferred.isDone(statusCalled)).toBe(false);
182 expect(yield* Deferred.isDone(prepared)).toBe(true);
183
184 yield* Deferred.succeed(commitTrial, undefined);
185 yield* Deferred.await(activation);
186 expect(yield* Deferred.await(statusCalled)).toBe(cwd);
187 // statusDetails can never finish; readiness must not depend on it.
188 yield* startup.awaitCommandReady;
189 }).pipe(
190 Effect.provide(
191 ServerRuntimeStartup.layerWithOptions({
192 activate: Deferred.succeed(activation, undefined).pipe(Effect.asVoid),
193 awaitAuxiliaryParked: Effect.void,
194 }).pipe(Layer.provide(dependencies)),
195 ),
196 Effect.provideService(ServerActivation, Deferred.await(activation)),
197 );
198 expect(yield* Deferred.isDone(statusInterrupted)).toBe(true);
199 }),
200 ),
201);