@lunora/testing 1.0.0-alpha.45 → 1.0.0-alpha.46

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.
package/dist/index.d.ts CHANGED
@@ -27,30 +27,30 @@ interface HarnessDispatch {
27
27
  type HarnessFunction = (args?: Record<string, unknown>) => unknown;
28
28
  interface AgentHarnessOptions {
29
29
  /**
30
- * Worker `env` bindings visible to tool `execute` and a dynamic
31
- * `instructions` thunk. Default `{ LUNORA_TEST: true }`.
32
- */
30
+ * Worker `env` bindings visible to tool `execute` and a dynamic
31
+ * `instructions` thunk. Default `{ LUNORA_TEST: true }`.
32
+ */
33
33
  env?: Record<string, unknown>;
34
34
  /**
35
- * The agent's `lunora/agents.ts` export name — used for thread attribution
36
- * and to derive the child-agent `AGENT_*` bindings a sub-agent tool targets.
37
- * Default `"agent"`.
38
- */
35
+ * The agent's `lunora/agents.ts` export name — used for thread attribution
36
+ * and to derive the child-agent `AGENT_*` bindings a sub-agent tool targets.
37
+ * Default `"agent"`.
38
+ */
39
39
  exportName?: string;
40
40
  /**
41
- * Stub the app functions a tool's `execute` (or the agent's `memory.source`)
42
- * dispatches through `ctx.run`, keyed by function path (`"weather:lookup"`).
43
- * A handler layered here shadows the built-in `agents:*` runtime double for
44
- * that path. An unstubbed non-`agents:*` dispatch throws, so a missing stub
45
- * fails loudly rather than silently returning `undefined`.
46
- */
41
+ * Stub the app functions a tool's `execute` (or the agent's `memory.source`)
42
+ * dispatches through `ctx.run`, keyed by function path (`"weather:lookup"`).
43
+ * A handler layered here shadows the built-in `agents:*` runtime double for
44
+ * that path. An unstubbed non-`agents:*` dispatch throws, so a missing stub
45
+ * fails loudly rather than silently returning `undefined`.
46
+ */
47
47
  functions?: Record<string, HarnessFunction>;
48
48
  /**
49
- * Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
50
- * the default script for {@link AgentHarness.run}. A terminal turn has an
51
- * empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
52
- * against the agent's own tools. Override per run via `run(..., { script })`.
53
- */
49
+ * Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
50
+ * the default script for {@link AgentHarness.run}. A terminal turn has an
51
+ * empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
52
+ * against the agent's own tools. Override per run via `run(..., { script })`.
53
+ */
54
54
  script: AgentGenerateResult[];
55
55
  }
56
56
  /** Per-run overrides for {@link AgentHarness.run}. */
@@ -62,49 +62,49 @@ interface AgentRunOverrides {
62
62
  }
