@voltro/testing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ import { applySchema } from '@voltro/database/sql';
2
+ import { DataStore } from '@voltro/database';
3
+ import { SqlDialect } from '@voltro/database';
4
+
5
+ export { applySchema }
6
+
7
+ /**
8
+ * Per-dialect fixture passed into the harness. `setup` runs before
9
+ * each scenario, `teardown` after. Both are responsible for whatever
10
+ * the dialect needs to give the scenario a clean slate — temp-file
11
+ * sqlite, scratch docker postgres, etc.
12
+ *
13
+ * `make` returns the live `DataStore` for the scenario. Most fixtures
14
+ * will close the store + drop the scratch state in `teardown`.
15
+ */
16
+ export declare interface DialectFixture {
17
+ readonly dialect: SqlDialect;
18
+ readonly name?: string;
19
+ readonly setup: () => Promise<void>;
20
+ readonly teardown: () => Promise<void>;
21
+ readonly make: () => Promise<DialectFixtureSession>;
22
+ }
23
+
24
+ export declare interface DialectFixtureSession {
25
+ readonly store: DataStore;
26
+ /** Apply `applySchema(tables, dialect.id)` against the same runtime
27
+ * the store is using, so DDL and DML hit the same database. The
28
+ * fixture knows how (most dialects expose a `run` method on their
29
+ * store; the fixture forwards). */
30
+ readonly migrate: (tables: ReadonlyArray<unknown>) => Promise<void>;
31
+ /** Tear down the session (close store, drop scratch DB, etc.). */
32
+ readonly dispose: () => Promise<void>;
33
+ }
34
+
35
+ /**
36
+ * Register the dialect's parity suite with vitest. Calls
37
+ * `describe(...)` so the dialect's name appears in test output.
38
+ *
39
+ * Caller imports this from `@voltro/testing` inside its
40
+ * `__tests__/parity.test.ts` and supplies a fixture. The fixture's
41
+ * lifecycle hooks (`setup` / `teardown` / `make.dispose`) coordinate
42
+ * docker / temp-file state.
43
+ *
44
+ * Returning early when the fixture's `setup` throws keeps CI green on
45
+ * machines without docker — the fixture reports a clean skip via its
46
+ * own logic before calling `runDialectParity`. We don't soft-skip here
47
+ * because that would mask real fixture bugs.
48
+ */
49
+ export declare const runDialectParity: (fixture: DialectFixture) => void;
50
+
51
+ export { }
@@ -0,0 +1,363 @@
1
+ import { boolean as e, eq as t, id as n, integer as r, table as i, text as a, timestamp as o } from "@voltro/database";
2
+ import { _voltroUndoLogTable as s, applyInverses as c, synthesizeInverse as l } from "@voltro/runtime";
3
+ import { afterEach as u, beforeEach as d, describe as f, expect as p, it as m } from "vitest";
4
+ import { applySchema as h } from "@voltro/database/sql";
5
+ //#region src/dialectParity.ts
6
+ var g = (e) => ({
7
+ table: e,
8
+ predicate: void 0,
9
+ order: [],
10
+ take: void 0,
11
+ skip: void 0,
12
+ projection: void 0
13
+ }), _ = i("voltro_parity_a", {
14
+ id: n(),
15
+ title: a(),
16
+ count: r()
17
+ }), v = i("voltro_parity_b", {
18
+ id: n(),
19
+ title: a()
20
+ }), y = i("voltro_parity_c", {
21
+ id: n(),
22
+ title: a(),
23
+ count: r(),
24
+ flag: e().default(!1)
25
+ }), b = i("voltro_parity_d", {
26
+ id: n(),
27
+ title: a(),
28
+ at: o().nullable(),
29
+ n: r().nullable()
30
+ }), x = (e) => {
31
+ f(`dialect parity: ${e.name ?? e.dialect.id}`, () => {
32
+ let n = null;
33
+ d(async () => {
34
+ await e.setup(), n = await e.make(), await n.migrate([
35
+ _,
36
+ v,
37
+ y,
38
+ b,
39
+ s
40
+ ]);
41
+ for (let e of [
42
+ "voltro_parity_a",
43
+ "voltro_parity_b",
44
+ "voltro_parity_c",
45
+ "voltro_parity_d",
46
+ "_voltro_undo_log"
47
+ ]) {
48
+ let t = await n.store.query(g(e));
49
+ for (let r of t) await n.store.delete(e, r.id);
50
+ }
51
+ }), u(async () => {
52
+ await n?.dispose(), await e.teardown(), n = null;
53
+ }), m("DDL is idempotent — re-applying the schema is a no-op", async () => {
54
+ await n.migrate([_, v]);
55
+ }), m("insert + query round-trips a row", async () => {
56
+ p(await n.store.insert("voltro_parity_a", {
57
+ id: "a1",
58
+ title: "one",
59
+ count: 1
60
+ })).toMatchObject({
61
+ id: "a1",
62
+ title: "one",
63
+ count: 1
64
+ });
65
+ let e = await n.store.query(g("voltro_parity_a"));
66
+ p(e.length).toBe(1), p(e[0]?.id).toBe("a1");
67
+ }), m("update returns the post-image", async () => {
68
+ await n.store.insert("voltro_parity_a", {
69
+ id: "a2",
70
+ title: "pre",
71
+ count: 1
72
+ }), p(await n.store.update("voltro_parity_a", "a2", { title: "post" })).toMatchObject({
73
+ id: "a2",
74
+ title: "post"
75
+ });
76
+ }), m("delete removes the row + reports true", async () => {
77
+ await n.store.insert("voltro_parity_a", {
78
+ id: "a3",
79
+ title: "x",
80
+ count: 1
81
+ }), p(await n.store.delete("voltro_parity_a", "a3")).toBe(!0), p((await n.store.query(g("voltro_parity_a"))).length).toBe(0);
82
+ }), m("undo: the _voltro_undo_log TEXT changes column round-trips a JSON ChangeSet", async () => {
83
+ let e = [{
84
+ table: "voltro_parity_c",
85
+ op: "update",
86
+ id: "c1",
87
+ prev: {
88
+ id: "c1",
89
+ title: "t",
90
+ count: 1,
91
+ flag: !0
92
+ },
93
+ next: {
94
+ id: "c1",
95
+ title: "t",
96
+ count: 2,
97
+ flag: !1
98
+ }
99
+ }];
100
+ await n.store.insert("_voltro_undo_log", {
101
+ id: "undo_p1",
102
+ tag: "parityC.update",
103
+ label: null,
104
+ subjectId: null,
105
+ tenantId: null,
106
+ traceId: null,
107
+ changes: JSON.stringify(e),
108
+ crossesAction: !1,
109
+ undone: !1,
110
+ createdAt: /* @__PURE__ */ new Date()
111
+ });
112
+ let t = await n.store.query(g("_voltro_undo_log"));
113
+ p(t.length).toBe(1), p(JSON.parse(t[0].changes)).toEqual(e);
114
+ }), m("undo: reverts an INSERT (inverse delete) on this dialect", async () => {
115
+ let e = {
116
+ invocationId: "inv1",
117
+ subject: null,
118
+ changes: [{
119
+ table: "voltro_parity_c",
120
+ op: "insert",
121
+ next: await n.store.insert("voltro_parity_c", {
122
+ id: "c2",
123
+ title: "new",
124
+ count: 1,
125
+ flag: !0
126
+ })
127
+ }]
128
+ };
129
+ await c(n.store, l(e)), p((await n.store.query(g("voltro_parity_c"))).find((e) => e.id === "c2")).toBeUndefined();
130
+ }), m("undo: reverts an UPDATE including the BOOLEAN, restoring the captured value FAITHFULLY", async () => {
131
+ await n.store.insert("voltro_parity_c", {
132
+ id: "c3",
133
+ title: "t",
134
+ count: 1,
135
+ flag: !0
136
+ });
137
+ let e = (await n.store.query({
138
+ ...g("voltro_parity_c"),
139
+ predicate: t("id", "c3")
140
+ }))[0], r = await n.store.update("voltro_parity_c", "c3", {
141
+ count: 2,
142
+ flag: !1
143
+ });
144
+ p(r.flag).not.toEqual(e.flag);
145
+ let i = {
146
+ invocationId: "inv2",
147
+ subject: null,
148
+ changes: [{
149
+ table: "voltro_parity_c",
150
+ op: "update",
151
+ id: "c3",
152
+ prev: e,
153
+ next: r
154
+ }]
155
+ };
156
+ await c(n.store, l(i));
157
+ let a = (await n.store.query({
158
+ ...g("voltro_parity_c"),
159
+ predicate: t("id", "c3")
160
+ }))[0];
161
+ p(a.flag).toEqual(e.flag), p(a.count).toEqual(e.count);
162
+ }), m("undo: reverts a DELETE (inverse re-insert) on this dialect", async () => {
163
+ let e = await n.store.insert("voltro_parity_c", {
164
+ id: "c4",
165
+ title: "gone",
166
+ count: 9,
167
+ flag: !0
168
+ });
169
+ await n.store.delete("voltro_parity_c", "c4");
170
+ let r = {
171
+ invocationId: "inv3",
172
+ subject: null,
173
+ changes: [{
174
+ table: "voltro_parity_c",
175
+ op: "delete",
176
+ id: "c4",
177
+ prev: e
178
+ }]
179
+ };
180
+ await c(n.store, l(r));
181
+ let i = (await n.store.query({
182
+ ...g("voltro_parity_c"),
183
+ predicate: t("id", "c4")
184
+ }))[0];
185
+ p(i.id).toBe("c4"), p(i.title).toBe("gone"), p(i.flag).toEqual(e.flag);
186
+ }), m("onChange fires insert + update + delete events", async () => {
187
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
188
+ await n.store.insert("voltro_parity_a", {
189
+ id: "a4",
190
+ title: "a",
191
+ count: 1
192
+ }), await n.store.update("voltro_parity_a", "a4", { title: "b" }), await n.store.delete("voltro_parity_a", "a4"), t(), p(e).toEqual([
193
+ "insert",
194
+ "update",
195
+ "delete"
196
+ ]);
197
+ }), m("transactional rolls back on throw + drops queued events", async () => {
198
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
199
+ await p(n.store.transactional(async (e) => {
200
+ throw await e.insert("voltro_parity_a", {
201
+ id: "rollback",
202
+ title: "r",
203
+ count: 0
204
+ }), Error("intentional rollback");
205
+ })).rejects.toThrow("intentional rollback"), t(), p(e).toEqual([]), p((await n.store.query(g("voltro_parity_a"))).length).toBe(0);
206
+ }), m("transactional commit drains queued events in order", async () => {
207
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
208
+ await n.store.transactional(async (e) => {
209
+ await e.insert("voltro_parity_a", {
210
+ id: "commit1",
211
+ title: "a",
212
+ count: 1
213
+ }), await e.insert("voltro_parity_a", {
214
+ id: "commit2",
215
+ title: "b",
216
+ count: 2
217
+ });
218
+ }), t(), p(e).toEqual(["insert", "insert"]);
219
+ }), m("upsert inserts a new row, then updates on conflict — no duplicate", async () => {
220
+ await n.store.upsert("voltro_parity_a", {
221
+ id: "u1",
222
+ title: "first",
223
+ count: 1
224
+ }, { conflictColumns: ["id"] }), await n.store.upsert("voltro_parity_a", {
225
+ id: "u1",
226
+ title: "second",
227
+ count: 2
228
+ }, {
229
+ conflictColumns: ["id"],
230
+ update: ["title", "count"]
231
+ });
232
+ let e = await n.store.query(g("voltro_parity_a"));
233
+ p(e.length).toBe(1), p(e[0]).toMatchObject({
234
+ id: "u1",
235
+ title: "second",
236
+ count: 2
237
+ });
238
+ }), m("insert + update bind an explicit NULL into a datetime2/int column", async () => {
239
+ p(await n.store.insert("voltro_parity_d", {
240
+ id: "nd1",
241
+ title: "nulls",
242
+ at: null,
243
+ n: null
244
+ })).toMatchObject({
245
+ id: "nd1",
246
+ title: "nulls",
247
+ at: null,
248
+ n: null
249
+ }), await n.store.insert("voltro_parity_d", {
250
+ id: "nd2",
251
+ title: "set",
252
+ at: /* @__PURE__ */ new Date(),
253
+ n: 7
254
+ }), p(await n.store.update("voltro_parity_d", "nd2", {
255
+ at: null,
256
+ n: null
257
+ })).toMatchObject({
258
+ id: "nd2",
259
+ at: null,
260
+ n: null
261
+ });
262
+ }), m("updateMany updates only the rows matching the predicate + returns the count", async () => {
263
+ await n.store.insert("voltro_parity_a", {
264
+ id: "m1",
265
+ title: "x",
266
+ count: 5
267
+ }), await n.store.insert("voltro_parity_a", {
268
+ id: "m2",
269
+ title: "y",
270
+ count: 5
271
+ }), await n.store.insert("voltro_parity_a", {
272
+ id: "m3",
273
+ title: "z",
274
+ count: 9
275
+ }), p(await n.store.updateMany("voltro_parity_a", { title: "hit" }, { where: {
276
+ column: "count",
277
+ op: "eq",
278
+ value: 5
279
+ } })).toBe(2), p((await n.store.query({
280
+ ...g("voltro_parity_a"),
281
+ predicate: {
282
+ column: "title",
283
+ op: "eq",
284
+ value: "hit"
285
+ }
286
+ })).length).toBe(2);
287
+ }), m("updateMany with an AND predicate is an atomic compare-and-set (the wakeup-claim pattern)", async () => {
288
+ await n.store.insert("voltro_parity_a", {
289
+ id: "cas",
290
+ title: "pending",
291
+ count: 1
292
+ });
293
+ let e = (e) => ({ and: [{
294
+ column: "id",
295
+ op: "eq",
296
+ value: "cas"
297
+ }, {
298
+ column: "title",
299
+ op: "eq",
300
+ value: e
301
+ }] });
302
+ p(await n.store.updateMany("voltro_parity_a", { title: "claimed" }, { where: e("pending") })).toBe(1), p(await n.store.updateMany("voltro_parity_a", { title: "claimed-again" }, { where: e("pending") })).toBe(0);
303
+ }), m("deleteMany removes only the rows matching the predicate + returns the count", async () => {
304
+ await n.store.insert("voltro_parity_a", {
305
+ id: "d1",
306
+ title: "x",
307
+ count: 5
308
+ }), await n.store.insert("voltro_parity_a", {
309
+ id: "d2",
310
+ title: "y",
311
+ count: 5
312
+ }), await n.store.insert("voltro_parity_a", {
313
+ id: "d3",
314
+ title: "z",
315
+ count: 9
316
+ }), p(await n.store.deleteMany("voltro_parity_a", { where: {
317
+ column: "count",
318
+ op: "eq",
319
+ value: 5
320
+ } })).toBe(2), p((await n.store.query(g("voltro_parity_a"))).map((e) => e.id)).toEqual(["d3"]);
321
+ }), m("deleteMany emits one delete ChangeEvent per removed row (old-image)", async () => {
322
+ await n.store.insert("voltro_parity_a", {
323
+ id: "e1",
324
+ title: "gone",
325
+ count: 1
326
+ }), await n.store.insert("voltro_parity_a", {
327
+ id: "e2",
328
+ title: "gone",
329
+ count: 1
330
+ }), await n.store.insert("voltro_parity_a", {
331
+ id: "e3",
332
+ title: "stay",
333
+ count: 2
334
+ });
335
+ let e = [], t = n.store.onChange((t) => e.push({
336
+ op: t.op,
337
+ id: t.old?.id
338
+ })), r = await n.store.deleteMany("voltro_parity_a", { where: {
339
+ column: "title",
340
+ op: "eq",
341
+ value: "gone"
342
+ } });
343
+ t(), p(r).toBe(2);
344
+ let i = e.filter((e) => e.op === "delete");
345
+ p(i).toHaveLength(2), p(i.map((e) => e.id).sort()).toEqual(["e1", "e2"]);
346
+ }), m("deleteMany with no match returns 0 and removes nothing", async () => {
347
+ await n.store.insert("voltro_parity_a", {
348
+ id: "k1",
349
+ title: "keep",
350
+ count: 1
351
+ }), p(await n.store.deleteMany("voltro_parity_a", { where: {
352
+ column: "title",
353
+ op: "eq",
354
+ value: "absent"
355
+ } })).toBe(0), p((await n.store.query(g("voltro_parity_a"))).length).toBe(1);
356
+ }), m("retryFilter recognises the dialect's own transient codes", () => {
357
+ let t = e.dialect.retryFilter(/* @__PURE__ */ Error("garbage"));
358
+ p(["retry", "noRetry"]).toContain(t);
359
+ });
360
+ });
361
+ };
362
+ //#endregion
363
+ export { h as applySchema, x as runDialectParity };
@@ -0,0 +1,202 @@
1
+ import { AppContext } from '@voltro/runtime';
2
+ import { Effect } from 'effect';
3
+ import { InspectedStep } from '@voltro/workflow';
4
+ import { InspectedWorkflow } from '@voltro/workflow';
5
+ import { Layer } from 'effect';
6
+ import { ParseResult } from 'effect';
7
+ import { ProcedureDescriptor } from '@voltro/protocol';
8
+ import { Row } from '@voltro/database';
9
+ import { Schema } from 'effect';
10
+ import { Subject } from '@voltro/protocol';
11
+ import { TableLike } from '@voltro/database';
12
+
13
+ /** Effect shape the runner erases a workflow's success/error/requirement
14
+ * channels down to. `unknown` for value/error (covariant — anything
15
+ * assigns), `never` for the requirement (the runner provides the engine
16
+ * layer itself). Callers pass concrete workflows cast via `as never` —
17
+ * `never` assigns to any field type, so the object-literal form stays
18
+ * ergonomic without an `any` in the public type. */
19
+ declare type ErasedWorkflowEffect = Effect.Effect<unknown, unknown, never>;
20
+
21
+ /**
22
+ * Decode `rawInput` through the procedure descriptor's `input` Schema, then
23
+ * call `executor` with the decoded value and `ctx`. Rejects with a
24
+ * `ParseError` if `rawInput` doesn't satisfy the input Schema — the same
25
+ * validation the live dispatcher runs before your handler sees the input.
26
+ *
27
+ * Only the input-decode hop is reproduced (see the file header): guards /
28
+ * middleware are dispatch-time rpc concerns, not descriptor-carried.
29
+ *
30
+ * ```ts
31
+ * import { createNote } from './notes.mutation' // descriptor
32
+ * import { createNoteHandler } from './notes.mutation.server' // executor
33
+ *
34
+ * const ctx = makeTestContext({ subject: user('A') })
35
+ * const note = await invoke(createNote, createNoteHandler, { title: 'hi' }, ctx)
36
+ * // a bad shape rejects at the decode, before the handler runs:
37
+ * await expect(invoke(createNote, createNoteHandler, { title: 42 }, ctx)).rejects.toThrow()
38
+ * ```
39
+ */
40
+ export declare const invoke: <D extends Pick<ProcedureDescriptor, "input">, Output>(descriptor: D, executor: ProcedureExecutor<Schema.Schema.Type<D["input"]>, Output>, rawInput: unknown, ctx: TestContext) => Promise<Output>;
41
+
42
+ export declare const makeTestContext: (options?: MakeTestContextOptions) => TestContext;
43
+
44
+ export declare interface MakeTestContextOptions {
45
+ /** The acting subject. Default: anonymous, no tenant. Read it back at
46
+ * `ctx.request.subject` (the same place a live handler reads it). */
47
+ readonly subject?: Subject;
48
+ /** Tables to wire into the store's schema. Default: all globally
49
+ * registered tables (whatever the test imported). */
50
+ readonly tables?: ReadonlyArray<TableLike>;
51
+ /** Seed rows keyed by table name (use `mockStore({...})`). */
52
+ readonly store?: Record<string, ReadonlyArray<Row>>;
53
+ /** Frozen clock start. Default 2026-01-01T00:00:00Z. */
54
+ readonly clockStart?: Date;
55
+ /** Injected AI mock (plan 05's `mockAi({...})`). */
56
+ readonly ai?: MockAi;
57
+ /** Queued LLM responses for the bundled `MockLLM`. */
58
+ readonly llmResponses?: ReadonlyArray<MockResponse>;
59
+ /** Env values sealed into the boot snapshot so handler code that reads
60
+ * `getSecret('X')` / `serverEnv.X` resolves under test. Without a snapshot
61
+ * those accessors throw ("read before the boot env gate ran") because the
62
+ * test harness never runs the real boot gate. Merged OVER `process.env`
63
+ * (these win). Default: just the ambient `process.env`. */
64
+ readonly env?: Record<string, string | undefined>;
65
+ }
66
+
67
+ export declare const makeWorkflowRunner: (opts: MakeWorkflowRunnerOptions) => WorkflowRunner;
68
+
69
+ export declare interface MakeWorkflowRunnerOptions {
70
+ readonly ctx: TestContext;
71
+ readonly workflows?: ReadonlyArray<WorkflowEntry>;
72
+ }
73
+
74
+ /** Minimal AI mock surface. The full `mockAi({...})` helper is owned by
75
+ * the AI package (plan 05); typed here as an optional injected interface
76
+ * so `@voltro/testing` carries no hard `@voltro/ai` dependency. */
77
+ export declare interface MockAi {
78
+ readonly generate?: (...args: ReadonlyArray<unknown>) => unknown;
79
+ readonly generateObject?: (...args: ReadonlyArray<unknown>) => unknown;
80
+ }
81
+
82
+ export declare class MockClock {
83
+ private currentMs;
84
+ constructor(initial?: Date | number);
85
+ /** Current mock time as an instant in ms. */
86
+ now(): number;
87
+ /** Current mock time as a Date. */
88
+ date(): Date;
89
+ /** Advance time by a duration (ms / seconds / minutes / hours / days). */
90
+ advance(amount: number | string): void;
91
+ }
92
+
93
+ export declare class MockEmail {
94
+ readonly sent: SentEmail[];
95
+ send(email: Omit<SentEmail, 'sentAt'>): void;
96
+ /** Find the most recent email to a recipient, or undefined. */
97
+ lastTo(recipient: string): SentEmail | undefined;
98
+ clear(): void;
99
+ }
100
+
101
+ export declare class MockLLM {
102
+ private readonly queue;
103
+ readonly calls: MockLLMCall[];
104
+ constructor(responses: ReadonlyArray<MockResponse>);
105
+ next(call: MockLLMCall): MockResponse;
106
+ remaining(): number;
107
+ }
108
+
109
+ export declare interface MockLLMCall {
110
+ readonly model: string;
111
+ readonly messages: ReadonlyArray<unknown>;
112
+ readonly tools?: ReadonlyArray<string>;
113
+ }
114
+
115
+ export declare type MockResponse = {
116
+ readonly text: string;
117
+ } | {
118
+ readonly toolCall: {
119
+ readonly name: string;
120
+ readonly input: unknown;
121
+ };
122
+ } | {
123
+ readonly error: {
124
+ readonly code: string;
125
+ readonly message?: string;
126
+ };
127
+ };
128
+
129
+ /** Seed helper: `makeTestContext({ store: mockStore({ docs: [...] }) })`.
130
+ * Identity over the table→rows map; exists so the call site reads well. */
131
+ export declare const mockStore: (seed: Record<string, ReadonlyArray<Row>>) => Record<string, ReadonlyArray<Row>>;
132
+
133
+ export { ParseResult }
134
+
135
+ /** Any procedure executor: takes the DECODED input + a context, returns a
136
+ * result (sync or async). Mirrors the `(input, ctx) => …` shape a real
137
+ * query / mutation / action handler has. */
138
+ export declare type ProcedureExecutor<Input, Output> = (input: Input, ctx: TestContext) => Output | Promise<Output>;
139
+
140
+ export declare interface SentEmail {
141
+ readonly to: string;
142
+ readonly template: string;
143
+ readonly props?: Readonly<Record<string, unknown>>;
144
+ readonly locale?: string;
145
+ readonly sentAt: Date;
146
+ }
147
+
148
+ /** The test context IS an `AppContext` (store + request + cache), plus the
149
+ * deterministic doubles and the subject/tenant re-scopers. Because it
150
+ * extends `AppContext`, you can pass it straight into an executor typed
151
+ * `(input, ctx: AppContext) => …`. */
152
+ export declare interface TestContext extends AppContext {
153
+ readonly clock: MockClock;
154
+ readonly email: MockEmail;
155
+ readonly llm: MockLLM;
156
+ readonly ai?: MockAi;
157
+ /** Re-scope to a different subject for one block (real subject swap, not
158
+ * a closure stub). Shares the underlying data so cross-subject reads
159
+ * exercise real tenant scoping. */
160
+ withSubject<T>(subject: Subject, fn: (ctx: TestContext) => T | Promise<T>): Promise<T>;
161
+ /** Re-scope to a different tenant (keeps the current subject identity). */
162
+ withTenant<T>(tenantId: string, fn: (ctx: TestContext) => T | Promise<T>): Promise<T>;
163
+ }
164
+
165
+ export declare const TESTING_PRESET_VERSION: 1;
166
+
167
+ /** A workflow + its execute function, as the framework pairs them via
168
+ * `workflow.toLayer(execute)`. Structural (no `@effect/workflow` generic
169
+ * signature) so callers can pass the value from `workflow({ name, ... })`
170
+ * plus its `(payload, executionId) => Effect` execute directly — type-erased
171
+ * over the workflow's payload/success/error/requirement shapes. */
172
+ export declare interface WorkflowEntry {
173
+ readonly workflow: {
174
+ readonly name: string;
175
+ readonly execute: (payload: unknown, options?: unknown) => ErasedWorkflowEffect;
176
+ readonly toLayer: (execute: (payload: unknown, executionId: string) => ErasedWorkflowEffect) => Layer.Layer<unknown, unknown, never>;
177
+ };
178
+ readonly execute: (payload: unknown, executionId: string) => ErasedWorkflowEffect;
179
+ }
180
+
181
+ export declare interface WorkflowRunner {
182
+ /** Start a workflow by its `name` (tag) with a payload and block until
183
+ * terminal. Records every step attempt so `result.steps[i].attempts`
184
+ * reflects real retries. */
185
+ start(workflowName: string, payload: unknown): Promise<WorkflowRunResult>;
186
+ /** Inspect a previously-started run by id without re-running it. Returns
187
+ * `null` for an unknown run id. */
188
+ inspect(runId: string): Promise<InspectedWorkflow | null>;
189
+ }
190
+
191
+ export declare interface WorkflowRunResult {
192
+ readonly status: 'succeeded' | 'failed';
193
+ readonly output: unknown;
194
+ readonly error: {
195
+ readonly tag: string | null;
196
+ readonly message: string;
197
+ } | null;
198
+ readonly steps: ReadonlyArray<InspectedStep>;
199
+ readonly runId: string;
200
+ }
201
+
202
+ export { }