@lunora/testing 0.0.0 → 1.0.0-alpha.2

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,265 @@
1
+ import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
2
+ export { type InboxOptions, type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
3
+ /** A pending job entry in the fake scheduler queue. */
4
+ interface FakeScheduledJob extends ScheduledJob {
5
+ /** The args the job was scheduled with. */
6
+ args: Record<string, unknown>;
7
+ }
8
+ /**
9
+ * A single scheduled-job failure captured during an `advance()` / `runPending()`
10
+ * sweep. Production's scheduler isolates per-job failures (one bad job does not
11
+ * abort the rest), so the fake scheduler does the same — but, being a test
12
+ * harness, it never swallows the error: every failure is recorded here so tests
13
+ * can still assert on it.
14
+ */
15
+ interface ScheduledJobFailure {
16
+ /** The args the job was dispatched with. */
17
+ args: Record<string, unknown>;
18
+ /** The error the job's handler threw (or that dispatch produced). */
19
+ error: unknown;
20
+ /** The function path the job targeted (e.g. `"messages:send"`). */
21
+ functionPath: string;
22
+ /** The id of the job that failed. */
23
+ id: string;
24
+ }
25
+ /**
26
+ * Controls exposed on the test harness for the fake in-memory scheduler.
27
+ * Access via `harness.scheduler`.
28
+ */
29
+ interface FakeSchedulerControls {
30
+ /**
31
+ * Advance the virtual clock by `ms` milliseconds, executing all jobs whose
32
+ * `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
33
+ * are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
34
+ * (scheduled by an executed job during the advance) are NOT re-evaluated in
35
+ * the same advance call — callers should advance again if needed.
36
+ *
37
+ * Per-job failures are isolated (matching production): a job that throws does
38
+ * NOT prevent the remaining due jobs from running. After every due job has
39
+ * run, the failures are surfaced — they are recorded on
40
+ * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
41
+ * still sees the error. A single failure is re-thrown verbatim; multiple
42
+ * failures are aggregated into an `AggregateError`. Pass
43
+ * `{ throwOnError: false }` to suppress the re-throw and inspect
44
+ * {@link FakeSchedulerControls.failures} (and the returned count) instead.
45
+ *
46
+ * Returns the number of jobs that were executed (including failed ones).
47
+ */
48
+ advance: (ms: number, options?: SweepOptions) => Promise<number>;
49
+ /**
50
+ * All scheduled-job failures captured so far, in execution order, across
51
+ * every `advance()` / `runPending()` call on this harness. Always available,
52
+ * even when `throwOnError: false` suppressed the re-throw. The list is a
53
+ * snapshot — mutating it does not affect the scheduler.
54
+ */
55
+ failures: () => ScheduledJobFailure[];
56
+ /**
57
+ * List all pending jobs (those not yet executed or cancelled) in the order
58
+ * they were enqueued.
59
+ */
60
+ list: () => FakeScheduledJob[];
61
+ /**
62
+ * Execute all currently pending jobs regardless of their `scheduledFor`
63
+ * time. Equivalent to advancing to `Infinity`. Returns the number of jobs
64
+ * executed (including failed ones).
65
+ *
66
+ * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
67
+ * one failing job does not abort the rest, and failures are recorded on
68
+ * {@link FakeSchedulerControls.failures} and re-thrown unless
69
+ * `{ throwOnError: false }` is passed.
70
+ */
71
+ runPending: (options?: SweepOptions) => Promise<number>;
72
+ }
73
+ /** Options controlling how an `advance()` / `runPending()` sweep surfaces failures. */
74
+ interface SweepOptions {
75
+ /**
76
+ * When `true` (the default), failures encountered during the sweep are
77
+ * re-thrown after every due job has run — a single failure verbatim, multiple
78
+ * as an `AggregateError`. When `false`, the sweep resolves normally and
79
+ * failures are observable only via {@link FakeSchedulerControls.failures}.
80
+ */
81
+ throwOnError?: boolean;
82
+ }
83
+ /**
84
+ * Top-level dispatch for a scheduled job — wired by the harness. Unlike the
85
+ * `runInternal` closure used by `ctx.run*` composition (which rides whatever
86
+ * transaction span is already open), a scheduled job is a fresh top-level entry:
87
+ * production dispatches it back to the Worker as its own RPC, so a scheduled
88
+ * `mutation` runs inside its own BEGIN/COMMIT span and notifies subscription
89
+ * listeners on success. The harness supplies a callback that reproduces those
90
+ * semantics (mutations wrapped + notified; actions run unwrapped).
91
+ */
92
+ /**
93
+ * The schema value produced by `@lunora/server`'s `defineSchema`. Accepted
94
+ * structurally; internally it is handed to `@lunora/do`'s `runShardMigrations` /
95
+ * `createShardCtxDb`, whose `SchemaLike` is the same shape declared in the DO
96
+ * package — the two are structurally compatible at runtime (only their
97
+ * independently-declared trigger nesting drifts at the type level), so the
98
+ * boundary cast below is sound.
99
+ */
100
+ type TestSchema = Schema<Record<string, TableDefinition>>;
101
+ /**
102
+ * A user-supplied identity, surfaced to handlers via `ctx.auth`. `userId` is the
103
+ * subject the handler reads from `ctx.auth.userId`; any additional fields are
104
+ * returned verbatim from `ctx.auth.getIdentity()` (mirroring a decoded JWT).
105
+ */
106
+ interface TestIdentity extends Record<string, unknown> {
107
+ userId?: null | string;
108
+ }
109
+ /**
110
+ * An async iterable/iterator returned by {@link TestHarness.subscribe}.
111
+ * Guarantees `return()` is always defined (unlike the optional `AsyncIterator.return`),
112
+ * so callers can always unsubscribe without a `?.` guard.
113
+ */
114
+ interface TestSubscription<R> extends AsyncIterable<R> {
115
+ next: () => Promise<IteratorResult<R, R>>;
116
+ return: () => Promise<IteratorResult<R, R>>;
117
+ }
118
+ /** An inline handler accepted by `query` / `mutation` / `run`, given direct context access. */
119
+ type InlineQueryFunction<R> = (context: QueryCtx) => Promise<R> | R;
120
+ type InlineMutationFunction<R> = (context: MutationCtx) => Promise<R> | R;
121
+ type InlineActionFunction<R> = (context: ActionCtx) => Promise<R> | R;
122
+ /**
123
+ * A map from function path strings (e.g. `"messages:send"`) to their
124
+ * registered function objects. Used by the fake scheduler to resolve
125
+ * `ctx.scheduler.runAfter(delay, "messages:send", args)` → handler invocation.
126
+ *
127
+ * Only mutations and actions can be scheduled in production; queries passed
128
+ * here will be accepted but produce a console.warn at dispatch time.
129
+ *
130
+ * The value type uses `any` because `RegisteredFunction` is contravariant in its
131
+ * args type parameter — a `RegisteredMutation` with concrete args is not assignable
132
+ * to `RegisteredMutation` with `ArgsValidator` at the type level even though at
133
+ * runtime it is sound (the fake scheduler passes `Record&lt;string, unknown>` to
134
+ * `handler` and ignores the return value).
135
+ */
136
+ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMutation<any, any> | RegisteredQuery<any, any>>;
137
+ /**
138
+ * Options accepted by {@link lunoraTest}.
139
+ *
140
+ * All options are optional — `lunoraTest(schema)` preserves v1 behaviour with
141
+ * clearly-throwing stubs for unsupported surfaces.
142
+ */
143
+ interface LunoraTestOptions {
144
+ /**
145
+ * Injectable `fetch` implementation for action contexts. When provided,
146
+ * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
147
+ * function rather than throwing the "not available in v1" stub.
148
+ *
149
+ * Pass `vi.fn()` or any `typeof globalThis.fetch` compatible implementation.
150
+ * @example
151
+ * ```ts
152
+ * const fakeFetch = vi.fn().mockResolvedValue(Response.json({ ok: true }));
153
+ * const t = lunoraTest(schema, { fetch: fakeFetch });
154
+ * ```
155
+ */
156
+ fetch?: typeof globalThis.fetch;
157
+ /**
158
+ * Function registry for the fake in-memory scheduler. Maps a
159
+ * `functionPath` string (the value passed as the second argument to
160
+ * `ctx.scheduler.runAfter` / `ctx.scheduler.runAt`) to the corresponding
161
+ * registered function object.
162
+ *
163
+ * Only required if your handlers schedule work. Scheduled jobs for paths
164
+ * NOT listed here produce a `console.warn` at dispatch time (matching prod
165
+ * behaviour for unknown paths).
166
+ * @example
167
+ * ```ts
168
+ * const t = lunoraTest(schema, {
169
+ * functions: { "messages:send": sendMutation },
170
+ * });
171
+ * ```
172
+ */
173
+ functions?: FunctionRegistry;
174
+ }
175
+ /**
176
+ * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
177
+ * five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
178
+ * / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
179
+ * write from one method is visible to a read from another (including across a
180
+ * `withIdentity` scope).
181
+ */
182
+ interface TestHarness {
183
+ /** Run a registered `action` (or an inline `async (context) => …`) against the harness. */
184
+ action: {
185
+ <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>): Promise<R>;
186
+ <R>(inline: InlineActionFunction<R>): Promise<R>;
187
+ };
188
+ /** Close the underlying in-memory SQLite database, releasing the native handle. Idempotent; safe to call on any `withIdentity` view. */
189
+ close: () => void;
190
+ /** Run a registered `mutation` (or an inline `async (context) => …`) against the harness. */
191
+ mutation: {
192
+ <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
193
+ <R>(inline: InlineMutationFunction<R>): Promise<R>;
194
+ };
195
+ /** Run a registered `query` (or an inline `async (context) => …`) against the harness. */
196
+ query: {
197
+ <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
198
+ <R>(inline: InlineQueryFunction<R>): Promise<R>;
199
+ };
200
+ /** Direct db access at mutation-level (read + write), mirroring `convexTest`'s `run`. */
201
+ run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
202
+ /**
203
+ * Controls for the fake in-memory scheduler. Always present; scheduler
204
+ * jobs only execute when you call `advance(ms)` or `runPending()`.
205
+ *
206
+ * - `list()` — snapshot of all pending jobs (enqueue order).
207
+ * - `advance(ms)` — tick the virtual clock forward by `ms` ms, executing every job
208
+ * whose `scheduledFor` is now at or below virtual now.
209
+ * - `runPending()` — execute all currently pending jobs regardless of their scheduled time.
210
+ *
211
+ * Scheduled jobs run through the same `runInternal` dispatch as
212
+ * `ctx.runMutation`, so they share the harness SQLite database.
213
+ */
214
+ scheduler: FakeSchedulerControls;
215
+ /**
216
+ * Subscribe to a registered query (or inline query function) and receive
217
+ * an async iterable of snapshots. The first value is emitted immediately
218
+ * (the current query result). Subsequent values are emitted after each
219
+ * `mutation` / `run` call on this harness completes.
220
+ *
221
+ * Subscriptions are table-agnostic — any mutation triggers a re-evaluation.
222
+ * This matches the harness's single-writer model and keeps the implementation
223
+ * free of DO machinery.
224
+ * @example
225
+ * ```ts
226
+ * const sub = t.subscribe(list, {});
227
+ * const first = await sub.next(); // current result
228
+ * await t.mutation(send, { author: "ada", body: "hi" });
229
+ * const second = await sub.next(); // updated result
230
+ * await sub.return(); // unsubscribe
231
+ * ```
232
+ *
233
+ * The iterable is lazy — it never buffers more than one pending result.
234
+ * If you do not consume fast enough and multiple mutations fire, the next
235
+ * `next()` call will reflect the most-recent state (intermediate snapshots
236
+ * are coalesced).
237
+ */
238
+ subscribe: {
239
+ <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
240
+ <R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
241
+ };
242
+ /** Return a harness view that shares this harness's db but reports the given identity on `ctx.auth`. */
243
+ withIdentity: (identity: TestIdentity) => TestHarness;
244
+ }
245
+ /**
246
+ * Spin up an in-memory Lunora function harness for `schema`.
247
+ *
248
+ * `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
249
+ * database, builds the same `ctx.db` writer the real Durable Object builds (via
250
+ * `@lunora/do`'s `createShardCtxDb`), and returns a harness whose `query` /
251
+ * `mutation` / `action` / `run` execute a registered function's `handler`
252
+ * directly — no Durable Object, no `wrangler`, no network.
253
+ *
254
+ * **v1 surfaces now supported:**
255
+ *
256
+ * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
+ * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
+ * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
259
+ * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
260
+ *
261
+ * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
262
+ * These are clearly documented follow-ups.
263
+ */
264
+ declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
265
+ export { type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type LunoraTestOptions, type ScheduledJobFailure, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, lunoraTest };
@@ -0,0 +1,265 @@
1
+ import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
2
+ export { type InboxOptions, type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
3
+ /** A pending job entry in the fake scheduler queue. */
4
+ interface FakeScheduledJob extends ScheduledJob {
5
+ /** The args the job was scheduled with. */
6
+ args: Record<string, unknown>;
7
+ }
8
+ /**
9
+ * A single scheduled-job failure captured during an `advance()` / `runPending()`
10
+ * sweep. Production's scheduler isolates per-job failures (one bad job does not
11
+ * abort the rest), so the fake scheduler does the same — but, being a test
12
+ * harness, it never swallows the error: every failure is recorded here so tests
13
+ * can still assert on it.
14
+ */
15
+ interface ScheduledJobFailure {
16
+ /** The args the job was dispatched with. */
17
+ args: Record<string, unknown>;
18
+ /** The error the job's handler threw (or that dispatch produced). */
19
+ error: unknown;
20
+ /** The function path the job targeted (e.g. `"messages:send"`). */
21
+ functionPath: string;
22
+ /** The id of the job that failed. */
23
+ id: string;
24
+ }
25
+ /**
26
+ * Controls exposed on the test harness for the fake in-memory scheduler.
27
+ * Access via `harness.scheduler`.
28
+ */
29
+ interface FakeSchedulerControls {
30
+ /**
31
+ * Advance the virtual clock by `ms` milliseconds, executing all jobs whose
32
+ * `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
33
+ * are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
34
+ * (scheduled by an executed job during the advance) are NOT re-evaluated in
35
+ * the same advance call — callers should advance again if needed.
36
+ *
37
+ * Per-job failures are isolated (matching production): a job that throws does
38
+ * NOT prevent the remaining due jobs from running. After every due job has
39
+ * run, the failures are surfaced — they are recorded on
40
+ * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
41
+ * still sees the error. A single failure is re-thrown verbatim; multiple
42
+ * failures are aggregated into an `AggregateError`. Pass
43
+ * `{ throwOnError: false }` to suppress the re-throw and inspect
44
+ * {@link FakeSchedulerControls.failures} (and the returned count) instead.
45
+ *
46
+ * Returns the number of jobs that were executed (including failed ones).
47
+ */
48
+ advance: (ms: number, options?: SweepOptions) => Promise<number>;
49
+ /**
50
+ * All scheduled-job failures captured so far, in execution order, across
51
+ * every `advance()` / `runPending()` call on this harness. Always available,
52
+ * even when `throwOnError: false` suppressed the re-throw. The list is a
53
+ * snapshot — mutating it does not affect the scheduler.
54
+ */
55
+ failures: () => ScheduledJobFailure[];
56
+ /**
57
+ * List all pending jobs (those not yet executed or cancelled) in the order
58
+ * they were enqueued.
59
+ */
60
+ list: () => FakeScheduledJob[];
61
+ /**
62
+ * Execute all currently pending jobs regardless of their `scheduledFor`
63
+ * time. Equivalent to advancing to `Infinity`. Returns the number of jobs
64
+ * executed (including failed ones).
65
+ *
66
+ * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
67
+ * one failing job does not abort the rest, and failures are recorded on
68
+ * {@link FakeSchedulerControls.failures} and re-thrown unless
69
+ * `{ throwOnError: false }` is passed.
70
+ */
71
+ runPending: (options?: SweepOptions) => Promise<number>;
72
+ }
73
+ /** Options controlling how an `advance()` / `runPending()` sweep surfaces failures. */
74
+ interface SweepOptions {
75
+ /**
76
+ * When `true` (the default), failures encountered during the sweep are
77
+ * re-thrown after every due job has run — a single failure verbatim, multiple
78
+ * as an `AggregateError`. When `false`, the sweep resolves normally and
79
+ * failures are observable only via {@link FakeSchedulerControls.failures}.
80
+ */
81
+ throwOnError?: boolean;
82
+ }
83
+ /**
84
+ * Top-level dispatch for a scheduled job — wired by the harness. Unlike the
85
+ * `runInternal` closure used by `ctx.run*` composition (which rides whatever
86
+ * transaction span is already open), a scheduled job is a fresh top-level entry:
87
+ * production dispatches it back to the Worker as its own RPC, so a scheduled
88
+ * `mutation` runs inside its own BEGIN/COMMIT span and notifies subscription
89
+ * listeners on success. The harness supplies a callback that reproduces those
90
+ * semantics (mutations wrapped + notified; actions run unwrapped).
91
+ */
92
+ /**
93
+ * The schema value produced by `@lunora/server`'s `defineSchema`. Accepted
94
+ * structurally; internally it is handed to `@lunora/do`'s `runShardMigrations` /
95
+ * `createShardCtxDb`, whose `SchemaLike` is the same shape declared in the DO
96
+ * package — the two are structurally compatible at runtime (only their
97
+ * independently-declared trigger nesting drifts at the type level), so the
98
+ * boundary cast below is sound.
99
+ */
100
+ type TestSchema = Schema<Record<string, TableDefinition>>;
101
+ /**
102
+ * A user-supplied identity, surfaced to handlers via `ctx.auth`. `userId` is the
103
+ * subject the handler reads from `ctx.auth.userId`; any additional fields are
104
+ * returned verbatim from `ctx.auth.getIdentity()` (mirroring a decoded JWT).
105
+ */
106
+ interface TestIdentity extends Record<string, unknown> {
107
+ userId?: null | string;
108
+ }
109
+ /**
110
+ * An async iterable/iterator returned by {@link TestHarness.subscribe}.
111
+ * Guarantees `return()` is always defined (unlike the optional `AsyncIterator.return`),
112
+ * so callers can always unsubscribe without a `?.` guard.
113
+ */
114
+ interface TestSubscription<R> extends AsyncIterable<R> {
115
+ next: () => Promise<IteratorResult<R, R>>;
116
+ return: () => Promise<IteratorResult<R, R>>;
117
+ }
118
+ /** An inline handler accepted by `query` / `mutation` / `run`, given direct context access. */
119
+ type InlineQueryFunction<R> = (context: QueryCtx) => Promise<R> | R;
120
+ type InlineMutationFunction<R> = (context: MutationCtx) => Promise<R> | R;
121
+ type InlineActionFunction<R> = (context: ActionCtx) => Promise<R> | R;
122
+ /**
123
+ * A map from function path strings (e.g. `"messages:send"`) to their
124
+ * registered function objects. Used by the fake scheduler to resolve
125
+ * `ctx.scheduler.runAfter(delay, "messages:send", args)` → handler invocation.
126
+ *
127
+ * Only mutations and actions can be scheduled in production; queries passed
128
+ * here will be accepted but produce a console.warn at dispatch time.
129
+ *
130
+ * The value type uses `any` because `RegisteredFunction` is contravariant in its
131
+ * args type parameter — a `RegisteredMutation` with concrete args is not assignable
132
+ * to `RegisteredMutation` with `ArgsValidator` at the type level even though at
133
+ * runtime it is sound (the fake scheduler passes `Record&lt;string, unknown>` to
134
+ * `handler` and ignores the return value).
135
+ */
136
+ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMutation<any, any> | RegisteredQuery<any, any>>;
137
+ /**
138
+ * Options accepted by {@link lunoraTest}.
139
+ *
140
+ * All options are optional — `lunoraTest(schema)` preserves v1 behaviour with
141
+ * clearly-throwing stubs for unsupported surfaces.
142
+ */
143
+ interface LunoraTestOptions {
144
+ /**
145
+ * Injectable `fetch` implementation for action contexts. When provided,
146
+ * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
147
+ * function rather than throwing the "not available in v1" stub.
148
+ *
149
+ * Pass `vi.fn()` or any `typeof globalThis.fetch` compatible implementation.
150
+ * @example
151
+ * ```ts
152
+ * const fakeFetch = vi.fn().mockResolvedValue(Response.json({ ok: true }));
153
+ * const t = lunoraTest(schema, { fetch: fakeFetch });
154
+ * ```
155
+ */
156
+ fetch?: typeof globalThis.fetch;
157
+ /**
158
+ * Function registry for the fake in-memory scheduler. Maps a
159
+ * `functionPath` string (the value passed as the second argument to
160
+ * `ctx.scheduler.runAfter` / `ctx.scheduler.runAt`) to the corresponding
161
+ * registered function object.
162
+ *
163
+ * Only required if your handlers schedule work. Scheduled jobs for paths
164
+ * NOT listed here produce a `console.warn` at dispatch time (matching prod
165
+ * behaviour for unknown paths).
166
+ * @example
167
+ * ```ts
168
+ * const t = lunoraTest(schema, {
169
+ * functions: { "messages:send": sendMutation },
170
+ * });
171
+ * ```
172
+ */
173
+ functions?: FunctionRegistry;
174
+ }
175
+ /**
176
+ * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
177
+ * five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
178
+ * / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
179
+ * write from one method is visible to a read from another (including across a
180
+ * `withIdentity` scope).
181
+ */
182
+ interface TestHarness {
183
+ /** Run a registered `action` (or an inline `async (context) => …`) against the harness. */
184
+ action: {
185
+ <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>): Promise<R>;
186
+ <R>(inline: InlineActionFunction<R>): Promise<R>;
187
+ };
188
+ /** Close the underlying in-memory SQLite database, releasing the native handle. Idempotent; safe to call on any `withIdentity` view. */
189
+ close: () => void;
190
+ /** Run a registered `mutation` (or an inline `async (context) => …`) against the harness. */
191
+ mutation: {
192
+ <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
193
+ <R>(inline: InlineMutationFunction<R>): Promise<R>;
194
+ };
195
+ /** Run a registered `query` (or an inline `async (context) => …`) against the harness. */
196
+ query: {
197
+ <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
198
+ <R>(inline: InlineQueryFunction<R>): Promise<R>;
199
+ };
200
+ /** Direct db access at mutation-level (read + write), mirroring `convexTest`'s `run`. */
201
+ run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
202
+ /**
203
+ * Controls for the fake in-memory scheduler. Always present; scheduler
204
+ * jobs only execute when you call `advance(ms)` or `runPending()`.
205
+ *
206
+ * - `list()` — snapshot of all pending jobs (enqueue order).
207
+ * - `advance(ms)` — tick the virtual clock forward by `ms` ms, executing every job
208
+ * whose `scheduledFor` is now at or below virtual now.
209
+ * - `runPending()` — execute all currently pending jobs regardless of their scheduled time.
210
+ *
211
+ * Scheduled jobs run through the same `runInternal` dispatch as
212
+ * `ctx.runMutation`, so they share the harness SQLite database.
213
+ */
214
+ scheduler: FakeSchedulerControls;
215
+ /**
216
+ * Subscribe to a registered query (or inline query function) and receive
217
+ * an async iterable of snapshots. The first value is emitted immediately
218
+ * (the current query result). Subsequent values are emitted after each
219
+ * `mutation` / `run` call on this harness completes.
220
+ *
221
+ * Subscriptions are table-agnostic — any mutation triggers a re-evaluation.
222
+ * This matches the harness's single-writer model and keeps the implementation
223
+ * free of DO machinery.
224
+ * @example
225
+ * ```ts
226
+ * const sub = t.subscribe(list, {});
227
+ * const first = await sub.next(); // current result
228
+ * await t.mutation(send, { author: "ada", body: "hi" });
229
+ * const second = await sub.next(); // updated result
230
+ * await sub.return(); // unsubscribe
231
+ * ```
232
+ *
233
+ * The iterable is lazy — it never buffers more than one pending result.
234
+ * If you do not consume fast enough and multiple mutations fire, the next
235
+ * `next()` call will reflect the most-recent state (intermediate snapshots
236
+ * are coalesced).
237
+ */
238
+ subscribe: {
239
+ <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
240
+ <R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
241
+ };
242
+ /** Return a harness view that shares this harness's db but reports the given identity on `ctx.auth`. */
243
+ withIdentity: (identity: TestIdentity) => TestHarness;
244
+ }
245
+ /**
246
+ * Spin up an in-memory Lunora function harness for `schema`.
247
+ *
248
+ * `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
249
+ * database, builds the same `ctx.db` writer the real Durable Object builds (via
250
+ * `@lunora/do`'s `createShardCtxDb`), and returns a harness whose `query` /
251
+ * `mutation` / `action` / `run` execute a registered function's `handler`
252
+ * directly — no Durable Object, no `wrangler`, no network.
253
+ *
254
+ * **v1 surfaces now supported:**
255
+ *
256
+ * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
+ * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
+ * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
259
+ * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
260
+ *
261
+ * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
262
+ * These are clearly documented follow-ups.
263
+ */
264
+ declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
265
+ export { type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type LunoraTestOptions, type ScheduledJobFailure, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, lunoraTest };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export { lunoraTest } from './packem_shared/lunoraTest-BUg4Ebv8.mjs';
2
+ export { extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';