@lunora/testing 1.0.0-alpha.140 → 1.0.0-alpha.142

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/README.md CHANGED
@@ -167,8 +167,14 @@ The virtual clock is **per-harness** — advancing one harness's clock does not
167
167
  affect other harnesses, so tests running in parallel are isolated.
168
168
 
169
169
  Provide a `functions` map (`{ "path:name": registeredFn }`) so the scheduler
170
- can dispatch jobs. Paths not in the map produce a `console.warn` and are
171
- silently dropped (matching production behaviour for unknown paths).
170
+ can dispatch jobs. A path not in the map fails the job with `FUNCTION_NOT_FOUND`,
171
+ which then walks the retry budget into `t.scheduler.failures()` — matching
172
+ production, where the DO fires the job at the Worker and dead-letters it.
173
+
174
+ A retryable failure is re-enqueued silently, so `failures()` only fills once the
175
+ whole backoff schedule (~930s of virtual clock) has elapsed — and at that point
176
+ `advance()`/`runPending()` re-throw the failure by default. Pass
177
+ `{ throwOnError: false }` to inspect `failures()` instead.
172
178
 
173
179
  #### Subscription testing
174
180
 
package/dist/index.d.mts CHANGED
@@ -592,7 +592,9 @@ interface RecordedWideEvent {
592
592
  * stays `undefined`, matching the optional `ctx.env?` field.
593
593
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
594
594
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
595
- * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
595
+ * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`. As in
596
+ * production it is transactional inside a mutation: a job scheduled by a mutation
597
+ * that then throws never becomes pending.
596
598
  * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
597
599
  *
598
600
  * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
package/dist/index.d.ts CHANGED
@@ -592,7 +592,9 @@ interface RecordedWideEvent {
592
592
  * stays `undefined`, matching the optional `ctx.env?` field.
593
593
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
594
594
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
595
- * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
595
+ * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`. As in
596
+ * production it is transactional inside a mutation: a job scheduled by a mutation
597
+ * that then throws never becomes pending.
596
598
  * - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
597
599
  *
598
600
  * **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
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-CYLgTP1P.mjs";import{lunoraTest as s}from"./packem_shared/lunoraTest-BOqU8mY2.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-PUpvwIBo.mjs";import{groundednessScorer as p,mrrScorer as u,ndcgAtK as x,precisionAtK as S,recallAtK as f}from"./packem_shared/groundednessScorer-kgXJCZDg.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-DoLfFOs6.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,2 @@
1
+ import{LunoraError as i}from"@lunora/errors";const p=/^\s*(-?\d+(?:\.\d+)?)(?=$|[\s\-–—])/u,u=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,h=e=>typeof e=="number"?{score:u(e)}:{score:u(e.score),...e.reason===void 0?{}:{reason:e.reason}},m=e=>e.length===0?0:e.reduce((t,n)=>t+n,0)/e.length,x=(e,t={})=>{if(e.length===0)throw new i("BAD_REQUEST","@lunora/testing: containsScorer requires a non-empty needle");return{name:`contains:${e}`,score:({output:n})=>{const a=t.caseSensitive?n:n.toLowerCase(),r=t.caseSensitive?e:e.toLowerCase();return a.includes(r)?1:0}}},v=(e,t="regex")=>{const n=new RegExp(e.source,e.flags.replaceAll(/[gy]/gu,""));return{name:t,score:({output:a})=>n.test(a)?1:0}},$=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),E=e=>{if(e.length===0)throw new i("BAD_REQUEST","@lunora/testing: keywordScorer requires at least one keyword");return{name:"keyword-coverage",score:({output:t})=>{const n=t.toLowerCase(),a=e.filter(r=>n.includes(r.toLowerCase())).length;return{reason:`${String(a)}/${String(e.length)} keywords present`,score:a/e.length}}}},w=(e,t)=>[`Rate the ASSISTANT OUTPUT against this criterion: ${e}`,"Respond with a single number from 0 (fails) to 1 (fully meets), then a dash and a one-line reason.",...t.input===void 0?[]:["",`Input: ${t.input}`],...t.expected===void 0?[]:["",`Reference answer: ${t.expected}`],"",`Assistant output: ${t.output}`].join(`
2
+ `),S=e=>{const t=p.exec(e),n=t===null?Number.NaN:Number(t[1]);if(!Number.isFinite(n)||n<0||n>1)throw new i("BAD_REQUEST",`@lunora/testing: the LLM judge did not answer with a score in [0, 1] — got ${JSON.stringify(e.trim().slice(0,200))}`);return{reason:e.trim(),score:n}},N=e=>({name:e.name??"llm-judge",score:async t=>S(await e.judge(w(e.criteria,t)))}),f=async(e,t)=>{const n=await Promise.all(t.map(async o=>({name:o.name,result:h(await o.score(e))}))),a={},r=new Map;for(const{name:o,result:c}of n){const s=r.get(o)??0;r.set(o,s+1),a[s===0?o:`${o}#${String(s+1)}`]=c}return{average:m(n.map(({result:o})=>o.score)),scores:a}},A=async(e,t,n)=>{const a=await Promise.all(e.map(async r=>{const o=await t(r.input),c=typeof o=="string"?o:o.output,s=typeof o=="string"||o.metadata===void 0?r.metadata:{...r.metadata,...o.metadata},g={input:r.input,output:c,...r.expected===void 0?{}:{expected:r.expected},...s===void 0?{}:{metadata:s}},{average:d,scores:l}=await f(g,n);return{average:d,input:r.input,output:c,scores:l}}));return{average:m(a.map(r=>r.average)),items:a}};export{x as containsScorer,A as evaluate,$ as exactMatchScorer,E as keywordScorer,N as llmScorer,S as parseJudgeScore,v as regexScorer,f as scoreSample};
@@ -1,3 +1,3 @@
1
- import{LunoraError as g}from"@lunora/errors";import{parseJudgeScore as h}from"./containsScorer-gv0Sdd86.mjs";const w="retrieved",v="relevant",l=(e,t)=>{const n=e.metadata?.[t];return Array.isArray(n)?n.filter(r=>typeof r=="string"&&r.length>0):[]},s=(e,t)=>{const n=l(e,t?.relevantKey??v);if(n.length!==0)return{relevant:new Set(n),retrieved:l(e,t?.retrievedKey??w)}},a=e=>({reason:`no gold ids under metadata.${e?.relevantKey??v} — cannot score retrieval`,score:0}),c=(e,t)=>{if(e!==void 0&&(!Number.isInteger(e)||e<1))throw new g("BAD_REQUEST",`@lunora/testing: ${t} \`k\` must be a positive integer`)},u=(e,t)=>t===void 0?e.retrieved:e.retrieved.slice(0,t),f=(e,t)=>{const n=new Set;for(const r of e)t.has(r)&&n.add(r);return n.size},S=(e,t)=>{const n=new Set;let r=0;for(const[o,i]of e.entries())t.has(i)&&!n.has(i)&&(n.add(i),r+=1/Math.log2(o+2));return r},m=e=>{let t=0;for(let n=0;n<e;n+=1)t+=1/Math.log2(n+2);return t},p=(e,t)=>(c(e,"recallAtK"),{name:e===void 0?"recall":`recall@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=f(u(r,e),r.relevant);return{reason:`${String(o)}/${String(r.relevant.size)} gold ids retrieved`,score:o/r.relevant.size}}}),x=(e,t)=>(c(e,"precisionAtK"),{name:e===void 0?"precision":`precision@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=f(o,r.relevant);return{reason:`${String(i)}/${String(o.length)} retrieved ids are gold`,score:i/o.length}}}),y=e=>({name:"mrr",score:t=>{const n=s(t,e);if(n===void 0)return a(e);const r=n.retrieved.findIndex(o=>n.relevant.has(o));return r===-1?{reason:"no gold id retrieved",score:0}:{reason:`first gold id at rank ${String(r+1)}`,score:1/(r+1)}}}),$=(e,t)=>(c(e,"ndcgAtK"),{name:e===void 0?"ndcg":`ndcg@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=S(o,r.relevant),d=m(e===void 0?r.relevant.size:Math.min(r.relevant.size,e));return{reason:`dcg ${i.toFixed(3)} / ideal ${d.toFixed(3)}`,score:d===0?0:i/d}}}),K=e=>{if(typeof e.judge!="function")throw new g("BAD_REQUEST","@lunora/testing: groundednessScorer requires an injected `judge` function");const t=e.contextKey??"context";return{name:e.name??"groundedness",score:async n=>{const r=n.metadata?.[t];if(typeof r!="string"||r.trim().length===0)return{reason:`no retrieved context under metadata.${t}`,score:0};const o=await e.judge(["Rate how well the ASSISTANT ANSWER is supported by the RETRIEVED CONTEXT below.","Score 1 if every claim in the answer is supported by the context, 0 if the answer asserts","anything the context does not support. Judge support only — do NOT reward correctness","the context does not contain.","Respond with a single number from 0 to 1, then a dash and a one-line reason.","",`Retrieved context:
1
+ import{LunoraError as g}from"@lunora/errors";import{parseJudgeScore as h}from"./containsScorer-DoLfFOs6.mjs";const w="retrieved",v="relevant",l=(e,t)=>{const n=e.metadata?.[t];return Array.isArray(n)?n.filter(r=>typeof r=="string"&&r.length>0):[]},s=(e,t)=>{const n=l(e,t?.relevantKey??v);if(n.length!==0)return{relevant:new Set(n),retrieved:l(e,t?.retrievedKey??w)}},a=e=>({reason:`no gold ids under metadata.${e?.relevantKey??v} — cannot score retrieval`,score:0}),c=(e,t)=>{if(e!==void 0&&(!Number.isInteger(e)||e<1))throw new g("BAD_REQUEST",`@lunora/testing: ${t} \`k\` must be a positive integer`)},u=(e,t)=>t===void 0?e.retrieved:e.retrieved.slice(0,t),f=(e,t)=>{const n=new Set;for(const r of e)t.has(r)&&n.add(r);return n.size},S=(e,t)=>{const n=new Set;let r=0;for(const[o,i]of e.entries())t.has(i)&&!n.has(i)&&(n.add(i),r+=1/Math.log2(o+2));return r},m=e=>{let t=0;for(let n=0;n<e;n+=1)t+=1/Math.log2(n+2);return t},p=(e,t)=>(c(e,"recallAtK"),{name:e===void 0?"recall":`recall@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=f(u(r,e),r.relevant);return{reason:`${String(o)}/${String(r.relevant.size)} gold ids retrieved`,score:o/r.relevant.size}}}),x=(e,t)=>(c(e,"precisionAtK"),{name:e===void 0?"precision":`precision@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=f(o,r.relevant);return{reason:`${String(i)}/${String(o.length)} retrieved ids are gold`,score:i/o.length}}}),y=e=>({name:"mrr",score:t=>{const n=s(t,e);if(n===void 0)return a(e);const r=n.retrieved.findIndex(o=>n.relevant.has(o));return r===-1?{reason:"no gold id retrieved",score:0}:{reason:`first gold id at rank ${String(r+1)}`,score:1/(r+1)}}}),$=(e,t)=>(c(e,"ndcgAtK"),{name:e===void 0?"ndcg":`ndcg@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=S(o,r.relevant),d=m(e===void 0?r.relevant.size:Math.min(r.relevant.size,e));return{reason:`dcg ${i.toFixed(3)} / ideal ${d.toFixed(3)}`,score:d===0?0:i/d}}}),K=e=>{if(typeof e.judge!="function")throw new g("BAD_REQUEST","@lunora/testing: groundednessScorer requires an injected `judge` function");const t=e.contextKey??"context";return{name:e.name??"groundedness",score:async n=>{const r=n.metadata?.[t];if(typeof r!="string"||r.trim().length===0)return{reason:`no retrieved context under metadata.${t}`,score:0};const o=await e.judge(["Rate how well the ASSISTANT ANSWER is supported by the RETRIEVED CONTEXT below.","Score 1 if every claim in the answer is supported by the context, 0 if the answer asserts","anything the context does not support. Judge support only — do NOT reward correctness","the context does not contain.","Respond with a single number from 0 to 1, then a dash and a one-line reason.","",`Retrieved context:
2
2
  ${r}`,"",`Assistant answer: ${n.output}`].join(`
3
3
  `));return h(o)}}};export{K as groundednessScorer,y as mrrScorer,$ as ndcgAtK,x as precisionAtK,p as recallAtK};
@@ -0,0 +1 @@
1
+ import{LunoraError as k}from"@lunora/errors";import{withDeferredSchedules as rt,beginDeferredSchedules as nt}from"@lunora/server";import{runShardMigrations as ot,TransactionHeadroomTracker as Y,createShardCtxDb as st,RLS_UNWRAP_SYMBOL as it}from"@lunora/shard-engine";import{evaluationAttributes as ct}from"./evaluationAttributes-CYLgTP1P.mjs";import{assertScheduleDelay as at,MAX_RETRY_ATTEMPTS as ut,RETRY_BASE_DELAY_MS as dt}from"@lunora/scheduler";import{DatabaseSync as lt}from"node:sqlite";const ht=(o,s,i,u,g)=>{let v=g,b=1;const d=new Map,m=[],h=(r,l,a={},t)=>{JSON.stringify(a);const e=t??`fake-job-${String(b)}`;return b+=1,d.set(e,{args:a,enqueuedAt:v,functionPath:l,id:e,scheduledFor:r}),e},S=r=>typeof r=="string"?r:"__lunoraRef"in r?r.__lunoraRef:r.name??r.binding??"",x={cancel:r=>{const l=d.has(r);return d.delete(r),Promise.resolve({cancelled:l})},get:r=>Promise.resolve(d.get(r)??null),list:()=>Promise.resolve([...d.values()]),runAfter:(r,l,a,t)=>{at(r,"ctx.scheduler.runAfter");const e=h(v+r,S(l),a,t?.id);return Promise.resolve(e)},runAt:(r,l,a,t)=>{const e=h(r,S(l),a,t?.id);return Promise.resolve(e)}},A=async r=>{d.delete(r.id);const l=u(),a=r.functionPath===void 0?void 0:l.get(r.functionPath);if(a===void 0)throw new k("FUNCTION_NOT_FOUND",`unknown functionPath "${r.functionPath??"(workflow-targeted)"}"`,{status:404});if(a.kind!=="mutation"&&a.kind!=="action")throw new k("FUNCTION_NOT_FOUND",`functionPath "${r.functionPath??"(workflow-targeted)"}" is a ${a.kind} — only mutations and actions can be scheduled`,{status:404});const t=o(),e=a.kind==="action"?i():s();await t(a.kind,a,{...e,now:v},r.args)},I=async r=>{const l=[...d.values()].filter(e=>e.scheduledFor<=r).toSorted((e,w)=>e.scheduledFor-w.scheduledFor),a=[];let t=0;for(const e of l)if(d.has(e.id)){t+=1;try{await A(e)}catch(w){const R=(e.attempts??0)+1;if(R>ut){const _={args:e.args,error:w,functionPath:e.functionPath??"",id:e.id};a.push(_),m.push(_)}else{const _=dt*2**(R-1);d.set(e.id,{...e,attempts:R,scheduledFor:v+_})}}}return{executed:t,failed:a}},T=async(r,l)=>{const{executed:a,failed:t}=await I(r);if(t.length>0&&(l?.throwOnError??!0)){const[e]=t;throw t.length===1&&e!==void 0?e.error:new AggregateError(t.map(w=>w.error),`${String(t.length)} scheduled jobs failed: ${t.map(w=>w.functionPath).join(", ")}`)}return a};return{controls:{advance:(r,l)=>(v+=r,T(v,l)),failures:()=>[...m],list:()=>[...d.values()],runPending:r=>T(Number.POSITIVE_INFINITY,r)},scheduler:x}},ft=()=>{const o=new lt(":memory:"),s=u=>({one(){if(u.length!==1)throw new k("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 b=o.prepare(u).all(...g);return s(b)}}}},vt=o=>{if(o===null||typeof o.userId!="string"||o.userId.length===0)return{claims:null,userId:null};const{userId:s,...i}=o;return{claims:Object.keys(i).length>0?i:null,userId:s}};class mt extends Y{current=new Y;reset(){this.current=new Y}headroom(){return this.current.headroom()}recordRead(s){this.current.recordRead(s)}recordWrite(s){this.current.recordWrite(s)}}const O=o=>{if(typeof o!="object"||o===null)return;const{kind:s}=o;if(s==="query"||s==="mutation"||s==="action")return s},wt=o=>typeof o=="object"&&o!==null&&o.visibility==="internal"?"internal":"public",B=o=>{throw new k("INTERNAL",`ctx.${o} is not available in the in-memory @lunora/testing harness (v1)`)},y=o=>new Proxy((...s)=>B(o),{apply:()=>B(o),get:()=>B(o)}),V={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},pt={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>V},gt=()=>{const o={attributes:{},events:[],links:[]},s={addEvent:(i,u)=>{o.events.push({...u===void 0?{}:{attributes:{...u}},name:i})},addLink:i=>{o.links.push({spanId:i.spanId,traceId:i.traceId})},recordEvaluation:i=>{Object.assign(o.attributes,ct(i))},recordException:i=>{const u={"exception.message":i instanceof Error?i.message:String(i),"exception.type":i instanceof Error?i.constructor.name:"Error"};i instanceof Error&&i.stack!==void 0&&(u["exception.stacktrace"]=i.stack),s.addEvent("exception",u)},setAttribute:(i,u)=>{o.attributes[i]=u},setAttributes:i=>{Object.assign(o.attributes,i)},spanContext:()=>V};return{handle:s,recorded:o}},U=async(o,s)=>await s(U,pt),H={count:()=>{},gauge:()=>{},record:()=>{}},Q={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>Q},yt=(o,s,i)=>(g,v)=>{let b=!1;const d=[];let m,h,S=0,x=0;const A=()=>O(g)?o("query",g,s,v,!1):Promise.resolve(g(s)),I=(t,e)=>{if(t<x)return;x=t;const w={done:!1,value:e};if(d.length===0)m=w,h=void 0;else{m=void 0,h=void 0;for(const R of d.splice(0))R.resolve(w)}},T=(t,e)=>{if(!(t<x))if(x=t,d.length===0)h={error:e},m=void 0;else{m=void 0,h=void 0;for(const w of d.splice(0))w.reject(e)}},P=t=>e=>{I(t,e)},r=t=>e=>{T(t,e)},l=()=>{if(b)return;S+=1;const t=S;A().then(P(t)).catch(r(t))};i.add(l);const a={[Symbol.asyncIterator](){return a},next:()=>{if(b)return Promise.resolve({done:!0,value:void 0});if(x===S){if(h!==void 0){const{error:t}=h;return h=void 0,Promise.reject(t)}if(m!==void 0){const t=m;return m=void 0,Promise.resolve(t)}}return x<S?new Promise((t,e)=>{d.push({reject:e,resolve:t})}):A().then(t=>{if(h!==void 0){const{error:e}=h;throw h=void 0,e}if(m!==void 0){const e=m;return m=void 0,e}return{done:!1,value:t}})},return:()=>{b=!0,i.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(P(0)).catch(r(0)),a},Nt=(o,s)=>{const{close:i,sql:u}=ft(),g=o;ot(u,g);const v=new mt,b=f=>{const p=st({auth:{identity:f.claims,userId:f.userId},enforceRls:s?.enforceRls??!0,headroom:v,schema:g,sql:u});return{database:p,rawDatabase:p[it]??p}},d=f=>{u.exec.call(u,f)};let m=Promise.resolve();const h=f=>{const p=async()=>{const M=nt({scheduler:W});v.reset(),d("BEGIN");try{const F=await f();return d("COMMIT"),await M(!0),F}catch(F){try{d("ROLLBACK")}catch{}throw await M(!1),F}},C=m.then(p);return m=C.then(()=>{},()=>{}),C};let S=!1;const x=()=>{S||(S=!0,i())},A=new Map(Object.entries(s?.functions??{}).map(([f,p])=>[f,p])),I=new Set,T=()=>{for(const f of I)f()},P=f=>(T(),f);let r,l,a;const t=s?.now??Date.now(),e=gt(),w=(f,p)=>{if(f===void 0)throw new k("INTERNAL",`[fake-scheduler] ${p} not yet available — scheduler.advance called before harness construction completed`);return f},{controls:R,scheduler:_}=ht(()=>w(r,"dispatch"),()=>w(l,"mutationContext"),()=>w(a,"actionContext"),()=>A,t),W=rt(_),J=f=>{const p=vt(f),C={getIdentity:()=>Promise.resolve(p.claims),userId:p.userId},{database:M,rawDatabase:F}=b(p),D={auth:C,db:M,env:s?.env,log:Q,metrics:H,now:t,span:e.handle,trace:U,runQuery:((n,c)=>N("query",n,D,c)),secrets:y("secrets"),storage:y("storage"),vectors:y("vectors")},L={auth:C,db:M,env:s?.env,log:Q,metrics:H,now:t,span:e.handle,trace:U,runMutation:((n,c)=>N("mutation",n,L,c)),runQuery:((n,c)=>N("query",n,D,c)),scheduler:W,secrets:y("secrets"),storage:y("storage"),vectors:y("vectors"),workflows:y("workflows")};l??=L;const X={...L,db:F},j={auth:C,db:M,env:s?.env,fetch:s?.fetch??y("fetch"),log:Q,metrics:H,now:t,span:e.handle,trace:U,runAction:((n,c)=>N("action",n,j,c)),runMutation:((n,c)=>h(()=>N("mutation",n,L,c)).then(P)),runQuery:((n,c)=>N("query",n,D,c)),scheduler:W,secrets:y("secrets"),storage:y("storage"),vectors:y("vectors"),workflows:y("workflows")};a??=j;const q=(n,c,E,$,et)=>{const K=O(c);if(K!==n)throw new k("INTERNAL",`expected a registered ${n}, received a ${K??"non-function"} reference`);if(!et&&wt(c)==="internal")throw new k("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(c.handler(E,$??{}))},N=(n,c,E,$)=>q(n,c,E,$,!0);r??=(n,c,E,$)=>n==="mutation"?h(()=>q("mutation",c,E,$,!0)).then(P):(v.reset(),N("action",c,E,$));const G=((n,c)=>(v.reset(),O(n)?q("query",n,D,c,!1):Promise.resolve(n(D)))),z=((n,c)=>{const E=O(n)?()=>q("mutation",n,L,c,!1):()=>n(L);return h(E).then(P)}),Z=((n,c)=>(v.reset(),O(n)?q("action",n,j,c,!1):Promise.resolve(n(j)))),tt=yt((...n)=>(v.reset(),q(...n)),D,I);return{action:Z,close:x,mutation:z,query:G,run:n=>h(()=>n(X)).then(P),scheduler:R,subscribe:tt,wideEvent:()=>e.recorded,withIdentity:n=>J(n)}};return J(null)};export{Nt as lunoraTest};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/testing",
3
- "version": "1.0.0-alpha.140",
3
+ "version": "1.0.0-alpha.142",
4
4
  "description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,12 +50,12 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/agent": "1.0.0-alpha.81",
54
- "@lunora/errors": "1.0.0-alpha.29",
55
- "@lunora/mail": "1.0.0-alpha.59",
56
- "@lunora/scheduler": "1.0.0-alpha.49",
57
- "@lunora/server": "1.0.0-alpha.99",
58
- "@lunora/shard-engine": "1.0.0-alpha.52"
53
+ "@lunora/agent": "1.0.0-alpha.83",
54
+ "@lunora/errors": "1.0.0-alpha.30",
55
+ "@lunora/mail": "1.0.0-alpha.61",
56
+ "@lunora/scheduler": "1.0.0-alpha.50",
57
+ "@lunora/server": "1.0.0-alpha.100",
58
+ "@lunora/shard-engine": "1.0.0-alpha.53"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@playwright/test": "^1.61.1"
@@ -1,2 +0,0 @@
1
- import{LunoraError as g}from"@lunora/errors";const l=/^\s*(-?\d+(?:\.\d+)?)/u,i=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,h=e=>typeof e=="number"?{score:i(e)}:{score:i(e.score),...e.reason===void 0?{}:{reason:e.reason}},u=e=>e.length===0?0:e.reduce((t,o)=>t+o,0)/e.length,y=(e,t={})=>({name:`contains:${e}`,score:({output:o})=>{const a=t.caseSensitive?o:o.toLowerCase(),r=t.caseSensitive?e:e.toLowerCase();return a.includes(r)?1:0}}),v=(e,t="regex")=>({name:t,score:({output:o})=>e.test(o)?1:0}),$=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),L=e=>{if(e.length===0)throw new g("BAD_REQUEST","@lunora/testing: keywordScorer requires at least one keyword");return{name:"keyword-coverage",score:({output:t})=>{const o=t.toLowerCase(),a=e.filter(r=>o.includes(r.toLowerCase())).length;return{reason:`${String(a)}/${String(e.length)} keywords present`,score:a/e.length}}}},S=(e,t)=>[`Rate the ASSISTANT OUTPUT against this criterion: ${e}`,"Respond with a single number from 0 (fails) to 1 (fully meets), then a dash and a one-line reason.",...t.input===void 0?[]:["",`Input: ${t.input}`],...t.expected===void 0?[]:["",`Reference answer: ${t.expected}`],"",`Assistant output: ${t.output}`].join(`
2
- `),w=e=>{const t=l.exec(e);return{reason:e.trim(),score:t?i(Number(t[1])):0}},b=e=>({name:e.name??"llm-judge",score:async t=>w(await e.judge(S(e.criteria,t)))}),f=async(e,t)=>{const o=await Promise.all(t.map(async n=>({name:n.name,result:h(await n.score(e))}))),a={},r=new Map;for(const{name:n,result:s}of o){const c=r.get(n)??0;r.set(n,c+1),a[c===0?n:`${n}#${String(c+1)}`]=s}return{average:u(o.map(({result:n})=>n.score)),scores:a}},A=async(e,t,o)=>{const a=await Promise.all(e.map(async r=>{const n=await t(r.input),s=typeof n=="string"?n:n.output,c=typeof n=="string"||n.metadata===void 0?r.metadata:{...r.metadata,...n.metadata},m={input:r.input,output:s,...r.expected===void 0?{}:{expected:r.expected},...c===void 0?{}:{metadata:c}},{average:d,scores:p}=await f(m,o);return{average:d,input:r.input,output:s,scores:p}}));return{average:u(a.map(r=>r.average)),items:a}};export{y as containsScorer,A as evaluate,$ as exactMatchScorer,L as keywordScorer,b as llmScorer,w as parseJudgeScore,v as regexScorer,f as scoreSample};
@@ -1 +0,0 @@
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??"",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)}},k=async e=>{d.delete(e.id);const l=u(),a=e.functionPath===void 0?void 0:l.get(e.functionPath);if(a===void 0){console.warn(`[fake-scheduler] unknown functionPath "${e.functionPath??"(workflow-targeted)"}" — 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??"(workflow-targeted)"}" 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:P}},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,P=0;const k=()=>q(g)?r("query",g,c,p,!1):Promise.resolve(g(c)),M=(t,n)=>{if(t<P)return;P=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<P))if(P=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(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,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 E=async()=>{d("BEGIN");try{const S=await m();return d("COMMIT"),S}catch(S){try{d("ROLLBACK")}catch{}throw S}},x=f.then(E);return f=x.then(()=>{},()=>{}),x};let b=!1;const P=()=>{b||(b=!0,s())},k=new Map(Object.entries(c?.functions??{}).map(([m,E])=>[m,E])),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,E)=>{if(m===void 0)throw new _("INTERNAL",`[fake-scheduler] ${E} 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 E={getIdentity:()=>Promise.resolve(m??null),userId:m?.userId??null},x={auth:E,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:E,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:E,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:P,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};