@lunora/testing 1.0.0-alpha.107 → 1.0.0-alpha.109
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 +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/agentHarness-DA9onGVO.mjs +1 -0
- package/dist/packem_shared/containsScorer-DDK7WEuL.mjs +2 -0
- package/dist/packem_shared/evaluationAttributes-BA1uUP-z.mjs +1 -0
- package/dist/packem_shared/lunoraTest-CC-mCvor.mjs +1 -0
- package/dist/playwright.mjs +1 -1
- package/package.json +6 -6
- package/dist/packem_shared/agentHarness-D2XNYX0F.mjs +0 -1
- package/dist/packem_shared/containsScorer-Dr5tjL-M.mjs +0 -2
- package/dist/packem_shared/evaluationAttributes-94eqWXgB.mjs +0 -1
- package/dist/packem_shared/lunoraTest-BP5oHAKD.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
2
|
import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, LogFields, TableDefinition, Schema } from '@lunora/server';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
/**
|
|
5
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
6
|
+
*
|
|
7
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
8
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
9
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
10
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
11
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
12
|
+
* password" deterministically.
|
|
13
|
+
*
|
|
14
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
15
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
16
|
+
* sub-package's `/testing` entry.
|
|
17
|
+
*/
|
|
18
|
+
type InboxOptions,
|
|
19
|
+
/**
|
|
20
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
21
|
+
*
|
|
22
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
23
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
24
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
25
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
26
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
27
|
+
* password" deterministically.
|
|
28
|
+
*
|
|
29
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
30
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
31
|
+
* sub-package's `/testing` entry.
|
|
32
|
+
*/
|
|
33
|
+
type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
|
|
4
34
|
/** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
|
|
5
35
|
interface HarnessMessage extends AgentMessageRow {
|
|
6
36
|
messageKey: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
2
|
import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, LogFields, TableDefinition, Schema } from '@lunora/server';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
/**
|
|
5
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
6
|
+
*
|
|
7
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
8
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
9
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
10
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
11
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
12
|
+
* password" deterministically.
|
|
13
|
+
*
|
|
14
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
15
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
16
|
+
* sub-package's `/testing` entry.
|
|
17
|
+
*/
|
|
18
|
+
type InboxOptions,
|
|
19
|
+
/**
|
|
20
|
+
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
21
|
+
*
|
|
22
|
+
* Today it surfaces the dev mail-catcher helpers from `@lunora/mail/testing`: in
|
|
23
|
+
* `lunora dev` every outbound email (sign-up verification, forgot-password, magic
|
|
24
|
+
* links) is captured into the studio's root-shard inbox instead of hitting a real
|
|
25
|
+
* provider, and these helpers read that inbox over the admin RPC so a Playwright
|
|
26
|
+
* test can drive "request reset → read the email → follow the link → set a new
|
|
27
|
+
* password" deterministically.
|
|
28
|
+
*
|
|
29
|
+
* This is the home for E2E fixtures to grow into — auth helpers and more — so
|
|
30
|
+
* tests import one package (`@lunora/testing`) rather than reaching into each
|
|
31
|
+
* sub-package's `/testing` entry.
|
|
32
|
+
*/
|
|
33
|
+
type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
|
|
4
34
|
/** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
|
|
5
35
|
interface HarnessMessage extends AgentMessageRow {
|
|
6
36
|
messageKey: string;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-
|
|
1
|
+
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as c,recordEvaluation as n}from"./packem_shared/evaluationAttributes-BA1uUP-z.mjs";import{lunoraTest as u}from"./packem_shared/lunoraTest-CC-mCvor.mjs";import{containsScorer as m,evaluate as p,exactMatchScorer as s,keywordScorer as f,llmScorer as S,regexScorer as d,scoreSample as v}from"./packem_shared/containsScorer-DDK7WEuL.mjs";import{extractLink as T,listCapturedMail as g,waitForMail as k}from"@lunora/mail/testing";export{o as agentHarness,m as containsScorer,p as evaluate,c as evaluationAttributes,s as exactMatchScorer,T as extractLink,t as finalTurn,f as keywordScorer,g as listCapturedMail,S as llmScorer,u as lunoraTest,n as recordEvaluation,d as regexScorer,v as scoreSample,a as toolCallTurn,k as waitForMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runAgentLoop as v,DEFAULT_AGENT_FUNCTION_PATHS as p}from"@lunora/agent";const k=a=>{const e=[...a];return()=>{const o=e.shift();if(!o)throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");return Promise.resolve(o)}};class T{invoked=[];entries=new Map;async do(e,o){const c=this.entries.get(e);if(c)return c.output;this.invoked.push(e);const i=await o();return this.entries.set(e,{output:i}),i}async waitForEvent(e,o){return new Promise(()=>{})}}const K=a=>{const e=new Map,o=new Map,c=[],i=t=>{const s=t?.key,n=t?.instanceId,r=e.get(s);return r?(r.status="running",delete r.error,n!==void 0&&(r.instanceId=n),{outcome:"continued"}):(e.set(s,{agent:t?.agent,instanceId:n,key:s,messageCount:0,owner:t?.owner,status:"running",title:t?.title}),{outcome:"created"})},l=t=>{const s=t?.threadKey,n=t?.messageKey,r=`${s}:${n}`,y=o.get(r);if(y)return{seq:y.seq};const m=e.get(s);if(!m)throw new Error(`agentHarness: append to unknown thread "${s}".`);const w=m.messageCount;return m.messageCount+=1,o.set(r,{...t,seq:w}),{seq:w}},f=t=>{const s=t?.key;return[...o.values()].filter(n=>n.threadKey===s).toSorted((n,r)=>n.seq-r.seq)},d=t=>{const s=e.get(t?.key);if(s)for(const[n,r]of Object.entries(t??{}))n!=="key"&&r!==void 0&&(s[n]=r)},u=t=>{const s=e.get(t?.key);if(!t||!s||s.instanceId!==t.instanceId)return{};for(const[n,r]of Object.entries(t))n!=="key"&&n!=="instanceId"&&r!==void 0&&(s[n]=r);return{}},h=new Map([[p.appendMessage,l],[p.completeRun,u],[p.ensureThread,i],[p.listMessages,f],[p.patchThread,d],...Object.entries(a)]);return{dispatches:c,messages:o,run:(t,s)=>{const n=t.__lunoraRef;c.push({args:s,path:n});const r=h.get(n);if(!r)throw new Error(`agentHarness: unstubbed dispatch "${n}" — pass a \`functions\` handler for it.`);return Promise.resolve(r(s))},threads:e}},I=(a,e)=>{const o=e.exportName??"agent",c=e.env??{LUNORA_TEST:!0},i=K(e.functions??{});let l=0;const f=(d,u)=>{l+=1;const h=u?.instanceId??`wf-${String(l)}`,g=u?.script??e.script;return v({agent:a,env:c,exportName:o,generate:k(g),instanceId:h,params:d,paths:p,run:i.run,step:new T})};return{dispatches:i.dispatches,messages:d=>[...i.messages.values()].filter(u=>u.threadKey===d).toSorted((u,h)=>u.seq-h.seq).map(({messageKey:u,threadKey:h,...g})=>g),run:f,thread:d=>i.threads.get(d)}},x=(a,e)=>({text:a,toolCalls:[],...e}),C=(a,e,o,c="")=>({text:c,toolCalls:[{id:a,input:o,name:e}]});export{I as agentHarness,x as finalTurn,C as toolCallTurn};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{LunoraError as d}from"@lunora/errors";const g=/^\s*(-?\d+(?:\.\d+)?)/u,i=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,p=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,n)=>t+n,0)/e.length,x=(e,t={})=>({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}}),f=(e,t="regex")=>({name:t,score:({output:n})=>e.test(n)?1:0}),v=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),y=e=>{if(e.length===0)throw new d("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}}}},l=(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
|
+
`),h=e=>{const t=g.exec(e);return{reason:e.trim(),score:t?i(Number(t[1])):0}},$=e=>({name:e.name??"llm-judge",score:async t=>h(await e.judge(l(e.criteria,t)))}),S=async(e,t)=>{const n=await Promise.all(t.map(async o=>({name:o.name,result:p(await o.score(e))}))),a={},r=new Map;for(const{name:o,result:s}of n){const c=r.get(o)??0;r.set(o,c+1),a[c===0?o:`${o}#${String(c+1)}`]=s}return{average:u(n.map(({result:o})=>o.score)),scores:a}},L=async(e,t,n)=>{const a=await Promise.all(e.map(async r=>{const o=await t(r.input),s={input:r.input,output:o,...r.expected===void 0?{}:{expected:r.expected},...r.metadata===void 0?{}:{metadata:r.metadata}},{average:c,scores:m}=await S(s,n);return{average:c,input:r.input,output:o,scores:m}}));return{average:u(a.map(r=>r.average)),items:a}};export{x as containsScorer,L as evaluate,v as exactMatchScorer,y as keywordScorer,$ as llmScorer,f as regexScorer,S as scoreSample};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
package/dist/playwright.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const T=(i,u)=>u?Object.entries(u).every(([f,w])=>i[f]===w):!0,H=async(i,u={})=>{const f=u.path??"/_lunora",w=`${f}/rpc`,M=`${f}/rpc-batch`,l=u.shapes??{},_=u.functions??{},m=new Map;for(const[e,t]of Object.entries(u.rows??{}))m.set(e,new Map(t.map(o=>[o._id,{...o}])));const d=new Map,c=new Map,k=new Map;let p=0,J=0,S=!1,b=!1,I=!1;const j=new Set,$=async()=>{await i.unroute(w),await i.unroute(M)};await $();const h=e=>{const t=m.get(e);if(t)return t;const o=new Map;return m.set(e,o),o},q=e=>{const t=l[e];if(!t)return[];const o=[];for(const n of t.tables)for(const s of h(n).values())T(s,t.where)&&o.push({row:s,table:n});return o},O=()=>Math.max(0,...d.values()),C=e=>{if(S||e.length===0)return;J+=1,p+=1;const t=`poke_${String(J)}`,o=O();for(const{shapeId:n}of e){const s=c.get(n);s&&s.send(JSON.stringify({baseCheckpoint:p-1,pokeId:t,type:"pokeStart"}))}for(const{ops:n,shapeId:s}of e){const a=c.get(s);a&&a.send(JSON.stringify({lastMutationId:o,pokeId:t,rowsPatch:n,shapeId:s,type:"pokePart"}))}for(const{shapeId:n}of e){const s=c.get(n);s&&s.send(JSON.stringify({checkpoint:p,pokeId:t,type:"pokeEnd"}))}},v=(e,t,o,n)=>{const s=[];for(const a of c.values()){const r=l[a.name];if(!r?.tables.includes(e))continue;const y=t!=="delete"&&n!==void 0&&!T(n,r.where)?"delete":t;s.push({ops:[{key:o,op:y,table:e,...y==="delete"?{}:{value:n}}],shapeId:a.id})}C(s)},x=e=>{const t=q(e.name).map(({row:o,table:n})=>({key:o._id,op:"insert",table:n,value:o}));C([{ops:t,shapeId:e.id}])},g=()=>{if(S)return;const e=O();for(const t of k.values())t.send(JSON.stringify({cursor:p,id:t.id,lastMutationId:e,type:"settled"}))},R=async e=>{const t=e.request(),o=t.postData();let n={};if(typeof o=="string")try{n=JSON.parse(o)}catch{await e.fulfill({body:JSON.stringify({error:{code:"BAD_REQUEST",message:"mockLunora: unparseable RPC body"}}),contentType:"application/json",status:400});return}const s=n.functionPath??"",a=t.headers(),r=a["x-lunora-client-id"]??"",y=Number.parseInt(a["x-lunora-client-seq"]??"",10),P=r!==""&&Number.isFinite(y);if(b!==!1&&P){await e.fulfill({body:JSON.stringify({error:{code:b,message:`mockLunora: writes are failing with ${b}`}}),contentType:"application/json",status:400});return}P&&d.set(r,Math.max(d.get(r)??0,y));const N=_[s],E=typeof N=="function"?N(n.args??{}):N;await e.fulfill({body:JSON.stringify({commitCursor:p,lastMutationId:P?d.get(r)??0:O(),result:E}),contentType:"application/json",status:200})};return await i.route(w,R),await i.route(M,R),await i.routeWebSocket(`${f}/ws`,e=>{const t=o=>{I||e.send(o)};e.onMessage(o=>{let n;try{n=JSON.parse(o)}catch{return}const s=n.id??"";switch(n.type){case"shape_subscribe":{const a=n.shape?.name??"",r={id:s,name:a,send:t};l[a]===void 0&&!j.has(a)&&(j.add(a),console.warn(`[mockLunora] the app subscribed to shape "${a}", which is not in \`shapes\` — it will replicate nothing and never poke. Declared: ${Object.keys(l).length===0?"(none)":Object.keys(l).join(", ")}.`)),c.set(s,r),t(JSON.stringify({id:s,type:"ack"})),x(r);return}case"shape_unsubscribe":{c.delete(s);return}case"subscribe":{k.set(s,{functionPath:n.query?.functionPath??"",id:s,send:t}),t(JSON.stringify({id:s,type:"ack"}));const a=_[n.query?.functionPath??""],r=typeof a=="function"?a({}):a??[];t(JSON.stringify({cursor:p,data:r,id:s,type:"data"}));return}case"unsubscribe":{k.delete(s);break}}})}),{dispose:async()=>{I=!0,c.clear(),k.clear(),await $()},failWrites:e=>{b=e},insert:async(e,t)=>{h(e).set(t._id,{...t}),v(e,"insert",t._id,{...t}),g()},patch:async(e,t,o)=>{const n=h(e),s=n.get(t);if(!s)throw new Error(`mockLunora.patch: no row "${t}" in table "${e}"`);const a={...s,...o,_id:t};n.set(t,a),v(e,"update",t,a),g()},remove:async(e,t)=>{h(e).delete(t),v(e,"delete",t),g()},resync:async()=>{for(const e of c.values())x(e);g()},rows:e=>[...h(e).values()].map(t=>({...t})),suppressPokes:(e=!0)=>{S=e},watermarks:()=>Object.fromEntries(d)}};export{H as mockLunora};
|
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.109",
|
|
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,11 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/agent": "1.0.0-alpha.
|
|
54
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
55
|
-
"@lunora/mail": "1.0.0-alpha.
|
|
56
|
-
"@lunora/server": "1.0.0-alpha.
|
|
57
|
-
"@lunora/shard-engine": "1.0.0-alpha.
|
|
53
|
+
"@lunora/agent": "1.0.0-alpha.55",
|
|
54
|
+
"@lunora/errors": "1.0.0-alpha.22",
|
|
55
|
+
"@lunora/mail": "1.0.0-alpha.50",
|
|
56
|
+
"@lunora/server": "1.0.0-alpha.74",
|
|
57
|
+
"@lunora/shard-engine": "1.0.0-alpha.28"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@playwright/test": "^1.61.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{runAgentLoop as y,DEFAULT_AGENT_FUNCTION_PATHS as g}from"@lunora/agent";const v=o=>{const t=[...o];return()=>{const a=t.shift();if(!a)throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");return Promise.resolve(a)}};class k{invoked=[];entries=new Map;async do(t,a){const i=this.entries.get(t);if(i)return i.output;this.invoked.push(t);const u=await a();return this.entries.set(t,{output:u}),u}async waitForEvent(t,a){return new Promise(()=>{})}}const T=o=>{const t=new Map,a=new Map,i=[],u=e=>{const n=e?.key,s=e?.instanceId,r=t.get(n);return r?(r.status="running",delete r.error,s!==void 0&&(r.instanceId=s),{outcome:"continued"}):(t.set(n,{agent:e?.agent,instanceId:s,key:n,messageCount:0,owner:e?.owner,status:"running",title:e?.title}),{outcome:"created"})},h=e=>{const n=e?.threadKey,s=e?.messageKey,r=`${n}:${s}`,m=a.get(r);if(m)return{seq:m.seq};const f=t.get(n);if(!f)throw new Error(`agentHarness: append to unknown thread "${n}".`);const w=f.messageCount;return f.messageCount+=1,a.set(r,{...e,seq:w}),{seq:w}},l=e=>{const n=e?.key;return[...a.values()].filter(s=>s.threadKey===n).toSorted((s,r)=>s.seq-r.seq)},d=e=>{const n=t.get(e?.key);if(n)for(const[s,r]of Object.entries(e??{}))s!=="key"&&r!==void 0&&(n[s]=r)},c=e=>{const n=t.get(e?.key);if(!e||!n||n.instanceId!==e.instanceId)return{};for(const[s,r]of Object.entries(e))s!=="key"&&s!=="instanceId"&&r!==void 0&&(n[s]=r);return{}},p=new Map([[g.appendMessage,h],[g.completeRun,c],[g.ensureThread,u],[g.listMessages,l],[g.patchThread,d],...Object.entries(o)]);return{dispatches:i,messages:a,run:(e,n)=>{const s=e.__lunoraRef;i.push({args:n,path:s});const r=p.get(s);if(!r)throw new Error(`agentHarness: unstubbed dispatch "${s}" — pass a \`functions\` handler for it.`);return Promise.resolve(r(n))},threads:t}},q=(o,t)=>{const a=t.exportName??"agent",i=t.env??{LUNORA_TEST:!0},u=T(t.functions??{});let h=0;const l=(d,c)=>{h+=1;const p=c?.instanceId??`wf-${String(h)}`,e=c?.script??t.script;return y({agent:o,env:i,exportName:a,generate:v(e),instanceId:p,params:d,paths:g,run:u.run,step:new k})};return{dispatches:u.dispatches,messages:d=>[...u.messages.values()].filter(c=>c.threadKey===d).toSorted((c,p)=>c.seq-p.seq).map(({messageKey:c,threadKey:p,...e})=>e),run:l,thread:d=>u.threads.get(d)}},C=(o,t)=>({text:o,toolCalls:[],...t}),E=(o,t,a,i="")=>({text:i,toolCalls:[{id:o,input:a,name:t}]});export{q as agentHarness,C as finalTurn,E as toolCallTurn};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{LunoraError as p}from"@lunora/errors";const d=/^\s*(-?\d+(?:\.\d+)?)/u,i=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,l=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,r)=>t+r,0)/e.length,v=(e,t={})=>({name:`contains:${e}`,score:({output:r})=>{const s=t.caseSensitive?r:r.toLowerCase(),a=t.caseSensitive?e:e.toLowerCase();return s.includes(a)?1:0}}),x=(e,t="regex")=>({name:t,score:({output:r})=>e.test(r)?1:0}),y=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),f=e=>{if(e.length===0)throw new p("BAD_REQUEST","@lunora/testing: keywordScorer requires at least one keyword");return{name:"keyword-coverage",score:({output:t})=>{const r=t.toLowerCase(),s=e.filter(a=>r.includes(a.toLowerCase())).length;return{reason:`${String(s)}/${String(e.length)} keywords present`,score:s/e.length}}}},g=(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=d.exec(e);return{reason:e.trim(),score:t?i(Number(t[1])):0}},$=e=>({name:e.name??"llm-judge",score:async t=>w(await e.judge(g(e.criteria,t)))}),h=async(e,t)=>{const r=await Promise.all(t.map(async o=>({name:o.name,result:l(await o.score(e))}))),s={},a=new Map;for(const{name:o,result:c}of r){const n=a.get(o)??0;a.set(o,n+1),s[n===0?o:`${o}#${String(n+1)}`]=c}return{average:u(r.map(({result:o})=>o.score)),scores:s}},b=async(e,t,r)=>{const s=await Promise.all(e.map(async a=>{const o=await t(a.input),c={input:a.input,output:o,...a.expected===void 0?{}:{expected:a.expected},...a.metadata===void 0?{}:{metadata:a.metadata}},{average:n,scores:m}=await h(c,r);return{average:n,input:a.input,output:o,scores:m}}));return{average:u(s.map(a=>a.average)),items:s}};export{v as containsScorer,b as evaluate,y as exactMatchScorer,f as keywordScorer,$ as llmScorer,x as regexScorer,h as scoreSample};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as a}from"@lunora/errors";const n=/[\w.-]/u,o=t=>{let e="";for(const r of t)e+=n.test(r)?r:"_";return e},i=t=>{if(typeof t.name!="string"||t.name.length===0)throw new a("BAD_REQUEST","@lunora/testing: recordEvaluation requires a non-empty `name`");if(typeof t.score!="number"||!Number.isFinite(t.score))throw new a("BAD_REQUEST","@lunora/testing: recordEvaluation `score` must be a finite number");const e=o(t.name),r={[`gen_ai.evaluation.${e}.score`]:t.score};return t.label!==void 0&&(r[`gen_ai.evaluation.${e}.label`]=t.label),r},l=t=>{const e=i(t);return t.span?.setAttributes(e),t.metrics?.gauge(`gen_ai.evaluation.${o(t.name)}.score`,t.score,t.label===void 0?void 0:{label:t.label}),e};export{i as evaluationAttributes,l as recordEvaluation};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as C}from"@lunora/errors";import{runShardMigrations as z,createShardCtxDb as X,RLS_UNWRAP_SYMBOL as Z}from"@lunora/shard-engine";import{evaluationAttributes as ee}from"./evaluationAttributes-94eqWXgB.mjs";import{DatabaseSync as te}from"node:sqlite";const re=(s,c,n,l,I)=>{let m=I,w=1;const u=new Map,h=[],b=(t,a,d={})=>{const e=`fake-job-${String(w)}`;return w+=1,u.set(e,{args:d,enqueuedAt:m,functionPath:a,id:e,scheduledFor:t}),e},p=t=>typeof t=="string"?t:t.name??t.binding??"",A={cancel:t=>{const a=u.has(t);return u.delete(t),Promise.resolve({cancelled:a})},get:t=>Promise.resolve(u.get(t)??null),list:()=>Promise.resolve([...u.values()]),runAfter:(t,a,d)=>{const e=b(m+t,p(a),d);return Promise.resolve(e)},runAt:(t,a,d)=>{const e=b(t,p(a),d);return Promise.resolve(e)}},R=async t=>{u.delete(t.id);const a=l().get(t.functionPath);if(a===void 0){console.warn(`[fake-scheduler] unknown functionPath "${t.functionPath}" — job ${t.id} dropped`);return}if(a.kind==="mutation"||a.kind==="action"){const d=s(),e=a.kind==="action"?n():c();await d(a.kind,a,e,t.args)}else console.warn(`[fake-scheduler] functionPath "${t.functionPath}" is a ${a.kind} — only mutations and actions can be scheduled; job ${t.id} dropped`)},S=async t=>{const a=[...u.values()].filter(o=>o.scheduledFor<=t).toSorted((o,f)=>o.scheduledFor-f.scheduledFor),d=[];let e=0;for(const o of a)if(u.has(o.id)){e+=1;try{await R(o)}catch(f){const k={args:o.args,error:f,functionPath:o.functionPath,id:o.id};d.push(k),h.push(k)}}return{executed:e,failed:d}},$=async(t,a)=>{const{executed:d,failed:e}=await S(t);if(e.length>0&&(a?.throwOnError??!0)){const[o]=e;throw e.length===1&&o!==void 0?o.error:new AggregateError(e.map(f=>f.error),`${String(e.length)} scheduled jobs failed: ${e.map(f=>f.functionPath).join(", ")}`)}return d};return{controls:{advance:(t,a)=>(m+=t,$(m,a)),failures:()=>[...h],list:()=>[...u.values()],runPending:t=>$(Number.POSITIVE_INFINITY,t)},scheduler:A}},ne=()=>{const s=new te(":memory:"),c=n=>({one(){if(n.length!==1)throw new C("INTERNAL",`expected exactly one row, received ${String(n.length)}`);const[l]=n;return l},[Symbol.iterator](){return n[Symbol.iterator]()},toArray(){return n}});return{close:()=>{s.close()},sql:{exec:(n,...l)=>{const I=s.prepare(n).all(...l);return c(I)}}}},q=s=>{if(typeof s!="object"||s===null)return;const{kind:c}=s;if(c==="query"||c==="mutation"||c==="action")return c},oe=s=>typeof s=="object"&&s!==null&&s.visibility==="internal"?"internal":"public",F=s=>{throw new C("INTERNAL",`ctx.${s} is not available in the in-memory @lunora/testing harness (v1)`)},g=s=>new Proxy((...c)=>F(s),{apply:()=>F(s),get:()=>F(s)}),U={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},se={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>U},ae=()=>{const s={attributes:{},events:[],links:[]},c={addEvent:(n,l)=>{s.events.push({...l===void 0?{}:{attributes:{...l}},name:n})},addLink:n=>{s.links.push({spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(s.attributes,ee(n))},recordException:n=>{const l={"exception.message":n instanceof Error?n.message:String(n),"exception.type":n instanceof Error?n.constructor.name:"Error"};n instanceof Error&&n.stack!==void 0&&(l["exception.stacktrace"]=n.stack),c.addEvent("exception",l)},setAttribute:(n,l)=>{s.attributes[n]=l},setAttributes:n=>{Object.assign(s.attributes,n)},spanContext:()=>U};return{handle:c,recorded:s}},M=async(s,c)=>await c(M,se),D={count:()=>{},gauge:()=>{},record:()=>{}},O={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>O},ie=(s,c,n)=>(l,I)=>{let m=!1;const w=[];let u,h,b=0,p=0;const A=()=>q(l)?s("query",l,c,I,!1):Promise.resolve(l(c)),R=(e,o)=>{if(e<p)return;p=e;const f={done:!1,value:o};if(w.length===0)u=f,h=void 0;else{u=void 0,h=void 0;for(const k of w.splice(0))k.resolve(f)}},S=(e,o)=>{if(!(e<p))if(p=e,w.length===0)h={error:o},u=void 0;else{u=void 0,h=void 0;for(const f of w.splice(0))f.reject(o)}},$=e=>o=>{R(e,o)},t=e=>o=>{S(e,o)},a=()=>{if(m)return;b+=1;const e=b;A().then($(e)).catch(t(e))};n.add(a);const d={[Symbol.asyncIterator](){return d},next:()=>{if(m)return Promise.resolve({done:!0,value:void 0});if(p===b){if(h!==void 0){const{error:e}=h;return h=void 0,Promise.reject(e)}if(u!==void 0){const e=u;return u=void 0,Promise.resolve(e)}}return p<b?new Promise((e,o)=>{w.push({reject:o,resolve:e})}):A().then(e=>{if(h!==void 0){const{error:o}=h;throw h=void 0,o}if(u!==void 0){const o=u;return u=void 0,o}return{done:!1,value:e}})},return:()=>{m=!0,n.delete(a);for(const e of w.splice(0))e.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return A().then($(0)).catch(t(0)),d},fe=(s,c)=>{const{close:n,sql:l}=ne(),I=s;z(l,I);const m=X({enforceRls:c?.enforceRls??!0,schema:I,sql:l}),w=m[Z]??m,u=v=>{l.exec.call(l,v)};let h=Promise.resolve();const b=v=>{const P=async()=>{u("BEGIN");try{const y=await v();return u("COMMIT"),y}catch(y){try{u("ROLLBACK")}catch{}throw y}},x=h.then(P);return h=x.then(()=>{},()=>{}),x};let p=!1;const A=()=>{p||(p=!0,n())},R=new Map(Object.entries(c?.functions??{}).map(([v,P])=>[v,P])),S=new Set,$=()=>{for(const v of S)v()},t=v=>($(),v);let a,d,e;const o=c?.now??Date.now(),f=ae(),k=(v,P)=>{if(v===void 0)throw new C("INTERNAL",`[fake-scheduler] ${P} not yet available — scheduler.advance called before harness construction completed`);return v},{controls:G,scheduler:B}=re(()=>k(a,"dispatch"),()=>k(d,"mutationContext"),()=>k(e,"actionContext"),()=>R,o),_=v=>{const P={getIdentity:()=>Promise.resolve(v??null),userId:v?.userId??null},x={auth:P,db:m,env:c?.env,log:O,metrics:D,now:o,span:f.handle,trace:M,runQuery:((r,i)=>N("query",r,x,i)),secrets:g("secrets"),storage:g("storage"),vectors:g("vectors")},y={auth:P,db:m,env:c?.env,log:O,metrics:D,now:o,span:f.handle,trace:M,runMutation:((r,i)=>N("mutation",r,y,i)),runQuery:((r,i)=>N("query",r,x,i)),scheduler:B,secrets:g("secrets"),storage:g("storage"),vectors:g("vectors"),workflows:g("workflows")};d??=y;const K={...y,db:w},T={auth:P,db:m,env:c?.env,fetch:c?.fetch??g("fetch"),log:O,metrics:D,now:o,span:f.handle,trace:M,runAction:((r,i)=>N("action",r,T,i)),runMutation:((r,i)=>N("mutation",r,y,i)),runQuery:((r,i)=>N("query",r,x,i)),scheduler:B,secrets:g("secrets"),storage:g("storage"),vectors:g("vectors"),workflows:g("workflows")};e??=T;const j=(r,i,E,L,J)=>{const Q=q(i);if(Q!==r)throw new C("INTERNAL",`expected a registered ${r}, received a ${Q??"non-function"} reference`);if(!J&&oe(i)==="internal")throw new C("INTERNAL",`This ${r} is an internal function — it is unreachable from the external RPC boundary in production. Call it through ctx.run${r.charAt(0).toUpperCase()}${r.slice(1)} from another function instead.`);return Promise.resolve(i.handler(E,L??{}))},N=(r,i,E,L)=>j(r,i,E,L,!0);a??=(r,i,E,L)=>r==="mutation"?b(()=>j("mutation",i,E,L,!0)).then(t):N("action",i,E,L);const V=((r,i)=>q(r)?j("query",r,x,i,!1):Promise.resolve(r(x))),W=((r,i)=>{const E=q(r)?()=>j("mutation",r,y,i,!1):()=>r(y);return b(E).then(t)}),Y=((r,i)=>q(r)?j("action",r,T,i,!1):Promise.resolve(r(T))),H=ie(j,x,S);return{action:Y,close:A,mutation:W,query:V,run:r=>b(()=>r(K)).then(t),scheduler:G,subscribe:H,wideEvent:()=>f.recorded,withIdentity:r=>_(r)}};return _(null)};export{fe as lunoraTest};
|