@lunora/testing 1.0.0-alpha.104 → 1.0.0-alpha.105

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
@@ -121,6 +121,14 @@ type EvaluationAttributeValue = number | string;
121
121
  interface EvaluationSpanHandle {
122
122
  setAttributes: (fields: Record<string, EvaluationAttributeValue>) => void;
123
123
  }
124
+ /**
125
+ * Structural slice of `ctx.metrics` — enough to record a score as a durable
126
+ * series. Declared here for the same reason as {@link EvaluationSpanHandle}: no
127
+ * dependency on `@lunora/server`, and the real handle is assignable.
128
+ */
129
+ interface EvaluationMetrics {
130
+ gauge: (name: string, value: number, attributes?: Record<string, unknown>) => void;
131
+ }
124
132
  /** One eval verdict to emit. */
125
133
  interface RecordEvaluationInput {
126
134
  /**
@@ -128,6 +136,15 @@ interface RecordEvaluationInput {
128
136
  * emitted as the `.label` attribute. Omitted → no label attribute.
129
137
  */
130
138
  label?: string;
139
+ /**
140
+ * Optional `ctx.metrics` handle. Passing it ALSO records the score as a
141
+ * `gen_ai.evaluation.<name>.score` gauge, which is what gives an eval a
142
+ * durable history: span attributes live in the shard's bounded in-memory
143
+ * ring and vanish on hibernation, while metrics are persisted in per-minute
144
+ * buckets and can be charted as a trend. Additive — the attributes are
145
+ * emitted either way.
146
+ */
147
+ metrics?: EvaluationMetrics;
131
148
  /**
132
149
  * The scorer/evaluation name — becomes the key's name segment. Any character
133
150
  * outside `[A-Za-z0-9._-]` is replaced with `_` so a scorer name carrying a
@@ -582,4 +599,4 @@ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>
582
599
  * Returns per-case results plus the mean of their averages.
583
600
  */
584
601
  declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
585
- export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
602
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
package/dist/index.d.ts CHANGED
@@ -121,6 +121,14 @@ type EvaluationAttributeValue = number | string;
121
121
  interface EvaluationSpanHandle {
122
122
  setAttributes: (fields: Record<string, EvaluationAttributeValue>) => void;
123
123
  }
124
+ /**
125
+ * Structural slice of `ctx.metrics` — enough to record a score as a durable
126
+ * series. Declared here for the same reason as {@link EvaluationSpanHandle}: no
127
+ * dependency on `@lunora/server`, and the real handle is assignable.
128
+ */
129
+ interface EvaluationMetrics {
130
+ gauge: (name: string, value: number, attributes?: Record<string, unknown>) => void;
131
+ }
124
132
  /** One eval verdict to emit. */
125
133
  interface RecordEvaluationInput {
126
134
  /**
@@ -128,6 +136,15 @@ interface RecordEvaluationInput {
128
136
  * emitted as the `.label` attribute. Omitted → no label attribute.
129
137
  */
130
138
  label?: string;
139
+ /**
140
+ * Optional `ctx.metrics` handle. Passing it ALSO records the score as a
141
+ * `gen_ai.evaluation.<name>.score` gauge, which is what gives an eval a
142
+ * durable history: span attributes live in the shard's bounded in-memory
143
+ * ring and vanish on hibernation, while metrics are persisted in per-minute
144
+ * buckets and can be charted as a trend. Additive — the attributes are
145
+ * emitted either way.
146
+ */
147
+ metrics?: EvaluationMetrics;
131
148
  /**
132
149
  * The scorer/evaluation name — becomes the key's name segment. Any character
133
150
  * outside `[A-Za-z0-9._-]` is replaced with `_` so a scorer name carrying a
@@ -582,4 +599,4 @@ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>
582
599
  * Returns per-case results plus the mean of their averages.
583
600
  */
584
601
  declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
585
- export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
602
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-0b923YYC.mjs";import{evaluationAttributes as c,recordEvaluation as n}from"./packem_shared/evaluationAttributes-GZvtFa1N.mjs";import{lunoraTest as u}from"./packem_shared/lunoraTest-RV15r0tW.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-Dr5tjL-M.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};
1
+ import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-D2XNYX0F.mjs";import{evaluationAttributes as c,recordEvaluation as n}from"./packem_shared/evaluationAttributes-94eqWXgB.mjs";import{lunoraTest as u}from"./packem_shared/lunoraTest-BP5oHAKD.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-Dr5tjL-M.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 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};
@@ -0,0 +1 @@
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 +1 @@
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-GZvtFa1N.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};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/testing",
3
- "version": "1.0.0-alpha.104",
3
+ "version": "1.0.0-alpha.105",
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.51",
54
- "@lunora/errors": "1.0.0-alpha.20",
55
- "@lunora/mail": "1.0.0-alpha.47",
56
- "@lunora/server": "1.0.0-alpha.70",
57
- "@lunora/shard-engine": "1.0.0-alpha.24"
53
+ "@lunora/agent": "1.0.0-alpha.52",
54
+ "@lunora/errors": "1.0.0-alpha.21",
55
+ "@lunora/mail": "1.0.0-alpha.48",
56
+ "@lunora/server": "1.0.0-alpha.71",
57
+ "@lunora/shard-engine": "1.0.0-alpha.25"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@playwright/test": "^1.61.1"
@@ -1 +0,0 @@
1
- import{runAgentLoop as w,DEFAULT_AGENT_FUNCTION_PATHS as g}from"@lunora/agent";const y=o=>{const e=[...o];return()=>{const a=e.shift();if(!a)throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");return Promise.resolve(a)}};class v{invoked=[];entries=new Map;async do(e,a){const i=this.entries.get(e);if(i)return i.output;this.invoked.push(e);const u=await a();return this.entries.set(e,{output:u}),u}async waitForEvent(e,a){return new Promise(()=>{})}}const T=o=>{const e=new Map,a=new Map,i=[],u=t=>{const s=t?.key,r=t?.instanceId,n=e.get(s);return n?(n.status="running",delete n.error,r!==void 0&&(n.instanceId=r),{created:!1}):(e.set(s,{agent:t?.agent,instanceId:r,key:s,messageCount:0,owner:t?.owner,status:"running",title:t?.title}),{created:!0})},p=t=>{const s=t?.threadKey,r=t?.messageKey,n=`${s}:${r}`,f=a.get(n);if(f)return{seq:f.seq};const l=e.get(s);if(!l)throw new Error(`agentHarness: append to unknown thread "${s}".`);const m=l.messageCount;return l.messageCount+=1,a.set(n,{...t,seq:m}),{seq:m}},h=t=>{const s=t?.key;return[...a.values()].filter(r=>r.threadKey===s).toSorted((r,n)=>r.seq-n.seq)},d=t=>{const s=e.get(t?.key);if(s)for(const[r,n]of Object.entries(t??{}))r!=="key"&&n!==void 0&&(s[r]=n)},c=new Map([[g.appendMessage,p],[g.ensureThread,u],[g.listMessages,h],[g.patchThread,d],...Object.entries(o)]);return{dispatches:i,messages:a,run:(t,s)=>{const r=t.__lunoraRef;i.push({args:s,path:r});const n=c.get(r);if(!n)throw new Error(`agentHarness: unstubbed dispatch "${r}" — pass a \`functions\` handler for it.`);return Promise.resolve(n(s))},threads:e}},q=(o,e)=>{const a=e.exportName??"agent",i=e.env??{LUNORA_TEST:!0},u=T(e.functions??{});let p=0;const h=(d,c)=>{p+=1;const t=c?.instanceId??`wf-${String(p)}`,s=c?.script??e.script;return w({agent:o,env:i,exportName:a,generate:y(s),instanceId:t,params:d,paths:g,run:u.run,step:new v})};return{dispatches:u.dispatches,messages:d=>[...u.messages.values()].filter(c=>c.threadKey===d).toSorted((c,t)=>c.seq-t.seq).map(({messageKey:c,threadKey:t,...s})=>s),run:h,thread:d=>u.threads.get(d)}},C=(o,e)=>({text:o,toolCalls:[],...e}),E=(o,e,a,i="")=>({text:i,toolCalls:[{id:o,input:a,name:e}]});export{q as agentHarness,C as finalTurn,E as toolCallTurn};
@@ -1 +0,0 @@
1
- import{LunoraError as o}from"@lunora/errors";const a=/[\w.-]/u,t=e=>{let r="";for(const n of e)r+=a.test(n)?n:"_";return r},i=e=>{if(typeof e.name!="string"||e.name.length===0)throw new o("BAD_REQUEST","@lunora/testing: recordEvaluation requires a non-empty `name`");if(typeof e.score!="number"||!Number.isFinite(e.score))throw new o("BAD_REQUEST","@lunora/testing: recordEvaluation `score` must be a finite number");const r=t(e.name),n={[`gen_ai.evaluation.${r}.score`]:e.score};return e.label!==void 0&&(n[`gen_ai.evaluation.${r}.label`]=e.label),n},u=e=>{const r=i(e);return e.span?.setAttributes(r),r};export{i as evaluationAttributes,u as recordEvaluation};