@lunora/testing 1.0.0-alpha.29 → 1.0.0-alpha.290
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 +27 -3
- package/dist/index.d.mts +963 -197
- package/dist/index.d.ts +963 -197
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/agentHarness-BM24QFnh.mjs +1 -0
- package/dist/packem_shared/containsScorer-DoLfFOs6.mjs +2 -0
- package/dist/packem_shared/createArtifactsFake-L0tjki11.mjs +1 -0
- package/dist/packem_shared/createFakeSecretValues-BTNOscqQ.mjs +1 -0
- package/dist/packem_shared/createStreamGate-Geg-yvx6.mjs +1 -0
- package/dist/packem_shared/evaluationAttributes-CYLgTP1P.mjs +1 -0
- package/dist/packem_shared/groundednessScorer-kgXJCZDg.mjs +3 -0
- package/dist/packem_shared/lunoraTest-VecnFIun.mjs +1 -0
- package/dist/playwright.d.mts +126 -0
- package/dist/playwright.d.ts +126 -0
- package/dist/playwright.mjs +1 -0
- package/package.json +25 -5
- package/dist/packem_shared/lunoraTest-DO9gV_fa.mjs +0 -433
package/dist/index.d.mts
CHANGED
|
@@ -1,17 +1,361 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { AgentMessageRow, AgentRunInput, AgentTokenDelta, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
|
+
import { SecretValueHost } from '@lunora/platform';
|
|
3
|
+
import { LanguageModel } from 'ai';
|
|
4
|
+
import { ArtifactsBindingLike, ArtifactsErrorCode, ArtifactsCommitMetadata, ArtifactsTreeEntry } from '@lunora/bindings/artifacts';
|
|
5
|
+
import { ScheduledJob, LogFields, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
6
|
+
import { NotifyDefinition } from '@lunora/notify';
|
|
7
|
+
import { ChannelType, ChannelPayloadMap } from '@visulima/notification';
|
|
8
|
+
import { QueueContentType } from '@lunora/queue';
|
|
9
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
10
|
+
export {
|
|
11
|
+
/**
|
|
12
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
13
|
+
*
|
|
14
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
15
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
16
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
17
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
18
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
19
|
+
* password" deterministically.
|
|
20
|
+
*
|
|
21
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
22
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
23
|
+
* sub-package's `/testing` entry.
|
|
24
|
+
*/
|
|
25
|
+
type InboxOptions,
|
|
26
|
+
/**
|
|
27
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
28
|
+
*
|
|
29
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
30
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
31
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
32
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
33
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
34
|
+
* password" deterministically.
|
|
35
|
+
*
|
|
36
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
37
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
38
|
+
* sub-package's `/testing` entry.
|
|
39
|
+
*/
|
|
40
|
+
type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
|
|
41
|
+
/** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
|
|
42
|
+
interface HarnessMessage extends AgentMessageRow {
|
|
43
|
+
messageKey: string;
|
|
44
|
+
threadKey: string;
|
|
45
|
+
}
|
|
46
|
+
/** A thread record as the in-memory runtime stores it. */
|
|
47
|
+
interface HarnessThread {
|
|
48
|
+
agent: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
instanceId?: string;
|
|
51
|
+
key: string;
|
|
52
|
+
messageCount: number;
|
|
53
|
+
owner?: string;
|
|
54
|
+
status: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
usage?: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** One recorded function dispatch the loop made through `ctx.run`. */
|
|
59
|
+
interface HarnessDispatch {
|
|
60
|
+
args: Record<string, unknown> | undefined;
|
|
61
|
+
path: string;
|
|
62
|
+
}
|
|
63
|
+
/** Extra function-dispatch handler layered over the built-in `agents:*` runtime double. */
|
|
64
|
+
type HarnessFunction = (args?: Record<string, unknown>) => unknown;
|
|
65
|
+
interface AgentHarnessOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Worker `env` bindings visible to tool `execute` and a dynamic
|
|
68
|
+
* `instructions` thunk. Default `{ LUNORA_TEST: true }`.
|
|
69
|
+
*/
|
|
70
|
+
env?: Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* The agent's `lunora/agents.ts` export name — used for thread attribution
|
|
73
|
+
* and to derive the child-agent export keys a sub-agent tool targets.
|
|
74
|
+
* Default `"agent"`.
|
|
75
|
+
*/
|
|
76
|
+
exportName?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Stub the app functions a tool's `execute` (or the agent's `memory.source`)
|
|
79
|
+
* dispatches through `ctx.run`, keyed by function path (`"weather:lookup"`).
|
|
80
|
+
* A handler layered here shadows the built-in `agents:*` runtime double for
|
|
81
|
+
* that path. An unstubbed non-`agents:*` dispatch throws, so a missing stub
|
|
82
|
+
* fails loudly rather than silently returning `undefined`.
|
|
83
|
+
*/
|
|
84
|
+
functions?: Record<string, HarnessFunction>;
|
|
85
|
+
/**
|
|
86
|
+
* Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
|
|
87
|
+
* the default script for {@link AgentHarness.run}. A terminal turn has an
|
|
88
|
+
* empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
|
|
89
|
+
* against the agent's own tools. Override per run via `run(..., { script })`,
|
|
90
|
+
* or replace it per run with a streamed `model` via `run(..., { model })`.
|
|
91
|
+
*/
|
|
92
|
+
script?: AgentGenerateResult[];
|
|
93
|
+
/**
|
|
94
|
+
* The secret store the agent's `requestSecretTool` values live in, shared by
|
|
95
|
+
* every run of this harness. Default: an in-memory store whose clock only moves
|
|
96
|
+
* through its `advanceClock`.
|
|
97
|
+
*/
|
|
98
|
+
secrets?: SecretValueHost;
|
|
99
|
+
}
|
|
100
|
+
/** Per-run overrides for {@link AgentHarness.run}. */
|
|
101
|
+
interface AgentRunOverrides {
|
|
102
|
+
/** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
|
|
103
|
+
instanceId?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The model for THIS run, replacing the scripted decisions. Each turn runs
|
|
106
|
+
* through the production seam over this model: `createStreamGenerate` (the
|
|
107
|
+
* `streamText` path, so a recorded stream from `recordedModel` is consumed
|
|
108
|
+
* part by part) with `onTokenDelta` as its live sink.
|
|
109
|
+
*/
|
|
110
|
+
model?: LanguageModel;
|
|
111
|
+
/**
|
|
112
|
+
* Live token-delta sink for a `model` run — receives each text delta as the
|
|
113
|
+
* recorded stream produces it. Ignored for scripted runs.
|
|
114
|
+
*/
|
|
115
|
+
onTokenDelta?: (delta: AgentTokenDelta) => void;
|
|
116
|
+
/** The model script for THIS run, replacing the harness default. */
|
|
117
|
+
script?: AgentGenerateResult[];
|
|
118
|
+
}
|
|
119
|
+
interface AgentHarness {
|
|
120
|
+
/**
|
|
121
|
+
* Resolve once the run `instanceId` has reached the wait named `name` (e.g.
|
|
122
|
+
* `secret:0:call_1`, for the first turn's call `call_1`), so a test can act on the run at a known point instead of
|
|
123
|
+
* after a delay.
|
|
124
|
+
*/
|
|
125
|
+
awaitWait: (instanceId: string, name: string) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Decline the `requestSecretTool` call `toolCallId` on `threadKey` and wake its
|
|
128
|
+
* wait, as the owner's decline does.
|
|
129
|
+
*/
|
|
130
|
+
decline: (threadKey: string, toolCallId: string) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Deliver the event the run `instanceId` is waiting on. Delivered before that
|
|
133
|
+
* wait is reached, it is held for it.
|
|
134
|
+
*/
|
|
135
|
+
deliver: (instanceId: string, name: string, payload: unknown) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Every function dispatch the loop has made through `ctx.run`, in order —
|
|
138
|
+
* both the `agents:*` runtime calls and the tool/memory app dispatches. Use
|
|
139
|
+
* it to assert a tool called the function you expected with the args you
|
|
140
|
+
* expected.
|
|
141
|
+
*/
|
|
142
|
+
readonly dispatches: ReadonlyArray<HarnessDispatch>;
|
|
143
|
+
/** Fail the run `instanceId`'s pending wait the way an elapsed host timeout does. */
|
|
144
|
+
expire: (instanceId: string, name: string) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Every journal entry the runs have written, in the order they were written:
|
|
147
|
+
* each completed step's output (`{ kind: "step", name, output }`) and each
|
|
148
|
+
* delivered wait payload (`{ kind: "wait", name, type, payload }`). A test
|
|
149
|
+
* asserts a secret is absent from this, as it must be from the durable journal.
|
|
150
|
+
*/
|
|
151
|
+
journalEntries: () => unknown[];
|
|
152
|
+
/** The persisted messages of a thread, ordered by `seq`. */
|
|
153
|
+
messages: (threadKey: string) => AgentMessageRow[];
|
|
154
|
+
/**
|
|
155
|
+
* Drive one durable agent run to completion against the in-memory journal +
|
|
156
|
+
* runtime double, returning the loop's {@link AgentRunResult}. Reuse the same
|
|
157
|
+
* `threadKey` across calls to continue a conversation (the persisted history
|
|
158
|
+
* carries over); each call runs under a fresh instance id, modelling a
|
|
159
|
+
* distinct workflow instance.
|
|
160
|
+
*/
|
|
161
|
+
run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
|
|
162
|
+
/** The secret store the runs use (see {@link AgentHarnessOptions.secrets}). */
|
|
163
|
+
readonly secrets: SecretValueHost;
|
|
164
|
+
/**
|
|
165
|
+
* Supply `value` for the `requestSecretTool` call `toolCallId` on `threadKey` and
|
|
166
|
+
* wake its wait, as the owner's supply does. The value goes to the secret store
|
|
167
|
+
* and only the request's handle reaches the run.
|
|
168
|
+
*/
|
|
169
|
+
supply: (threadKey: string, toolCallId: string, value: string) => Promise<void>;
|
|
170
|
+
/** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
|
|
171
|
+
thread: (threadKey: string) => HarnessThread | undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A unit-test harness for a `defineAgent` tool-loop that runs WITHOUT a real
|
|
175
|
+
* model or network. It drives {@link runAgentLoop} over the agent's own
|
|
176
|
+
* `AgentGenerate` seam — the model is a script of per-turn decisions, tool calls
|
|
177
|
+
* run the agent's real `execute` functions inside an in-memory durable-step
|
|
178
|
+
* journal, and thread/message persistence goes through an in-memory `agents:*`
|
|
179
|
+
* runtime double. Mock any function a tool (or the agent's memory) dispatches
|
|
180
|
+
* through the `functions` option.
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const harness = agentHarness(support, {
|
|
184
|
+
* script: [toolCallTurn("c1", "lookup", { id: "o_1" }), finalTurn("It shipped.")],
|
|
185
|
+
* functions: { "orders:lookup": () => ({ status: "shipped" }) },
|
|
186
|
+
* });
|
|
187
|
+
* const result = await harness.run({ input: "where is my order?", threadKey: "t1" });
|
|
188
|
+
* expect(result.text).toBe("It shipped.");
|
|
189
|
+
* expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* The harness deliberately does NOT model the concurrency guard. A
|
|
193
|
+
* human-in-the-loop pause (an approval, or a `requestSecretTool` request) stops
|
|
194
|
+
* the run at its wait; the test resolves it with `deliver` or `expire`, and the
|
|
195
|
+
* harness does not route a decision through the owner-gated mutations. The
|
|
196
|
+
* harness is for exercising an agent's tools and turn logic, not the durable
|
|
197
|
+
* orchestration itself (that is covered inside `@lunora/agent`).
|
|
198
|
+
*/
|
|
199
|
+
declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
|
|
200
|
+
/** A terminal (final-answer) turn — no tool calls. */
|
|
201
|
+
declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
|
|
202
|
+
/** A single-tool-call turn: the loop runs the named tool with `input`. */
|
|
203
|
+
declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
|
|
204
|
+
/**
|
|
205
|
+
* Shared, bundler-inlined builder for the OpenTelemetry `gen_ai.evaluation.*`
|
|
206
|
+
* attributes an AI **evaluation** verdict (a scorer's `{name, score, label?}`)
|
|
207
|
+
* contributes to a **generation span**.
|
|
208
|
+
*
|
|
209
|
+
* This is the emit-time counterpart of the cloud OTLP decoder, which reads
|
|
210
|
+
* `gen_ai.evaluation.<name>.score` (number) and optional
|
|
211
|
+
* `gen_ai.evaluation.<name>.label` (string) attribute pairs back off a generation
|
|
212
|
+
* span (`EVALUATION_PREFIX = "gen_ai.evaluation."`). The framework emits exactly
|
|
213
|
+
* that pair here so a score rides the same trace as the generation it grades.
|
|
214
|
+
*
|
|
215
|
+
* It lives in `shared/` because more than one layer needs the identical wire
|
|
216
|
+
* format with no runtime dependency edge between them: `@lunora/do` builds it into
|
|
217
|
+
* a live `ctx.trace` span's post-hoc attributes (`SpanHandle.recordEvaluation`),
|
|
218
|
+
* and `@lunora/server` mirrors the handle shape structurally. `@lunora/testing`
|
|
219
|
+
* ships a parallel test-time helper (`recordEvaluation` / `evaluationAttributes`)
|
|
220
|
+
* that targets the same `gen_ai.evaluation.*` contract for span-less eval
|
|
221
|
+
* events/metrics. Keep this file genuinely zero-dependency so inlining stays sound.
|
|
222
|
+
*/
|
|
223
|
+
/** The primitive an eval contributes to a span: a numeric score or a string label. */
|
|
224
|
+
type EvaluationAttributeValue = number | string;
|
|
225
|
+
/**
|
|
226
|
+
* Structural slice of the post-hoc span handle `ctx.trace` hands its body (see the
|
|
227
|
+
* server `SpanHandle`) — enough to attach an eval's attributes. Declared here
|
|
228
|
+
* rather than imported so `@lunora/testing` takes no dependency on `@lunora/server`
|
|
229
|
+
* or `@lunora/do`; the real handle is assignable to it.
|
|
230
|
+
*/
|
|
231
|
+
interface EvaluationSpanHandle {
|
|
232
|
+
setAttributes: (fields: Record<string, EvaluationAttributeValue>) => void;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Structural slice of `ctx.metrics` — enough to record a score as a durable
|
|
236
|
+
* series. Declared here for the same reason as {@link EvaluationSpanHandle}: no
|
|
237
|
+
* dependency on `@lunora/server`, and the real handle is assignable.
|
|
238
|
+
*/
|
|
239
|
+
interface EvaluationMetrics {
|
|
240
|
+
gauge: (name: string, value: number, attributes?: Record<string, unknown>) => void;
|
|
241
|
+
}
|
|
242
|
+
/** One eval verdict to emit. */
|
|
243
|
+
interface RecordEvaluationInput {
|
|
244
|
+
/**
|
|
245
|
+
* Optional categorical label (e.g. `"pass"` / `"fail"` / a rubric bucket),
|
|
246
|
+
* emitted as the `.label` attribute. Omitted → no label attribute.
|
|
247
|
+
*/
|
|
248
|
+
label?: string;
|
|
249
|
+
/**
|
|
250
|
+
* Optional `ctx.metrics` handle. Passing it ALSO records the score as a
|
|
251
|
+
* `gen_ai.evaluation.<name>.score` gauge, which is what gives an eval a
|
|
252
|
+
* durable history: span attributes live in the shard's bounded in-memory
|
|
253
|
+
* ring and vanish on hibernation, while metrics are persisted in per-minute
|
|
254
|
+
* buckets and can be charted as a trend. Additive — the attributes are
|
|
255
|
+
* emitted either way.
|
|
256
|
+
*/
|
|
257
|
+
metrics?: EvaluationMetrics;
|
|
258
|
+
/**
|
|
259
|
+
* The scorer/evaluation name — becomes the key's name segment. Any character
|
|
260
|
+
* outside `[A-Za-z0-9._-]` is replaced with `_` so a scorer name carrying a
|
|
261
|
+
* colon (e.g. `"contains:shipped"`) still yields a well-formed attribute key.
|
|
262
|
+
*/
|
|
263
|
+
name: string;
|
|
264
|
+
/** The numeric score (typically `[0, 1]`), emitted as the `.score` attribute. */
|
|
265
|
+
score: number;
|
|
266
|
+
/**
|
|
267
|
+
* Optional generation span to attach the attributes to — the post-hoc
|
|
268
|
+
* `SpanHandle` a `ctx.trace` body receives. Omitted → nothing is attached and
|
|
269
|
+
* the caller uses the returned bag for a standalone event/metric.
|
|
270
|
+
*/
|
|
271
|
+
span?: EvaluationSpanHandle;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Build the `gen_ai.evaluation.NAME.*` attribute bag for one eval verdict — the
|
|
275
|
+
* `.score` (number) always, the `.label` (string) when a label is given. Exported
|
|
276
|
+
* so a caller can emit the score as a standalone event/metric without a span.
|
|
277
|
+
*
|
|
278
|
+
* Delegates to the shared `shared/evaluation-attributes.ts` builder so
|
|
279
|
+
* `@lunora/testing`'s scorers and the runtime's own `recordEvaluation` emit the
|
|
280
|
+
* identical wire format — only the thrown error type differs, wrapped here as a
|
|
281
|
+
* `LunoraError` to match this package's public error contract.
|
|
282
|
+
*/
|
|
283
|
+
declare const evaluationAttributes: (input: Pick<RecordEvaluationInput, "label" | "name" | "score">) => Record<string, EvaluationAttributeValue>;
|
|
284
|
+
/**
|
|
285
|
+
* Emit one eval verdict as `gen_ai.evaluation.NAME.*` attributes. Attaches them to
|
|
286
|
+
* `input.span` when supplied (the post-hoc generation-span handle), and always
|
|
287
|
+
* returns the attribute bag so a span-less caller can ship it as an eval
|
|
288
|
+
* event/metric. Privacy-safe: only the name, score, and optional label are
|
|
289
|
+
* emitted — never the graded prompt or output.
|
|
290
|
+
*/
|
|
291
|
+
declare const recordEvaluation: (input: RecordEvaluationInput) => Record<string, EvaluationAttributeValue>;
|
|
292
|
+
/** Test controls returned alongside the fake binding. */
|
|
293
|
+
interface ArtifactsFake {
|
|
294
|
+
/** The fake `env.ARTIFACTS` binding. */
|
|
295
|
+
binding: ArtifactsBindingLike;
|
|
296
|
+
/** Make the next binding or repo call throw an `ArtifactsError` with `code` (e.g. `UPSTREAM_UNAVAILABLE`). */
|
|
297
|
+
failNext: (code: ArtifactsErrorCode) => void;
|
|
298
|
+
/** How many repo handles `get()` has handed out and how many were disposed — a leak shows as `opened > disposed`. */
|
|
299
|
+
readonly handles: {
|
|
300
|
+
disposed: number;
|
|
301
|
+
opened: number;
|
|
302
|
+
};
|
|
303
|
+
/** Stage a blob by object id. Returns the stored `Blob` (the exact instance `readBlob` hands back). */
|
|
304
|
+
putBlob: (repo: string, hash: string, content: Blob | string) => Blob;
|
|
305
|
+
/** Append a commit to the repo's history (newest first in `log()`), readable by `readCommit`. */
|
|
306
|
+
putCommit: (repo: string, commit: ArtifactsCommitMetadata) => void;
|
|
307
|
+
/** Stage a file at a ref, standing in for a push. Returns the stored `Blob` (the exact instance `readFile` hands back). */
|
|
308
|
+
putFile: (repo: string, file: {
|
|
309
|
+
content: Blob | string;
|
|
310
|
+
path: string;
|
|
311
|
+
ref: string;
|
|
312
|
+
type?: string;
|
|
313
|
+
}) => Blob;
|
|
314
|
+
/** Stage a tree by object id. */
|
|
315
|
+
putTree: (repo: string, hash: string, entries: ArtifactsTreeEntry[]) => void;
|
|
316
|
+
/** The repo names currently in the namespace. */
|
|
317
|
+
repoNames: () => string[];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build an in-memory Artifacts binding plus seeding and inspection controls.
|
|
321
|
+
*
|
|
322
|
+
* ```ts
|
|
323
|
+
* const fake = createArtifactsFake({ namespace: "default" });
|
|
324
|
+
* const artifacts = createArtifacts({ binding: fake.binding });
|
|
325
|
+
* await artifacts.create("docs");
|
|
326
|
+
* fake.putFile("docs", { ref: "main", path: "README.md", content: "# hi" });
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
declare const createArtifactsFake: (options?: {
|
|
330
|
+
namespace?: string;
|
|
331
|
+
}) => ArtifactsFake;
|
|
332
|
+
/** A {@link SecretValueHost} whose clock the test moves forward by hand. */
|
|
333
|
+
interface FakeSecretValues extends SecretValueHost {
|
|
334
|
+
/** Move the store's clock forward, so a value put earlier can expire. */
|
|
335
|
+
advanceClock: (ms: number) => void;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* In-memory {@link SecretValueHost} for harness suites. Follows the contract in
|
|
339
|
+
* `@lunora/platform` (unguessable refs, scope, one claimant, expiry refuses
|
|
340
|
+
* reads) with a clock that starts at zero, so a test controls every expiry
|
|
341
|
+
* through {@link FakeSecretValues.advanceClock}.
|
|
342
|
+
*/
|
|
343
|
+
declare const createFakeSecretValues: () => FakeSecretValues;
|
|
3
344
|
/** A pending job entry in the fake scheduler queue. */
|
|
4
345
|
interface FakeScheduledJob extends ScheduledJob {
|
|
5
346
|
/** The args the job was scheduled with. */
|
|
6
347
|
args: Record<string, unknown>;
|
|
7
348
|
}
|
|
8
349
|
/**
|
|
9
|
-
* A single scheduled-job failure
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
350
|
+
* A single scheduled-job failure that exhausted its retry budget, captured
|
|
351
|
+
* during an `advance()` / `runPending()` sweep. Mirrors `SchedulerDO`'s
|
|
352
|
+
* dead-letter park (`recordRetry()`, `packages/scheduler/src/scheduler-do.ts:875-909`):
|
|
353
|
+
* a job that fails while it still has retries left is silently re-enqueued
|
|
354
|
+
* with backoff and is NOT recorded here. Only once a job's `attempts` exceeds
|
|
355
|
+
* `@lunora/scheduler`'s `MAX_RETRY_ATTEMPTS` does it land here — being a test
|
|
356
|
+
* harness, the fake scheduler never swallows a terminal failure, so tests can
|
|
357
|
+
* still assert on it.
|
|
358
|
+
*/
|
|
15
359
|
interface ScheduledJobFailure {
|
|
16
360
|
/** The args the job was dispatched with. */
|
|
17
361
|
args: Record<string, unknown>;
|
|
@@ -23,94 +367,168 @@ interface ScheduledJobFailure {
|
|
|
23
367
|
id: string;
|
|
24
368
|
}
|
|
25
369
|
/**
|
|
26
|
-
* Controls exposed on the test harness for the fake in-memory scheduler.
|
|
27
|
-
* Access via `harness.scheduler`.
|
|
28
|
-
*/
|
|
370
|
+
* Controls exposed on the test harness for the fake in-memory scheduler.
|
|
371
|
+
* Access via `harness.scheduler`.
|
|
372
|
+
*/
|
|
29
373
|
interface FakeSchedulerControls {
|
|
30
374
|
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
375
|
+
* Advance the virtual clock by `ms` milliseconds, executing all jobs whose
|
|
376
|
+
* `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
|
|
377
|
+
* are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
|
|
378
|
+
* (scheduled by an executed job, or re-enqueued as a retry, during the
|
|
379
|
+
* advance) are NOT re-evaluated in the same advance call — callers should
|
|
380
|
+
* advance again if needed.
|
|
381
|
+
*
|
|
382
|
+
* Per-job failures are isolated (matching production): a job that throws does
|
|
383
|
+
* NOT prevent the remaining due jobs from running. A failure with retries
|
|
384
|
+
* left is silently re-enqueued with exponential backoff on the virtual clock
|
|
385
|
+
* — mirroring `SchedulerDO`'s default retry policy (`MAX_RETRY_ATTEMPTS`
|
|
386
|
+
* retries, `RETRY_BASE_DELAY_MS` base delay, doubling; both imported from
|
|
387
|
+
* `@lunora/scheduler`) — rather than surfaced. Advancing far enough to
|
|
388
|
+
* observe a terminal failure therefore costs the WHOLE backoff schedule
|
|
389
|
+
* (30s + 60s + 120s + 240s + 480s = 930s of virtual clock at today's
|
|
390
|
+
* defaults), not a single tick.
|
|
391
|
+
* Only once a job's retry budget is exhausted is the failure surfaced: it is
|
|
392
|
+
* recorded on {@link FakeSchedulerControls.failures} and, by default,
|
|
393
|
+
* re-thrown so a test still sees the error. A single such failure is
|
|
394
|
+
* re-thrown verbatim; multiple are aggregated into an `AggregateError`. Pass
|
|
395
|
+
* `{ throwOnError: false }` to suppress the re-throw and inspect
|
|
396
|
+
* {@link FakeSchedulerControls.failures} (and the returned count) instead.
|
|
397
|
+
*
|
|
398
|
+
* Returns the number of jobs dispatched this sweep, including ones that
|
|
399
|
+
* failed and were silently retried, and ones that failed terminally.
|
|
400
|
+
*/
|
|
48
401
|
advance: (ms: number, options?: SweepOptions) => Promise<number>;
|
|
49
402
|
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
403
|
+
* All scheduled-job failures that exhausted their retry budget, in
|
|
404
|
+
* execution order, across every `advance()` / `runPending()` call on this
|
|
405
|
+
* harness. A failure with retries remaining is NOT recorded here — see
|
|
406
|
+
* {@link ScheduledJobFailure}. Always available, even when
|
|
407
|
+
* `throwOnError: false` suppressed the re-throw. The list is a snapshot —
|
|
408
|
+
* mutating it does not affect the scheduler.
|
|
409
|
+
*/
|
|
55
410
|
failures: () => ScheduledJobFailure[];
|
|
56
411
|
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
412
|
+
* List all pending jobs (those not yet executed or cancelled) in the order
|
|
413
|
+
* they were enqueued. A job currently waiting out its retry backoff is
|
|
414
|
+
* still pending (visible here with its `attempts` count incremented and
|
|
415
|
+
* `scheduledFor` pushed out) until its budget is exhausted.
|
|
416
|
+
*/
|
|
60
417
|
list: () => FakeScheduledJob[];
|
|
61
418
|
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
419
|
+
* Execute all currently pending jobs regardless of their `scheduledFor`
|
|
420
|
+
* time. Equivalent to advancing to `Infinity`. Returns the number of jobs
|
|
421
|
+
* dispatched this sweep (including failed ones, retried or terminal).
|
|
422
|
+
*
|
|
423
|
+
* Failure isolation, retry, and surfacing match
|
|
424
|
+
* {@link FakeSchedulerControls.advance}: one failing job does not abort the
|
|
425
|
+
* rest, a failure under the retry budget is silently re-enqueued rather
|
|
426
|
+
* than surfaced, and only a terminal failure is recorded on
|
|
427
|
+
* {@link FakeSchedulerControls.failures} and re-thrown unless
|
|
428
|
+
* `{ throwOnError: false }` is passed.
|
|
429
|
+
*/
|
|
71
430
|
runPending: (options?: SweepOptions) => Promise<number>;
|
|
72
431
|
}
|
|
73
432
|
/** Options controlling how an `advance()` / `runPending()` sweep surfaces failures. */
|
|
74
433
|
interface SweepOptions {
|
|
75
434
|
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
435
|
+
* When `true` (the default), failures encountered during the sweep are
|
|
436
|
+
* re-thrown after every due job has run — a single failure verbatim, multiple
|
|
437
|
+
* as an `AggregateError`. When `false`, the sweep resolves normally and
|
|
438
|
+
* failures are observable only via {@link FakeSchedulerControls.failures}.
|
|
439
|
+
*/
|
|
81
440
|
throwOnError?: boolean;
|
|
82
441
|
}
|
|
442
|
+
/** What a handler attached to the dispatch's span (`ctx.span`) during a harness run. */
|
|
443
|
+
interface RecordedWideEvent {
|
|
444
|
+
/**
|
|
445
|
+
* Attributes accumulated across the run, merged in call order. Values are
|
|
446
|
+
* recorded AS PASSED (not coerced the way the real pipeline normalizes them),
|
|
447
|
+
* so a test asserts on what the handler meant rather than on the wire form.
|
|
448
|
+
*/
|
|
449
|
+
attributes: LogFields;
|
|
450
|
+
/** Span events recorded via `ctx.span.addEvent` / `recordException`, in order. */
|
|
451
|
+
events: {
|
|
452
|
+
attributes?: LogFields;
|
|
453
|
+
name: string;
|
|
454
|
+
}[];
|
|
455
|
+
/** Links recorded via `ctx.span.addLink`, in order. */
|
|
456
|
+
links: {
|
|
457
|
+
spanId: string;
|
|
458
|
+
traceId: string;
|
|
459
|
+
}[];
|
|
460
|
+
}
|
|
461
|
+
/** The channels `ctx.notify` can deliver on. */
|
|
462
|
+
type NotifyChannel = Extract<ChannelType, "chat" | "inapp" | "push" | "webhook">;
|
|
463
|
+
/**
|
|
464
|
+
* One delivery a handler made through `ctx.notify` / `ctx.push`. For push,
|
|
465
|
+
* `payload.to` is the device target: the FCM token, or for Web Push the
|
|
466
|
+
* JSON-stringified subscription (keys included).
|
|
467
|
+
*/
|
|
468
|
+
type SentNotification = { [C in NotifyChannel]: {
|
|
469
|
+
channel: C;
|
|
470
|
+
payload: ChannelPayloadMap[C];
|
|
471
|
+
}; }[NotifyChannel];
|
|
472
|
+
/** Inspect what handlers delivered through `ctx.notify` / `ctx.push`. */
|
|
473
|
+
interface FakeNotifyControls {
|
|
474
|
+
/** Forget every recorded delivery (registered subscriptions are kept). */
|
|
475
|
+
clear: () => void;
|
|
476
|
+
/** Recorded deliveries in send order, optionally only those on `channel`. Throws when `options.notify` was not passed. */
|
|
477
|
+
sent: (channel?: NotifyChannel) => SentNotification[];
|
|
478
|
+
}
|
|
479
|
+
/** One message a handler enqueued through `ctx.queues.<name>`, as the consumer decodes it. */
|
|
480
|
+
interface SentQueueMessage {
|
|
481
|
+
body: unknown;
|
|
482
|
+
contentType?: QueueContentType;
|
|
483
|
+
delaySeconds?: number;
|
|
484
|
+
/** The `lunora/queues.ts` export name the message was sent through. */
|
|
485
|
+
queue: string;
|
|
486
|
+
}
|
|
83
487
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
488
|
+
* Inspect what handlers enqueued. Production sends are not transactional — a
|
|
489
|
+
* mutation that throws after `ctx.queues.x.send(...)` has still sent it — so the
|
|
490
|
+
* record keeps those too. Nothing is consumed: a test asserts on the messages and
|
|
491
|
+
* drives the consumer itself if it wants to.
|
|
492
|
+
*/
|
|
493
|
+
interface FakeQueueControls {
|
|
494
|
+
/** Forget every recorded message. */
|
|
495
|
+
clear: () => void;
|
|
496
|
+
/** Recorded messages in send order, optionally only those sent to `queue`; an undeclared `queue` throws. */
|
|
497
|
+
sent: (queue?: string) => SentQueueMessage[];
|
|
498
|
+
}
|
|
499
|
+
/** One message a handler published through `ctx.topics.<name>`, as a subscriber decodes it. */
|
|
500
|
+
interface PublishedTopicMessage {
|
|
501
|
+
body: unknown;
|
|
502
|
+
contentType?: QueueContentType;
|
|
503
|
+
delaySeconds?: number;
|
|
504
|
+
/** The `lunora/queues.ts` topic export name the message was published to. */
|
|
505
|
+
topic: string;
|
|
506
|
+
}
|
|
92
507
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*/
|
|
508
|
+
* Inspect what handlers published. Like queue sends, a publish is not
|
|
509
|
+
* transactional, so one made by a mutation that then threw stays recorded.
|
|
510
|
+
*/
|
|
511
|
+
interface FakeTopicControls {
|
|
512
|
+
/** Forget every recorded message. */
|
|
513
|
+
clear: () => void;
|
|
514
|
+
/** Recorded messages in publish order, optionally only those published to `topic`; an undeclared `topic` throws. */
|
|
515
|
+
published: (topic?: string) => PublishedTopicMessage[];
|
|
516
|
+
}
|
|
517
|
+
/** The schema value produced by `@lunora/server`'s `defineSchema`. */
|
|
100
518
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
101
519
|
/**
|
|
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
|
-
*/
|
|
520
|
+
* A user-supplied identity, surfaced to handlers via `ctx.auth`. `userId` is the
|
|
521
|
+
* subject the handler reads from `ctx.auth.userId`; any additional fields are
|
|
522
|
+
* returned verbatim from `ctx.auth.getIdentity()` (mirroring a decoded JWT).
|
|
523
|
+
*/
|
|
106
524
|
interface TestIdentity extends Record<string, unknown> {
|
|
107
525
|
userId?: null | string;
|
|
108
526
|
}
|
|
109
527
|
/**
|
|
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
|
-
*/
|
|
528
|
+
* An async iterable/iterator returned by {@link TestHarness.subscribe}.
|
|
529
|
+
* Guarantees `return()` is always defined (unlike the optional `AsyncIterator.return`),
|
|
530
|
+
* so callers can always unsubscribe without a `?.` guard.
|
|
531
|
+
*/
|
|
114
532
|
interface TestSubscription<R> extends AsyncIterable<R> {
|
|
115
533
|
next: () => Promise<IteratorResult<R, R>>;
|
|
116
534
|
return: () => Promise<IteratorResult<R, R>>;
|
|
@@ -120,88 +538,187 @@ type InlineQueryFunction<R> = (context: QueryCtx) => Promise<R> | R;
|
|
|
120
538
|
type InlineMutationFunction<R> = (context: MutationCtx) => Promise<R> | R;
|
|
121
539
|
type InlineActionFunction<R> = (context: ActionCtx) => Promise<R> | R;
|
|
122
540
|
/**
|
|
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
|
|
134
|
-
* `handler` and ignores the return value).
|
|
135
|
-
*/
|
|
541
|
+
* A map from function path strings (e.g. `"messages:send"`) to their
|
|
542
|
+
* registered function objects. Used by the fake scheduler to resolve
|
|
543
|
+
* `ctx.scheduler.runAfter(delay, "messages:send", args)` → handler invocation.
|
|
544
|
+
*
|
|
545
|
+
* Only mutations and actions can be scheduled in production; queries passed
|
|
546
|
+
* here will be accepted but produce a console.warn at dispatch time.
|
|
547
|
+
*
|
|
548
|
+
* The value type uses `any` because `RegisteredFunction` is contravariant in its
|
|
549
|
+
* args type parameter — a `RegisteredMutation` with concrete args is not assignable
|
|
550
|
+
* to `RegisteredMutation` with `ArgsValidator` at the type level even though at
|
|
551
|
+
* runtime it is sound (the fake scheduler passes `Record<string, unknown>` to
|
|
552
|
+
* `handler` and ignores the return value).
|
|
553
|
+
*/
|
|
136
554
|
type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMutation<any, any> | RegisteredQuery<any, any>>;
|
|
137
555
|
/**
|
|
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
|
-
*/
|
|
556
|
+
* Options accepted by {@link lunoraTest}.
|
|
557
|
+
*
|
|
558
|
+
* All options are optional — `lunoraTest(schema)` preserves v1 behaviour with
|
|
559
|
+
* clearly-throwing stubs for unsupported surfaces.
|
|
560
|
+
*/
|
|
143
561
|
interface LunoraTestOptions {
|
|
144
562
|
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
563
|
+
* Enforce the secure-by-default RLS guard on the writer registered
|
|
564
|
+
* procedures dispatch through (`query`/`mutation`/`action`, via
|
|
565
|
+
* `reference.handler`) — the same `enforceRls: true` production's generated
|
|
566
|
+
* `buildCtx` always passes. Under a `.rls("required")` schema, a procedure
|
|
567
|
+
* that touches a known, non-`.public()` table without `.use(rls(...))` in
|
|
568
|
+
* its chain rejects with `RlsRequiredError`, exactly as it would on first
|
|
569
|
+
* dispatch in production. Defaults to `true` so a green suite means the
|
|
570
|
+
* deploy is RLS-safe; the harness's other surfaces — `t.run` and any
|
|
571
|
+
* `@lunora/seed` helper built on it — stay on the trusted, UNGUARDED writer
|
|
572
|
+
* regardless of this flag (mirroring production's admin/migration system
|
|
573
|
+
* paths).
|
|
574
|
+
*
|
|
575
|
+
* Set to `false` to opt back into the pre-guard permissive behaviour (every
|
|
576
|
+
* `lunoraTest` release before this option existed): every procedure's
|
|
577
|
+
* `ctx.db` goes unguarded even under a `.rls("required")` schema. This
|
|
578
|
+
* forfeits the "a passing suite means the deploy is safe" guarantee — a
|
|
579
|
+
* procedure that forgot `.use(rls(...))` will pass in tests and throw
|
|
580
|
+
* `RlsRequiredError` on its first production request. No effect when the
|
|
581
|
+
* schema does not declare `.rls("required")` (the guard is a no-op there
|
|
582
|
+
* either way).
|
|
583
|
+
* @default true
|
|
584
|
+
* @example
|
|
585
|
+
* ```ts
|
|
586
|
+
* // Restores the old permissive behavior for a suite not yet migrated.
|
|
587
|
+
* const t = lunoraTest(schema, { enforceRls: false });
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
enforceRls?: boolean;
|
|
591
|
+
/**
|
|
592
|
+
* Injectable `ctx.env` for every context (query / mutation / action). When
|
|
593
|
+
* provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
|
|
594
|
+
* surface) see this object. Left unset it stays `undefined` — matching the
|
|
595
|
+
* optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
|
|
596
|
+
* `undefined` rather than throwing. Not a throwing stub for exactly that
|
|
597
|
+
* reason: `env` is designed to be legitimately absent.
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
156
603
|
env?: Record<string, unknown>;
|
|
157
604
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
605
|
+
* Injectable `fetch` implementation for action contexts. When provided,
|
|
606
|
+
* `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
|
|
607
|
+
* function rather than throwing the "not available in v1" stub.
|
|
608
|
+
*
|
|
609
|
+
* Pass `vi.fn()` or any `typeof globalThis.fetch` compatible implementation.
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* const fakeFetch = vi.fn(async () => Response.json({ ok: true }));
|
|
613
|
+
* const t = lunoraTest(schema, { fetch: fakeFetch });
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
169
616
|
fetch?: typeof globalThis.fetch;
|
|
170
617
|
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
618
|
+
* Function registry for the fake in-memory scheduler. Maps a
|
|
619
|
+
* `functionPath` string (the value passed as the second argument to
|
|
620
|
+
* `ctx.scheduler.runAfter` / `ctx.scheduler.runAt`) to the corresponding
|
|
621
|
+
* registered function object.
|
|
622
|
+
*
|
|
623
|
+
* Only required if your handlers schedule work. Scheduled jobs for paths
|
|
624
|
+
* NOT listed here produce a `console.warn` at dispatch time (matching prod
|
|
625
|
+
* behaviour for unknown paths).
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* const t = lunoraTest(schema, {
|
|
629
|
+
* functions: { "messages:send": sendMutation },
|
|
630
|
+
* });
|
|
631
|
+
* ```
|
|
632
|
+
*/
|
|
186
633
|
functions?: FunctionRegistry;
|
|
187
634
|
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
635
|
+
* The app's `lunora/notify.ts` default export. Enables `ctx.notify` and its
|
|
636
|
+
* `ctx.push` alias on every context, built by `@lunora/notify`'s own
|
|
637
|
+
* `createNotify` — so register / list / unregister / broadcast behave as in
|
|
638
|
+
* production, and channels are wired exactly when production wires them
|
|
639
|
+
* (each factory is called with `options.env`; a push target for a transport
|
|
640
|
+
* that resolved to nothing fails). Differences: deliveries are recorded and
|
|
641
|
+
* report accepted instead of reaching a service (no send-time DNS re-check,
|
|
642
|
+
* retry or circuit breaker), and subscriptions live in an in-memory store —
|
|
643
|
+
* the definition's `store` is never called. Assert on deliveries with
|
|
644
|
+
* `harness.notify.sent()`. Left unset, touching `ctx.notify` / `ctx.push` —
|
|
645
|
+
* or calling `harness.notify.sent()` — throws, naming this option.
|
|
646
|
+
* @example
|
|
647
|
+
* ```ts
|
|
648
|
+
* import notify from "../lunora/notify";
|
|
649
|
+
*
|
|
650
|
+
* const t = lunoraTest(schema, { notify });
|
|
651
|
+
* await t.action(alertOwner, { orderId });
|
|
652
|
+
* expect(t.notify.sent("push")).toHaveLength(1);
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
notify?: NotifyDefinition;
|
|
656
|
+
/**
|
|
657
|
+
* Fixed value for `ctx.now` (epoch ms) in every context. Production captures
|
|
658
|
+
* `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
|
|
659
|
+
* handlers deterministic. Defaults to the wall clock at harness creation.
|
|
660
|
+
* @example
|
|
661
|
+
* ```ts
|
|
662
|
+
* const t = lunoraTest(schema, { now: 1_700_000_000_000 });
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
196
665
|
now?: number;
|
|
666
|
+
/**
|
|
667
|
+
* The export names of the queues in `lunora/queues.ts` (the keys of
|
|
668
|
+
* `ctx.queues`). Each gets a recording producer on mutation and action
|
|
669
|
+
* contexts, behind `@lunora/queue`'s own validation (batch cap, delay
|
|
670
|
+
* ceiling), so a send the platform would refuse rejects here too. A name not
|
|
671
|
+
* listed rejects on use, as an undeclared queue does in production. Assert
|
|
672
|
+
* on sends with `harness.queues.sent(name)`. Left unset, touching
|
|
673
|
+
* `ctx.queues` — or calling `harness.queues.sent()` — throws, naming this
|
|
674
|
+
* option.
|
|
675
|
+
* @example
|
|
676
|
+
* ```ts
|
|
677
|
+
* const t = lunoraTest(schema, { queues: ["jobs"] });
|
|
678
|
+
* await t.mutation(enqueueJob, { id: "a" });
|
|
679
|
+
* expect(t.queues.sent("jobs")).toEqual([{ body: { id: "a" }, queue: "jobs" }]);
|
|
680
|
+
* ```
|
|
681
|
+
*/
|
|
682
|
+
queues?: ReadonlyArray<string>;
|
|
683
|
+
/**
|
|
684
|
+
* Fakes for `ctx.services` (the `lunora.config` `services` an app calls),
|
|
685
|
+
* keyed like `ctx.services`. Set on action contexts only — queries and
|
|
686
|
+
* mutations have no `ctx.services` at runtime either. A key with no fake
|
|
687
|
+
* throws on use, naming the option to add it to.
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* const t = lunoraTest(schema, {
|
|
691
|
+
* services: { documentParser: { fetch: vi.fn(async () => Response.json({ text: "…" })) } },
|
|
692
|
+
* });
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
services?: Record<string, object>;
|
|
696
|
+
/**
|
|
697
|
+
* The export names of the topics in `lunora/queues.ts` (the keys of
|
|
698
|
+
* `ctx.topics`). Each gets a recording publisher on mutation and action
|
|
699
|
+
* contexts, built by `@lunora/queue`'s own `createTopicContext` — so the
|
|
700
|
+
* batch cap, delay ceiling and reserved-key check reject as in production,
|
|
701
|
+
* and a name not listed rejects on use. Bodies are recorded and size-checked
|
|
702
|
+
* as `queues` records them, once per message however many subscriptions the
|
|
703
|
+
* app declares; no subscription handler runs. Assert with
|
|
704
|
+
* `harness.topics.published(name)`. Left unset, touching `ctx.topics` — or
|
|
705
|
+
* calling `harness.topics.published()` — throws, naming this option.
|
|
706
|
+
* @example
|
|
707
|
+
* ```ts
|
|
708
|
+
* const t = lunoraTest(schema, { topics: ["signups"] });
|
|
709
|
+
* await t.mutation(signUp, { email: "a@example.test" });
|
|
710
|
+
* expect(t.topics.published("signups")).toHaveLength(1);
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
topics?: ReadonlyArray<string>;
|
|
197
714
|
}
|
|
198
715
|
/**
|
|
199
|
-
* The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
|
|
200
|
-
* five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
|
|
201
|
-
* / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
|
|
202
|
-
* write from one method is visible to a read from another (including across a
|
|
203
|
-
* `withIdentity` scope).
|
|
204
|
-
*/
|
|
716
|
+
* The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
|
|
717
|
+
* five methods of Convex's `convexTest`: `query` / `mutation` / `action` / `run`
|
|
718
|
+
* / `withIdentity`. All five share one in-memory `node:sqlite` backend, so a
|
|
719
|
+
* write from one method is visible to a read from another (including across a
|
|
720
|
+
* `withIdentity` scope).
|
|
721
|
+
*/
|
|
205
722
|
interface TestHarness {
|
|
206
723
|
/** Run a registered `action` (or an inline `async (context) => …`) against the harness. */
|
|
207
724
|
action: {
|
|
@@ -215,76 +732,325 @@ interface TestHarness {
|
|
|
215
732
|
<A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
|
|
216
733
|
<R>(inline: InlineMutationFunction<R>): Promise<R>;
|
|
217
734
|
};
|
|
735
|
+
/**
|
|
736
|
+
* What handlers delivered through `ctx.notify` / `ctx.push` (enabled by
|
|
737
|
+
* `options.notify`): `sent(channel?)` in send order, `clear()` to reset.
|
|
738
|
+
* Both throw when the option was not passed. Shared with any `withIdentity` view.
|
|
739
|
+
*/
|
|
740
|
+
notify: FakeNotifyControls;
|
|
218
741
|
/** Run a registered `query` (or an inline `async (context) => …`) against the harness. */
|
|
219
742
|
query: {
|
|
220
743
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
|
|
221
744
|
<R>(inline: InlineQueryFunction<R>): Promise<R>;
|
|
222
745
|
};
|
|
223
|
-
/**
|
|
746
|
+
/**
|
|
747
|
+
* What handlers enqueued through `ctx.queues` (declared by `options.queues`):
|
|
748
|
+
* `sent(name?)` in send order, `clear()` to reset. Sends are not
|
|
749
|
+
* transactional in production, so one made by a mutation that then threw is
|
|
750
|
+
* recorded too. Both throw when the option was not passed. Shared with any
|
|
751
|
+
* `withIdentity` view.
|
|
752
|
+
*/
|
|
753
|
+
queues: FakeQueueControls;
|
|
754
|
+
/**
|
|
755
|
+
* Direct db access at mutation-level (read + write), mirroring `convexTest`'s
|
|
756
|
+
* `run`. This is the harness's trusted escape hatch: `ctx.db` here is always
|
|
757
|
+
* the UNGUARDED writer, regardless of `options.enforceRls` or the schema's
|
|
758
|
+
* RLS mode — seeding/asserting against a protected table never trips the
|
|
759
|
+
* secure-by-default guard. A `ctx.runMutation`/`ctx.runQuery` call from
|
|
760
|
+
* inside the body still dispatches the target as a real registered
|
|
761
|
+
* procedure, so it is guarded exactly as `t.mutation`/`t.query` would guard
|
|
762
|
+
* it.
|
|
763
|
+
*/
|
|
224
764
|
run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
|
|
225
765
|
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
766
|
+
* Controls for the fake in-memory scheduler. Always present; scheduler
|
|
767
|
+
* jobs only execute when you call `advance(ms)` or `runPending()`.
|
|
768
|
+
*
|
|
769
|
+
* - `list()` — snapshot of all pending jobs (enqueue order).
|
|
770
|
+
* - `advance(ms)` — tick the virtual clock forward by `ms` ms, executing every job
|
|
771
|
+
* whose `scheduledFor` is now at or below virtual now.
|
|
772
|
+
* - `runPending()` — execute all currently pending jobs regardless of their scheduled time.
|
|
773
|
+
*
|
|
774
|
+
* Scheduled jobs run through the same `runInternal` dispatch as
|
|
775
|
+
* `ctx.runMutation`, so they share the harness SQLite database.
|
|
776
|
+
*/
|
|
237
777
|
scheduler: FakeSchedulerControls;
|
|
238
778
|
/**
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
779
|
+
* Subscribe to a registered query (or inline query function) and receive
|
|
780
|
+
* an async iterable of snapshots. The first value is emitted immediately
|
|
781
|
+
* (the current query result). Subsequent values are emitted after each
|
|
782
|
+
* `mutation` / `run` call on this harness completes.
|
|
783
|
+
*
|
|
784
|
+
* Subscriptions are table-agnostic — any mutation triggers a re-evaluation.
|
|
785
|
+
* This matches the harness's single-writer model and keeps the implementation
|
|
786
|
+
* free of DO machinery.
|
|
787
|
+
* @example
|
|
788
|
+
* ```ts
|
|
789
|
+
* const sub = t.subscribe(list, {});
|
|
790
|
+
* const first = await sub.next(); // current result
|
|
791
|
+
* await t.mutation(send, { author: "ada", body: "hi" });
|
|
792
|
+
* const second = await sub.next(); // updated result
|
|
793
|
+
* await sub.return(); // unsubscribe
|
|
794
|
+
* ```
|
|
795
|
+
*
|
|
796
|
+
* The iterable is lazy — it never buffers more than one pending result.
|
|
797
|
+
* If you do not consume fast enough and multiple mutations fire, the next
|
|
798
|
+
* `next()` call will reflect the most-recent state (intermediate snapshots
|
|
799
|
+
* are coalesced).
|
|
800
|
+
*/
|
|
261
801
|
subscribe: {
|
|
262
802
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
|
|
263
803
|
<R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
|
|
264
804
|
};
|
|
805
|
+
/**
|
|
806
|
+
* What handlers published through `ctx.topics` (declared by `options.topics`):
|
|
807
|
+
* `published(name?)` in publish order, `clear()` to reset. Like queue sends,
|
|
808
|
+
* publishes are not transactional. Both throw when the option was not passed.
|
|
809
|
+
* Shared with any `withIdentity` view.
|
|
810
|
+
*/
|
|
811
|
+
topics: FakeTopicControls;
|
|
812
|
+
/**
|
|
813
|
+
* What handlers attached to `ctx.span` — the **wide event** — during this
|
|
814
|
+
* harness's runs, so a test can assert the instrumentation itself:
|
|
815
|
+
*
|
|
816
|
+
* ```ts
|
|
817
|
+
* await t.mutation(checkout, { items: 3 });
|
|
818
|
+
* expect(t.wideEvent().attributes["cart.items"]).toBe(3);
|
|
819
|
+
* ```
|
|
820
|
+
*
|
|
821
|
+
* Accumulates across calls on this view (it is not reset per run), mirroring
|
|
822
|
+
* the harness's single shared database. Shared with any `withIdentity` view.
|
|
823
|
+
*/
|
|
824
|
+
wideEvent: () => RecordedWideEvent;
|
|
265
825
|
/** Return a harness view that shares this harness's db but reports the given identity on `ctx.auth`. */
|
|
266
826
|
withIdentity: (identity: TestIdentity) => TestHarness;
|
|
267
827
|
}
|
|
268
828
|
/**
|
|
269
|
-
* Spin up an in-memory Lunora function harness for `schema`.
|
|
270
|
-
*
|
|
271
|
-
* `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
|
|
272
|
-
* database, builds the same `ctx.db` writer the real Durable Object builds (via
|
|
273
|
-
* `@lunora/
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* - `
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
829
|
+
* Spin up an in-memory Lunora function harness for `schema`.
|
|
830
|
+
*
|
|
831
|
+
* `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
|
|
832
|
+
* database, builds the same `ctx.db` writer the real Durable Object builds (via
|
|
833
|
+
* `@lunora/shard-engine`'s `createShardCtxDb`, with the same `enforceRls: true`
|
|
834
|
+
* production's generated `buildCtx` passes), and returns a harness whose
|
|
835
|
+
* `query` / `mutation` / `action` execute a registered function's `handler`
|
|
836
|
+
* directly — no Durable Object, no `wrangler`, no network. Under a
|
|
837
|
+
* `.rls("required")` schema a procedure missing `.use(rls(...))` therefore
|
|
838
|
+
* rejects here exactly as it would on its first production dispatch — see
|
|
839
|
+
* `LunoraTestOptions.enforceRls` to opt out, and `run`'s doc for the trusted
|
|
840
|
+
* escape hatch (always unguarded).
|
|
841
|
+
*
|
|
842
|
+
* **v1 surfaces now supported:**
|
|
843
|
+
*
|
|
844
|
+
* - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
|
|
845
|
+
* stays `undefined`, matching the optional `ctx.env?` field.
|
|
846
|
+
* - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
|
|
847
|
+
* - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
|
|
848
|
+
* control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`. As in
|
|
849
|
+
* production it is transactional inside a mutation: a job scheduled by a mutation
|
|
850
|
+
* that then throws never becomes pending.
|
|
851
|
+
* - `ctx.queues` (mutations + actions): recording producers for `options.queues`;
|
|
852
|
+
* inspect via `harness.queues.sent(name)`.
|
|
853
|
+
* - `ctx.topics` (mutations + actions): recording publishers for `options.topics`;
|
|
854
|
+
* inspect via `harness.topics.published(name)`.
|
|
855
|
+
* - `ctx.notify` / `ctx.push` (all contexts): built from `options.notify` with
|
|
856
|
+
* recorded deliveries and in-memory subscriptions; inspect via `harness.notify.sent()`.
|
|
857
|
+
* - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
|
|
858
|
+
*
|
|
859
|
+
* **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
|
|
860
|
+
* These are clearly documented follow-ups.
|
|
861
|
+
*/
|
|
289
862
|
declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
|
|
290
|
-
|
|
863
|
+
/** One provider stream part as a model's `doStream` emits it (`text-delta`, `tool-call`, `finish`, …). */
|
|
864
|
+
type ModelStreamResult = Awaited<ReturnType<InstanceType<typeof MockLanguageModelV4>["doStream"]>>;
|
|
865
|
+
type RecordedStreamPart = ModelStreamResult["stream"] extends ReadableStream<infer Part> ? Part : never;
|
|
866
|
+
/**
|
|
867
|
+
* A pause point in a recorded turn. The stream emits `after` parts, then stops
|
|
868
|
+
* until {@link StreamGate.release} is called — a test observes or cancels the
|
|
869
|
+
* run while it is mid-stream without sleeping.
|
|
870
|
+
*/
|
|
871
|
+
interface StreamGate {
|
|
872
|
+
/** Resolves once the stream has reached the gate and is waiting on {@link StreamGate.release}. */
|
|
873
|
+
readonly reached: Promise<void>;
|
|
874
|
+
/** Let the paused stream continue. Idempotent. */
|
|
875
|
+
release: () => void;
|
|
876
|
+
/** Called by the recorded stream when it reaches the gate; the stream resumes after `release`. */
|
|
877
|
+
wait: () => Promise<void>;
|
|
878
|
+
}
|
|
879
|
+
/** One recorded model call: the stream parts it emits, plus optional pause and failure points. */
|
|
880
|
+
interface RecordedTurn {
|
|
881
|
+
/** Fail the stream with `cause` once `after` parts have been emitted (a mid-stream provider error). */
|
|
882
|
+
error?: {
|
|
883
|
+
after: number;
|
|
884
|
+
cause: unknown;
|
|
885
|
+
};
|
|
886
|
+
/** The provider stream parts for one model call, in emission order. */
|
|
887
|
+
parts: ReadonlyArray<RecordedStreamPart>;
|
|
888
|
+
/** Pause after `after` parts until `gate` is released. */
|
|
889
|
+
pause?: {
|
|
890
|
+
after: number;
|
|
891
|
+
gate: StreamGate;
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** Create a {@link StreamGate} to pause a recorded turn on. */
|
|
895
|
+
declare const createStreamGate: () => StreamGate;
|
|
896
|
+
/**
|
|
897
|
+
* A language model that replays recorded stream parts, one {@link RecordedTurn}
|
|
898
|
+
* per model call, in order. Pass it as an agent's `model` (or to
|
|
899
|
+
* `agentHarness(...).run(..., { model })`) to drive the production streaming
|
|
900
|
+
* seam against a recorded stream. Each call pops the next turn; a call past the
|
|
901
|
+
* last recorded turn fails the stream loudly rather than inventing an answer.
|
|
902
|
+
* `doStreamCalls` records the call options each turn was requested with.
|
|
903
|
+
*/
|
|
904
|
+
declare const recordedModel: (turns: ReadonlyArray<RecordedTurn>) => MockLanguageModelV4;
|
|
905
|
+
/** One sample handed to a {@link Scorer}. */
|
|
906
|
+
interface ScorerSample {
|
|
907
|
+
/** The reference/gold answer, when the scorer compares against one. */
|
|
908
|
+
expected?: string;
|
|
909
|
+
/** The prompt/input that produced the output (for LLM-judge context). */
|
|
910
|
+
input?: string;
|
|
911
|
+
/** Arbitrary per-sample metadata carried through. */
|
|
912
|
+
metadata?: Record<string, unknown>;
|
|
913
|
+
/** The model/agent output under test. */
|
|
914
|
+
output: string;
|
|
915
|
+
}
|
|
916
|
+
/** A scorer's verdict: a `[0, 1]` score and an optional human-readable reason. */
|
|
917
|
+
interface ScoreResult {
|
|
918
|
+
reason?: string;
|
|
919
|
+
score: number;
|
|
920
|
+
}
|
|
921
|
+
/** A named scorer — returns a `[0, 1]` score (or a {@link ScoreResult}) for a sample. */
|
|
922
|
+
interface Scorer {
|
|
923
|
+
name: string;
|
|
924
|
+
score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* What a `produce` runner may return: the output text alone, or the text plus
|
|
928
|
+
* metadata describing what the run actually did.
|
|
929
|
+
*
|
|
930
|
+
* The metadata form exists for scorers that judge more than the final string —
|
|
931
|
+
* a retrieval scorer needs the ranked ids the run retrieved, which only the run
|
|
932
|
+
* can know. It is merged OVER the case's own metadata before scoring.
|
|
933
|
+
*/
|
|
934
|
+
interface ProducedOutput {
|
|
935
|
+
/** Merged over the case's `metadata`, then handed to every scorer. */
|
|
936
|
+
metadata?: Record<string, unknown>;
|
|
937
|
+
/** The output text under test. */
|
|
938
|
+
output: string;
|
|
939
|
+
}
|
|
940
|
+
/** One dataset case: an input and its optional gold answer/metadata. */
|
|
941
|
+
interface EvalCase {
|
|
942
|
+
expected?: string;
|
|
943
|
+
input: string;
|
|
944
|
+
metadata?: Record<string, unknown>;
|
|
945
|
+
}
|
|
946
|
+
/** The per-case eval result: the produced output plus each scorer's verdict and the mean. */
|
|
947
|
+
interface EvalItemResult {
|
|
948
|
+
average: number;
|
|
949
|
+
input: string;
|
|
950
|
+
output: string;
|
|
951
|
+
scores: Record<string, ScoreResult>;
|
|
952
|
+
}
|
|
953
|
+
/** The whole eval run: per-case results plus the mean of their averages. */
|
|
954
|
+
interface EvalResult {
|
|
955
|
+
average: number;
|
|
956
|
+
items: EvalItemResult[];
|
|
957
|
+
}
|
|
958
|
+
/** Score 1 if the output contains `needle` (case-insensitive unless `caseSensitive`). */
|
|
959
|
+
declare const containsScorer: (needle: string, options?: {
|
|
960
|
+
caseSensitive?: boolean;
|
|
961
|
+
}) => Scorer;
|
|
962
|
+
/** Score 1 when `pattern` matches the output. */
|
|
963
|
+
declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
|
|
964
|
+
/** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
|
|
965
|
+
declare const exactMatchScorer: () => Scorer;
|
|
966
|
+
/** Score the fraction of `keywords` (case-insensitive) present in the output. */
|
|
967
|
+
declare const keywordScorer: (keywords: ReadonlyArray<string>) => Scorer;
|
|
968
|
+
/**
|
|
969
|
+
* An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise<string>`
|
|
970
|
+
* you wire to your model (e.g. via `ctx.ai` / `generateText`), so this module
|
|
971
|
+
* stays model-agnostic and the judge is mockable in tests. It returns the model's
|
|
972
|
+
* numeric verdict (`[0, 1]`) plus its reason.
|
|
973
|
+
*/
|
|
974
|
+
declare const llmScorer: (options: {
|
|
975
|
+
criteria: string;
|
|
976
|
+
judge: (prompt: string) => Promise<string>;
|
|
977
|
+
name?: string;
|
|
978
|
+
}) => Scorer;
|
|
979
|
+
/** Run every scorer over one sample (concurrently) → each verdict keyed by name + their mean. */
|
|
980
|
+
declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>) => Promise<{
|
|
981
|
+
average: number;
|
|
982
|
+
scores: Record<string, ScoreResult>;
|
|
983
|
+
}>;
|
|
984
|
+
/**
|
|
985
|
+
* Run a dataset through `produce` (e.g. an `agentHarness.run` wrapper returning
|
|
986
|
+
* the output text) and score each output with `scorers`. Cases run concurrently;
|
|
987
|
+
* give each its own thread/key inside `produce` if the producer is stateful.
|
|
988
|
+
* Returns per-case results plus the mean of their averages.
|
|
989
|
+
*/
|
|
990
|
+
declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<ProducedOutput | string> | ProducedOutput | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
|
|
991
|
+
/** Where a retrieval scorer reads its two id lists from. */
|
|
992
|
+
interface RetrievalScorerOptions {
|
|
993
|
+
/** Metadata key holding the gold relevant ids. Default `"relevant"`. */
|
|
994
|
+
relevantKey?: string;
|
|
995
|
+
/** Metadata key holding the run's ranked retrieved ids. Default `"retrieved"`. */
|
|
996
|
+
retrievedKey?: string;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Recall@k scorer — what fraction of the gold passages made it into the top
|
|
1000
|
+
* `k`.
|
|
1001
|
+
*
|
|
1002
|
+
* This is the ceiling on everything downstream: a passage retrieval never
|
|
1003
|
+
* returned is one no reranker can promote and no prompt can cite. Omit `k` to
|
|
1004
|
+
* score the whole retrieved list.
|
|
1005
|
+
*/
|
|
1006
|
+
declare const recallAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
|
|
1007
|
+
/**
|
|
1008
|
+
* Precision@k scorer — what fraction of the top `k` retrieved passages are
|
|
1009
|
+
* gold.
|
|
1010
|
+
*
|
|
1011
|
+
* This is the counterweight to recall: padding `topK` raises recall for free
|
|
1012
|
+
* while burying the answer in noise the model has to read past, and pay for.
|
|
1013
|
+
* Omit `k` to score the whole retrieved list.
|
|
1014
|
+
*/
|
|
1015
|
+
declare const precisionAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
|
|
1016
|
+
/**
|
|
1017
|
+
* Mean Reciprocal Rank scorer — `1 / rank` of the first gold passage: 1 if it
|
|
1018
|
+
* is first, 0.5 if second, 0 if absent.
|
|
1019
|
+
*
|
|
1020
|
+
* This is the metric that notices ordering, which recall cannot. A gold passage
|
|
1021
|
+
* at rank 20 counts the same as rank 1 for recall@20, but only one of those
|
|
1022
|
+
* survives a context-window trim or a model that skims the top of its prompt.
|
|
1023
|
+
*/
|
|
1024
|
+
declare const mrrScorer: (options?: RetrievalScorerOptions) => Scorer;
|
|
1025
|
+
/**
|
|
1026
|
+
* Normalized Discounted Cumulative Gain scorer — relevance discounted
|
|
1027
|
+
* logarithmically by rank, divided by the best achievable arrangement.
|
|
1028
|
+
*
|
|
1029
|
+
* This is the one to gate on when comparing retrieval strategies. Unlike recall
|
|
1030
|
+
* it is sensitive to order, and unlike MRR it credits every gold passage rather
|
|
1031
|
+
* than only the first — so it is the metric that can actually say whether a
|
|
1032
|
+
* reranker or a hybrid leg helped.
|
|
1033
|
+
*
|
|
1034
|
+
* Relevance is binary: an id is gold or it is not, which is what a gold-id set
|
|
1035
|
+
* expresses. Graded relevance would need per-id weights.
|
|
1036
|
+
*/
|
|
1037
|
+
declare const ndcgAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
|
|
1038
|
+
/**
|
|
1039
|
+
* Groundedness scorer — does the answer assert only what the retrieved context
|
|
1040
|
+
* supports?
|
|
1041
|
+
*
|
|
1042
|
+
* This is the generation-side counterpart to the metrics above: perfect
|
|
1043
|
+
* retrieval still fails if the model answers from its own weights. It scores
|
|
1044
|
+
* the output against the context via an injected `judge`, the same shape
|
|
1045
|
+
* `llmScorer` takes, so this stays model-agnostic and mockable.
|
|
1046
|
+
*
|
|
1047
|
+
* Reads the context from `metadata.context` by default — the `context` string
|
|
1048
|
+
* `retrieve()` already returns. Fails closed: no context means nothing could
|
|
1049
|
+
* have grounded the answer.
|
|
1050
|
+
*/
|
|
1051
|
+
declare const groundednessScorer: (options: {
|
|
1052
|
+
contextKey?: string;
|
|
1053
|
+
judge: (prompt: string) => Promise<string>;
|
|
1054
|
+
name?: string;
|
|
1055
|
+
}) => Scorer;
|
|
1056
|
+
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|