Skip to content
Source
Sign inSign up

SecretCheck.test.ts

363 lines14.3 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";
4import {
5 ProviderDriverKind,
6 ProviderInstanceId,
7 SecretProviderId,
8 SecretProviderKind,
9 type SecretReference,
10 SettingsSecretValue,
11 type ServerProvider,
12} from "@t3tools/contracts";
13import type { AnyProviderDriver, ProviderInstance } from "@t3tools/provider-core/server/driver";
14import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
15import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
16import * as SecretResolver from "@t3tools/secret-provider-core/server/SecretResolver";
17import { defineSecretProviderDriver } from "@t3tools/secret-provider-core/server/driver";
18import { defineSecretProvider } from "@t3tools/secret-provider-core/shared/definition";
19import * as Deferred from "effect/Deferred";
20import * as Effect from "effect/Effect";
21import * as Fiber from "effect/Fiber";
22import { HttpClient, HttpClientResponse } from "effect/http";
23import * as Layer from "effect/Layer";
24import * as PubSub from "effect/PubSub";
25import * as Schema from "effect/Schema";
26import * as Stream from "effect/Stream";
27​
28import * as ServerConfig from "../config.ts";
29import * as ModelManifest from "../provider/ModelManifest.ts";
30import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts";
31import * as ProviderInstanceRegistryMutator from "../provider/ProviderInstanceRegistryMutator.ts";
32import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
33import * as SecretCheck from "./SecretCheck.ts";
34​
35const FAKE_KIND = SecretProviderKind.make("fake");
36const secretProvider = SecretProviderId.make("personal");
37const wanted: SecretReference = { provider: secretProvider, reference: "fake://wanted" };
38const other: SecretReference = { provider: secretProvider, reference: "fake://other" };
39const locked: SecretReference = { provider: secretProvider, reference: "fake://locked" };
40const driverKind = ProviderDriverKind.make("fake-provider");
41const dependent = ProviderInstanceId.make("dependent");
42const unrelated = ProviderInstanceId.make("unrelated");
43​
44const providerSnapshot = (instanceId: ProviderInstanceId): ServerProvider => ({
45 instanceId,
46 driver: driverKind,
47 status: "ready",
48 enabled: true,
49 installed: true,
50 auth: { status: "authenticated" },
51 checkedAt: "2026-10-03T00:00:00.000Z",
52 version: "1.0.0",
53 models: [],
54 slashCommands: [],
55 skills: [],
56});
57​
58/**
59 * A server whose `dependent` instance reads `dependentReads` and `unrelated` reads `other`, all
60 * through the real resolver, instance registry and provider registry. `vault` answers each read of
61 * a reference with the next queued value, repeating the last one, after `beforeRead`.
62 */
63const setup = Effect.fn("SecretCheckTest.setup")(function* (
64 vault: Map<string, Array<string | undefined>>,
65 dependentReads: ReadonlyArray<SecretReference> = [wanted],
66 options: {
67 readonly dependentConfig?: unknown;
68 readonly beforeRead?: (reference: string) => Effect.Effect<void>;
69 } = {},
70) {
71 const reads: Array<string> = [];
72 const created: Array<{ instanceId: ProviderInstanceId; secrets: ReadonlyArray<unknown> }> = [];
73 const createdConfigs: Array<unknown> = [];
74 const refreshed: Array<ProviderInstanceId> = [];
75​
76 const secretDriver = defineSecretProviderDriver({
77 kind: FAKE_KIND,
78 definition: defineSecretProvider({
79 kind: FAKE_KIND,
80 label: "Fake",
81 configSchema: Schema.Struct({}),
82 isReference: (value) => value.startsWith("fake://"),
83 referencePlaceholder: "fake://item",
84 invalidReferenceMessage: "Use fake://item.",
85 }),
86 read: (_config, references) =>
87 Effect.forEach(references, (reference) =>
88 (options.beforeRead?.(reference) ?? Effect.void).pipe(
89 Effect.andThen(
90 Effect.sync(() => {
91 reads.push(reference);
92 const answers = vault.get(reference) ?? [];
93 return answers.length > 1 ? answers.shift() : answers[0];
94 }),
95 ),
96 ),
97 ),
98 });
99​
100 const driver: AnyProviderDriver = {
101 driverKind,
102 metadata: { displayName: "Fake" },
103 configSchema: Schema.Struct({
104 keys: Schema.Record(Schema.String, SettingsSecretValue).pipe(
105 Schema.withDecodingDefault(Effect.succeed({})),
106 ),
107 }),
108 defaultConfig: () => ({}),
109 create: (input) =>
110 Effect.sync(() => {
111 created.push({
112 instanceId: input.instanceId,
113 secrets: input.environment.map((variable) => variable.value),
114 });
115 createdConfigs.push(input.config);
116 const snapshot = providerSnapshot(input.instanceId);
117 return {
118 instanceId: input.instanceId,
119 driverKind,
120 continuationIdentity: { driverKind, continuationKey: `fake:${input.instanceId}` },
121 displayName: undefined,
122 enabled: input.enabled,
123 snapshot: {
124 resolveMaintenance: () =>
125 Effect.succeed(
126 makeManualOnlyProviderMaintenanceCapabilities({
127 provider: driverKind,
128 packageName: null,
129 }),
130 ),
131 getSnapshot: Effect.succeed(snapshot),
132 refresh: Effect.sync(() => {
133 refreshed.push(input.instanceId);
134 return snapshot;
135 }),
136 streamChanges: Stream.empty,
137 applyUsageLimits: () => Effect.void,
138 },
139 orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
140 textGeneration: {} as ProviderInstance["textGeneration"],
141 } satisfies ProviderInstance;
142 }),
143 };
144 const instanceConfig = (secrets: ReadonlyArray<SecretReference>, config: unknown = {}) => ({
145 driver: driverKind,
146 config,
147 environment: secrets.map((secret, index) => ({
148 name: `API_KEY_${index}`,
149 value: secret,
150 sensitive: true,
151 })),
152 });
153​
154 const resolverContext = yield* Layer.build(
155 SecretResolver.layer({
156 drivers: [secretDriver],
157 secretProviders: Effect.succeed({
158 [secretProvider]: { kind: FAKE_KIND, label: "Personal", config: {} },
159 }),
160 }),
161 );
162 const { registry, mutator } = yield* ProviderInstanceRegistry.makeProviderInstanceRegistry({
163 drivers: [driver],
164 configMap: {
165 [dependent]: instanceConfig(dependentReads, options.dependentConfig),
166 [unrelated]: instanceConfig([other]),
167 },
168 }).pipe(Effect.provide(resolverContext));
169 // Secrets are read after startup, so wait for the change that applies them.
170 const changes = yield* registry.subscribeChanges;
171 yield* PubSub.take(changes);
172​
173 const services = yield* Layer.build(
174 SecretCheck.layer.pipe(
175 Layer.provideMerge(ProviderRegistry.layer),
176 Layer.provide(
177 Layer.mergeAll(
178 Layer.succeedContext(resolverContext),
179 Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, registry),
180 Layer.succeed(ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator, mutator),
181 ),
182 ),
183 ),
184 );
185 const secretCheck = yield* SecretCheck.SecretCheck.pipe(Effect.provide(services));
186 const resolver = yield* SecretResolver.SecretResolver.pipe(Effect.provide(resolverContext));
187 return { secretCheck, registry, mutator, resolver, reads, created, createdConfigs, refreshed };
188});
189​
190const layerTest = Layer.mergeAll(
191 ServerConfig.layerTest(process.cwd(), { prefix: "t3-secret-check-" }),
192 ModelManifest.layerTest,
193 ProviderLatestVersions.layer,
194 Layer.succeed(
195 HttpClient.HttpClient,
196 HttpClient.make((request) =>
197 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),
198 ),
199 ),
200).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)));
201​
202it.layer(layerTest)("SecretCheck", (it) => {
203 it.effect(
204 "a readable secret rebuilds and refreshes only the instances that read it, from that one read",
205 () =>
206 Effect.gen(function* () {
207 const vault = new Map<string, Array<string | undefined>>([
208 [wanted.reference, [undefined]],
209 [other.reference, ["other-key"]],
210 ]);
211 const server = yield* setup(vault);
212 assert.deepStrictEqual(
213 (yield* server.registry.listUnavailable).map((entry) => entry.instanceId),
214 [dependent],
215 );
216 const createdBefore = server.created.length;
217 const readsBefore = server.reads.length;
218​
219 // Unlocked for the check's own read only: any second read would fail again.
220 vault.set(wanted.reference, ["wanted-key", undefined]);
221 const result = yield* server.secretCheck.check(wanted);
222​
223 assert.deepStrictEqual(result, { _tag: "Resolved" });
224 assert.notInclude(JSON.stringify(result), "wanted-key");
225 assert.deepStrictEqual(server.reads.slice(readsBefore), [wanted.reference]);
226 assert.deepStrictEqual(server.created.slice(createdBefore), [
227 { instanceId: dependent, secrets: ["wanted-key"] },
228 ]);
229 assert.deepStrictEqual(server.refreshed, [dependent]);
230 assert.deepStrictEqual(yield* server.registry.listUnavailable, []);
231 }),
232 );
233​
234 it.effect("an unreadable secret fails naming its provider and leaves instances alone", () =>
235 Effect.gen(function* () {
236 const vault = new Map<string, Array<string | undefined>>([
237 [wanted.reference, [undefined]],
238 [other.reference, ["other-key"]],
239 ]);
240 const server = yield* setup(vault);
241 const createdBefore = server.created.length;
242​
243 const result = yield* server.secretCheck.check(wanted);
244​
245 assert.strictEqual(result._tag, "Failed");
246 if (result._tag === "Failed") assert.include(result.detail, "Personal");
247 assert.deepStrictEqual(server.created.slice(createdBefore), []);
248 assert.deepStrictEqual(server.refreshed, []);
249 assert.deepStrictEqual(
250 (yield* server.registry.listUnavailable).map((entry) => entry.instanceId),
251 [dependent],
252 );
253 }),
254 );
255​
256 it.effect("a failed check keeps the value the server already holds", () =>
257 Effect.gen(function* () {
258 const vault = new Map<string, Array<string | undefined>>([
259 [wanted.reference, ["wanted-key"]],
260 [locked.reference, ["locked-key", undefined]],
261 [other.reference, ["other-key"]],
262 ]);
263 const server = yield* setup(vault, [wanted, locked]);
264 const readsBefore = server.reads.length;
265​
266 assert.strictEqual((yield* server.secretCheck.check(locked))._tag, "Failed");
267​
268 assert.deepStrictEqual(yield* server.resolver.read([locked]), ["locked-key"]);
269 assert.deepStrictEqual(server.reads.slice(readsBefore), [locked.reference]);
270 }),
271 );
272​
273 it.effect("a check applies to an instance whose other secret failed a later refresh", () =>
274 Effect.gen(function* () {
275 const vault = new Map<string, Array<string | undefined>>([
276 [wanted.reference, ["wanted-old"]],
277 [locked.reference, ["locked-key", undefined]],
278 [other.reference, ["other-key"]],
279 ]);
280 const server = yield* setup(vault, [wanted, locked]);
281 // A Settings refresh while the vault is locked keeps the live instance and caches the failure.
282 yield* server.mutator.reloadSecrets(dependent);
283 vault.set(wanted.reference, ["wanted-new"]);
284​
285 assert.deepStrictEqual(yield* server.secretCheck.check(wanted), { _tag: "Resolved" });
286​
287 assert.deepStrictEqual(server.created.at(-1), {
288 instanceId: dependent,
289 secrets: ["wanted-new", "locked-key"],
290 });
291 assert.deepStrictEqual(yield* server.registry.listUnavailable, []);
292 }),
293 );
294​
295 it.effect("a check applies the value it read when a refresh re-reads that secret first", () =>
296 Effect.forEach(
297 // A live instance holding the old value, and one parked on a locked vault.
298 ["wanted-old", undefined],
299 (initial) =>
300 Effect.gen(function* () {
301 const vault = new Map<string, Array<string | undefined>>([
302 [wanted.reference, [initial]],
303 [other.reference, ["other-key"]],
304 ]);
305 const readStarted = yield* Deferred.make<void>();
306 const release = yield* Deferred.make<void>();
307 let gate: Effect.Effect<void> = Effect.void;
308 const server = yield* setup(vault, [wanted], {
309 beforeRead: () => {
310 const current = gate;
311 gate = Effect.void;
312 return current;
313 },
314 });
315 // The check reads the rotated value; the refresh's read after it finds the vault locked.
316 vault.set(wanted.reference, ["wanted-new", undefined]);
317 gate = Deferred.succeed(readStarted, undefined).pipe(
318 Effect.andThen(Deferred.await(release)),
319 );
320​
321 const check = yield* server.secretCheck
322 .check(wanted)
323 .pipe(Effect.forkChild({ startImmediately: true }));
324 yield* Deferred.await(readStarted);
325 // The refresh takes the registry lock, then waits on the resolver for the check's read.
326 const refresh = yield* server.mutator
327 .reloadSecrets(dependent)
328 .pipe(Effect.forkChild({ startImmediately: true }));
329 yield* Deferred.succeed(release, undefined);
330​
331 assert.deepStrictEqual(yield* Fiber.join(check), { _tag: "Resolved" });
332 yield* Fiber.join(refresh);
333 assert.deepStrictEqual(server.created.at(-1), {
334 instanceId: dependent,
335 secrets: ["wanted-new"],
336 });
337 assert.deepStrictEqual(yield* server.registry.listUnavailable, []);
338 }).pipe(Effect.scoped),
339 { discard: true },
340 ),
341 );
342​
343 it.effect("a check replaces only its own key of a record-valued config secret", () =>
344 Effect.gen(function* () {
345 const vault = new Map<string, Array<string | undefined>>([
346 [wanted.reference, ["wanted-old"]],
347 [locked.reference, ["locked-key"]],
348 [other.reference, ["other-key"]],
349 ]);
350 const server = yield* setup(vault, [], {
351 dependentConfig: { keys: { a: wanted, b: locked } },
352 });
353 vault.set(wanted.reference, ["wanted-new"]);
354​
355 assert.deepStrictEqual(yield* server.secretCheck.check(wanted), { _tag: "Resolved" });
356​
357 assert.deepStrictEqual(server.created.at(-1), { instanceId: dependent, secrets: [] });
358 assert.deepStrictEqual(server.createdConfigs.at(-1), {
359 keys: { a: "wanted-new", b: "locked-key" },
360 });
361 }),
362 );
363});