1import * as PgClient from "@effect/sql-pg/PgClient";
2import {
3 makePostgresState,
4 type PostgresStateOptions,
5} from "@/State/PostgresState";
6import { StateStoreError, type StateService } from "@/State/State";
7import { describe, expect, it } from "alchemy-test";
8import * as Config from "effect/Config";
9import * as Deferred from "effect/Deferred";
10import * as Effect from "effect/Effect";
11import * as Fiber from "effect/Fiber";
12import * as Redacted from "effect/Redacted";
13import * as Stream from "effect/Stream";
14import * as Reactivity from "effect/reactivity/Reactivity";
15import * as SqlClient from "effect/sql/SqlClient";
16import type * as SqlConnection from "effect/sql/SqlConnection";
17import * as SqlError from "effect/sql/SqlError";
18
19interface FakeQuery {
20 text: string;
21 values: ReadonlyArray<unknown>;
22}
23
24interface FakeConnection extends SqlConnection.Connection {
25 queries: FakeQuery[];
26 released: boolean;
27}
28
29const unsupported = (operation: string) =>
30 Effect.die(new Error(`fake postgres does not support ${operation}`));
31
32const sqlFailure = (message: string) =>
33 new SqlError.SqlError({
34 reason: new SqlError.ConnectionError({
35 cause: new Error(message),
36 message,
37 operation: "execute",
38 }),
39 });
40
41/**
42 * Hermetic fake of the `@effect/sql` driver surface the store uses: a real
43 * `SqlClient` built on the real `@effect/sql-pg` statement compiler, whose
44 * connections are in-memory. It recognizes the store's fixed SQL statements
45 * and keeps rows in in-memory maps, so tests exercise the real store logic
46 * (lock acquisition, lease guard, encoding) without a database.
47 *
48 * Every acquired connection records its own queries and answers
49 * `pg_backend_pid()` with its own id, so tests can prove which connection
50 * ran what and can tell a pool apart from a single shared connection.
51 *
52 * What this fake cannot do is model a real pool: `singleConnection: true`
53 * reproduces `PgClient.makeClient`/`fromClient`, where reserving and
54 * running a plain statement both land on the same connection, but nothing
55 * here reproduces pool exhaustion, queueing, or a killed backend. That is
56 * why the store asks Postgres itself which backend answered the liveness
57 * check instead of trusting the client to hand out a second connection.
58 */
59const makeFakePostgres = (options: { singleConnection?: boolean } = {}) => {
60 const resources = new Map<string, unknown>();
61 const outputs = new Map<string, unknown>();
62 const connections: FakeConnection[] = [];
63 const control = {
64 lockAcquired: true,
65 lockLive: true,
66 lockQueries: 0,
67 failLockQuery: false,
68 // Holds the lock query in flight: `started` fires when the store issues
69 // it, and it only answers once `release` completes.
70 lockGate: undefined as
71 | { started: Deferred.Deferred<void>; release: Deferred.Deferred<void> }
72 | undefined,
73 };
74
75 const resourceKey = (stack: unknown, stage: unknown, fqn: unknown) =>
76 `${stack} ${stage} ${fqn}`;
77 const outputKey = (stack: unknown, stage: unknown) => `${stack} ${stage}`;
78
79 const rows = (
80 values: ReadonlyArray<SqlConnection.Row>,
81 ): Effect.Effect<ReadonlyArray<SqlConnection.Row>, SqlError.SqlError> =>
82 Effect.succeed(values);
83
84 const handle = (
85 text: string,
86 values: ReadonlyArray<unknown>,
87 backendPid: number,
88 ): Effect.Effect<ReadonlyArray<SqlConnection.Row>, SqlError.SqlError> => {
89 const sql = text.replaceAll(/\s+/g, " ").trim().toLowerCase();
90 if (sql === "begin" || sql === "commit" || sql === "rollback") {
91 return rows([]);
92 }
93 if (sql.includes("pg_try_advisory_lock")) {
94 control.lockQueries += 1;
95 if (control.failLockQuery) {
96 return Effect.fail(sqlFailure("connection terminated unexpectedly"));
97 }
98 const acquired = rows([
99 { acquired: control.lockAcquired, pid: backendPid },
100 ]);
101 const gate = control.lockGate;
102 return gate === undefined
103 ? acquired
104 : Effect.sync(() => {
105 Deferred.doneUnsafe(gate.started, Effect.void);
106 }).pipe(
107 Effect.andThen(Deferred.await(gate.release)),
108 Effect.andThen(acquired),
109 );
110 }
111 if (sql.includes("pg_advisory_xact_lock")) {
112 return rows([{}]);
113 }
114 if (sql.includes("pg_advisory_unlock")) {
115 return rows([{}]);
116 }
117 if (sql.includes("from pg_locks")) {
118 return rows([{ live: control.lockLive, checker_pid: backendPid }]);
119 }
120 if (sql.startsWith("create table")) {
121 return rows([]);
122 }
123 if (sql.startsWith("insert into alchemy_resource_state")) {
124 const [stack, stage, fqn, json] = values;
125 resources.set(resourceKey(stack, stage, fqn), JSON.parse(String(json)));
126 return rows([]);
127 }
128 if (sql.startsWith("insert into alchemy_stack_output")) {
129 const [stack, stage, json] = values;
130 outputs.set(outputKey(stack, stage), JSON.parse(String(json)));
131 return rows([]);
132 }
133 if (sql.includes("value ->> 'status'")) {
134 const [stack, stage] = values;
135 return rows(
136 Array.from(resources.entries())
137 .filter(([key]) => key.startsWith(`${stack} ${stage} `))
138 .map(([, value]) => ({ value }))
139 .filter(
140 (row) =>
141 (row.value as { status?: string } | undefined)?.status ===
142 "replaced",
143 ),
144 );
145 }
146 if (sql.startsWith("select value from alchemy_resource_state")) {
147 const [stack, stage, fqn] = values;
148 const value = resources.get(resourceKey(stack, stage, fqn));
149 return rows(value === undefined ? [] : [{ value }]);
150 }
151 if (sql.startsWith("select value from alchemy_stack_output")) {
152 const [stack, stage] = values;
153 const value = outputs.get(outputKey(stack, stage));
154 return rows(value === undefined ? [] : [{ value }]);
155 }
156 if (sql.startsWith("select fqn from alchemy_resource_state")) {
157 const [stack, stage] = values;
158 return rows(
159 Array.from(resources.keys())
160 .filter((key) => key.startsWith(`${stack} ${stage} `))
161 .map((key) => ({ fqn: key.split(" ")[2] }))
162 .sort((a, b) => String(a.fqn).localeCompare(String(b.fqn))),
163 );
164 }
165 if (sql.startsWith("select stack from alchemy_resource_state")) {
166 const stacks = new Set<string>();
167 for (const key of resources.keys()) stacks.add(key.split(" ")[0]!);
168 for (const key of outputs.keys()) stacks.add(key.split(" ")[0]!);
169 return rows(
170 Array.from(stacks)
171 .sort()
172 .map((stack) => ({ stack })),
173 );
174 }
175 if (sql.startsWith("select stage from alchemy_resource_state")) {
176 const [stack] = values;
177 const stages = new Set<string>();
178 for (const key of resources.keys()) {
179 const [s, stage] = key.split(" ");
180 if (s === stack) stages.add(stage!);
181 }
182 for (const key of outputs.keys()) {
183 const [s, stage] = key.split(" ");
184 if (s === stack) stages.add(stage!);
185 }
186 return rows(
187 Array.from(stages)
188 .sort()
189 .map((stage) => ({ stage })),
190 );
191 }
192 if (sql.startsWith("delete from alchemy_resource_state")) {
193 const [stack, stage, fqn] = values;
194 if (fqn !== undefined) {
195 resources.delete(resourceKey(stack, stage, fqn));
196 } else if (stage !== undefined) {
197 for (const key of Array.from(resources.keys())) {
198 if (key.startsWith(`${stack} ${stage} `)) {
199 resources.delete(key);
200 }
201 }
202 } else {
203 for (const key of Array.from(resources.keys())) {
204 if (key.startsWith(`${stack} `)) resources.delete(key);
205 }
206 }
207 return rows([]);
208 }
209 if (sql.startsWith("delete from alchemy_stack_output")) {
210 const [stack, stage] = values;
211 for (const key of Array.from(outputs.keys())) {
212 const [s, keyStage] = key.split(" ");
213 if (s === stack && (stage === undefined || keyStage === stage)) {
214 outputs.delete(key);
215 }
216 }
217 return rows([]);
218 }
219 return Effect.die(new Error(`fake postgres does not recognize: ${sql}`));
220 };
221
222 let nextBackendPid = 100;
223
224 const makeConnection = (): FakeConnection => {
225 const queries: FakeQuery[] = [];
226 const backendPid = nextBackendPid++;
227 const execute = (text: string, values: ReadonlyArray<unknown>) =>
228 Effect.suspend(() => {
229 queries.push({ text, values });
230 return handle(text, values, backendPid);
231 });
232 const connection: FakeConnection = {
233 queries,
234 released: false,
235 execute,
236 executeUnprepared: execute,
237 executeRaw: execute,
238 executeValues: () => unsupported("executeValues"),
239 executeValuesUnprepared: () => unsupported("executeValuesUnprepared"),
240 executeStream: () => Stream.fromEffect(unsupported("executeStream")),
241 };
242 connections.push(connection);
243 return connection;
244 };
245
246 // A scoped acquirer, exactly like a pool's: every acquisition hands out a
247 // fresh connection and marks it released when its scope closes. With
248 // `singleConnection`, every acquisition returns the same connection —
249 // what `PgClient.makeClient` does, where `reserve` and plain statements
250 // share one backend.
251 const shared = options.singleConnection ? makeConnection() : undefined;
252 const acquirer: SqlConnection.Acquirer =
253 shared === undefined
254 ? Effect.acquireRelease(Effect.sync(makeConnection), (connection) =>
255 Effect.sync(() => {
256 connection.released = true;
257 }),
258 )
259 : Effect.succeed(shared);
260
261 const client = SqlClient.make({
262 acquirer,
263 compiler: PgClient.makeCompiler(),
264 spanAttributes: [],
265 }).pipe(Effect.provide(Reactivity.layer));
266
267 return { client, connections, resources, outputs, control };
268};
269
270const connectionRunning = (
271 connections: FakeConnection[],
272 fragment: string,
273): FakeConnection | undefined =>
274 connections.find((connection) =>
275 connection.queries.some((query) => query.text.includes(fragment)),
276 );
277
278type FakePostgres = ReturnType<typeof makeFakePostgres>;
279
280const withStore = <A, E>(
281 fake: FakePostgres,
282 options: Omit<PostgresStateOptions, "client">,
283 use: (store: StateService) => Effect.Effect<A, E>,
284): Effect.Effect<A, E | StateStoreError> =>
285 Effect.gen(function* () {
286 const scope = yield* Effect.scope;
287 const store = yield* makePostgresState(
288 { ...options, client: yield* fake.client },
289 scope,
290 );
291 return yield* use(store);
292 }).pipe(Effect.scoped);
293
294/** Builds the store without a client, to exercise option validation. */
295const withoutClient = <A, E, E2>(
296 options: PostgresStateOptions<E2>,
297 use: (store: StateService) => Effect.Effect<A, E>,
298): Effect.Effect<A, E | StateStoreError> =>
299 Effect.gen(function* () {
300 const scope = yield* Effect.scope;
301 const store = yield* makePostgresState(options, scope);
302 return yield* use(store);
303 }).pipe(Effect.scoped);
304
305const request = { stack: "app", stage: "prod", fqn: "app/prod/db" };
306
307const sampleState = {
308 kind: "resource",
309 status: "created",
310 logicalId: "db",
311 output: { password: Redacted.make("s3cret") },
312} as never;
313
314describe("Postgres state store", { tags: ["unit", "local"] }, () => {
315 it.effect("requires exactly one of client or url", () => {
316 const fake = makeFakePostgres();
317 return Effect.gen(function* () {
318 const neither = yield* withoutClient({}, (store) =>
319 store.get(request),
320 ).pipe(Effect.flip);
321 expect(neither).toBeInstanceOf(StateStoreError);
322 expect(neither.message).toContain("exactly one of `client` or `url`");
323
324 const both = yield* withStore(
325 fake,
326 { url: Redacted.make("postgres://localhost/state") },
327 (store) => store.get(request),
328 ).pipe(Effect.flip);
329 expect(both).toBeInstanceOf(StateStoreError);
330 expect((both as StateStoreError).message).toContain(
331 "exactly one of `client` or `url`",
332 );
333 });
334 });
335
336 it.effect("reports an unresolvable url config as a state store error", () => {
337 // `url` accepts any Effect of a Redacted string, so
338 // `Config.Redacted(...)` can be passed straight through; a missing
339 // variable surfaces as a StateStoreError like any other failure.
340 return withoutClient(
341 { url: Config.Redacted("ALCHEMY_TEST_MISSING_STATE_DATABASE_URL") },
342 (store) => store.get(request),
343 ).pipe(
344 Effect.flip,
345 Effect.map((error) => {
346 expect(error).toBeInstanceOf(StateStoreError);
347 expect(error.message).toContain(
348 "ALCHEMY_TEST_MISSING_STATE_DATABASE_URL",
349 );
350 }),
351 );
352 });
353
354 it.effect("round-trips resource state including Redacted values", () => {
355 const fake = makeFakePostgres();
356 return withStore(fake, {}, (store) =>
357 Effect.gen(function* () {
358 expect(yield* store.get(request)).toBeUndefined();
359
360 yield* store.set({ ...request, value: sampleState });
361 const revived = (yield* store.get(request)) as {
362 status: string;
363 output: { password: Redacted.Redacted<string> };
364 };
365
366 expect(revived.status).toBe("created");
367 expect(Redacted.isRedacted(revived.output.password)).toBe(true);
368 expect(Redacted.value(revived.output.password)).toBe("s3cret");
369
370 // The jsonb column stores the encoded (redaction-marked) form, so
371 // the raw secret round-trips through encodeState, not plain JSON.
372 const stored = fake.resources.get("app prod app/prod/db");
373 expect(JSON.stringify(stored)).toContain("__redacted__");
374 }),
375 );
376 });
377
378 it.effect("serializes the schema migration under an advisory lock", () => {
379 const fake = makeFakePostgres();
380 return withStore(fake, {}, (store) =>
381 Effect.gen(function* () {
382 yield* store.get(request);
383
384 const migration = connectionRunning(fake.connections, "create table");
385 expect(migration).toBeDefined();
386 const statements = migration!.queries.map((query) =>
387 query.text.replaceAll(/\s+/g, " ").trim().toLowerCase(),
388 );
389 expect(statements[0]).toBe("begin");
390 expect(statements[1]).toContain("pg_advisory_xact_lock");
391 expect(migration!.queries[1]?.values).toEqual(["alchemy:schema"]);
392 expect(statements[2]).toContain(
393 "create table if not exists alchemy_resource_state",
394 );
395 expect(statements[3]).toContain(
396 "create table if not exists alchemy_stack_output",
397 );
398 expect(statements[4]).toBe("commit");
399 expect(migration!.released).toBe(true);
400 }),
401 );
402 });
403
404 it.effect("acquires the advisory lock once per stack/stage", () => {
405 const fake = makeFakePostgres();
406 return withStore(fake, {}, (store) =>
407 Effect.gen(function* () {
408 yield* store.set({ ...request, value: sampleState });
409 yield* store.get(request);
410 yield* store.list(request);
411 yield* store.setOutput({ ...request, value: { url: "https://x" } });
412 expect(yield* store.getOutput(request)).toEqual({ url: "https://x" });
413
414 expect(fake.control.lockQueries).toBe(1);
415 const lock = connectionRunning(
416 fake.connections,
417 "pg_try_advisory_lock",
418 );
419 expect(lock?.queries[0]?.values).toEqual(["alchemy:app/prod"]);
420 }),
421 );
422 });
423
424 it.effect(
425 "never runs the lease check on the reserved lock connection",
426 () => {
427 const fake = makeFakePostgres();
428 return withStore(fake, { leaseCheckTtlMs: 0 }, (store) =>
429 Effect.gen(function* () {
430 yield* store.set({ ...request, value: sampleState });
431 yield* store.get(request);
432
433 const lock = connectionRunning(
434 fake.connections,
435 "pg_try_advisory_lock",
436 );
437 expect(lock).toBeDefined();
438 // The reserved connection only ever takes the lock; the liveness
439 // check must ask a different backend, because the reserved
440 // connection cannot reliably report on itself once its backend
441 // has been killed server-side.
442 expect(
443 lock!.queries.some((query) => query.text.includes("pg_locks")),
444 ).toBe(false);
445 expect(
446 fake.connections
447 .filter((connection) => connection !== lock)
448 .some((connection) =>
449 connection.queries.some((query) =>
450 query.text.includes("pg_locks"),
451 ),
452 ),
453 ).toBe(true);
454 }),
455 );
456 },
457 );
458
459 it.effect(
460 "refuses a client that cannot verify the lock independently",
461 () => {
462 // A single-connection client routes the liveness check straight back to
463 // the backend holding the lock, which cannot vouch for itself. The
464 // store notices because the check reports its own backend pid.
465 const fake = makeFakePostgres({ singleConnection: true });
466 return withStore(fake, { leaseCheckTtlMs: 0 }, (store) =>
467 Effect.gen(function* () {
468 const error = yield* store.get(request).pipe(Effect.flip);
469 expect(error).toBeInstanceOf(StateStoreError);
470 expect(error.message).toContain("cannot be verified");
471 expect(error.message).toContain("not pool-backed");
472 }),
473 );
474 },
475 );
476
477 it.effect("prefixes the lock key with lockKeyPrefix", () => {
478 const fake = makeFakePostgres();
479 return withStore(fake, { lockKeyPrefix: "my-app" }, (store) =>
480 Effect.gen(function* () {
481 yield* store.get(request);
482 const lock = connectionRunning(
483 fake.connections,
484 "pg_try_advisory_lock",
485 );
486 expect(lock?.queries[0]?.values).toEqual(["my-app:app/prod"]);
487 }),
488 );
489 });
490
491 it.effect("fails immediately when another deploy holds the lock", () => {
492 const fake = makeFakePostgres();
493 fake.control.lockAcquired = false;
494 return withStore(fake, {}, (store) =>
495 Effect.gen(function* () {
496 const result = yield* store.get(request).pipe(Effect.flip);
497 expect(result).toBeInstanceOf(StateStoreError);
498 expect(result.message).toContain(
499 "another deploy holds the Postgres state lock",
500 );
501 // The reserved connection is released on contention.
502 const lock = connectionRunning(
503 fake.connections,
504 "pg_try_advisory_lock",
505 );
506 expect(lock?.released).toBe(true);
507 }),
508 );
509 });
510
511 it.effect(
512 "releases the reserved connection when the lock query fails",
513 () => {
514 const fake = makeFakePostgres();
515 // The lock attempt fails at the driver, so the reserved connection must
516 // go back to the pool rather than leak.
517 fake.control.failLockQuery = true;
518 return withStore(fake, {}, (store) =>
519 Effect.gen(function* () {
520 const error = yield* store.get(request).pipe(Effect.flip);
521 expect(error).toBeInstanceOf(StateStoreError);
522 expect(error.message).toContain("connection terminated unexpectedly");
523
524 const lock = connectionRunning(
525 fake.connections,
526 "pg_try_advisory_lock",
527 );
528 expect(lock?.released).toBe(true);
529 }),
530 );
531 },
532 );
533
534 it.effect(
535 "registers the unlock finalizer even when interrupted mid-acquisition",
536 () => {
537 const fake = makeFakePostgres();
538 return Effect.gen(function* () {
539 const started = yield* Deferred.make<void>();
540 const release = yield* Deferred.make<void>();
541 fake.control.lockGate = { started, release };
542
543 const fiber = yield* withStore(fake, {}, (store) =>
544 store.get(request),
545 ).pipe(Effect.forkChild({ startImmediately: true }));
546
547 // Interrupt while the lock query is still in flight. Taking the
548 // lock and registering its unlock finalizer is uninterruptible, so
549 // the store must finish both before the interruption is honored —
550 // otherwise the session lock would stay held on a connection handed
551 // back to the pool, and every later deploy of this stack/stage
552 // would fail on it.
553 yield* Deferred.await(started);
554 const interrupting = yield* Fiber.interrupt(fiber).pipe(
555 Effect.forkChild({ startImmediately: true }),
556 );
557 yield* Deferred.succeed(release, undefined);
558 yield* Fiber.join(interrupting);
559
560 const lock = connectionRunning(
561 fake.connections,
562 "pg_try_advisory_lock",
563 );
564 expect(lock).toBeDefined();
565 expect(
566 lock!.queries.some((query) =>
567 query.text.includes("pg_advisory_unlock"),
568 ),
569 ).toBe(true);
570 expect(lock!.released).toBe(true);
571 });
572 },
573 );
574
575 it.effect("unlocks and releases the lock connection on scope close", () => {
576 const fake = makeFakePostgres();
577 return withStore(fake, {}, (store) =>
578 store.set({ ...request, value: sampleState }),
579 ).pipe(
580 Effect.andThen(
581 Effect.sync(() => {
582 const lock = connectionRunning(
583 fake.connections,
584 "pg_try_advisory_lock",
585 );
586 expect(lock).toBeDefined();
587 expect(
588 lock!.queries.some((query) =>
589 query.text.includes("pg_advisory_unlock"),
590 ),
591 ).toBe(true);
592 expect(lock!.released).toBe(true);
593 }),
594 ),
595 );
596 });
597
598 it.effect("fails loudly when the lock lease is lost mid-run", () => {
599 const fake = makeFakePostgres();
600 return withStore(fake, { leaseCheckTtlMs: 0 }, (store) =>
601 Effect.gen(function* () {
602 yield* store.set({ ...request, value: sampleState });
603
604 fake.control.lockLive = false;
605 const result = yield* store.get(request).pipe(Effect.flip);
606 expect(result).toBeInstanceOf(StateStoreError);
607 expect(result.message).toContain("was lost mid-run");
608
609 // Stage-less operations re-verify held leases too.
610 const listResult = yield* store.listStacks().pipe(Effect.flip);
611 expect(listResult.message).toContain("was lost mid-run");
612 }),
613 );
614 });
615
616 it.effect("filters replaced resources in SQL", () => {
617 const fake = makeFakePostgres();
618 return withStore(fake, {}, (store) =>
619 Effect.gen(function* () {
620 yield* store.set({ ...request, value: sampleState });
621 yield* store.set({
622 ...request,
623 fqn: "app/prod/old",
624 value: { ...(sampleState as object), status: "replaced" } as never,
625 });
626
627 const replaced = yield* store.getReplacedResources(request);
628 expect(replaced).toHaveLength(1);
629 expect(replaced[0]?.status).toBe("replaced");
630 }),
631 );
632 });
633
634 it.effect("lists stacks, stages, and fqns", () => {
635 const fake = makeFakePostgres();
636 return withStore(fake, {}, (store) =>
637 Effect.gen(function* () {
638 yield* store.set({ ...request, value: sampleState });
639 yield* store.set({
640 ...request,
641 fqn: "app/prod/api",
642 value: sampleState,
643 });
644 yield* store.setOutput({
645 stack: "other",
646 stage: "dev",
647 value: { ok: true },
648 });
649
650 expect(yield* store.listStacks()).toEqual(["app", "other"]);
651 expect(yield* store.listStages("app")).toEqual(["prod"]);
652 expect(yield* store.list(request)).toEqual([
653 "app/prod/api",
654 "app/prod/db",
655 ]);
656 }),
657 );
658 });
659
660 it.effect("deleteStack removes resource rows and stack outputs", () => {
661 const fake = makeFakePostgres();
662 return withStore(fake, {}, (store) =>
663 Effect.gen(function* () {
664 yield* store.set({ ...request, value: sampleState });
665 yield* store.setOutput({ ...request, value: { ok: true } });
666
667 yield* store.deleteStack({ stack: "app", stage: "prod" });
668 expect(yield* store.get(request)).toBeUndefined();
669 expect(yield* store.getOutput(request)).toBeUndefined();
670
671 yield* store.set({ ...request, value: sampleState });
672 yield* store.deleteStack({ stack: "app" });
673 expect(fake.resources.size).toBe(0);
674 expect(fake.outputs.size).toBe(0);
675 }),
676 );
677 });
678
679 it.effect("stage-less deleteStack locks every stage before deleting", () => {
680 const fake = makeFakePostgres();
681 // Rows written by an earlier run: this store has not touched the stack
682 // yet, so it holds no leases when deleteStack starts.
683 fake.resources.set("app prod app/prod/db", { status: "created" });
684 fake.outputs.set("app prod", { ok: true });
685
686 return withStore(fake, {}, (store) =>
687 Effect.gen(function* () {
688 yield* store.deleteStack({ stack: "app" });
689
690 const lock = connectionRunning(
691 fake.connections,
692 "pg_try_advisory_lock",
693 );
694 expect(lock?.queries[0]?.values).toEqual(["alchemy:app/prod"]);
695 expect(fake.resources.size).toBe(0);
696 expect(fake.outputs.size).toBe(0);
697 }),
698 );
699 });
700
701 it.effect("delete removes a single resource", () => {
702 const fake = makeFakePostgres();
703 return withStore(fake, {}, (store) =>
704 Effect.gen(function* () {
705 yield* store.set({ ...request, value: sampleState });
706 yield* store.delete(request);
707 expect(yield* store.get(request)).toBeUndefined();
708 }),
709 );
710 });
711});