| 1 | /** |
| 2 | * A `ProviderHost.ProviderHost` for driver and adapter tests. Its directories live in a |
| 3 | * scoped temp directory, settings are fixed, and background work always runs |
| 4 | * unless the test says otherwise. |
| 5 | * |
| 6 | * @module provider-testing/TestProviderHost |
| 7 | */ |
| 8 | import { DEFAULT_SERVER_SETTINGS, type ServerSettings } from "@t3tools/contracts"; |
| 9 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 10 | import * as Context from "effect/Context"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Layer from "effect/Layer"; |
| 14 | import * as Option from "effect/Option"; |
| 15 | import * as Path from "effect/Path"; |
| 16 | import * as Ref from "effect/Ref"; |
| 17 | import * as Stream from "effect/Stream"; |
| 18 | |
| 19 | export interface TestProviderHostOptions { |
| 20 | /** Session fallback cwd. Defaults to the test process cwd. */ |
| 21 | readonly cwd?: string; |
| 22 | /** Initial settings. A test changes them through `TestProviderHostSettings`. */ |
| 23 | readonly settings?: ServerSettings; |
| 24 | /** Whether background work such as status probes may run. Defaults to `true`. */ |
| 25 | readonly runBackgroundWork?: boolean; |
| 26 | } |
| 27 | |
| 28 | /** Lets a test change the settings its `layer` reports. */ |
| 29 | export class TestProviderHostSettings extends Context.Service< |
| 30 | TestProviderHostSettings, |
| 31 | { readonly set: (settings: ServerSettings) => Effect.Effect<void> } |
| 32 | >()("@t3tools/provider-testing/TestProviderHost/TestProviderHostSettings") {} |
| 33 | |
| 34 | export const layer = ( |
| 35 | options: TestProviderHostOptions = {}, |
| 36 | ): Layer.Layer< |
| 37 | ProviderHost.ProviderHost | TestProviderHostSettings, |
| 38 | never, |
| 39 | FileSystem.FileSystem | Path.Path |
| 40 | > => |
| 41 | Layer.effectContext( |
| 42 | Effect.gen(function* () { |
| 43 | const fileSystem = yield* FileSystem.FileSystem; |
| 44 | const path = yield* Path.Path; |
| 45 | const baseDir = yield* fileSystem |
| 46 | .makeTempDirectoryScoped({ prefix: "t3-provider-host-" }) |
| 47 | .pipe(Effect.orDie); |
| 48 | const stateDir = path.join(baseDir, "userdata"); |
| 49 | const providerStatusCacheDir = path.join(baseDir, "caches"); |
| 50 | const attachmentsDir = path.join(stateDir, "attachments"); |
| 51 | for (const directory of [stateDir, providerStatusCacheDir, attachmentsDir]) { |
| 52 | yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie); |
| 53 | } |
| 54 | const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS); |
| 55 | const credentials = new Map<string, Uint8Array>(); |
| 56 | const host = ProviderHost.ProviderHost.of({ |
| 57 | paths: { |
| 58 | cwd: options.cwd ?? process.cwd(), |
| 59 | baseDir, |
| 60 | stateDir, |
| 61 | providerStatusCacheDir, |
| 62 | attachmentsDir, |
| 63 | }, |
| 64 | settings: { |
| 65 | get: Ref.get(settings), |
| 66 | withSnapshot: (use) => Ref.get(settings).pipe(Effect.flatMap(use)), |
| 67 | changes: Stream.empty, |
| 68 | subscribe: Effect.succeed(Stream.empty), |
| 69 | }, |
| 70 | shouldRunBackgroundWork: () => Effect.succeed(options.runBackgroundWork ?? true), |
| 71 | // Tests store attachments flat under the attachments directory by id. |
| 72 | resolveAttachmentPath: (attachment) => path.join(attachmentsDir, attachment.id), |
| 73 | // Credentials live in memory for the layer's lifetime. |
| 74 | credentials: (namespace, bindingId) => |
| 75 | Effect.sync(() => { |
| 76 | const key = `${namespace}:${bindingId}`; |
| 77 | return { |
| 78 | binding: { owner: "t3" as const, key }, |
| 79 | get: Effect.sync(() => Option.fromUndefinedOr(credentials.get(key))), |
| 80 | set: (value: Uint8Array) => Effect.sync(() => void credentials.set(key, value)), |
| 81 | remove: Effect.sync(() => void credentials.delete(key)), |
| 82 | }; |
| 83 | }), |
| 84 | }); |
| 85 | return Context.make(ProviderHost.ProviderHost, host).pipe( |
| 86 | Context.add(TestProviderHostSettings, { |
| 87 | set: (next) => Ref.set(settings, next), |
| 88 | }), |
| 89 | ); |
| 90 | }), |
| 91 | ); |