@lunora/testing 1.0.0-alpha.112 → 1.0.0-alpha.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +64 -22
- package/dist/index.d.ts +64 -22
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/evaluationAttributes-CYLgTP1P.mjs +1 -0
- package/dist/packem_shared/lunoraTest-UV1TxREP.mjs +1 -0
- package/package.json +6 -5
- package/dist/packem_shared/evaluationAttributes-BA1uUP-z.mjs +0 -1
- package/dist/packem_shared/lunoraTest-CC-mCvor.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -140,7 +140,26 @@ declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOption
|
|
|
140
140
|
declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
|
|
141
141
|
/** A single-tool-call turn: the loop runs the named tool with `input`. */
|
|
142
142
|
declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
|
|
143
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Shared, bundler-inlined builder for the OpenTelemetry `gen_ai.evaluation.*`
|
|
145
|
+
* attributes an AI **evaluation** verdict (a scorer's `{name, score, label?}`)
|
|
146
|
+
* contributes to a **generation span**.
|
|
147
|
+
*
|
|
148
|
+
* This is the emit-time counterpart of the cloud OTLP decoder, which reads
|
|
149
|
+
* `gen_ai.evaluation.<name>.score` (number) and optional
|
|
150
|
+
* `gen_ai.evaluation.<name>.label` (string) attribute pairs back off a generation
|
|
151
|
+
* span (`EVALUATION_PREFIX = "gen_ai.evaluation."`). The framework emits exactly
|
|
152
|
+
* that pair here so a score rides the same trace as the generation it grades.
|
|
153
|
+
*
|
|
154
|
+
* It lives in `shared/` because more than one layer needs the identical wire
|
|
155
|
+
* format with no runtime dependency edge between them: `@lunora/do` builds it into
|
|
156
|
+
* a live `ctx.trace` span's post-hoc attributes (`SpanHandle.recordEvaluation`),
|
|
157
|
+
* and `@lunora/server` mirrors the handle shape structurally. `@lunora/testing`
|
|
158
|
+
* ships a parallel test-time helper (`recordEvaluation` / `evaluationAttributes`)
|
|
159
|
+
* that targets the same `gen_ai.evaluation.*` contract for span-less eval
|
|
160
|
+
* events/metrics. Keep this file genuinely zero-dependency so inlining stays sound.
|
|
161
|
+
*/
|
|
162
|
+
/** The primitive an eval contributes to a span: a numeric score or a string label. */
|
|
144
163
|
type EvaluationAttributeValue = number | string;
|
|
145
164
|
/**
|
|
146
165
|
* Structural slice of the post-hoc span handle `ctx.trace` hands its body (see the
|
|
@@ -194,6 +213,11 @@ interface RecordEvaluationInput {
|
|
|
194
213
|
* Build the `gen_ai.evaluation.NAME.*` attribute bag for one eval verdict — the
|
|
195
214
|
* `.score` (number) always, the `.label` (string) when a label is given. Exported
|
|
196
215
|
* so a caller can emit the score as a standalone event/metric without a span.
|
|
216
|
+
*
|
|
217
|
+
* Delegates to the shared `shared/evaluation-attributes.ts` builder so
|
|
218
|
+
* `@lunora/testing`'s scorers and the runtime's own `recordEvaluation` emit the
|
|
219
|
+
* identical wire format — only the thrown error type differs, wrapped here as a
|
|
220
|
+
* `LunoraError` to match this package's public error contract.
|
|
197
221
|
*/
|
|
198
222
|
declare const evaluationAttributes: (input: Pick<RecordEvaluationInput, "label" | "name" | "score">) => Record<string, EvaluationAttributeValue>;
|
|
199
223
|
/**
|
|
@@ -210,11 +234,14 @@ interface FakeScheduledJob extends ScheduledJob {
|
|
|
210
234
|
args: Record<string, unknown>;
|
|
211
235
|
}
|
|
212
236
|
/**
|
|
213
|
-
* A single scheduled-job failure
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
237
|
+
* A single scheduled-job failure that exhausted its retry budget, captured
|
|
238
|
+
* during an `advance()` / `runPending()` sweep. Mirrors `SchedulerDO`'s
|
|
239
|
+
* dead-letter park (`recordRetry()`, `packages/scheduler/src/scheduler-do.ts:875-909`):
|
|
240
|
+
* a job that fails while it still has retries left is silently re-enqueued
|
|
241
|
+
* with backoff and is NOT recorded here. Only once a job's `attempts` exceeds
|
|
242
|
+
* `@lunora/scheduler`'s `MAX_RETRY_ATTEMPTS` does it land here — being a test
|
|
243
|
+
* harness, the fake scheduler never swallows a terminal failure, so tests can
|
|
244
|
+
* still assert on it.
|
|
218
245
|
*/
|
|
219
246
|
interface ScheduledJobFailure {
|
|
220
247
|
/** The args the job was dispatched with. */
|
|
@@ -235,40 +262,55 @@ interface FakeSchedulerControls {
|
|
|
235
262
|
* Advance the virtual clock by `ms` milliseconds, executing all jobs whose
|
|
236
263
|
* `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
|
|
237
264
|
* are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
|
|
238
|
-
* (scheduled by an executed job
|
|
239
|
-
* the same advance call — callers should
|
|
265
|
+
* (scheduled by an executed job, or re-enqueued as a retry, during the
|
|
266
|
+
* advance) are NOT re-evaluated in the same advance call — callers should
|
|
267
|
+
* advance again if needed.
|
|
240
268
|
*
|
|
241
269
|
* Per-job failures are isolated (matching production): a job that throws does
|
|
242
|
-
* NOT prevent the remaining due jobs from running.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
270
|
+
* NOT prevent the remaining due jobs from running. A failure with retries
|
|
271
|
+
* left is silently re-enqueued with exponential backoff on the virtual clock
|
|
272
|
+
* — mirroring `SchedulerDO`'s default retry policy (`MAX_RETRY_ATTEMPTS`
|
|
273
|
+
* retries, `RETRY_BASE_DELAY_MS` base delay, doubling; both imported from
|
|
274
|
+
* `@lunora/scheduler`) — rather than surfaced. Advancing far enough to
|
|
275
|
+
* observe a terminal failure therefore costs the WHOLE backoff schedule
|
|
276
|
+
* (30s + 60s + 120s + 240s + 480s = 930s of virtual clock at today's
|
|
277
|
+
* defaults), not a single tick.
|
|
278
|
+
* Only once a job's retry budget is exhausted is the failure surfaced: it is
|
|
279
|
+
* recorded on {@link FakeSchedulerControls.failures} and, by default,
|
|
280
|
+
* re-thrown so a test still sees the error. A single such failure is
|
|
281
|
+
* re-thrown verbatim; multiple are aggregated into an `AggregateError`. Pass
|
|
247
282
|
* `{ throwOnError: false }` to suppress the re-throw and inspect
|
|
248
283
|
* {@link FakeSchedulerControls.failures} (and the returned count) instead.
|
|
249
284
|
*
|
|
250
|
-
* Returns the number of jobs
|
|
285
|
+
* Returns the number of jobs dispatched this sweep, including ones that
|
|
286
|
+
* failed and were silently retried, and ones that failed terminally.
|
|
251
287
|
*/
|
|
252
288
|
advance: (ms: number, options?: SweepOptions) => Promise<number>;
|
|
253
289
|
/**
|
|
254
|
-
* All scheduled-job failures
|
|
255
|
-
* every `advance()` / `runPending()` call on this
|
|
256
|
-
*
|
|
257
|
-
*
|
|
290
|
+
* All scheduled-job failures that exhausted their retry budget, in
|
|
291
|
+
* execution order, across every `advance()` / `runPending()` call on this
|
|
292
|
+
* harness. A failure with retries remaining is NOT recorded here — see
|
|
293
|
+
* {@link ScheduledJobFailure}. Always available, even when
|
|
294
|
+
* `throwOnError: false` suppressed the re-throw. The list is a snapshot —
|
|
295
|
+
* mutating it does not affect the scheduler.
|
|
258
296
|
*/
|
|
259
297
|
failures: () => ScheduledJobFailure[];
|
|
260
298
|
/**
|
|
261
299
|
* List all pending jobs (those not yet executed or cancelled) in the order
|
|
262
|
-
* they were enqueued.
|
|
300
|
+
* they were enqueued. A job currently waiting out its retry backoff is
|
|
301
|
+
* still pending (visible here with its `attempts` count incremented and
|
|
302
|
+
* `scheduledFor` pushed out) until its budget is exhausted.
|
|
263
303
|
*/
|
|
264
304
|
list: () => FakeScheduledJob[];
|
|
265
305
|
/**
|
|
266
306
|
* Execute all currently pending jobs regardless of their `scheduledFor`
|
|
267
307
|
* time. Equivalent to advancing to `Infinity`. Returns the number of jobs
|
|
268
|
-
*
|
|
308
|
+
* dispatched this sweep (including failed ones, retried or terminal).
|
|
269
309
|
*
|
|
270
|
-
* Failure isolation and surfacing match
|
|
271
|
-
* one failing job does not abort the
|
|
310
|
+
* Failure isolation, retry, and surfacing match
|
|
311
|
+
* {@link FakeSchedulerControls.advance}: one failing job does not abort the
|
|
312
|
+
* rest, a failure under the retry budget is silently re-enqueued rather
|
|
313
|
+
* than surfaced, and only a terminal failure is recorded on
|
|
272
314
|
* {@link FakeSchedulerControls.failures} and re-thrown unless
|
|
273
315
|
* `{ throwOnError: false }` is passed.
|
|
274
316
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -140,7 +140,26 @@ declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOption
|
|
|
140
140
|
declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
|
|
141
141
|
/** A single-tool-call turn: the loop runs the named tool with `input`. */
|
|
142
142
|
declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
|
|
143
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Shared, bundler-inlined builder for the OpenTelemetry `gen_ai.evaluation.*`
|
|
145
|
+
* attributes an AI **evaluation** verdict (a scorer's `{name, score, label?}`)
|
|
146
|
+
* contributes to a **generation span**.
|
|
147
|
+
*
|
|
148
|
+
* This is the emit-time counterpart of the cloud OTLP decoder, which reads
|
|
149
|
+
* `gen_ai.evaluation.<name>.score` (number) and optional
|
|
150
|
+
* `gen_ai.evaluation.<name>.label` (string) attribute pairs back off a generation
|
|
151
|
+
* span (`EVALUATION_PREFIX = "gen_ai.evaluation."`). The framework emits exactly
|
|
152
|
+
* that pair here so a score rides the same trace as the generation it grades.
|
|
153
|
+
*
|
|
154
|
+
* It lives in `shared/` because more than one layer needs the identical wire
|
|
155
|
+
* format with no runtime dependency edge between them: `@lunora/do` builds it into
|
|
156
|
+
* a live `ctx.trace` span's post-hoc attributes (`SpanHandle.recordEvaluation`),
|
|
157
|
+
* and `@lunora/server` mirrors the handle shape structurally. `@lunora/testing`
|
|
158
|
+
* ships a parallel test-time helper (`recordEvaluation` / `evaluationAttributes`)
|
|
159
|
+
* that targets the same `gen_ai.evaluation.*` contract for span-less eval
|
|
160
|
+
* events/metrics. Keep this file genuinely zero-dependency so inlining stays sound.
|
|
161
|
+
*/
|
|
162
|
+
/** The primitive an eval contributes to a span: a numeric score or a string label. */
|
|
144
163
|
type EvaluationAttributeValue = number | string;
|
|
145
164
|
/**
|
|
146
165
|
* Structural slice of the post-hoc span handle `ctx.trace` hands its body (see the
|
|
@@ -194,6 +213,11 @@ interface RecordEvaluationInput {
|
|
|
194
213
|
* Build the `gen_ai.evaluation.NAME.*` attribute bag for one eval verdict — the
|
|
195
214
|
* `.score` (number) always, the `.label` (string) when a label is given. Exported
|
|
196
215
|
* so a caller can emit the score as a standalone event/metric without a span.
|
|
216
|
+
*
|
|
217
|
+
* Delegates to the shared `shared/evaluation-attributes.ts` builder so
|
|
218
|
+
* `@lunora/testing`'s scorers and the runtime's own `recordEvaluation` emit the
|
|
219
|
+
* identical wire format — only the thrown error type differs, wrapped here as a
|
|
220
|
+
* `LunoraError` to match this package's public error contract.
|
|
197
221
|
*/
|
|
198
222
|
declare const evaluationAttributes: (input: Pick<RecordEvaluationInput, "label" | "name" | "score">) => Record<string, EvaluationAttributeValue>;
|
|
199
223
|
/**
|
|
@@ -210,11 +234,14 @@ interface FakeScheduledJob extends ScheduledJob {
|
|
|
210
234
|
args: Record<string, unknown>;
|
|
211
235
|
}
|
|
212
236
|
/**
|
|
213
|
-
* A single scheduled-job failure
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
237
|
+
* A single scheduled-job failure that exhausted its retry budget, captured
|
|
238
|
+
* during an `advance()` / `runPending()` sweep. Mirrors `SchedulerDO`'s
|
|
239
|
+
* dead-letter park (`recordRetry()`, `packages/scheduler/src/scheduler-do.ts:875-909`):
|
|
240
|
+
* a job that fails while it still has retries left is silently re-enqueued
|
|
241
|
+
* with backoff and is NOT recorded here. Only once a job's `attempts` exceeds
|
|
242
|
+
* `@lunora/scheduler`'s `MAX_RETRY_ATTEMPTS` does it land here — being a test
|
|
243
|
+
* harness, the fake scheduler never swallows a terminal failure, so tests can
|
|
244
|
+
* still assert on it.
|
|
218
245
|
*/
|
|
219
246
|
interface ScheduledJobFailure {
|
|
220
247
|
/** The args the job was dispatched with. */
|
|
@@ -235,40 +262,55 @@ interface FakeSchedulerControls {
|
|
|
235
262
|
* Advance the virtual clock by `ms` milliseconds, executing all jobs whose
|
|
236
263
|
* `scheduledFor` timestamp is now at or before the new virtual "now". Jobs
|
|
237
264
|
* are dispatched in `scheduledFor` order (oldest first). Newly queued jobs
|
|
238
|
-
* (scheduled by an executed job
|
|
239
|
-
* the same advance call — callers should
|
|
265
|
+
* (scheduled by an executed job, or re-enqueued as a retry, during the
|
|
266
|
+
* advance) are NOT re-evaluated in the same advance call — callers should
|
|
267
|
+
* advance again if needed.
|
|
240
268
|
*
|
|
241
269
|
* Per-job failures are isolated (matching production): a job that throws does
|
|
242
|
-
* NOT prevent the remaining due jobs from running.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
270
|
+
* NOT prevent the remaining due jobs from running. A failure with retries
|
|
271
|
+
* left is silently re-enqueued with exponential backoff on the virtual clock
|
|
272
|
+
* — mirroring `SchedulerDO`'s default retry policy (`MAX_RETRY_ATTEMPTS`
|
|
273
|
+
* retries, `RETRY_BASE_DELAY_MS` base delay, doubling; both imported from
|
|
274
|
+
* `@lunora/scheduler`) — rather than surfaced. Advancing far enough to
|
|
275
|
+
* observe a terminal failure therefore costs the WHOLE backoff schedule
|
|
276
|
+
* (30s + 60s + 120s + 240s + 480s = 930s of virtual clock at today's
|
|
277
|
+
* defaults), not a single tick.
|
|
278
|
+
* Only once a job's retry budget is exhausted is the failure surfaced: it is
|
|
279
|
+
* recorded on {@link FakeSchedulerControls.failures} and, by default,
|
|
280
|
+
* re-thrown so a test still sees the error. A single such failure is
|
|
281
|
+
* re-thrown verbatim; multiple are aggregated into an `AggregateError`. Pass
|
|
247
282
|
* `{ throwOnError: false }` to suppress the re-throw and inspect
|
|
248
283
|
* {@link FakeSchedulerControls.failures} (and the returned count) instead.
|
|
249
284
|
*
|
|
250
|
-
* Returns the number of jobs
|
|
285
|
+
* Returns the number of jobs dispatched this sweep, including ones that
|
|
286
|
+
* failed and were silently retried, and ones that failed terminally.
|
|
251
287
|
*/
|
|
252
288
|
advance: (ms: number, options?: SweepOptions) => Promise<number>;
|
|
253
289
|
/**
|
|
254
|
-
* All scheduled-job failures
|
|
255
|
-
* every `advance()` / `runPending()` call on this
|
|
256
|
-
*
|
|
257
|
-
*
|
|
290
|
+
* All scheduled-job failures that exhausted their retry budget, in
|
|
291
|
+
* execution order, across every `advance()` / `runPending()` call on this
|
|
292
|
+
* harness. A failure with retries remaining is NOT recorded here — see
|
|
293
|
+
* {@link ScheduledJobFailure}. Always available, even when
|
|
294
|
+
* `throwOnError: false` suppressed the re-throw. The list is a snapshot —
|
|
295
|
+
* mutating it does not affect the scheduler.
|
|
258
296
|
*/
|
|
259
297
|
failures: () => ScheduledJobFailure[];
|
|
260
298
|
/**
|
|
261
299
|
* List all pending jobs (those not yet executed or cancelled) in the order
|
|
262
|
-
* they were enqueued.
|
|
300
|
+
* they were enqueued. A job currently waiting out its retry backoff is
|
|
301
|
+
* still pending (visible here with its `attempts` count incremented and
|
|
302
|
+
* `scheduledFor` pushed out) until its budget is exhausted.
|
|
263
303
|
*/
|
|
264
304
|
list: () => FakeScheduledJob[];
|
|
265
305
|
/**
|
|
266
306
|
* Execute all currently pending jobs regardless of their `scheduledFor`
|
|
267
307
|
* time. Equivalent to advancing to `Infinity`. Returns the number of jobs
|
|
268
|
-
*
|
|
308
|
+
* dispatched this sweep (including failed ones, retried or terminal).
|
|
269
309
|
*
|
|
270
|
-
* Failure isolation and surfacing match
|
|
271
|
-
* one failing job does not abort the
|
|
310
|
+
* Failure isolation, retry, and surfacing match
|
|
311
|
+
* {@link FakeSchedulerControls.advance}: one failing job does not abort the
|
|
312
|
+
* rest, a failure under the retry budget is silently re-enqueued rather
|
|
313
|
+
* than surfaced, and only a terminal failure is recorded on
|
|
272
314
|
* {@link FakeSchedulerControls.failures} and re-thrown unless
|
|
273
315
|
* `{ throwOnError: false }` is passed.
|
|
274
316
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-
|
|
1
|
+
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{lunoraTest as s}from"./packem_shared/lunoraTest-UV1TxREP.mjs";import{groundednessScorer as p,mrrScorer as u,ndcgAtK as x,precisionAtK as S,recallAtK as f}from"./packem_shared/groundednessScorer-CznJBIjn.mjs";import{containsScorer as g,evaluate as A,exactMatchScorer as v,keywordScorer as K,llmScorer as M,regexScorer as T,scoreSample as k}from"./packem_shared/containsScorer-gv0Sdd86.mjs";import{extractLink as C,listCapturedMail as b,waitForMail as h}from"@lunora/mail/testing";export{o as agentHarness,g as containsScorer,A as evaluate,l as evaluationAttributes,v as exactMatchScorer,C as extractLink,t as finalTurn,p as groundednessScorer,K as keywordScorer,b as listCapturedMail,M as llmScorer,s as lunoraTest,u as mrrScorer,x as ndcgAtK,S as precisionAtK,f as recallAtK,n as recordEvaluation,T as regexScorer,k as scoreSample,a as toolCallTurn,h as waitForMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";const n=/[\w.-]/u,t=e=>{let r="";for(const o of e)r+=n.test(o)?o:"_";return r},s=e=>{if(typeof e.name!="string"||e.name.length===0)throw new TypeError("recordEvaluation requires a non-empty `name`");if(typeof e.score!="number"||!Number.isFinite(e.score))throw new TypeError("recordEvaluation `score` must be a finite number");const r=t(e.name),o={[`gen_ai.evaluation.${r}.score`]:e.score};return e.label!==void 0&&(o[`gen_ai.evaluation.${r}.label`]=e.label),o},c=e=>{try{return s(e)}catch(r){throw r instanceof TypeError?new a("BAD_REQUEST",`@lunora/testing: ${r.message}`):r}},l=e=>{const r=c(e);return e.span?.setAttributes(r),e.metrics?.gauge(`gen_ai.evaluation.${t(e.name)}.score`,e.score,e.label===void 0?void 0:{label:e.label}),r};export{c as evaluationAttributes,l as recordEvaluation};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as _}from"@lunora/errors";import{runShardMigrations as O,createShardCtxDb as z,RLS_UNWRAP_SYMBOL as Z}from"@lunora/shard-engine";import{evaluationAttributes as tt}from"./evaluationAttributes-CYLgTP1P.mjs";import{MAX_RETRY_ATTEMPTS as et,RETRY_BASE_DELAY_MS as nt}from"@lunora/scheduler";import{DatabaseSync as ot}from"node:sqlite";const rt=(r,c,s,u,g)=>{let p=g,y=1;const d=new Map,f=[],h=(e,l,a={})=>{const t=`fake-job-${String(y)}`;return y+=1,d.set(t,{args:a,enqueuedAt:p,functionPath:l,id:t,scheduledFor:e}),t},b=e=>typeof e=="string"?e:e.name??e.binding??"",E={cancel:e=>{const l=d.has(e);return d.delete(e),Promise.resolve({cancelled:l})},get:e=>Promise.resolve(d.get(e)??null),list:()=>Promise.resolve([...d.values()]),runAfter:(e,l,a)=>{const t=h(p+e,b(l),a);return Promise.resolve(t)},runAt:(e,l,a)=>{const t=h(e,b(l),a);return Promise.resolve(t)}},k=async e=>{d.delete(e.id);const a=u().get(e.functionPath);if(a===void 0){console.warn(`[fake-scheduler] unknown functionPath "${e.functionPath}" — job ${e.id} dropped`);return}if(a.kind==="mutation"||a.kind==="action"){const t=r(),n=a.kind==="action"?s():c();await t(a.kind,a,n,e.args)}else console.warn(`[fake-scheduler] functionPath "${e.functionPath}" is a ${a.kind} — only mutations and actions can be scheduled; job ${e.id} dropped`)},M=async e=>{const l=[...d.values()].filter(n=>n.scheduledFor<=e).toSorted((n,v)=>n.scheduledFor-v.scheduledFor),a=[];let t=0;for(const n of l)if(d.has(n.id)){t+=1;try{await k(n)}catch(v){const T=(n.attempts??0)+1;if(T>et){const N={args:n.args,error:v,functionPath:n.functionPath,id:n.id};a.push(N),f.push(N)}else{const N=nt*2**(T-1);d.set(n.id,{...n,attempts:T,scheduledFor:p+N})}}}return{executed:t,failed:a}},C=async(e,l)=>{const{executed:a,failed:t}=await M(e);if(t.length>0&&(l?.throwOnError??!0)){const[n]=t;throw t.length===1&&n!==void 0?n.error:new AggregateError(t.map(v=>v.error),`${String(t.length)} scheduled jobs failed: ${t.map(v=>v.functionPath).join(", ")}`)}return a};return{controls:{advance:(e,l)=>(p+=e,C(p,l)),failures:()=>[...f],list:()=>[...d.values()],runPending:e=>C(Number.POSITIVE_INFINITY,e)},scheduler:E}},st=()=>{const r=new ot(":memory:"),c=u=>({one(){if(u.length!==1)throw new _("INTERNAL",`expected exactly one row, received ${String(u.length)}`);const[g]=u;return g},[Symbol.iterator](){return u[Symbol.iterator]()},toArray(){return u}});return{close:()=>{r.close()},sql:{exec:(u,...g)=>{const y=r.prepare(u).all(...g);return c(y)}}}},q=r=>{if(typeof r!="object"||r===null)return;const{kind:c}=r;if(c==="query"||c==="mutation"||c==="action")return c},it=r=>typeof r=="object"&&r!==null&&r.visibility==="internal"?"internal":"public",Q=r=>{throw new _("INTERNAL",`ctx.${r} is not available in the in-memory @lunora/testing harness (v1)`)},w=r=>new Proxy((...c)=>Q(r),{apply:()=>Q(r),get:()=>Q(r)}),K={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},ct={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>K},at=()=>{const r={attributes:{},events:[],links:[]},c={addEvent:(s,u)=>{r.events.push({...u===void 0?{}:{attributes:{...u}},name:s})},addLink:s=>{r.links.push({spanId:s.spanId,traceId:s.traceId})},recordEvaluation:s=>{Object.assign(r.attributes,tt(s))},recordException:s=>{const u={"exception.message":s instanceof Error?s.message:String(s),"exception.type":s instanceof Error?s.constructor.name:"Error"};s instanceof Error&&s.stack!==void 0&&(u["exception.stacktrace"]=s.stack),c.addEvent("exception",u)},setAttribute:(s,u)=>{r.attributes[s]=u},setAttributes:s=>{Object.assign(r.attributes,s)},spanContext:()=>K};return{handle:c,recorded:r}},D=async(r,c)=>await c(D,ct),Y={count:()=>{},gauge:()=>{},record:()=>{}},j={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>j},ut=(r,c,s)=>(g,p)=>{let y=!1;const d=[];let f,h,b=0,E=0;const k=()=>q(g)?r("query",g,c,p,!1):Promise.resolve(g(c)),M=(t,n)=>{if(t<E)return;E=t;const v={done:!1,value:n};if(d.length===0)f=v,h=void 0;else{f=void 0,h=void 0;for(const T of d.splice(0))T.resolve(v)}},C=(t,n)=>{if(!(t<E))if(E=t,d.length===0)h={error:n},f=void 0;else{f=void 0,h=void 0;for(const v of d.splice(0))v.reject(n)}},R=t=>n=>{M(t,n)},e=t=>n=>{C(t,n)},l=()=>{if(y)return;b+=1;const t=b;k().then(R(t)).catch(e(t))};s.add(l);const a={[Symbol.asyncIterator](){return a},next:()=>{if(y)return Promise.resolve({done:!0,value:void 0});if(E===b){if(h!==void 0){const{error:t}=h;return h=void 0,Promise.reject(t)}if(f!==void 0){const t=f;return f=void 0,Promise.resolve(t)}}return E<b?new Promise((t,n)=>{d.push({reject:n,resolve:t})}):k().then(t=>{if(h!==void 0){const{error:n}=h;throw h=void 0,n}if(f!==void 0){const n=f;return f=void 0,n}return{done:!1,value:t}})},return:()=>{y=!0,s.delete(l);for(const t of d.splice(0))t.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return k().then(R(0)).catch(e(0)),a},pt=(r,c)=>{const{close:s,sql:u}=st(),g=r;O(u,g);const p=z({enforceRls:c?.enforceRls??!0,schema:g,sql:u}),y=p[Z]??p,d=m=>{u.exec.call(u,m)};let f=Promise.resolve();const h=m=>{const P=async()=>{d("BEGIN");try{const S=await m();return d("COMMIT"),S}catch(S){try{d("ROLLBACK")}catch{}throw S}},x=f.then(P);return f=x.then(()=>{},()=>{}),x};let b=!1;const E=()=>{b||(b=!0,s())},k=new Map(Object.entries(c?.functions??{}).map(([m,P])=>[m,P])),M=new Set,C=()=>{for(const m of M)m()},R=m=>(C(),m);let e,l,a;const t=c?.now??Date.now(),n=at(),v=(m,P)=>{if(m===void 0)throw new _("INTERNAL",`[fake-scheduler] ${P} not yet available — scheduler.advance called before harness construction completed`);return m},{controls:T,scheduler:N}=rt(()=>v(e,"dispatch"),()=>v(l,"mutationContext"),()=>v(a,"actionContext"),()=>k,t),B=m=>{const P={getIdentity:()=>Promise.resolve(m??null),userId:m?.userId??null},x={auth:P,db:p,env:c?.env,log:j,metrics:Y,now:t,span:n.handle,trace:D,runQuery:((o,i)=>I("query",o,x,i)),secrets:w("secrets"),storage:w("storage"),vectors:w("vectors")},S={auth:P,db:p,env:c?.env,log:j,metrics:Y,now:t,span:n.handle,trace:D,runMutation:((o,i)=>I("mutation",o,S,i)),runQuery:((o,i)=>I("query",o,x,i)),scheduler:N,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};l??=S;const U={...S,db:y},F={auth:P,db:p,env:c?.env,fetch:c?.fetch??w("fetch"),log:j,metrics:Y,now:t,span:n.handle,trace:D,runAction:((o,i)=>I("action",o,F,i)),runMutation:((o,i)=>I("mutation",o,S,i)),runQuery:((o,i)=>I("query",o,x,i)),scheduler:N,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};a??=F;const $=(o,i,A,L,J)=>{const H=q(i);if(H!==o)throw new _("INTERNAL",`expected a registered ${o}, received a ${H??"non-function"} reference`);if(!J&&it(i)==="internal")throw new _("INTERNAL",`This ${o} is an internal function — it is unreachable from the external RPC boundary in production. Call it through ctx.run${o.charAt(0).toUpperCase()}${o.slice(1)} from another function instead.`);return Promise.resolve(i.handler(A,L??{}))},I=(o,i,A,L)=>$(o,i,A,L,!0);e??=(o,i,A,L)=>o==="mutation"?h(()=>$("mutation",i,A,L,!0)).then(R):I("action",i,A,L);const V=((o,i)=>q(o)?$("query",o,x,i,!1):Promise.resolve(o(x))),W=((o,i)=>{const A=q(o)?()=>$("mutation",o,S,i,!1):()=>o(S);return h(A).then(R)}),X=((o,i)=>q(o)?$("action",o,F,i,!1):Promise.resolve(o(F))),G=ut($,x,M);return{action:X,close:E,mutation:W,query:V,run:o=>h(()=>o(U)).then(R),scheduler:T,subscribe:G,wideEvent:()=>n.recorded,withIdentity:o=>B(o)}};return B(null)};export{pt as lunoraTest};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/testing",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.114",
|
|
4
4
|
"description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,11 +50,12 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/agent": "1.0.0-alpha.
|
|
53
|
+
"@lunora/agent": "1.0.0-alpha.59",
|
|
54
54
|
"@lunora/errors": "1.0.0-alpha.22",
|
|
55
|
-
"@lunora/mail": "1.0.0-alpha.
|
|
56
|
-
"@lunora/
|
|
57
|
-
"@lunora/
|
|
55
|
+
"@lunora/mail": "1.0.0-alpha.51",
|
|
56
|
+
"@lunora/scheduler": "1.0.0-alpha.34",
|
|
57
|
+
"@lunora/server": "1.0.0-alpha.78",
|
|
58
|
+
"@lunora/shard-engine": "1.0.0-alpha.32"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"@playwright/test": "^1.61.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as a}from"@lunora/errors";const n=/[\w.-]/u,t=e=>{let o="";for(const r of e)o+=n.test(r)?r:"_";return o},s=e=>{if(typeof e.name!="string"||e.name.length===0)throw new a("BAD_REQUEST","@lunora/testing: recordEvaluation requires a non-empty `name`");if(typeof e.score!="number"||!Number.isFinite(e.score))throw new a("BAD_REQUEST","@lunora/testing: recordEvaluation `score` must be a finite number");const o=t(e.name),r={[`gen_ai.evaluation.${o}.score`]:e.score};return e.label!==void 0&&(r[`gen_ai.evaluation.${o}.label`]=e.label),r},c=e=>{const o=s(e);return e.span?.setAttributes(o),e.metrics?.gauge(`gen_ai.evaluation.${t(e.name)}.score`,e.score,e.label===void 0?void 0:{label:e.label}),o};export{s as evaluationAttributes,c as recordEvaluation};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as M}from"@lunora/errors";import{runShardMigrations as X,createShardCtxDb as z,RLS_UNWRAP_SYMBOL as Z}from"@lunora/shard-engine";import{evaluationAttributes as tt}from"./evaluationAttributes-BA1uUP-z.mjs";import{DatabaseSync as et}from"node:sqlite";const nt=(o,c,s,u,g)=>{let p=g,y=1;const d=new Map,f=[],h=(e,l,a={})=>{const t=`fake-job-${String(y)}`;return y+=1,d.set(t,{args:a,enqueuedAt:p,functionPath:l,id:t,scheduledFor:e}),t},b=e=>typeof e=="string"?e:e.name??e.binding??"",P={cancel:e=>{const l=d.has(e);return d.delete(e),Promise.resolve({cancelled:l})},get:e=>Promise.resolve(d.get(e)??null),list:()=>Promise.resolve([...d.values()]),runAfter:(e,l,a)=>{const t=h(p+e,b(l),a);return Promise.resolve(t)},runAt:(e,l,a)=>{const t=h(e,b(l),a);return Promise.resolve(t)}},A=async e=>{d.delete(e.id);const a=u().get(e.functionPath);if(a===void 0){console.warn(`[fake-scheduler] unknown functionPath "${e.functionPath}" — job ${e.id} dropped`);return}if(a.kind==="mutation"||a.kind==="action"){const t=o(),r=a.kind==="action"?s():c();await t(a.kind,a,r,e.args)}else console.warn(`[fake-scheduler] functionPath "${e.functionPath}" is a ${a.kind} — only mutations and actions can be scheduled; job ${e.id} dropped`)},I=async e=>{const l=[...d.values()].filter(r=>r.scheduledFor<=e).toSorted((r,v)=>r.scheduledFor-v.scheduledFor),a=[];let t=0;for(const r of l)if(d.has(r.id)){t+=1;try{await A(r)}catch(v){const $={args:r.args,error:v,functionPath:r.functionPath,id:r.id};a.push($),f.push($)}}return{executed:t,failed:a}},C=async(e,l)=>{const{executed:a,failed:t}=await I(e);if(t.length>0&&(l?.throwOnError??!0)){const[r]=t;throw t.length===1&&r!==void 0?r.error:new AggregateError(t.map(v=>v.error),`${String(t.length)} scheduled jobs failed: ${t.map(v=>v.functionPath).join(", ")}`)}return a};return{controls:{advance:(e,l)=>(p+=e,C(p,l)),failures:()=>[...f],list:()=>[...d.values()],runPending:e=>C(Number.POSITIVE_INFINITY,e)},scheduler:P}},rt=()=>{const o=new et(":memory:"),c=u=>({one(){if(u.length!==1)throw new M("INTERNAL",`expected exactly one row, received ${String(u.length)}`);const[g]=u;return g},[Symbol.iterator](){return u[Symbol.iterator]()},toArray(){return u}});return{close:()=>{o.close()},sql:{exec:(u,...g)=>{const y=o.prepare(u).all(...g);return c(y)}}}},L=o=>{if(typeof o!="object"||o===null)return;const{kind:c}=o;if(c==="query"||c==="mutation"||c==="action")return c},ot=o=>typeof o=="object"&&o!==null&&o.visibility==="internal"?"internal":"public",_=o=>{throw new M("INTERNAL",`ctx.${o} is not available in the in-memory @lunora/testing harness (v1)`)},w=o=>new Proxy((...c)=>_(o),{apply:()=>_(o),get:()=>_(o)}),U={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},st={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>U},it=()=>{const o={attributes:{},events:[],links:[]},c={addEvent:(s,u)=>{o.events.push({...u===void 0?{}:{attributes:{...u}},name:s})},addLink:s=>{o.links.push({spanId:s.spanId,traceId:s.traceId})},recordEvaluation:s=>{Object.assign(o.attributes,tt(s))},recordException:s=>{const u={"exception.message":s instanceof Error?s.message:String(s),"exception.type":s instanceof Error?s.constructor.name:"Error"};s instanceof Error&&s.stack!==void 0&&(u["exception.stacktrace"]=s.stack),c.addEvent("exception",u)},setAttribute:(s,u)=>{o.attributes[s]=u},setAttributes:s=>{Object.assign(o.attributes,s)},spanContext:()=>U};return{handle:c,recorded:o}},j=async(o,c)=>await c(j,st),Q={count:()=>{},gauge:()=>{},record:()=>{}},D={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>D},ct=(o,c,s)=>(g,p)=>{let y=!1;const d=[];let f,h,b=0,P=0;const A=()=>L(g)?o("query",g,c,p,!1):Promise.resolve(g(c)),I=(t,r)=>{if(t<P)return;P=t;const v={done:!1,value:r};if(d.length===0)f=v,h=void 0;else{f=void 0,h=void 0;for(const $ of d.splice(0))$.resolve(v)}},C=(t,r)=>{if(!(t<P))if(P=t,d.length===0)h={error:r},f=void 0;else{f=void 0,h=void 0;for(const v of d.splice(0))v.reject(r)}},R=t=>r=>{I(t,r)},e=t=>r=>{C(t,r)},l=()=>{if(y)return;b+=1;const t=b;A().then(R(t)).catch(e(t))};s.add(l);const a={[Symbol.asyncIterator](){return a},next:()=>{if(y)return Promise.resolve({done:!0,value:void 0});if(P===b){if(h!==void 0){const{error:t}=h;return h=void 0,Promise.reject(t)}if(f!==void 0){const t=f;return f=void 0,Promise.resolve(t)}}return P<b?new Promise((t,r)=>{d.push({reject:r,resolve:t})}):A().then(t=>{if(h!==void 0){const{error:r}=h;throw h=void 0,r}if(f!==void 0){const r=f;return f=void 0,r}return{done:!1,value:t}})},return:()=>{y=!0,s.delete(l);for(const t of d.splice(0))t.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return A().then(R(0)).catch(e(0)),a},ft=(o,c)=>{const{close:s,sql:u}=rt(),g=o;X(u,g);const p=z({enforceRls:c?.enforceRls??!0,schema:g,sql:u}),y=p[Z]??p,d=m=>{u.exec.call(u,m)};let f=Promise.resolve();const h=m=>{const x=async()=>{d("BEGIN");try{const S=await m();return d("COMMIT"),S}catch(S){try{d("ROLLBACK")}catch{}throw S}},E=f.then(x);return f=E.then(()=>{},()=>{}),E};let b=!1;const P=()=>{b||(b=!0,s())},A=new Map(Object.entries(c?.functions??{}).map(([m,x])=>[m,x])),I=new Set,C=()=>{for(const m of I)m()},R=m=>(C(),m);let e,l,a;const t=c?.now??Date.now(),r=it(),v=(m,x)=>{if(m===void 0)throw new M("INTERNAL",`[fake-scheduler] ${x} not yet available — scheduler.advance called before harness construction completed`);return m},{controls:$,scheduler:B}=nt(()=>v(e,"dispatch"),()=>v(l,"mutationContext"),()=>v(a,"actionContext"),()=>A,t),H=m=>{const x={getIdentity:()=>Promise.resolve(m??null),userId:m?.userId??null},E={auth:x,db:p,env:c?.env,log:D,metrics:Q,now:t,span:r.handle,trace:j,runQuery:((n,i)=>N("query",n,E,i)),secrets:w("secrets"),storage:w("storage"),vectors:w("vectors")},S={auth:x,db:p,env:c?.env,log:D,metrics:Q,now:t,span:r.handle,trace:j,runMutation:((n,i)=>N("mutation",n,S,i)),runQuery:((n,i)=>N("query",n,E,i)),scheduler:B,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};l??=S;const V={...S,db:y},F={auth:x,db:p,env:c?.env,fetch:c?.fetch??w("fetch"),log:D,metrics:Q,now:t,span:r.handle,trace:j,runAction:((n,i)=>N("action",n,F,i)),runMutation:((n,i)=>N("mutation",n,S,i)),runQuery:((n,i)=>N("query",n,E,i)),scheduler:B,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};a??=F;const T=(n,i,k,q,O)=>{const K=L(i);if(K!==n)throw new M("INTERNAL",`expected a registered ${n}, received a ${K??"non-function"} reference`);if(!O&&ot(i)==="internal")throw new M("INTERNAL",`This ${n} is an internal function — it is unreachable from the external RPC boundary in production. Call it through ctx.run${n.charAt(0).toUpperCase()}${n.slice(1)} from another function instead.`);return Promise.resolve(i.handler(k,q??{}))},N=(n,i,k,q)=>T(n,i,k,q,!0);e??=(n,i,k,q)=>n==="mutation"?h(()=>T("mutation",i,k,q,!0)).then(R):N("action",i,k,q);const W=((n,i)=>L(n)?T("query",n,E,i,!1):Promise.resolve(n(E))),Y=((n,i)=>{const k=L(n)?()=>T("mutation",n,S,i,!1):()=>n(S);return h(k).then(R)}),G=((n,i)=>L(n)?T("action",n,F,i,!1):Promise.resolve(n(F))),J=ct(T,E,I);return{action:G,close:P,mutation:Y,query:W,run:n=>h(()=>n(V)).then(R),scheduler:$,subscribe:J,wideEvent:()=>r.recorded,withIdentity:n=>H(n)}};return H(null)};export{ft as lunoraTest};
|