1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import {
4 ProviderDriverKind,
5 ProviderInstanceId,
6 type ProviderInstallState,
7} from "@t3tools/contracts";
8import * as HostProcess from "@t3tools/shared/HostProcess";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as Path from "effect/Path";
13import * as Stream from "effect/Stream";
14
15import * as ServerSettings from "../serverSettings.ts";
16import * as CodexInstallation from "./CodexInstallation.ts";
17import * as AntigravityInstallation from "./AntigravityInstallation.ts";
18import type { ProviderInstance } from "@t3tools/provider-core/server/driver";
19import { makeProviderInstallation } from "./providerInstallation.ts";
20import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts";
21import * as ProviderRegistry from "./ProviderRegistry.ts";
22
23const instanceId = ProviderInstanceId.make("antigravity");
24const driver = ProviderDriverKind.make("antigravity");
25const state: ProviderInstallState = {
26 driver,
27 operationId: null,
28 phase: "idle",
29 downloadedBytes: 0,
30 totalBytes: null,
31 version: null,
32 installedVersion: null,
33 canRemove: false,
34 message: null,
35};
36
37function instance(kind = driver, id = instanceId): ProviderInstance {
38 return {
39 instanceId: id,
40 driverKind: kind,
41 enabled: false,
42 displayName: undefined,
43 continuationIdentity: { driverKind: kind, continuationKey: instanceId },
44 get orchestrationAdapter(): never {
45 throw new Error("Installation must not start a provider session.");
46 },
47 get snapshot(): never {
48 throw new Error("Installation routing must not probe the provider.");
49 },
50 get textGeneration(): never {
51 throw new Error("Installation must not generate text.");
52 },
53 };
54}
55
56const makeHarness = Effect.fn("providerInstallation.test.makeHarness")(function* (
57 input: {
58 instance?: ProviderInstance;
59 settings?: Parameters<typeof ServerSettings.layerTest>[0];
60 } = {},
61) {
62 const calls: string[] = [];
63 let protectedPaths: ReadonlyArray<string> = [];
64 const configured = input.instance ?? instance();
65 const router = yield* makeProviderInstallation().pipe(
66 Effect.provide(
67 Layer.mergeAll(
68 ServerSettings.layerTest(input.settings),
69 Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({
70 getInstance: (id) =>
71 Effect.succeed(id === configured.instanceId ? configured : undefined),
72 listInstances: Effect.succeed([configured]),
73 }),
74 Layer.mock(ProviderRegistry.ProviderRegistry)({
75 refreshInstance: () =>
76 Effect.sync(() => {
77 calls.push("refresh");
78 return [];
79 }),
80 }),
81 Layer.mock(CodexInstallation.CodexInstallation)({
82 managedDirectory: "/unused-managed-codex",
83 start: Effect.sync(() => {
84 calls.push("codex-start");
85 return { ...state, driver: ProviderDriverKind.make("codex") };
86 }),
87 cancel: () =>
88 Effect.sync(() => {
89 calls.push("codex-cancel");
90 return { ...state, driver: ProviderDriverKind.make("codex") };
91 }),
92 state: Effect.succeed({ ...state, driver: ProviderDriverKind.make("codex") }),
93 changes: Stream.succeed({ ...state, driver: ProviderDriverKind.make("codex") }),
94 remove: (paths) =>
95 Effect.sync(() => {
96 protectedPaths = paths ?? [];
97 calls.push("codex-remove");
98 }),
99 }),
100 Layer.mock(AntigravityInstallation.AntigravityInstallation)({
101 managedDirectory: "/unused-managed-runtime",
102 start: Effect.sync(() => {
103 calls.push("start");
104 return state;
105 }),
106 cancel: () =>
107 Effect.sync(() => {
108 calls.push("cancel");
109 return state;
110 }),
111 state: Effect.succeed(state),
112 changes: Stream.succeed(state),
113 remove: (paths) =>
114 Effect.sync(() => {
115 protectedPaths = paths ?? [];
116 calls.push("remove");
117 }),
118 }),
119 ),
120 ),
121 );
122 return { router, calls, protectedPaths: () => protectedPaths };
123});
124
125describe("provider installation routing", () => {
126 it.effect("allows explicit installation while the provider is disabled", () =>
127 Effect.gen(function* () {
128 const harness = yield* makeHarness();
129 yield* harness.router.start({ instanceId });
130 assert.deepEqual(harness.calls, ["start"]);
131 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
132 );
133
134 it.effect("rejects another driver and unknown instances before installation", () =>
135 Effect.gen(function* () {
136 const harness = yield* makeHarness({ instance: instance(ProviderDriverKind.make("codex")) });
137 const wrongDriver = yield* Effect.flip(harness.router.start({ instanceId }));
138 const missing = yield* Effect.flip(
139 harness.router.start({ instanceId: ProviderInstanceId.make("missing") }),
140 );
141 assert.equal(wrongDriver._tag, "ProviderSetupError");
142 assert.equal(missing._tag, "ProviderSetupError");
143 assert.deepEqual(harness.calls, []);
144 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
145 );
146
147 it.effect("keeps external installs manual without hiding shared install status", () =>
148 Effect.gen(function* () {
149 const harness = yield* makeHarness({
150 settings: {
151 providerInstances: {
152 [ProviderInstanceId.make("antigravity")]: {
153 driver: ProviderDriverKind.make("antigravity"),
154 config: { binaryPath: "/external/agy" },
155 },
156 },
157 },
158 });
159 const start = yield* Effect.flip(harness.router.start({ instanceId }));
160 const remove = yield* Effect.flip(harness.router.remove({ instanceId }));
161 assert.include(start.detail, "custom executable");
162 assert.include(remove.detail, "custom executable");
163 const observed = yield* Stream.runCollect(harness.router.subscribe({ instanceId }));
164 assert.deepEqual(Array.from(observed), [state]);
165 assert.deepEqual(harness.calls, []);
166 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
167 );
168
169 it.effect("routes a managed Codex instance through its own installer and refreshes removal", () =>
170 Effect.gen(function* () {
171 const codexId = ProviderInstanceId.make("codex");
172 const harness = yield* makeHarness({
173 instance: instance(ProviderDriverKind.make("codex"), codexId),
174 settings: {
175 providerInstances: {
176 [ProviderInstanceId.make("codex")]: {
177 driver: ProviderDriverKind.make("codex"),
178 config: { setupMode: "managed" },
179 },
180 },
181 },
182 });
183 assert.equal((yield* harness.router.start({ instanceId: codexId })).driver, "codex");
184 yield* harness.router.cancel({ instanceId: codexId, operationId: "operation" });
185 const observed = yield* Stream.runCollect(harness.router.subscribe({ instanceId: codexId }));
186 assert.equal(Array.from(observed)[0]?.driver, "codex");
187 yield* harness.router.remove({ instanceId: codexId });
188 assert.deepEqual(harness.calls, ["codex-start", "codex-cancel", "codex-remove", "refresh"]);
189 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
190 );
191
192 it.effect("keeps native Codex installation outside managed setup", () =>
193 Effect.gen(function* () {
194 const codexId = ProviderInstanceId.make("codex");
195 const harness = yield* makeHarness({
196 instance: instance(ProviderDriverKind.make("codex"), codexId),
197 settings: {
198 providerInstances: {
199 [ProviderInstanceId.make("codex")]: {
200 driver: ProviderDriverKind.make("codex"),
201 config: { setupMode: "existing" },
202 },
203 },
204 },
205 });
206 assert.include(
207 (yield* Effect.flip(harness.router.start({ instanceId: codexId }))).detail,
208 "Choose managed setup",
209 );
210 assert.deepEqual(harness.calls, []);
211 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
212 );
213
214 it.effect("protects another instance's binary found through its own PATH", () =>
215 Effect.gen(function* () {
216 const fs = yield* FileSystem.FileSystem;
217 const path = yield* Path.Path;
218 const platform = yield* HostProcess.Platform;
219 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-install-route-" });
220 const binary = platform === "win32" ? "agy-test.exe" : "agy-test";
221 const executable = path.join(directory, binary);
222 yield* fs.writeFileString(executable, "test");
223 yield* fs.chmod(executable, 0o755);
224 const other = ProviderInstanceId.make("antigravity-work");
225 const harness = yield* makeHarness({
226 settings: {
227 providerInstances: {
228 [other]: {
229 driver,
230 config: { binaryPath: binary },
231 environment: [{ name: "PATH", value: directory, sensitive: false }],
232 },
233 },
234 },
235 });
236 yield* harness.router.remove({ instanceId });
237 assert.include(harness.protectedPaths(), executable);
238 assert.deepEqual(harness.calls, ["remove", "refresh"]);
239 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
240 );
241
242 it.effect("keeps only the configured path when another instance's binary is missing", () =>
243 Effect.gen(function* () {
244 const fs = yield* FileSystem.FileSystem;
245 const path = yield* Path.Path;
246 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-install-route-" });
247 const binaryPath = path.join(directory, "missing");
248 const harness = yield* makeHarness({
249 settings: {
250 providerInstances: {
251 [ProviderInstanceId.make("antigravity-work")]: {
252 driver,
253 config: { binaryPath },
254 },
255 },
256 },
257 });
258 yield* harness.router.remove({ instanceId });
259 assert.deepEqual(harness.protectedPaths(), [binaryPath]);
260 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
261 );
262});