1/**
2 * Per-file registration state.
3 *
4 * While a test file's module body is evaluating, `describe`/`test`/hook
5 * calls register nodes against that file's collector. The runner imports
6 * all test files in PARALLEL; attribution stays correct because the
7 * collector is carried by AsyncLocalStorage — the module loader propagates
8 * the async context of the `import()` call into the module's top-level
9 * evaluation (and into microtasks queued from it), so each file's
10 * registrations resolve to its own root no matter how many imports are in
11 * flight.
12 *
13 * The storage lives on `globalThis` so that a duplicated module instance
14 * (e.g. two resolutions of the package) still shares one registry.
15 */
16import { AsyncLocalStorage } from "node:async_hooks";
17import { makeFileSuite, type FileSuite, type Suite } from "./Model.ts";
18
19interface FileContext {
20 /** Suite that `describe`/`test` calls currently attach to. */
21 current: Suite;
22}
23
24const key = Symbol.for("alchemy-test/registry");
25
26const storage: AsyncLocalStorage<FileContext> = ((globalThis as any)[key] ??=
27 new AsyncLocalStorage<FileContext>());
28
29/**
30 * Collect one file: run `f` (the file's dynamic import + microtask flush)
31 * with a fresh root as the ambient collector, and return the root.
32 */
33export const collect = async (
34 file: string,
35 f: () => Promise<void>,
36): Promise<FileSuite> => {
37 const root = makeFileSuite(file);
38 await storage.run({ current: root }, f);
39 return root;
40};
41
42const currentContext = (): FileContext => {
43 const context = storage.getStore();
44 if (context === undefined) {
45 throw new Error(
46 "alchemy-test: describe/test/hook called outside of a test file collection. " +
47 "Run tests with the `alchemy-test` CLI.",
48 );
49 }
50 return context;
51};
52
53export const currentSuite = (): Suite => currentContext().current;
54
55/**
56 * The file currently being collected (path relative to the run root, e.g.
57 * `test/Cloudflare/R2/Bucket.test.ts`), or `undefined` when called outside
58 * of a collection (e.g. from a non-alchemy-test runner). Adapters use this
59 * at registration time to namespace per-test durable state by file.
60 */
61export const currentFile = (): string | undefined => {
62 let suite: Suite | undefined = storage.getStore()?.current;
63 while (suite?.parent !== undefined) suite = suite.parent;
64 return suite !== undefined && "file" in suite
65 ? (suite as FileSuite).file
66 : undefined;
67};
68
69/** Run `f` with `suite` as the current registration target. */
70export const withSuite = (suite: Suite, f: () => void): void => {
71 const context = currentContext();
72 const previous = context.current;
73 context.current = suite;
74 try {
75 f();
76 } finally {
77 context.current = previous;
78 }
79};