@lunora/testing 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE.md +6 -0
- package/README.md +12 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +370 -182
- package/dist/index.d.ts +370 -182
- package/dist/index.mjs +3 -1
- package/dist/packem_shared/agentHarness-GTPhWdQ8.mjs +144 -0
- package/dist/packem_shared/containsScorer-DGwtvUNp.mjs +94 -0
- package/dist/packem_shared/{lunoraTest-D8kUyDzR.mjs → lunoraTest-CHix6VWB.mjs} +135 -54
- package/package.json +7 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,17 +1,127 @@
|
|
|
1
|
+
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
1
2
|
import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
2
3
|
export { type InboxOptions, type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
|
|
4
|
+
/** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
|
|
5
|
+
interface HarnessMessage extends AgentMessageRow {
|
|
6
|
+
messageKey: string;
|
|
7
|
+
threadKey: string;
|
|
8
|
+
}
|
|
9
|
+
/** A thread record as the in-memory runtime stores it. */
|
|
10
|
+
interface HarnessThread {
|
|
11
|
+
agent: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
instanceId?: string;
|
|
14
|
+
key: string;
|
|
15
|
+
messageCount: number;
|
|
16
|
+
owner?: string;
|
|
17
|
+
status: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
usage?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** One recorded function dispatch the loop made through `ctx.run`. */
|
|
22
|
+
interface HarnessDispatch {
|
|
23
|
+
args: Record<string, unknown> | undefined;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
/** Extra function-dispatch handler layered over the built-in `agents:*` runtime double. */
|
|
27
|
+
type HarnessFunction = (args?: Record<string, unknown>) => unknown;
|
|
28
|
+
interface AgentHarnessOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Worker `env` bindings visible to tool `execute` and a dynamic
|
|
31
|
+
* `instructions` thunk. Default `{ LUNORA_TEST: true }`.
|
|
32
|
+
*/
|
|
33
|
+
env?: Record<string, unknown>;
|
|
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
|
+
*/
|
|
39
|
+
exportName?: string;
|
|
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
|
+
*/
|
|
47
|
+
functions?: Record<string, HarnessFunction>;
|
|
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
|
+
*/
|
|
54
|
+
script: AgentGenerateResult[];
|
|
55
|
+
}
|
|
56
|
+
/** Per-run overrides for {@link AgentHarness.run}. */
|
|
57
|
+
interface AgentRunOverrides {
|
|
58
|
+
/** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
|
|
59
|
+
instanceId?: string;
|
|
60
|
+
/** The model script for THIS run, replacing the harness default. */
|
|
61
|
+
script?: AgentGenerateResult[];
|
|
62
|
+
}
|
|
63
|
+
interface AgentHarness {
|
|
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
|
+
*/
|
|
70
|
+
readonly dispatches: ReadonlyArray<HarnessDispatch>;
|
|
71
|
+
/** The persisted messages of a thread, ordered by `seq`. */
|
|
72
|
+
messages: (threadKey: string) => AgentMessageRow[];
|
|
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
|
+
*/
|
|
80
|
+
run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
|
|
81
|
+
/** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
|
|
82
|
+
thread: (threadKey: string) => HarnessThread | undefined;
|
|
83
|
+
}
|
|
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
|
+
*/
|
|
108
|
+
declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
|
|
109
|
+
/** A terminal (final-answer) turn — no tool calls. */
|
|
110
|
+
declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
|
|
111
|
+
/** A single-tool-call turn: the loop runs the named tool with `input`. */
|
|
112
|
+
declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
|
|
3
113
|
/** A pending job entry in the fake scheduler queue. */
|
|
4
114
|
interface FakeScheduledJob extends ScheduledJob {
|
|
5
115
|
/** The args the job was scheduled with. */
|
|
6
116
|
args: Record<string, unknown>;
|
|
7
117
|
}
|
|
8
118
|
/**
|
|
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
|
-
*/
|
|
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
|
+
*/
|
|
15
125
|
interface ScheduledJobFailure {
|
|
16
126
|
/** The args the job was dispatched with. */
|
|
17
127
|
args: Record<string, unknown>;
|
|
@@ -23,94 +133,85 @@ interface ScheduledJobFailure {
|
|
|
23
133
|
id: string;
|
|
24
134
|
}
|
|
25
135
|
/**
|
|
26
|
-
* Controls exposed on the test harness for the fake in-memory scheduler.
|
|
27
|
-
* Access via `harness.scheduler`.
|
|
28
|
-
*/
|
|
136
|
+
* Controls exposed on the test harness for the fake in-memory scheduler.
|
|
137
|
+
* Access via `harness.scheduler`.
|
|
138
|
+
*/
|
|
29
139
|
interface FakeSchedulerControls {
|
|
30
140
|
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
*/
|
|
48
158
|
advance: (ms: number, options?: SweepOptions) => Promise<number>;
|
|
49
159
|
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
*/
|
|
55
165
|
failures: () => ScheduledJobFailure[];
|
|
56
166
|
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
167
|
+
* List all pending jobs (those not yet executed or cancelled) in the order
|
|
168
|
+
* they were enqueued.
|
|
169
|
+
*/
|
|
60
170
|
list: () => FakeScheduledJob[];
|
|
61
171
|
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
*/
|
|
71
181
|
runPending: (options?: SweepOptions) => Promise<number>;
|
|
72
182
|
}
|
|
73
183
|
/** Options controlling how an `advance()` / `runPending()` sweep surfaces failures. */
|
|
74
184
|
interface SweepOptions {
|
|
75
185
|
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
*/
|
|
81
191
|
throwOnError?: boolean;
|
|
82
192
|
}
|
|
83
193
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
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
|
-
*/
|
|
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
|
+
*/
|
|
100
201
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
101
202
|
/**
|
|
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
|
-
*/
|
|
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
|
+
*/
|
|
106
207
|
interface TestIdentity extends Record<string, unknown> {
|
|
107
208
|
userId?: null | string;
|
|
108
209
|
}
|
|
109
210
|
/**
|
|
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
|
-
*/
|
|
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
|
+
*/
|
|
114
215
|
interface TestSubscription<R> extends AsyncIterable<R> {
|
|
115
216
|
next: () => Promise<IteratorResult<R, R>>;
|
|
116
217
|
return: () => Promise<IteratorResult<R, R>>;
|
|
@@ -120,75 +221,88 @@ type InlineQueryFunction<R> = (context: QueryCtx) => Promise<R> | R;
|
|
|
120
221
|
type InlineMutationFunction<R> = (context: MutationCtx) => Promise<R> | R;
|
|
121
222
|
type InlineActionFunction<R> = (context: ActionCtx) => Promise<R> | R;
|
|
122
223
|
/**
|
|
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<string, unknown>` to
|
|
134
|
-
* `handler` and ignores the return value).
|
|
135
|
-
*/
|
|
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<string, unknown>` to
|
|
235
|
+
* `handler` and ignores the return value).
|
|
236
|
+
*/
|
|
136
237
|
type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMutation<any, any> | RegisteredQuery<any, any>>;
|
|
137
238
|
/**
|
|
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
|
-
*/
|
|
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
|
+
*/
|
|
143
244
|
interface LunoraTestOptions {
|
|
144
245
|
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
*/
|
|
257
|
+
env?: Record<string, unknown>;
|
|
258
|
+
/**
|
|
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
|
+
*/
|
|
156
270
|
fetch?: typeof globalThis.fetch;
|
|
157
271
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
*/
|
|
173
287
|
functions?: FunctionRegistry;
|
|
174
288
|
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
*/
|
|
183
297
|
now?: number;
|
|
184
298
|
}
|
|
185
299
|
/**
|
|
186
|
-
* The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
|
|
187
|
-
* five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
|
|
188
|
-
* / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
|
|
189
|
-
* write from one method is visible to a read from another (including across a
|
|
190
|
-
* `withIdentity` scope).
|
|
191
|
-
*/
|
|
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
|
+
*/
|
|
192
306
|
interface TestHarness {
|
|
193
307
|
/** Run a registered `action` (or an inline `async (context) => …`) against the harness. */
|
|
194
308
|
action: {
|
|
@@ -210,41 +324,41 @@ interface TestHarness {
|
|
|
210
324
|
/** Direct db access at mutation-level (read + write), mirroring `convexTest`'s `run`. */
|
|
211
325
|
run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
|
|
212
326
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
*/
|
|
224
338
|
scheduler: FakeSchedulerControls;
|
|
225
339
|
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
*/
|
|
248
362
|
subscribe: {
|
|
249
363
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
|
|
250
364
|
<R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
|
|
@@ -253,23 +367,97 @@ interface TestHarness {
|
|
|
253
367
|
withIdentity: (identity: TestIdentity) => TestHarness;
|
|
254
368
|
}
|
|
255
369
|
/**
|
|
256
|
-
* Spin up an in-memory Lunora function harness for `schema`.
|
|
257
|
-
*
|
|
258
|
-
* `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
|
|
259
|
-
* database, builds the same `ctx.db` writer the real Durable Object builds (via
|
|
260
|
-
* `@lunora/do`'s `createShardCtxDb`), and returns a harness whose `query` /
|
|
261
|
-
* `mutation` / `action` / `run` execute a registered function's `handler`
|
|
262
|
-
* directly — no Durable Object, no `wrangler`, no network.
|
|
263
|
-
*
|
|
264
|
-
* **v1 surfaces now supported:**
|
|
265
|
-
*
|
|
266
|
-
* - `ctx.
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* - `
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
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
|
+
*/
|
|
274
390
|
declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
|
|
275
|
-
|
|
391
|
+
/** One sample handed to a {@link Scorer}. */
|
|
392
|
+
interface ScorerSample {
|
|
393
|
+
/** The reference/gold answer, when the scorer compares against one. */
|
|
394
|
+
expected?: string;
|
|
395
|
+
/** The prompt/input that produced the output (for LLM-judge context). */
|
|
396
|
+
input?: string;
|
|
397
|
+
/** Arbitrary per-sample metadata carried through. */
|
|
398
|
+
metadata?: Record<string, unknown>;
|
|
399
|
+
/** The model/agent output under test. */
|
|
400
|
+
output: string;
|
|
401
|
+
}
|
|
402
|
+
/** A scorer's verdict: a `[0, 1]` score and an optional human-readable reason. */
|
|
403
|
+
interface ScoreResult {
|
|
404
|
+
reason?: string;
|
|
405
|
+
score: number;
|
|
406
|
+
}
|
|
407
|
+
/** A named scorer — returns a `[0, 1]` score (or a {@link ScoreResult}) for a sample. */
|
|
408
|
+
interface Scorer {
|
|
409
|
+
name: string;
|
|
410
|
+
score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
|
|
411
|
+
}
|
|
412
|
+
/** One dataset case: an input and its optional gold answer/metadata. */
|
|
413
|
+
interface EvalCase {
|
|
414
|
+
expected?: string;
|
|
415
|
+
input: string;
|
|
416
|
+
metadata?: Record<string, unknown>;
|
|
417
|
+
}
|
|
418
|
+
/** The per-case eval result: the produced output plus each scorer's verdict and the mean. */
|
|
419
|
+
interface EvalItemResult {
|
|
420
|
+
average: number;
|
|
421
|
+
input: string;
|
|
422
|
+
output: string;
|
|
423
|
+
scores: Record<string, ScoreResult>;
|
|
424
|
+
}
|
|
425
|
+
/** The whole eval run: per-case results plus the mean of their averages. */
|
|
426
|
+
interface EvalResult {
|
|
427
|
+
average: number;
|
|
428
|
+
items: EvalItemResult[];
|
|
429
|
+
}
|
|
430
|
+
/** Score 1 if the output contains `needle` (case-insensitive unless `caseSensitive`). */
|
|
431
|
+
declare const containsScorer: (needle: string, options?: {
|
|
432
|
+
caseSensitive?: boolean;
|
|
433
|
+
}) => Scorer;
|
|
434
|
+
/** Score 1 when `pattern` matches the output. */
|
|
435
|
+
declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
|
|
436
|
+
/** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
|
|
437
|
+
declare const exactMatchScorer: () => Scorer;
|
|
438
|
+
/** Score the fraction of `keywords` (case-insensitive) present in the output. */
|
|
439
|
+
declare const keywordScorer: (keywords: ReadonlyArray<string>) => Scorer;
|
|
440
|
+
/**
|
|
441
|
+
* An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise<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
|
+
*/
|
|
446
|
+
declare const llmScorer: (options: {
|
|
447
|
+
criteria: string;
|
|
448
|
+
judge: (prompt: string) => Promise<string>;
|
|
449
|
+
name?: string;
|
|
450
|
+
}) => Scorer;
|
|
451
|
+
/** Run every scorer over one sample (concurrently) → each verdict keyed by name + their mean. */
|
|
452
|
+
declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>) => Promise<{
|
|
453
|
+
average: number;
|
|
454
|
+
scores: Record<string, ScoreResult>;
|
|
455
|
+
}>;
|
|
456
|
+
/**
|
|
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
|
+
*/
|
|
462
|
+
declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
|
|
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 };
|