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 */
8import { DEFAULT_SERVER_SETTINGS, type ServerSettings } from "@t3tools/contracts";
9import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
10import * as Context from "effect/Context";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Layer from "effect/Layer";
14import * as Option from "effect/Option";
15import * as Path from "effect/Path";
16import * as Ref from "effect/Ref";
17import * as Stream from "effect/Stream";
18
19export 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. */
29export class TestProviderHostSettings extends Context.Service<
30 TestProviderHostSettings,
31 { readonly set: (settings: ServerSettings) => Effect.Effect<void> }
32>()("@t3tools/provider-testing/TestProviderHost/TestProviderHostSettings") {}
33
34export 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 );