diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts index 1872eeb64ec2a..571240c1174df 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts @@ -23,6 +23,8 @@ import * as HostProcess from "@t3tools/shared/HostProcess"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Scope from "effect/Scope"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -41,6 +43,7 @@ import * as ModelManifest from "./ModelManifest.ts"; import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts"; import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; import { makeProviderInstanceRegistry } from "./ProviderInstanceRegistry.ts"; +import { ProviderDriverError } from "./Errors.ts"; import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts"; import * as PortScanner from "../preview/PortScanner.ts"; import * as ProviderHostLive from "./ProviderHostLive.ts"; @@ -202,6 +205,81 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => { Layer.provideMerge(layerBase), ); + it.effect.each(["replacement", "removal", "creation failure"] as const)( + "detaches retired provider scopes after repeated %s", + (scenario) => + Effect.gen(function* () { + const parent = yield* Scope.make(); + const fileSystem = yield* FileSystem.FileSystem; + const homePath = yield* fileSystem.makeTempDirectoryScoped(); + let creates = 0; + let closes = 0; + const driver: typeof CodexDriver = { + ...CodexDriver, + create: Effect.fn("ProviderInstanceRegistry.test.create")(function* ( + input: Parameters[0], + ) { + creates++; + yield* Effect.addFinalizer(() => + Effect.sync(() => { + closes++; + }), + ); + if (input.config.binaryPath === "creation-failure") { + return yield* new ProviderDriverError({ + driver: CodexDriver.driverKind, + instanceId: input.instanceId, + detail: "fixture creation failed", + }); + } + return yield* CodexDriver.create(input); + }), + }; + const siblingId = ProviderInstanceId.make("sibling"); + const retiredId = ProviderInstanceId.make("retired"); + const entry = { + driver: CodexDriver.driverKind, + enabled: false, + config: makeCodexConfig({ homePath }), + }; + yield* Effect.gen(function* () { + const { registry, mutator } = yield* makeProviderInstanceRegistry({ + drivers: [driver], + configMap: { [siblingId]: entry }, + }); + const sibling = yield* registry.getInstance(siblingId); + const finalizerCount = () => { + const state = parent.state; + return state._tag === "Open" ? (state.finalizers?.size ?? 1) : 0; + }; + const baseline = finalizerCount(); + for (let index = 0; index < 20; index++) { + yield* mutator.reconcile({ + [siblingId]: entry, + [retiredId]: { + ...entry, + config: makeCodexConfig({ + homePath, + binaryPath: scenario === "creation failure" ? "creation-failure" : "unused-codex", + customModels: [`revision-${index}`], + }), + }, + }); + if (scenario === "removal") yield* mutator.reconcile({ [siblingId]: entry }); + } + yield* mutator.reconcile({ [siblingId]: entry }); + expect(yield* registry.getInstance(siblingId)).toBe(sibling); + expect(closes).toBe(creates - 1); + expect(parent.state._tag).toBe("Open"); + expect(finalizerCount()).toBe(baseline); + }).pipe( + Effect.provideService(Scope.Scope, parent), + Effect.ensuring(Scope.close(parent, Exit.void)), + ); + expect(closes).toBe(creates); + }).pipe(Effect.provide(layerTest)), + ); + it.effect.each(["replacement", "removal"] as const)( "hides retired instances during reconcile (%s)", (scenario) => diff --git a/apps/server/src/provider/ProviderInstanceRegistry.ts b/apps/server/src/provider/ProviderInstanceRegistry.ts index 37f0aa55e09c5..466ace232eb33 100644 --- a/apps/server/src/provider/ProviderInstanceRegistry.ts +++ b/apps/server/src/provider/ProviderInstanceRegistry.ts @@ -194,8 +194,7 @@ const buildEntry = Effect.fn("ProviderInstanceRegistry.buildEntry")(function*