1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
3import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { assert, it } from "@effect/vitest";
6import {
7 EnvironmentId,
8 CommandId,
9 type ModelSelection,
10 MessageId,
11 ProjectId,
12 ProviderDriverKind,
13 ProviderInstanceId,
14 ThreadId,
15} from "@t3tools/contracts";
16import * as Console from "effect/Console";
17import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";
19import * as ResetCreditCoordinator from "../provider/resetCreditCoordinator.ts";
20import { FetchHttpClient } from "effect/http";
21import { describe } from "vite-plus/test";
22
23import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
24import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
25import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
26import * as ServerConfig from "../config.ts";
27import * as SqlitePersistence from "../persistence/Sqlite.ts";
28import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
29import * as CodexInstallation from "../provider/CodexInstallation.ts";
30import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
31import * as ModelManifest from "../provider/ModelManifest.ts";
32import * as ProviderInstanceRegistryHydration from "../provider/ProviderInstanceRegistryHydration.ts";
33import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
34import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
35import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
36import * as OpenCodeRuntime from "@t3tools/provider-opencode/server/OpenCodeRuntime";
37import * as OpenCodeServerLedger from "@t3tools/provider-opencode/server/OpenCodeServerLedger";
38import * as ServerSettings from "../serverSettings.ts";
39import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
40import * as VcsProcess from "../vcs/VcsProcess.ts";
41import * as Orchestrator from "./Orchestrator.ts";
42import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
43import * as RuntimeLayer from "./runtimeLayer.ts";
44import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
45
46// The Antigravity switch is a durable, named conformance fixture for Google's
47// official Registry distribution. It uses credentials already owned by the
48// Antigravity agent and never stores them in the test database.
49//
50// T3_ACP_ANTIGRAVITY_LIVE=1 ../../node_modules/.bin/vp test run \
51// src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts
52const layerPlatformTest = Layer.merge(
53 NodeServices.layer,
54 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
55 resolveLink: () => Effect.die("unused title link"),
56 }),
57);
58
59const runAntigravityFixture = process.env.T3_ACP_ANTIGRAVITY_LIVE === "1";
60const liveAgentId = runAntigravityFixture
61 ? "antigravity-acp"
62 : process.env.T3_ACP_REGISTRY_LIVE_AGENT_ID?.trim() || "devin";
63const liveCommandPath = process.env.T3_ACP_REGISTRY_LIVE_COMMAND?.trim();
64const liveInstanceId = ProviderInstanceId.make("acpRegistry_live");
65const liveModelSelection = {
66 instanceId: liveInstanceId,
67 model: "default",
68} satisfies ModelSelection;
69
70const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
71 prefix: "t3-acp-registry-v2-live-",
72});
73
74const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
75 Layer.provide(VcsProcess.layer),
76 Layer.provide(layerServerConfig),
77 Layer.provide(layerPlatformTest),
78);
79
80const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
81
82const layerServerSettings = ServerSettings.layerTest({
83 providerInstances: {
84 [liveInstanceId]: {
85 driver: ProviderDriverKind.make("acpRegistry"),
86 displayName: `ACP Registry: ${liveAgentId}`,
87 enabled: true,
88 config: {
89 agentId: liveAgentId,
90 ...(liveCommandPath ? { commandPath: liveCommandPath } : {}),
91 },
92 },
93 },
94});
95const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
96 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
97 Layer.provide(layerServerSettings),
98);
99const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
100 Layer.provide(
101 Layer.mergeAll(
102 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
103 layerServerSettings,
104 ServerSecretStore.layer.pipe(
105 Layer.provide(layerServerConfig),
106 Layer.provide(layerPlatformTest),
107 ),
108 NodeServices.layer,
109 FetchHttpClient.layer,
110 OpenCodeRuntime.layer.pipe(
111 Layer.provide(OpenCodeServerLedger.layerTest),
112 Layer.provide(layerPlatformTest),
113 ),
114 Layer.succeed(
115 ProviderEventLoggers.ProviderEventLoggers,
116 ProviderEventLoggers.NoOpProviderEventLoggers,
117 ),
118 ProviderLatestVersions.layer,
119 ModelManifest.layerTest,
120 AntigravityInstallation.AntigravityInstallation.layer.pipe(
121 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
122 Layer.provide(FetchHttpClient.layer),
123 Layer.provide(layerPlatformTest),
124 ),
125 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
126 Layer.mock(CodexInstallation.CodexInstallation)({
127 managedDirectory: "unused-managed-installation",
128 }),
129 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
130 getEnvironmentId: Effect.succeed(
131 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
132 ),
133 }),
134 ),
135 ),
136);
137
138const layerLive = RuntimeLayer.layer.pipe(
139 Layer.provide(McpSessionRegistryTestkit.layer),
140 Layer.provide(SqlitePersistence.layerMemory),
141 Layer.provide(layerCheckpointStore),
142 Layer.provide(layerServerConfig),
143 Layer.provide(layerServerSettings),
144 Layer.provide(layerProviderInstanceRegistry),
145 Layer.provide(ResetCreditCoordinator.layer),
146 Layer.provide(layerBackgroundPolicy),
147 Layer.provide(ProviderTurnStartServiceTestkit.layer),
148 Layer.provide(layerPlatformTest),
149 Layer.provide(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)),
150);
151
152const waitForIdle = Effect.fn("AcpRegistryOrchestratorV2Live.waitForIdle")(function* (
153 threadId: ThreadId,
154 expectedRunCount: number,
155) {
156 const orchestrator = yield* Orchestrator.OrchestratorV2;
157 for (let attempt = 0; attempt < 900; attempt += 1) {
158 const projection = yield* orchestrator.getThreadProjection(threadId);
159 if (
160 projection.runs.length >= expectedRunCount &&
161 projection.runs.every(
162 (run) => !["queued", "starting", "running", "waiting"].includes(run.status),
163 )
164 ) {
165 return projection;
166 }
167 yield* Effect.sleep("500 millis");
168 }
169 return yield* Effect.die(new Error(`Timed out waiting for ACP Registry thread ${threadId}.`));
170});
171
172describe.runIf(runAntigravityFixture || process.env.T3_ACP_REGISTRY_LIVE_ORCHESTRATOR === "1")(
173 "ACP Registry V2 live orchestrator",
174 () => {
175 it.live(
176 `runs and resumes ${runAntigravityFixture ? "Google Antigravity" : "a real registry agent"} through the production V2 harness`,
177 () =>
178 Effect.gen(function* () {
179 const orchestrator = yield* Orchestrator.OrchestratorV2;
180 const projectId = ProjectId.make("project:acp-registry-live");
181 const threadId = ThreadId.make("thread:acp-registry-live");
182 const marker = "ACP_REGISTRY_LIVE_7H3Q";
183
184 yield* orchestrator.dispatch({
185 type: "thread.create",
186 createdBy: "user",
187 creationSource: "web",
188 commandId: CommandId.make("command:acp-registry-live:create"),
189 threadId,
190 projectId,
191 title: `ACP Registry live: ${liveAgentId}`,
192 modelSelection: liveModelSelection,
193 runtimeMode: "full-access",
194 interactionMode: "default",
195 branch: null,
196 worktreePath: null,
197 });
198 yield* Console.log(
199 `ACP Registry thread created for '${liveAgentId}'; dispatching first prompt.`,
200 );
201 yield* orchestrator.dispatch({
202 type: "message.dispatch",
203 createdBy: "user",
204 creationSource: "web",
205 commandId: CommandId.make("command:acp-registry-live:first"),
206 threadId,
207 messageId: MessageId.make("message:acp-registry-live:first"),
208 text: `Remember this opaque marker. Respond with exactly: ${marker}`,
209 attachments: [],
210 modelSelection: liveModelSelection,
211 dispatchMode: { type: "start_immediately" },
212 });
213 const firstProjection = yield* waitForIdle(threadId, 1);
214 const firstAssistant = firstProjection.messages.findLast(
215 (message) => message.role === "assistant",
216 )?.text;
217
218 assert.deepEqual(
219 firstProjection.runs.map((run) => [run.providerInstanceId, run.status]),
220 [[liveInstanceId, "completed"]],
221 );
222 assert.include(firstAssistant ?? "", marker);
223
224 yield* Console.log("First ACP turn completed; dispatching continuation prompt.");
225 yield* orchestrator.dispatch({
226 type: "message.dispatch",
227 createdBy: "user",
228 creationSource: "web",
229 commandId: CommandId.make("command:acp-registry-live:second"),
230 threadId,
231 messageId: MessageId.make("message:acp-registry-live:second"),
232 text: "Return the opaque marker from the previous turn. Respond with only the marker.",
233 attachments: [],
234 modelSelection: liveModelSelection,
235 dispatchMode: { type: "start_immediately" },
236 });
237 const finalProjection = yield* waitForIdle(threadId, 2);
238 const finalAssistant = finalProjection.messages.findLast(
239 (message) => message.role === "assistant",
240 )?.text;
241
242 assert.deepEqual(
243 finalProjection.runs.map((run) => [run.providerInstanceId, run.status]),
244 [
245 [liveInstanceId, "completed"],
246 [liveInstanceId, "completed"],
247 ],
248 );
249 assert.include(finalAssistant ?? "", marker);
250 assert.deepEqual(finalProjection.providerSessions.length, 2);
251 assert.isAtLeast(finalProjection.providerThreads.length, 1);
252 assert.deepEqual(
253 finalProjection.providerTurns.map((turn) => turn.status),
254 ["completed", "completed"],
255 );
256 }).pipe(Effect.provide(layerLive), Effect.scoped),
257 480_000,
258 );
259 },
260);