1import { State } from "@/State";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { TestLayers, TestResource } from "./test.resources.ts";
7
8const { test } = Test.make({ providers: TestLayers() });
9
10// `test.provider` scratch state must be DURABLE (disk-backed) when the
11// adapter can namespace it by test file. A run killed while a provider
12// delete is still in flight (the runner abandons teardown 10s after a test
13// timeout) leaves its `deleting` rows behind; the next run's leading
14// `stack.destroy()` must see those rows and resume the delete. With the old
15// in-memory scratch store the rows died with the process, the next destroy
16// planned "no changes", and the cloud resource was orphaned invisibly.
17//
18// Cross-process durability is exercised here as cross-INSTANCE durability:
19// each `Core.scratchStack` call builds a fresh store handle, so a second
20// instance only sees the first instance's rows if they were persisted.
21describe(
22 "test.provider scratch state durability",
23 { tags: ["unit", "local"] },
24 () => {
25 const options = { providers: TestLayers() };
26 const NAME = "durability-probe";
27 const FILE = "test/scratch-state.test.ts";
28
29 const listRows = (scratch: Core.ScratchStack, stackName: string) =>
30 Effect.gen(function* () {
31 const state = yield* yield* State;
32 return yield* state.list({ stack: stackName, stage: scratch.stage });
33 }).pipe(Effect.provide(scratch.state));
34
35 test(
36 "rows persist across scratch instances and a later destroy drains them",
37 Effect.gen(function* () {
38 // First "run": deploy a resource, then walk away without destroying —
39 // the shape of a run whose teardown was abandoned.
40 const run1 = Core.scratchStack(options, NAME, FILE);
41 yield* run1.deploy(
42 Effect.gen(function* () {
43 yield* TestResource("A", { string: "v1" });
44 }),
45 );
46
47 // Second "run": a fresh scratch instance for the same (file, test)
48 // must see the persisted row...
49 const run2 = Core.scratchStack(options, NAME, FILE);
50 expect(run2.name).toEqual(run1.name);
51 expect(yield* listRows(run2, run2.name)).toEqual(["A"]);
52
53 // ...and its destroy must reclaim it.
54 yield* run2.destroy();
55 const run3 = Core.scratchStack(options, NAME, FILE);
56 expect(yield* listRows(run3, run3.name)).toEqual([]);
57 }),
58 );
59
60 test(
61 "scratch stack names are namespaced by file",
62 Effect.gen(function* () {
63 // Test names repeat across files ("create and delete bucket with
64 // default props" exists in several suites); the durable store must
65 // never collide across files or one test's destroy would delete
66 // another's live cloud resources.
67 const a = Core.scratchStack(options, "same name", "test/A/One.test.ts");
68 const b = Core.scratchStack(options, "same name", "test/B/One.test.ts");
69 expect(a.name).not.toEqual(b.name);
70
71 // Without a file (bun/vitest adapters), the store stays in-memory and
72 // the name stays the bare test name — the pre-existing behavior.
73 const bare = Core.scratchStack(options, "same name");
74 expect(bare.name).toEqual("same-name");
75 }),
76 );
77
78 test(
79 "scratch stacks default to test_${USER}",
80 Effect.sync(() => {
81 const expected = `test_${process.env.USER || process.env.USERNAME || "unknown"}`;
82 expect(Core.defaultStage()).toBe(expected);
83 const scratch = Core.scratchStack(options, NAME, FILE);
84 expect(scratch.stage).toBe(expected);
85 const overridden = Core.scratchStack(
86 { ...options, stage: "custom" },
87 NAME,
88 FILE,
89 );
90 expect(overridden.stage).toBe("custom");
91 }),
92 );
93 },
94);