63
63
  interface AgentHarness {
64
64
  /**
65
- * Every function dispatch the loop has made through `ctx.run`, in order —
66
- * both the `agents:*` runtime calls and the tool/memory app dispatches. Use
67
- * it to assert a tool called the function you expected with the args you
68
- * expected.
69
- */
65
+ * Every function dispatch the loop has made through `ctx.run`, in order —
66
+ * both the `agents:*` runtime calls and the tool/memory app dispatches. Use
67
+ * it to assert a tool called the function you expected with the args you
68
+ * expected.
69
+ */
70
70
  readonly dispatches: ReadonlyArray<HarnessDispatch>;
71
71
  /** The persisted messages of a thread, ordered by `seq`. */
72
72
  messages: (threadKey: string) => AgentMessageRow[];
73
73
  /**
74
- * Drive one durable agent run to completion against the in-memory journal +
75
- * runtime double, returning the loop's {@link AgentRunResult}. Reuse the same
76
- * `threadKey` across calls to continue a conversation (the persisted history
77
- * carries over); each call runs under a fresh instance id, modelling a
78
- * distinct workflow instance.
79
- */
74
+ * Drive one durable agent run to completion against the in-memory journal +
75
+ * runtime double, returning the loop's {@link AgentRunResult}. Reuse the same
76
+ * `threadKey` across calls to continue a conversation (the persisted history
77
+ * carries over); each call runs under a fresh instance id, modelling a
78
+ * distinct workflow instance.
79
+ */
80
80
  run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
81
81
  /** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
82
82
  thread: (threadKey: string) => HarnessThread | undefined;
83
83
  }
84
84
  /**
85
- * A unit-test harness for a `defineAgent` tool-loop that runs WITHOUT a real
86
- * model or network. It drives {@link runAgentLoop} over the agent's own
87
- * `AgentGenerate` seam — the model is a script of per-turn decisions, tool calls
88
- * run the agent's real `execute` functions inside an in-memory durable-step
89
- * journal, and thread/message persistence goes through an in-memory `agents:*`
90
- * runtime double. Mock any function a tool (or the agent's memory) dispatches
91
- * through the `functions` option.
92
- *
93
- * ```ts
94
- * const harness = agentHarness(support, {
95
- * script: [toolCallTurn("c1", "lookup", { id: "o_1" }), finalTurn("It shipped.")],
96
- * functions: { "orders:lookup": () => ({ status: "shipped" }) },
97
- * });
98
- * const result = await harness.run({ input: "where is my order?", threadKey: "t1" });
99
- * expect(result.text).toBe("It shipped.");
100
- * expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
101
- * ```
102
- *
103
- * The harness deliberately does NOT model the concurrency guard or a
104
- * human-in-the-loop approval pause — it is for exercising an agent's tools and
105
- * turn logic, not the durable orchestration itself (that is covered inside
106
- * `@lunora/agent`).
107
- */
85
+ * A unit-test harness for a `defineAgent` tool-loop that runs WITHOUT a real
86
+ * model or network. It drives {@link runAgentLoop} over the agent's own
87
+ * `AgentGenerate` seam — the model is a script of per-turn decisions, tool calls
88
+ * run the agent's real `execute` functions inside an in-memory durable-step
89
+ * journal, and thread/message persistence goes through an in-memory `agents:*`
90
+ * runtime double. Mock any function a tool (or the agent's memory) dispatches
91
+ * through the `functions` option.
92
+ *
93
+ * ```ts
94
+ * const harness = agentHarness(support, {
95
+ * script: [toolCallTurn("c1", "lookup", { id: "o_1" }), finalTurn("It shipped.")],
96
+ * functions: { "orders:lookup": () => ({ status: "shipped" }) },
97
+ * });
98
+ * const result = await harness.run({ input: "where is my order?", threadKey: "t1" });
99
+ * expect(result.text).toBe("It shipped.");
100
+ * expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
101
+ * ```
102
+ *
103
+ * The harness deliberately does NOT model the concurrency guard or a
104
+ * human-in-the-loop approval pause — it is for exercising an agent's tools and
105
+ * turn logic, not the durable orchestration itself (that is covered inside
106
+ * `@lunora/agent`).
107
+ */
108
108
  declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
109
109
  /** A terminal (final-answer) turn — no tool calls. */
110
110
  declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
@@ -116,12 +116,12 @@ interface FakeScheduledJob extends ScheduledJob {
116
116
  args: Record<string, unknown>;
117
117
  }
118
118
  /**
119
- * A single scheduled-job failure captured during an `advance()` / `runPending()`
120
- * sweep. Production's scheduler isolates per-job failures (one bad job does not
121
- * abort the rest), so the fake scheduler does the same — but, being a test
122
- * harness, it never swallows the error: every failure is recorded here so tests
123
- * can still assert on it.
124
- */
119
+ * A single scheduled-job failure captured during an `advance()` / `runPending()`
120
+ * sweep. Production's scheduler isolates per-job failures (one bad job does not
121
+ * abort the rest), so the fake scheduler does the same — but, being a test
122
+ * harness, it never swallows the error: every failure is recorded here so tests
123
+ * can still assert on it.
124
+ */
125
125
  interface ScheduledJobFailure {
126
126
  /** The args the job was dispatched with. */
127
127
  args: Record<string, unknown>;
@@ -133,94 +133,85 @@ interface ScheduledJobFailure {
133
133
  id: string;
134
134
  }
135
135
  /**
136
- * Controls exposed on the test harness for the fake in-memory scheduler.
137
- * Access via `harness.scheduler`.
138
- */
136
+ * Controls exposed on the test harness for the fake in-memory scheduler.
137
+ * Access via `harness.scheduler`.
138
+ */
139
139
  interface FakeSchedulerControls {
140
140
  /**
141
- * Advance the virtual clock by `ms` milliseconds, executing all jobs whose
142
- * `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
143
- * are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
144
- * (scheduled by an executed job during the advance) are NOT re-evaluated in
145
- * the same advance call — callers should advance again if needed.
146
- *
147
- * Per-job failures are isolated (matching production): a job that throws does
148
- * NOT prevent the remaining due jobs from running. After every due job has
149
- * run, the failures are surfaced — they are recorded on
150
- * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
151
- * still sees the error. A single failure is re-thrown verbatim; multiple
152
- * failures are aggregated into an `AggregateError`. Pass
153
- * `{ throwOnError: false }` to suppress the re-throw and inspect
154
- * {@link FakeSchedulerControls.failures} (and the returned count) instead.
155
- *
156
- * Returns the number of jobs that were executed (including failed ones).
157
- */
141
+ * Advance the virtual clock by `ms` milliseconds, executing all jobs whose
142
+ * `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
143
+ * are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
144
+ * (scheduled by an executed job during the advance) are NOT re-evaluated in
145
+ * the same advance call — callers should advance again if needed.
146
+ *
147
+ * Per-job failures are isolated (matching production): a job that throws does
148
+ * NOT prevent the remaining due jobs from running. After every due job has
149
+ * run, the failures are surfaced — they are recorded on
150
+ * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
151
+ * still sees the error. A single failure is re-thrown verbatim; multiple
152
+ * failures are aggregated into an `AggregateError`. Pass
153
+ * `{ throwOnError: false }` to suppress the re-throw and inspect
154
+ * {@link FakeSchedulerControls.failures} (and the returned count) instead.
155
+ *
156
+ * Returns the number of jobs that were executed (including failed ones).
157
+ */
158
158
  advance: (ms: number, options?: SweepOptions) => Promise<number>;
159
159
  /**
160
- * All scheduled-job failures captured so far, in execution order, across
161
- * every `advance()` / `runPending()` call on this harness. Always available,
162
- * even when `throwOnError: false` suppressed the re-throw. The list is a
163
- * snapshot — mutating it does not affect the scheduler.
164
- */
160
+ * All scheduled-job failures captured so far, in execution order, across
161
+ * every `advance()` / `runPending()` call on this harness. Always available,
162
+ * even when `throwOnError: false` suppressed the re-throw. The list is a
163
+ * snapshot — mutating it does not affect the scheduler.
164
+ */
165
165
  failures: () => ScheduledJobFailure[];
166
166
  /**
167
- * List all pending jobs (those not yet executed or cancelled) in the order
168
- * they were enqueued.
169
- */
167
+ * List all pending jobs (those not yet executed or cancelled) in the order
168
+ * they were enqueued.
169
+ */
170
170
  list: () => FakeScheduledJob[];
171
171
  /**
172
- * Execute all currently pending jobs regardless of their `scheduledFor`
173
- * time. Equivalent to advancing to `Infinity`. Returns the number of jobs
174
- * executed (including failed ones).
175
- *
176
- * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
177
- * one failing job does not abort the rest, and failures are recorded on
178
- * {@link FakeSchedulerControls.failures} and re-thrown unless
179
- * `{ throwOnError: false }` is passed.
180
- */
172
+ * Execute all currently pending jobs regardless of their `scheduledFor`
173
+ * time. Equivalent to advancing to `Infinity`. Returns the number of jobs
174
+ * executed (including failed ones).
175
+ *
176
+ * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
177
+ * one failing job does not abort the rest, and failures are recorded on
178
+ * {@link FakeSchedulerControls.failures} and re-thrown unless
179
+ * `{ throwOnError: false }` is passed.
180
+ */
181
181
  runPending: (options?: SweepOptions) => Promise<number>;
182
182
  }
183
183
  /** Options controlling how an `advance()` / `runPending()` sweep surfaces failures. */
184
184
  interface SweepOptions {
185
185
  /**
186
- * When `true` (the default), failures encountered during the sweep are
187
- * re-thrown after every due job has run — a single failure verbatim, multiple
188
- * as an `AggregateError`. When `false`, the sweep resolves normally and
189
- * failures are observable only via {@link FakeSchedulerControls.failures}.
190
- */
186
+ * When `true` (the default), failures encountered during the sweep are
187
+ * re-thrown after every due job has run — a single failure verbatim, multiple
188
+ * as an `AggregateError`. When `false`, the sweep resolves normally and
189
+ * failures are observable only via {@link FakeSchedulerControls.failures}.
190
+ */
191
191
  throwOnError?: boolean;
192
192
  }
193
193
  /**
194
- * Top-level dispatch for a scheduled job — wired by the harness. Unlike the
195
- * `runInternal` closure used by `ctx.run*` composition (which rides whatever
196
- * transaction span is already open), a scheduled job is a fresh top-level entry:
197
- * production dispatches it back to the Worker as its own RPC, so a scheduled
198
- * `mutation` runs inside its own BEGIN/COMMIT span and notifies subscription
199
- * listeners on success. The harness supplies a callback that reproduces those
200
- * semantics (mutations wrapped + notified; actions run unwrapped).
201
- */
202
- /**
203
- * The schema value produced by `@lunora/server`'s `defineSchema`. Accepted
204
- * structurally; internally it is handed to `@lunora/do`'s `runShardMigrations` /
205
- * `createShardCtxDb`, whose `SchemaLike` is the same shape declared in the DO
206
- * package — the two are structurally compatible at runtime (only their
207
- * independently-declared trigger nesting drifts at the type level), so the
208
- * boundary cast below is sound.
209
- */
194
+ * The schema value produced by `@lunora/server`'s `defineSchema`. Accepted
195
+ * structurally; internally it is handed to `@lunora/do`'s `runShardMigrations` /
196
+ * `createShardCtxDb`, whose `SchemaLike` is the same shape declared in the DO
197
+ * package the two are structurally compatible at runtime (only their
198
+ * independently-declared trigger nesting drifts at the type level), so the
199
+ * boundary cast below is sound.
200
+ */
210
201
  type TestSchema = Schema<Record<string, TableDefinition>>;
211
202
  /**
212
- * A user-supplied identity, surfaced to handlers via `ctx.auth`. `userId` is the
213
- * subject the handler reads from `ctx.auth.userId`; any additional fields are
214
- * returned verbatim from `ctx.auth.getIdentity()` (mirroring a decoded JWT).
215
- */
203
+ * A user-supplied identity, surfaced to handlers via `ctx.auth`. `userId` is the
204
+ * subject the handler reads from `ctx.auth.userId`; any additional fields are
205
+ * returned verbatim from `ctx.auth.getIdentity()` (mirroring a decoded JWT).
206
+ */
216
207
  interface TestIdentity extends Record<string, unknown> {
217
208
  userId?: null | string;
218
209
  }
219
210
  /**
220
- * An async iterable/iterator returned by {@link TestHarness.subscribe}.
221
- * Guarantees `return()` is always defined (unlike the optional `AsyncIterator.return`),
222
- * so callers can always unsubscribe without a `?.` guard.
223
- */
211
+ * An async iterable/iterator returned by {@link TestHarness.subscribe}.
212
+ * Guarantees `return()` is always defined (unlike the optional `AsyncIterator.return`),
213
+ * so callers can always unsubscribe without a `?.` guard.
214
+ */
224
215
  interface TestSubscription<R> extends AsyncIterable<R> {
225
216
  next: () => Promise<IteratorResult<R, R>>;
226
217
  return: () => Promise<IteratorResult<R, R>>;
@@ -230,88 +221,88 @@ type InlineQueryFunction<R> = (context: QueryCtx) => Promise<R> | R;
230
221
  type InlineMutationFunction<R> = (context: MutationCtx) => Promise<R> | R;
231
222
  type InlineActionFunction<R> = (context: ActionCtx) => Promise<R> | R;
232
223
  /**
233
- * A map from function path strings (e.g. `"messages:send"`) to their
234
- * registered function objects. Used by the fake scheduler to resolve
235
- * `ctx.scheduler.runAfter(delay, "messages:send", args)` → handler invocation.
236
- *
237
- * Only mutations and actions can be scheduled in production; queries passed
238
- * here will be accepted but produce a console.warn at dispatch time.
239
- *
240
- * The value type uses `any` because `RegisteredFunction` is contravariant in its
241
- * args type parameter — a `RegisteredMutation` with concrete args is not assignable
242
- * to `RegisteredMutation` with `ArgsValidator` at the type level even though at
243
- * runtime it is sound (the fake scheduler passes `Record&lt;string, unknown>` to
244
- * `handler` and ignores the return value).
245
- */
224
+ * A map from function path strings (e.g. `"messages:send"`) to their
225
+ * registered function objects. Used by the fake scheduler to resolve
226
+ * `ctx.scheduler.runAfter(delay, "messages:send", args)` → handler invocation.
227
+ *
228
+ * Only mutations and actions can be scheduled in production; queries passed
229
+ * here will be accepted but produce a console.warn at dispatch time.
230
+ *
231
+ * The value type uses `any` because `RegisteredFunction` is contravariant in its
232
+ * args type parameter — a `RegisteredMutation` with concrete args is not assignable
233
+ * to `RegisteredMutation` with `ArgsValidator` at the type level even though at
234
+ * runtime it is sound (the fake scheduler passes `Record&lt;string, unknown>` to
235
+ * `handler` and ignores the return value).
236
+ */
246
237
  type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMutation<any, any> | RegisteredQuery<any, any>>;
247
238
  /**
248
- * Options accepted by {@link lunoraTest}.
249
- *
250
- * All options are optional — `lunoraTest(schema)` preserves v1 behaviour with
251
- * clearly-throwing stubs for unsupported surfaces.
252
- */
239
+ * Options accepted by {@link lunoraTest}.
240
+ *
241
+ * All options are optional — `lunoraTest(schema)` preserves v1 behaviour with
242
+ * clearly-throwing stubs for unsupported surfaces.
243
+ */
253
244
  interface LunoraTestOptions {
254
245
  /**
255
- * Injectable `ctx.env` for every context (query / mutation / action). When
256
- * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
257
- * surface) see this object. Left unset it stays `undefined` — matching the
258
- * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
259
- * `undefined` rather than throwing. Not a throwing stub for exactly that
260
- * reason: `env` is designed to be legitimately absent.
261
- * @example
262
- * ```ts
263
- * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
264
- * ```
265
- */
246
+ * Injectable `ctx.env` for every context (query / mutation / action). When
247
+ * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
248
+ * surface) see this object. Left unset it stays `undefined` — matching the
249
+ * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
250
+ * `undefined` rather than throwing. Not a throwing stub for exactly that
251
+ * reason: `env` is designed to be legitimately absent.
252
+ * @example
253
+ * ```ts
254
+ * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
255
+ * ```
256
+ */
266
257
  env?: Record<string, unknown>;
267
258
  /**
268
- * Injectable `fetch` implementation for action contexts. When provided,
269
- * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
270
- * function rather than throwing the "not available in v1" stub.
271
- *
272
- * Pass `vi.fn()` or any `typeof globalThis.fetch` compatible implementation.
273
- * @example
274
- * ```ts
275
- * const fakeFetch = vi.fn().mockResolvedValue(Response.json({ ok: true }));
276
- * const t = lunoraTest(schema, { fetch: fakeFetch });
277
- * ```
278
- */
259
+ * Injectable `fetch` implementation for action contexts. When provided,
260
+ * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
261
+ * function rather than throwing the "not available in v1" stub.
262
+ *
263
+ * Pass `vi.fn()` or any `typeof globalThis.fetch` compatible implementation.
264
+ * @example
265
+ * ```ts
266
+ * const fakeFetch = vi.fn().mockResolvedValue(Response.json({ ok: true }));
267
+ * const t = lunoraTest(schema, { fetch: fakeFetch });
268
+ * ```
269
+ */
279
270
  fetch?: typeof globalThis.fetch;
280
271
  /**
281
- * Function registry for the fake in-memory scheduler. Maps a
282
- * `functionPath` string (the value passed as the second argument to
283
- * `ctx.scheduler.runAfter` / `ctx.scheduler.runAt`) to the corresponding
284
- * registered function object.
285
- *
286
- * Only required if your handlers schedule work. Scheduled jobs for paths
287
- * NOT listed here produce a `console.warn` at dispatch time (matching prod
288
- * behaviour for unknown paths).
289
- * @example
290
- * ```ts
291
- * const t = lunoraTest(schema, {
292
- * functions: { "messages:send": sendMutation },
293
- * });
294
- * ```
295
- */
272
+ * Function registry for the fake in-memory scheduler. Maps a
273
+ * `functionPath` string (the value passed as the second argument to
274
+ * `ctx.scheduler.runAfter` / `ctx.scheduler.runAt`) to the corresponding
275
+ * registered function object.
276
+ *
277
+ * Only required if your handlers schedule work. Scheduled jobs for paths
278
+ * NOT listed here produce a `console.warn` at dispatch time (matching prod
279
+ * behaviour for unknown paths).
280
+ * @example
281
+ * ```ts
282
+ * const t = lunoraTest(schema, {
283
+ * functions: { "messages:send": sendMutation },
284
+ * });
285
+ * ```
286
+ */
296
287
  functions?: FunctionRegistry;
297
288
  /**
298
- * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
299
- * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
300
- * handlers deterministic. Defaults to the wall clock at harness creation.
301
- * @example
302
- * ```ts
303
- * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
304
- * ```
305
- */
289
+ * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
290
+ * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
291
+ * handlers deterministic. Defaults to the wall clock at harness creation.
292
+ * @example
293
+ * ```ts
294
+ * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
295
+ * ```
296
+ */
306
297
  now?: number;
307
298
  }
308
299
  /**
309
- * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
310
- * five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
311
- * / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
312
- * write from one method is visible to a read from another (including across a
313
- * `withIdentity` scope).
314
- */
300
+ * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
301
+ * five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
302
+ * / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
303
+ * write from one method is visible to a read from another (including across a
304
+ * `withIdentity` scope).
305
+ */
315
306
  interface TestHarness {
316
307
  /** Run a registered `action` (or an inline `async (context) => …`) against the harness. */
317
308
  action: {
@@ -333,41 +324,41 @@ interface TestHarness {
333
324
  /** Direct db access at mutation-level (read + write), mirroring `convexTest`'s `run`. */
334
325
  run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
335
326
  /**
336
- * Controls for the fake in-memory scheduler. Always present; scheduler
337
- * jobs only execute when you call `advance(ms)` or `runPending()`.
338
- *
339
- * - `list()` — snapshot of all pending jobs (enqueue order).
340
- * - `advance(ms)` — tick the virtual clock forward by `ms` ms, executing every job
341
- * whose `scheduledFor` is now at or below virtual now.
342
- * - `runPending()` — execute all currently pending jobs regardless of their scheduled time.
343
- *
344
- * Scheduled jobs run through the same `runInternal` dispatch as
345
- * `ctx.runMutation`, so they share the harness SQLite database.
346
- */
327
+ * Controls for the fake in-memory scheduler. Always present; scheduler
328
+ * jobs only execute when you call `advance(ms)` or `runPending()`.
329
+ *
330
+ * - `list()` — snapshot of all pending jobs (enqueue order).
331
+ * - `advance(ms)` — tick the virtual clock forward by `ms` ms, executing every job
332
+ * whose `scheduledFor` is now at or below virtual now.
333
+ * - `runPending()` — execute all currently pending jobs regardless of their scheduled time.
334
+ *
335
+ * Scheduled jobs run through the same `runInternal` dispatch as
336
+ * `ctx.runMutation`, so they share the harness SQLite database.
337
+ */
347
338
  scheduler: FakeSchedulerControls;
348
339
  /**
349
- * Subscribe to a registered query (or inline query function) and receive
350
- * an async iterable of snapshots. The first value is emitted immediately
351
- * (the current query result). Subsequent values are emitted after each
352
- * `mutation` / `run` call on this harness completes.
353
- *
354
- * Subscriptions are table-agnostic — any mutation triggers a re-evaluation.
355
- * This matches the harness's single-writer model and keeps the implementation
356
- * free of DO machinery.
357
- * @example
358
- * ```ts
359
- * const sub = t.subscribe(list, {});
360
- * const first = await sub.next(); // current result
361
- * await t.mutation(send, { author: "ada", body: "hi" });
362
- * const second = await sub.next(); // updated result
363
- * await sub.return(); // unsubscribe
364
- * ```
365
- *
366
- * The iterable is lazy — it never buffers more than one pending result.
367
- * If you do not consume fast enough and multiple mutations fire, the next
368
- * `next()` call will reflect the most-recent state (intermediate snapshots
369
- * are coalesced).
370
- */
340
+ * Subscribe to a registered query (or inline query function) and receive
341
+ * an async iterable of snapshots. The first value is emitted immediately
342
+ * (the current query result). Subsequent values are emitted after each
343
+ * `mutation` / `run` call on this harness completes.
344
+ *
345
+ * Subscriptions are table-agnostic — any mutation triggers a re-evaluation.
346
+ * This matches the harness's single-writer model and keeps the implementation
347
+ * free of DO machinery.
348
+ * @example
349
+ * ```ts
350
+ * const sub = t.subscribe(list, {});
351
+ * const first = await sub.next(); // current result
352
+ * await t.mutation(send, { author: "ada", body: "hi" });
353
+ * const second = await sub.next(); // updated result
354
+ * await sub.return(); // unsubscribe
355
+ * ```
356
+ *
357
+ * The iterable is lazy — it never buffers more than one pending result.
358
+ * If you do not consume fast enough and multiple mutations fire, the next
359
+ * `next()` call will reflect the most-recent state (intermediate snapshots
360
+ * are coalesced).
361
+ */
371
362
  subscribe: {
372
363
  <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
373
364
  <R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
@@ -376,26 +367,26 @@ interface TestHarness {
376
367
  withIdentity: (identity: TestIdentity) => TestHarness;
377
368
  }
378
369
  /**
379
- * Spin up an in-memory Lunora function harness for `schema`.
380
- *
381
- * `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
382
- * database, builds the same `ctx.db` writer the real Durable Object builds (via
383
- * `@lunora/do`'s `createShardCtxDb`), and returns a harness whose `query` /
384
- * `mutation` / `action` / `run` execute a registered function's `handler`
385
- * directly — no Durable Object, no `wrangler`, no network.
386
- *
387
- * **v1 surfaces now supported:**
388
- *
389
- * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
390
- * stays `undefined`, matching the optional `ctx.env?` field.
391
- * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
392
- * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
393
- * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
394
- * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
395
- *
396
- * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
397
- * These are clearly documented follow-ups.
398
- */
370
+ * Spin up an in-memory Lunora function harness for `schema`.
371
+ *
372
+ * `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
373
+ * database, builds the same `ctx.db` writer the real Durable Object builds (via
374
+ * `@lunora/do`'s `createShardCtxDb`), and returns a harness whose `query` /
375
+ * `mutation` / `action` / `run` execute a registered function's `handler`
376
+ * directly — no Durable Object, no `wrangler`, no network.
377
+ *
378
+ * **v1 surfaces now supported:**
379
+ *
380
+ * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
381
+ * stays `undefined`, matching the optional `ctx.env?` field.
382
+ * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
383
+ * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
384
+ * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
385
+ * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
386
+ *
387
+ * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
388
+ * These are clearly documented follow-ups.
389
+ */
399
390
  declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
400
391
  /** One sample handed to a {@link Scorer}. */
401
392
  interface ScorerSample {
@@ -447,11 +438,11 @@ declare const exactMatchScorer: () => Scorer;
447
438
  /** Score the fraction of `keywords` (case-insensitive) present in the output. */
448
439
  declare const keywordScorer: (keywords: ReadonlyArray<string>) => Scorer;
449
440
  /**
450
- * An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise&lt;string>`
451
- * you wire to your model (e.g. via `ctx.ai` / `generateText`), so this module
452
- * stays model-agnostic and the judge is mockable in tests. It returns the model's
453
- * numeric verdict (`[0, 1]`) plus its reason.
454
- */
441
+ * An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise&lt;string>`
442
+ * you wire to your model (e.g. via `ctx.ai` / `generateText`), so this module
443
+ * stays model-agnostic and the judge is mockable in tests. It returns the model's
444
+ * numeric verdict (`[0, 1]`) plus its reason.
445
+ */
455
446
  declare const llmScorer: (options: {
456
447
  criteria: string;
457
448
  judge: (prompt: string) => Promise<string>;
@@ -463,10 +454,10 @@ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>
463
454
  scores: Record<string, ScoreResult>;
464
455
  }>;
465
456
  /**
466
- * Run a dataset through `produce` (e.g. an `agentHarness.run` wrapper returning
467
- * the output text) and score each output with `scorers`. Cases run concurrently;
468
- * give each its own thread/key inside `produce` if the producer is stateful.
469
- * Returns per-case results plus the mean of their averages.
470
- */
457
+ * Run a dataset through `produce` (e.g. an `agentHarness.run` wrapper returning
458
+ * the output text) and score each output with `scorers`. Cases run concurrently;
459
+ * give each its own thread/key inside `produce` if the producer is stateful.
460
+ * Returns per-case results plus the mean of their averages.
461
+ */
471
462
  declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
472
463
  export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, regexScorer, scoreSample, toolCallTurn };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  export { agentHarness, finalTurn, toolCallTurn } from './packem_shared/agentHarness-GTPhWdQ8.mjs';
2
- export { lunoraTest } from './packem_shared/lunoraTest-BuoclXGw.mjs';
2
+ export { lunoraTest } from './packem_shared/lunoraTest-zjGOq924.mjs';
3
3
  export { containsScorer, evaluate, exactMatchScorer, keywordScorer, llmScorer, regexScorer, scoreSample } from './packem_shared/containsScorer-DGwtvUNp.mjs';
4
4
  export { extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
@@ -154,9 +154,12 @@ const stubProxy = (surface) => /* @__PURE__ */ new Proxy((..._args) => unavailab
154
154
  const noopLog = {
155
155
  debug: () => void 0,
156
156
  error: () => void 0,
157
+ fatal: () => void 0,
157
158
  info: () => void 0,
158
159
  log: () => void 0,
159
- warn: () => void 0
160
+ trace: () => void 0,
161
+ warn: () => void 0,
162
+ with: () => noopLog
160
163
  };
161
164
  const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
162
165
  const factory = (referenceOrInline, args) => {