@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 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
- /** The primitive an eval attaches to a span: a numeric score or a string label. */
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 captured during an `advance()` / `runPending()`
214
- * sweep. Production's scheduler isolates per-job failures (one bad job does not
215
- * abort the rest), so the fake scheduler does the same — but, being a test
216
- * harness, it never swallows the error: every failure is recorded here so tests
217
- * can still assert on it.
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 during the advance) are NOT re-evaluated in
239
- * the same advance call — callers should advance again if needed.
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. After every due job has
243
- * run, the failures are surfaced they are recorded on
244
- * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
245
- * still sees the error. A single failure is re-thrown verbatim; multiple
246
- * failures are aggregated into an `AggregateError`. Pass
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 that were executed (including failed ones).
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 captured so far, in execution order, across
255
- * every `advance()` / `runPending()` call on this harness. Always available,
256
- * even when `throwOnError: false` suppressed the re-throw. The list is a
257
- * snapshot mutating it does not affect the scheduler.
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
- * executed (including failed ones).
308
+ * dispatched this sweep (including failed ones, retried or terminal).
269
309
  *
270
- * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
271
- * one failing job does not abort the rest, and failures are recorded on
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
- /** The primitive an eval attaches to a span: a numeric score or a string label. */
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 captured during an `advance()` / `runPending()`
214
- * sweep. Production's scheduler isolates per-job failures (one bad job does not
215
- * abort the rest), so the fake scheduler does the same — but, being a test
216
- * harness, it never swallows the error: every failure is recorded here so tests
217
- * can still assert on it.
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 during the advance) are NOT re-evaluated in
239
- * the same advance call — callers should advance again if needed.
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. After every due job has
243
- * run, the failures are surfaced they are recorded on
244
- * {@link FakeSchedulerControls.failures} and, by default, re-thrown so a test
245
- * still sees the error. A single failure is re-thrown verbatim; multiple
246
- * failures are aggregated into an `AggregateError`. Pass
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 that were executed (including failed ones).
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 captured so far, in execution order, across
255
- * every `advance()` / `runPending()` call on this harness. Always available,
256
- * even when `throwOnError: false` suppressed the re-throw. The list is a
257
- * snapshot mutating it does not affect the scheduler.
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
- * executed (including failed ones).
308
+ * dispatched this sweep (including failed ones, retried or terminal).
269
309
  *
270
- * Failure isolation and surfacing match {@link FakeSchedulerControls.advance}:
271
- * one failing job does not abort the rest, and failures are recorded on
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-BA1uUP-z.mjs";import{lunoraTest as s}from"./packem_shared/lunoraTest-CC-mCvor.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};
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.112",
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.58",
53
+ "@lunora/agent": "1.0.0-alpha.59",
54
54
  "@lunora/errors": "1.0.0-alpha.22",
55
- "@lunora/mail": "1.0.0-alpha.50",
56
- "@lunora/server": "1.0.0-alpha.77",
57
- "@lunora/shard-engine": "1.0.0-alpha.30"
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};