| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | import { |
| 4 | ProviderDriverKind, |
| 5 | ProviderInstanceId, |
| 6 | type ProviderInstallState, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as FileSystem from "effect/FileSystem"; |
| 11 | import * as Layer from "effect/Layer"; |
| 12 | import * as Path from "effect/Path"; |
| 13 | import * as Stream from "effect/Stream"; |
| 14 | |
| 15 | import * as ServerSettings from "../serverSettings.ts"; |
| 16 | import * as CodexInstallation from "./CodexInstallation.ts"; |
| 17 | import * as AntigravityInstallation from "./AntigravityInstallation.ts"; |
| 18 | import type { ProviderInstance } from "@t3tools/provider-core/server/driver"; |
| 19 | import { makeProviderInstallation } from "./providerInstallation.ts"; |
| 20 | import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts"; |
| 21 | import * as ProviderRegistry from "./ProviderRegistry.ts"; |
| 22 | |
| 23 | const instanceId = ProviderInstanceId.make("antigravity"); |
| 24 | const driver = ProviderDriverKind.make("antigravity"); |
| 25 | const 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 | |
| 37 | function 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 | |
| 56 | const 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 | |
| 125 | describe("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 | }); |