@lunora/testing 1.0.0-alpha.320 → 1.0.0-alpha.322
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 +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/absentScorer-tcGbGxnq.mjs +2 -0
- package/dist/packem_shared/groundednessScorer-D8-EWo1j.mjs +3 -0
- package/dist/packem_shared/lunoraTest-fMV6QTPd.mjs +1 -0
- package/package.json +10 -10
- package/dist/packem_shared/containsScorer-DoLfFOs6.mjs +0 -2
- package/dist/packem_shared/groundednessScorer-kgXJCZDg.mjs +0 -3
- package/dist/packem_shared/lunoraTest-KJJjjDCt.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -970,6 +970,20 @@ interface EvalResult {
|
|
|
970
970
|
declare const containsScorer: (needle: string, options?: {
|
|
971
971
|
caseSensitive?: boolean;
|
|
972
972
|
}) => Scorer;
|
|
973
|
+
/**
|
|
974
|
+
* Score 1 when the output does NOT contain `needle` — the negative half of
|
|
975
|
+
* {@link containsScorer}.
|
|
976
|
+
*
|
|
977
|
+
* Every other scorer here asserts something is present, which cannot express
|
|
978
|
+
* the expectations that matter most for a refusal or a leak: "never dispatched
|
|
979
|
+
* the write", "never offered the tool this tier forbids", "the injected fence
|
|
980
|
+
* marker did not survive". Spelling those as a negative-lookahead
|
|
981
|
+
* {@link regexScorer} works but reads as a puzzle, and gets the escaping wrong
|
|
982
|
+
* the first time someone copies it.
|
|
983
|
+
*/
|
|
984
|
+
declare const absentScorer: (needle: string, options?: {
|
|
985
|
+
caseSensitive?: boolean;
|
|
986
|
+
}) => Scorer;
|
|
973
987
|
/** Score 1 when `pattern` matches the output. */
|
|
974
988
|
declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
|
|
975
989
|
/** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
|
|
@@ -1064,4 +1078,4 @@ declare const groundednessScorer: (options: {
|
|
|
1064
1078
|
judge: (prompt: string) => Promise<string>;
|
|
1065
1079
|
name?: string;
|
|
1066
1080
|
}) => Scorer;
|
|
1067
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|
|
1081
|
+
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, absentScorer, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|
package/dist/index.d.ts
CHANGED
|
@@ -970,6 +970,20 @@ interface EvalResult {
|
|
|
970
970
|
declare const containsScorer: (needle: string, options?: {
|
|
971
971
|
caseSensitive?: boolean;
|
|
972
972
|
}) => Scorer;
|
|
973
|
+
/**
|
|
974
|
+
* Score 1 when the output does NOT contain `needle` — the negative half of
|
|
975
|
+
* {@link containsScorer}.
|
|
976
|
+
*
|
|
977
|
+
* Every other scorer here asserts something is present, which cannot express
|
|
978
|
+
* the expectations that matter most for a refusal or a leak: "never dispatched
|
|
979
|
+
* the write", "never offered the tool this tier forbids", "the injected fence
|
|
980
|
+
* marker did not survive". Spelling those as a negative-lookahead
|
|
981
|
+
* {@link regexScorer} works but reads as a puzzle, and gets the escaping wrong
|
|
982
|
+
* the first time someone copies it.
|
|
983
|
+
*/
|
|
984
|
+
declare const absentScorer: (needle: string, options?: {
|
|
985
|
+
caseSensitive?: boolean;
|
|
986
|
+
}) => Scorer;
|
|
973
987
|
/** Score 1 when `pattern` matches the output. */
|
|
974
988
|
declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
|
|
975
989
|
/** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
|
|
@@ -1064,4 +1078,4 @@ declare const groundednessScorer: (options: {
|
|
|
1064
1078
|
judge: (prompt: string) => Promise<string>;
|
|
1065
1079
|
name?: string;
|
|
1066
1080
|
}) => Scorer;
|
|
1067
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|
|
1081
|
+
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, absentScorer, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, 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-BM24QFnh.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{createArtifactsFake as m}from"./packem_shared/createArtifactsFake-L0tjki11.mjs";import{createFakeSecretValues as
|
|
1
|
+
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-BM24QFnh.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{createArtifactsFake as m}from"./packem_shared/createArtifactsFake-L0tjki11.mjs";import{createFakeSecretValues as p}from"./packem_shared/createFakeSecretValues-BTNOscqQ.mjs";import{lunoraTest as f}from"./packem_shared/lunoraTest-fMV6QTPd.mjs";import{createStreamGate as u,recordedModel as d}from"./packem_shared/createStreamGate-Geg-yvx6.mjs";import{groundednessScorer as g,mrrScorer as k,ndcgAtK as M,precisionAtK as v,recallAtK as F}from"./packem_shared/groundednessScorer-D8-EWo1j.mjs";import{absentScorer as T,containsScorer as b,evaluate as w,exactMatchScorer as C,keywordScorer as h,llmScorer as y,regexScorer as E,scoreSample as G}from"./packem_shared/absentScorer-tcGbGxnq.mjs";import{extractLink as L,listCapturedMail as V,waitForMail as j}from"@lunora/mail/testing";export{T as absentScorer,o as agentHarness,b as containsScorer,m as createArtifactsFake,p as createFakeSecretValues,u as createStreamGate,w as evaluate,l as evaluationAttributes,C as exactMatchScorer,L as extractLink,t as finalTurn,g as groundednessScorer,h as keywordScorer,V as listCapturedMail,y as llmScorer,f as lunoraTest,k as mrrScorer,M as ndcgAtK,v as precisionAtK,F as recallAtK,n as recordEvaluation,d as recordedModel,E as regexScorer,G as scoreSample,a as toolCallTurn,j 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,S=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,v=(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}}},x=(e,t={})=>{if(e.length===0)throw new i("BAD_REQUEST","@lunora/testing: absentScorer requires a non-empty needle");return{name:`absent:${e}`,score:({output:n})=>{const a=t.caseSensitive?n:n.toLowerCase(),r=t.caseSensitive?e:e.toLowerCase();return a.includes(r)?0:1}}},E=(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}),b=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}}}},h=(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=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}},L=e=>({name:e.name??"llm-judge",score:async t=>w(await e.judge(h(e.criteria,t)))}),f=async(e,t)=>{const n=await Promise.all(t.map(async o=>({name:o.name,result:S(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:l,scores:d}=await f(g,n);return{average:l,input:r.input,output:c,scores:d}}));return{average:m(a.map(r=>r.average)),items:a}};export{x as absentScorer,v as containsScorer,A as evaluate,$ as exactMatchScorer,b as keywordScorer,L as llmScorer,w as parseJudgeScore,E as regexScorer,f as scoreSample};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{LunoraError as g}from"@lunora/errors";import{parseJudgeScore as h}from"./absentScorer-tcGbGxnq.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
|
+
${r}`,"",`Assistant answer: ${n.output}`].join(`
|
|
3
|
+
`));return h(o)}}};export{K as groundednessScorer,y as mrrScorer,$ as ndcgAtK,x as precisionAtK,p as recallAtK};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AsyncLocalStorage as ve}from"node:async_hooks";import{LunoraError as E}from"@lunora/errors";import{withDeferredSchedules as me}from"@lunora/server";import{createInProcessRuntime as pe,resolveInProcessIdentity as ye,runRegisteredFunction as D,registeredFunctionKind as V}from"@lunora/server/in-process";import{evaluationAttributes as ge}from"./evaluationAttributes-CYLgTP1P.mjs";import{memorySubscriptionStore as we,createNotify as be}from"@lunora/notify";import{createNotification as Se}from"@visulima/notification";import{serialize as Ae,deserialize as Ee}from"node:v8";import{createQueues as Ne,createTopicContext as Re}from"@lunora/queue";import{assertScheduleDelay as Te,TERMINAL_RETENTION_MS as Pe,MAX_RETRY_ATTEMPTS as _e,RETRY_BASE_DELAY_MS as Ie}from"@lunora/scheduler";import{encodeWire as xe,decodeWire as Oe}from"@lunora/wire";import{DatabaseSync as $e}from"node:sqlite";const I=(t,e)=>{throw new E("INTERNAL",e===void 0?`ctx.${t} is not available in the in-memory @lunora/testing harness (v1)`:`ctx.${t} is not enabled in the @lunora/testing harness — pass lunoraTest(schema, { ${e} })`)},ke=new Set(["$$typeof","asymmetricMatch","then","toJSON"]),qe=t=>new Proxy({...t},{get(e,r){if(!(ke.has(r)&&!Object.hasOwn(e,r))){if(typeof r=="symbol"||Object.hasOwn(e,r)||r in Object.prototype)return Reflect.get(e,r);throw new E("INTERNAL",`ctx.services.${r} has no fake in the @lunora/testing harness — pass lunoraTest(schema, { services: { ${r}: … } })`)}}}),b=(t,e)=>new Proxy((...r)=>I(t,e),{apply:()=>I(t,e),get:()=>I(t,e)}),oe={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},Ce={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>oe},De=()=>{const t={attributes:{},events:[],links:[]},e={addEvent:(r,i)=>{t.events.push({...i===void 0?{}:{attributes:{...i}},name:r})},addLink:r=>{t.links.push({spanId:r.spanId,traceId:r.traceId})},recordEvaluation:r=>{Object.assign(t.attributes,ge(r))},recordException:r=>{const i={"exception.message":r instanceof Error?r.message:String(r),"exception.type":r instanceof Error?r.constructor.name:"Error"};r instanceof Error&&r.stack!==void 0&&(i["exception.stacktrace"]=r.stack),e.addEvent("exception",i)},setAttribute:(r,i)=>{t.attributes[r]=i},setAttributes:r=>{Object.assign(t.attributes,r)},spanContext:()=>oe};return{handle:e,recorded:t}},Y=async(t,e)=>await e(Y,Ce),X={count:()=>{},gauge:()=>{},record:()=>{}},J={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>J},Q="notify",Le=t=>{let e=t;if(typeof t=="string"){if(!t.startsWith("{"))return;try{e=JSON.parse(t)}catch{return}}const r=e?.endpoint;return typeof r=="string"?r:void 0},M=(t,e)=>typeof t=="function"?t(e):t,Me=(t,e)=>{const r=[],i=(f,g)=>(r.push({channel:f,payload:structuredClone(g)}),{data:{messageId:`${f}-${String(r.length)}`,sent:!0,timestamp:new Date},success:!0}),y=f=>({channel:f,id:`lunora-testing-${f}`,initialize:()=>{},isAvailable:()=>!0,send:g=>i(f,g)}),a=M(t.webPush,e)!==void 0,w=M(t.fcm,e)!==void 0,l={...y("push"),send:f=>{const g=Array.isArray(f.to)?f.to:[f.to];if(g.length===0)throw new E("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");for(const x of g){const T=Le(x)!==void 0;if(T?!a:!w)throw new Error(T?"@lunora/notify: received a web-push target but no `webPush` channel is configured":"@lunora/notify: received an FCM token target but no `fcm` channel is configured")}return i("push",f)}},m={...a||w?{push:l}:{},...M(t.chat,e)===void 0?{}:{chat:y("chat")},...M(t.inApp,e)===void 0?{}:{inapp:y("inapp")},...M(t.webhook,e)===void 0?{}:{webhook:y("webhook")}},d=we(),{notify:h,push:A}=be({...t,store:()=>d},e,{engine:Se(m),silent:!0});return{controls:{clear:()=>{r.length=0},sent:f=>f===void 0?[...r]:r.filter(g=>g.channel===f)},surfaces:{notify:h,push:A}}},Fe=(t,e)=>t!==void 0?Me(t,e):{controls:{clear:()=>I("notify",Q),sent:()=>I("notify",Q)},surfaces:{notify:b("notify",Q),push:b("push",Q)}},G="queues: [...]",te=128*1024,re=256*1024,je="json",ne=new TextEncoder,Be=(t,e)=>{if(e==="v8"){const i=Ae(t);return{body:Ee(i),bytes:i.byteLength}}if(e==="text"){if(typeof t!="string")throw new E("VALIDATION_ERROR",`@lunora/queue: a "text" message body must be a string, got ${typeof t}`);return{body:t,bytes:ne.encode(t).byteLength}}if(e==="bytes"){if(!(t instanceof ArrayBuffer)&&!ArrayBuffer.isView(t))throw new E("VALIDATION_ERROR",'@lunora/queue: a "bytes" message body must be an ArrayBuffer or an ArrayBufferView');return{body:t,bytes:t.byteLength}}const r=JSON.stringify(t);if(typeof r!="string")throw new E("VALIDATION_ERROR",`@lunora/queue: a "json" message body must be JSON-serializable, got ${typeof t}`);return{body:JSON.parse(r),bytes:ne.encode(r).byteLength}},se=t=>{const e=new Set(t),r=[],i=(a,w,l)=>{const m=l?.contentType??je,d=Be(w,m);if(d.bytes>te)throw new E("VALIDATION_ERROR",`@lunora/queue: ${a} message is ${String(d.bytes)} bytes, over the Cloudflare Queues limit of ${String(te)} (128 KB)`);return{bytes:d.bytes,message:{body:d.body,contentType:m,...l?.delaySeconds===void 0?{}:{delaySeconds:l.delaySeconds},destination:a}}},y=a=>({send:(w,l)=>(r.push(i(a,w,l).message),Promise.resolve()),sendBatch:(w,l)=>{const m=[...w].map(h=>i(a,h.body,{contentType:h.contentType,delaySeconds:h.delaySeconds??l?.delaySeconds})),d=m.reduce((h,A)=>h+A.bytes,0);if(d>re)throw new E("VALIDATION_ERROR",`@lunora/queue: ${a} batch is ${String(d)} bytes, over the Cloudflare Queues limit of ${String(re)} (256 KB)`);return r.push(...m.map(h=>h.message)),Promise.resolve()}});return{bindings:Object.fromEntries([...e].map(a=>[a,y(a)])),clear:()=>{r.length=0},read:(a,w,l)=>{if(a===void 0)return[...r];if(!e.has(a))throw new E("INTERNAL",`${w}("${a}"): no such ${l} — declared: ${[...e].join(", ")||"(none)"}`);return r.filter(m=>m.destination===a)}}},Ue=t=>{const e=se(t);return{controls:{clear:e.clear,sent:r=>e.read(r,"harness.queues.sent","queue").map(({destination:i,...y})=>({...y,queue:i}))},surfaces:{queues:Ne({bindings:e.bindings})}}},Ke=t=>t!==void 0?Ue(t):{controls:{clear:()=>I("queues",G),sent:()=>I("queues",G)},surfaces:{queues:b("queues",G)}},Qe=(t,e,r,i,y)=>{let a=y,w=1;const l=new Map,m=new Map,d=new Map,h=[],A=(o,v,n={},s)=>{const c=xe(n);JSON.stringify(c);const S=Oe(c),N=s??`fake-job-${String(w)}`;return w+=1,d.delete(N),l.set(N,{args:S,enqueuedAt:a,functionPath:v,id:N,scheduledFor:o}),N},f=o=>typeof o=="string"?o:"__lunoraRef"in o?o.__lunoraRef:o.name??o.className??"",g={cancel:o=>{const v=l.get(o),n=v!==void 0;return v!==void 0&&d.set(o,{...v,completedAt:a,state:"canceled"}),l.delete(o),Promise.resolve({cancelled:n})},get:o=>{const v=l.get(o),n=m.get(o);let s=d.get(o);return s?.completedAt!==void 0&&s.completedAt<=a-Pe&&(s=void 0),v!==void 0?s={...v,state:"pending"}:n!==void 0&&(s={...n,state:"running"}),Promise.resolve(s??null)},list:()=>Promise.resolve([...l.values()]),runAfter:(o,v,n,s)=>{Te(o,"ctx.scheduler.runAfter");const c=A(a+o,f(v),n,s?.id);return Promise.resolve(c)},runAt:(o,v,n,s)=>{const c=A(o,f(v),n,s?.id);return Promise.resolve(c)}},x=async o=>{l.delete(o.id);const v=i(),n=o.functionPath===void 0?void 0:v.get(o.functionPath);if(n===void 0)throw new E("FUNCTION_NOT_FOUND",`unknown functionPath "${o.functionPath??"(workflow-targeted)"}"`,{status:404});if(n.kind!=="mutation"&&n.kind!=="action")throw new E("FUNCTION_NOT_FOUND",`functionPath "${o.functionPath??"(workflow-targeted)"}" is a ${n.kind} — only mutations and actions can be scheduled`,{status:404});const s=t(),c=n.kind==="action"?r():e();await s(n.kind,n,{...c,now:a},o.args)},T=async o=>{const v=[...l.values()].filter(c=>c.scheduledFor<=o).toSorted((c,S)=>c.scheduledFor-S.scheduledFor),n=[];let s=0;for(const c of v)if(l.has(c.id)){s+=1;try{m.set(c.id,c),await x(c),d.set(c.id,{...c,completedAt:a,state:"succeeded"})}catch(S){const N=(c.attempts??0)+1;if(N>_e){const $={args:c.args,error:S,functionPath:c.functionPath??"",id:c.id};n.push($),h.push($),d.set(c.id,{...c,attempts:N,completedAt:a,error:`dead-lettered after ${String(N)} attempts`,state:"failed"})}else{const $=Ie*2**(N-1);l.set(c.id,{...c,attempts:N,scheduledFor:a+$})}}finally{m.delete(c.id)}}return{executed:s,failed:n}},P=async(o,v)=>{const{executed:n,failed:s}=await T(o);if(s.length>0&&(v?.throwOnError??!0)){const[c]=s;throw s.length===1&&c!==void 0?c.error:new AggregateError(s.map(S=>S.error),`${String(s.length)} scheduled jobs failed: ${s.map(S=>S.functionPath).join(", ")}`)}return n};return{controls:{advance:(o,v)=>(a+=o,P(a,v)),failures:()=>[...h],list:()=>[...l.values()],runPending:o=>P(Number.POSITIVE_INFINITY,o)},scheduler:g}},Z="topics: [...]",Ve=t=>{if(t===void 0)return{controls:{clear:()=>I("topics",Z),published:()=>I("topics",Z)},surfaces:{topics:b("topics",Z)}};const e=se(t),r=Re(e.bindings,t.map(i=>({exportName:i,subscriptions:[{binding:i,exportName:i}]})));return{controls:{clear:e.clear,published:i=>e.read(i,"harness.topics.published","topic").map(({destination:y,...a})=>({...a,topic:y}))},surfaces:{topics:r}}},Ye=()=>{const t=new $e(":memory:"),e=i=>({one(){if(i.length!==1)throw new E("INTERNAL",`expected exactly one row, received ${String(i.length)}`);const[y]=i;return y},[Symbol.iterator](){return i[Symbol.iterator]()},toArray(){return i}});return{close:()=>{t.close()},sql:{exec:(i,...y)=>{const w=t.prepare(i).all(...y);return e(w)}}}},F=new ve,Je=(t,e,r,i)=>(a,w)=>{let l=!1;const m=[];let d,h,A=0,f=0;const g=()=>F.run(i(),()=>V(a)?t("query",a,e,w,!1):Promise.resolve(a(e))),x=(n,s)=>{if(n<f)return;f=n;const c={done:!1,value:s};if(m.length===0)d=c,h=void 0;else{d=void 0,h=void 0;for(const S of m.splice(0))S.resolve(c)}},T=(n,s)=>{if(!(n<f))if(f=n,m.length===0)h={error:s},d=void 0;else{d=void 0,h=void 0;for(const c of m.splice(0))c.reject(s)}},P=n=>s=>{x(n,s)},L=n=>s=>{T(n,s)},o=()=>{if(l)return;A+=1;const n=A;g().then(P(n)).catch(L(n))};r.add(o);const v={[Symbol.asyncIterator](){return v},next:()=>{if(l)return Promise.resolve({done:!0,value:void 0});if(f===A){if(h!==void 0){const{error:n}=h;return h=void 0,Promise.reject(n)}if(d!==void 0){const n=d;return d=void 0,Promise.resolve(n)}}return f<A?new Promise((n,s)=>{m.push({reject:s,resolve:n})}):g().then(n=>{if(h!==void 0){const{error:s}=h;throw h=void 0,s}if(d!==void 0){const s=d;return d=void 0,s}return{done:!1,value:n}})},return:()=>{l=!0,r.delete(o);for(const n of m.splice(0))n.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return g().then(P(0)).catch(L(0)),v},it=(t,e)=>{const{close:r,sql:i}=Ye();let y=!1;const a=()=>{y||(y=!0,r())},w=new Map(Object.entries(e?.functions??{}).map(([R,_])=>[R,_])),l=new Set,m=()=>{for(const R of l)R()},d=R=>(m(),R);let h,A,f;const g=()=>e?.now??Date.now(),x=g(),T=R=>((..._)=>F.run(g(),()=>R(..._))),P=De(),{controls:L,surfaces:o}=Ke(e?.queues),{controls:v,surfaces:n}=Ve(e?.topics),{controls:s,surfaces:c}=Fe(e?.notify,e?.env??{}),S=(R,_)=>{if(R===void 0)throw new E("INTERNAL",`[fake-scheduler] ${_} not yet available — scheduler.advance called before harness construction completed`);return R},{controls:N,scheduler:$}=Qe(()=>S(h,"dispatch"),()=>S(A,"mutationContext"),()=>S(f,"actionContext"),()=>w,x),W=me($),{createWriters:ce,resetHeadroom:j,runInTransaction:B}=pe(t,{enforceRls:e?.enforceRls,scheduler:W,sql:i}),ee=R=>{const _=ye(R),z={getIdentity:()=>Promise.resolve(_.claims),userId:_.userId},{database:H,rawDatabase:ie}=ce(_),k={...c,auth:z,db:H,env:e?.env,log:J,metrics:X,get now(){return F.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:P.handle,trace:Y,runQuery:((u,p)=>O("query",u,k,p)),secrets:b("secrets"),storage:b("storage"),vectors:b("vectors")},q={...c,...o,...n,auth:z,db:H,env:e?.env,log:J,metrics:X,get now(){return F.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:P.handle,trace:Y,runMutation:((u,p)=>O("mutation",u,q,p)),runQuery:((u,p)=>O("query",u,k,p)),scheduler:W,secrets:b("secrets"),storage:b("storage"),vectors:b("vectors"),workflows:b("workflows")};A??=q;const ae=Object.defineProperties({},{...Object.getOwnPropertyDescriptors(q),db:{enumerable:!0,value:ie}}),U={...c,...o,...n,auth:z,db:H,env:e?.env,fetch:e?.fetch??b("fetch"),log:J,metrics:X,get now(){return F.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:P.handle,trace:Y,runAction:((u,p)=>O("action",u,U,p)),runMutation:((u,p)=>B(()=>O("mutation",u,q,p)).then(d)),runQuery:((u,p)=>O("query",u,k,p)),scheduler:W,secrets:b("secrets"),services:qe(e?.services),storage:b("storage"),vectors:b("vectors"),workflows:b("workflows")};f??=U;const O=(u,p,C,K)=>D(u,p,C,K,!0);h??=(u,p,C,K)=>u==="mutation"?B(()=>D("mutation",p,C,K,!0)).then(d):(j(),O("action",p,C,K));const ue=((u,p)=>(j(),V(u)?D("query",u,k,p,!1):Promise.resolve(u(k)))),de=((u,p)=>{const C=V(u)?()=>D("mutation",u,q,p,!1):()=>u(q);return B(C).then(d)}),le=((u,p)=>(j(),V(u)?D("action",u,U,p,!1):Promise.resolve(u(U)))),fe=Je((...u)=>(j(),D(...u)),k,l,()=>e?.now??Date.now()),he=u=>B(()=>u(ae)).then(d);return{action:T(le),close:a,mutation:T(de),notify:s,query:T(ue),queues:L,run:T(he),scheduler:N,subscribe:fe,topics:v,wideEvent:()=>P.recorded,withIdentity:u=>ee(u)}};return ee(null)};export{it as lunoraTest};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/testing",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.322",
|
|
4
4
|
"description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/agent": "1.0.0-alpha.
|
|
54
|
-
"@lunora/bindings": "1.0.0-alpha.
|
|
53
|
+
"@lunora/agent": "1.0.0-alpha.269",
|
|
54
|
+
"@lunora/bindings": "1.0.0-alpha.145",
|
|
55
55
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
56
|
-
"@lunora/mail": "1.0.0-alpha.
|
|
57
|
-
"@lunora/notify": "1.0.0-alpha.
|
|
58
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
59
|
-
"@lunora/queue": "1.0.0-alpha.
|
|
60
|
-
"@lunora/scheduler": "1.0.0-alpha.
|
|
61
|
-
"@lunora/server": "1.0.0-alpha.
|
|
62
|
-
"@lunora/wire": "1.0.0-alpha.
|
|
56
|
+
"@lunora/mail": "1.0.0-alpha.133",
|
|
57
|
+
"@lunora/notify": "1.0.0-alpha.105",
|
|
58
|
+
"@lunora/platform": "1.0.0-alpha.77",
|
|
59
|
+
"@lunora/queue": "1.0.0-alpha.137",
|
|
60
|
+
"@lunora/scheduler": "1.0.0-alpha.162",
|
|
61
|
+
"@lunora/server": "1.0.0-alpha.259",
|
|
62
|
+
"@lunora/wire": "1.0.0-alpha.6",
|
|
63
63
|
"@visulima/notification": "2.0.2",
|
|
64
64
|
"ai": "7.0.127"
|
|
65
65
|
},
|
|
@@ -1,2 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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
|
-
${r}`,"",`Assistant answer: ${n.output}`].join(`
|
|
3
|
-
`));return h(o)}}};export{K as groundednessScorer,y as mrrScorer,$ as ndcgAtK,x as precisionAtK,p as recallAtK};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{AsyncLocalStorage as me}from"node:async_hooks";import{LunoraError as N}from"@lunora/errors";import{withDeferredSchedules as ve}from"@lunora/server";import{createInProcessRuntime as pe,resolveInProcessIdentity as ye,runRegisteredFunction as C,registeredFunctionKind as K}from"@lunora/server/in-process";import{evaluationAttributes as ge}from"./evaluationAttributes-CYLgTP1P.mjs";import{memorySubscriptionStore as we,createNotify as be}from"@lunora/notify";import{createNotification as Se}from"@visulima/notification";import{serialize as Ae,deserialize as Ee}from"node:v8";import{createQueues as Ne,createTopicContext as Pe}from"@lunora/queue";import{assertScheduleDelay as Re,MAX_RETRY_ATTEMPTS as Te,RETRY_BASE_DELAY_MS as _e}from"@lunora/scheduler";import{encodeWire as Ie,decodeWire as xe}from"@lunora/wire";import{DatabaseSync as Oe}from"node:sqlite";const I=(t,e)=>{throw new N("INTERNAL",e===void 0?`ctx.${t} is not available in the in-memory @lunora/testing harness (v1)`:`ctx.${t} is not enabled in the @lunora/testing harness — pass lunoraTest(schema, { ${e} })`)},ke=new Set(["$$typeof","asymmetricMatch","then","toJSON"]),qe=t=>new Proxy({...t},{get(e,r){if(!(ke.has(r)&&!Object.hasOwn(e,r))){if(typeof r=="symbol"||Object.hasOwn(e,r)||r in Object.prototype)return Reflect.get(e,r);throw new N("INTERNAL",`ctx.services.${r} has no fake in the @lunora/testing harness — pass lunoraTest(schema, { services: { ${r}: … } })`)}}}),A=(t,e)=>new Proxy((...r)=>I(t,e),{apply:()=>I(t,e),get:()=>I(t,e)}),re={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},$e={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>re},Ce=()=>{const t={attributes:{},events:[],links:[]},e={addEvent:(r,s)=>{t.events.push({...s===void 0?{}:{attributes:{...s}},name:r})},addLink:r=>{t.links.push({spanId:r.spanId,traceId:r.traceId})},recordEvaluation:r=>{Object.assign(t.attributes,ge(r))},recordException:r=>{const s={"exception.message":r instanceof Error?r.message:String(r),"exception.type":r instanceof Error?r.constructor.name:"Error"};r instanceof Error&&r.stack!==void 0&&(s["exception.stacktrace"]=r.stack),e.addEvent("exception",s)},setAttribute:(r,s)=>{t.attributes[r]=s},setAttributes:r=>{Object.assign(t.attributes,r)},spanContext:()=>re};return{handle:e,recorded:t}},Q=async(t,e)=>await e(Q,$e),z={count:()=>{},gauge:()=>{},record:()=>{}},V={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>V},U="notify",De=t=>{let e=t;if(typeof t=="string"){if(!t.startsWith("{"))return;try{e=JSON.parse(t)}catch{return}}const r=e?.endpoint;return typeof r=="string"?r:void 0},D=(t,e)=>typeof t=="function"?t(e):t,Le=(t,e)=>{const r=[],s=(l,g)=>(r.push({channel:l,payload:structuredClone(g)}),{data:{messageId:`${l}-${String(r.length)}`,sent:!0,timestamp:new Date},success:!0}),y=l=>({channel:l,id:`lunora-testing-${l}`,initialize:()=>{},isAvailable:()=>!0,send:g=>s(l,g)}),c=D(t.webPush,e)!==void 0,b=D(t.fcm,e)!==void 0,a={...y("push"),send:l=>{const g=Array.isArray(l.to)?l.to:[l.to];if(g.length===0)throw new N("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");for(const x of g){const R=De(x)!==void 0;if(R?!c:!b)throw new Error(R?"@lunora/notify: received a web-push target but no `webPush` channel is configured":"@lunora/notify: received an FCM token target but no `fcm` channel is configured")}return s("push",l)}},p={...c||b?{push:a}:{},...D(t.chat,e)===void 0?{}:{chat:y("chat")},...D(t.inApp,e)===void 0?{}:{inapp:y("inapp")},...D(t.webhook,e)===void 0?{}:{webhook:y("webhook")}},u=we(),{notify:d,push:E}=be({...t,store:()=>u},e,{engine:Se(p),silent:!0});return{controls:{clear:()=>{r.length=0},sent:l=>l===void 0?[...r]:r.filter(g=>g.channel===l)},surfaces:{notify:d,push:E}}},Fe=(t,e)=>t!==void 0?Le(t,e):{controls:{clear:()=>I("notify",U),sent:()=>I("notify",U)},surfaces:{notify:A("notify",U),push:A("push",U)}},H="queues: [...]",Z=128*1024,ee=256*1024,Me="json",te=new TextEncoder,je=(t,e)=>{if(e==="v8"){const s=Ae(t);return{body:Ee(s),bytes:s.byteLength}}if(e==="text"){if(typeof t!="string")throw new N("VALIDATION_ERROR",`@lunora/queue: a "text" message body must be a string, got ${typeof t}`);return{body:t,bytes:te.encode(t).byteLength}}if(e==="bytes"){if(!(t instanceof ArrayBuffer)&&!ArrayBuffer.isView(t))throw new N("VALIDATION_ERROR",'@lunora/queue: a "bytes" message body must be an ArrayBuffer or an ArrayBufferView');return{body:t,bytes:t.byteLength}}const r=JSON.stringify(t);if(typeof r!="string")throw new N("VALIDATION_ERROR",`@lunora/queue: a "json" message body must be JSON-serializable, got ${typeof t}`);return{body:JSON.parse(r),bytes:te.encode(r).byteLength}},ne=t=>{const e=new Set(t),r=[],s=(c,b,a)=>{const p=a?.contentType??Me,u=je(b,p);if(u.bytes>Z)throw new N("VALIDATION_ERROR",`@lunora/queue: ${c} message is ${String(u.bytes)} bytes, over the Cloudflare Queues limit of ${String(Z)} (128 KB)`);return{bytes:u.bytes,message:{body:u.body,contentType:p,...a?.delaySeconds===void 0?{}:{delaySeconds:a.delaySeconds},destination:c}}},y=c=>({send:(b,a)=>(r.push(s(c,b,a).message),Promise.resolve()),sendBatch:(b,a)=>{const p=[...b].map(d=>s(c,d.body,{contentType:d.contentType,delaySeconds:d.delaySeconds??a?.delaySeconds})),u=p.reduce((d,E)=>d+E.bytes,0);if(u>ee)throw new N("VALIDATION_ERROR",`@lunora/queue: ${c} batch is ${String(u)} bytes, over the Cloudflare Queues limit of ${String(ee)} (256 KB)`);return r.push(...p.map(d=>d.message)),Promise.resolve()}});return{bindings:Object.fromEntries([...e].map(c=>[c,y(c)])),clear:()=>{r.length=0},read:(c,b,a)=>{if(c===void 0)return[...r];if(!e.has(c))throw new N("INTERNAL",`${b}("${c}"): no such ${a} — declared: ${[...e].join(", ")||"(none)"}`);return r.filter(p=>p.destination===c)}}},Be=t=>{const e=ne(t);return{controls:{clear:e.clear,sent:r=>e.read(r,"harness.queues.sent","queue").map(({destination:s,...y})=>({...y,queue:s}))},surfaces:{queues:Ne({bindings:e.bindings})}}},Ue=t=>t!==void 0?Be(t):{controls:{clear:()=>I("queues",H),sent:()=>I("queues",H)},surfaces:{queues:A("queues",H)}},Ke=(t,e,r,s,y)=>{let c=y,b=1;const a=new Map,p=[],u=(o,w,h={},m)=>{const n=Ie(h);JSON.stringify(n);const f=xe(n),S=m??`fake-job-${String(b)}`;return b+=1,a.set(S,{args:f,enqueuedAt:c,functionPath:w,id:S,scheduledFor:o}),S},d=o=>typeof o=="string"?o:"__lunoraRef"in o?o.__lunoraRef:o.name??o.className??"",E={cancel:o=>{const w=a.has(o);return a.delete(o),Promise.resolve({cancelled:w})},get:o=>Promise.resolve(a.get(o)??null),list:()=>Promise.resolve([...a.values()]),runAfter:(o,w,h,m)=>{Re(o,"ctx.scheduler.runAfter");const n=u(c+o,d(w),h,m?.id);return Promise.resolve(n)},runAt:(o,w,h,m)=>{const n=u(o,d(w),h,m?.id);return Promise.resolve(n)}},l=async o=>{a.delete(o.id);const w=s(),h=o.functionPath===void 0?void 0:w.get(o.functionPath);if(h===void 0)throw new N("FUNCTION_NOT_FOUND",`unknown functionPath "${o.functionPath??"(workflow-targeted)"}"`,{status:404});if(h.kind!=="mutation"&&h.kind!=="action")throw new N("FUNCTION_NOT_FOUND",`functionPath "${o.functionPath??"(workflow-targeted)"}" is a ${h.kind} — only mutations and actions can be scheduled`,{status:404});const m=t(),n=h.kind==="action"?r():e();await m(h.kind,h,{...n,now:c},o.args)},g=async o=>{const w=[...a.values()].filter(n=>n.scheduledFor<=o).toSorted((n,f)=>n.scheduledFor-f.scheduledFor),h=[];let m=0;for(const n of w)if(a.has(n.id)){m+=1;try{await l(n)}catch(f){const S=(n.attempts??0)+1;if(S>Te){const T={args:n.args,error:f,functionPath:n.functionPath??"",id:n.id};h.push(T),p.push(T)}else{const T=_e*2**(S-1);a.set(n.id,{...n,attempts:S,scheduledFor:c+T})}}}return{executed:m,failed:h}},x=async(o,w)=>{const{executed:h,failed:m}=await g(o);if(m.length>0&&(w?.throwOnError??!0)){const[n]=m;throw m.length===1&&n!==void 0?n.error:new AggregateError(m.map(f=>f.error),`${String(m.length)} scheduled jobs failed: ${m.map(f=>f.functionPath).join(", ")}`)}return h};return{controls:{advance:(o,w)=>(c+=o,x(c,w)),failures:()=>[...p],list:()=>[...a.values()],runPending:o=>x(Number.POSITIVE_INFINITY,o)},scheduler:E}},X="topics: [...]",Qe=t=>{if(t===void 0)return{controls:{clear:()=>I("topics",X),published:()=>I("topics",X)},surfaces:{topics:A("topics",X)}};const e=ne(t),r=Pe(e.bindings,t.map(s=>({exportName:s,subscriptions:[{binding:s,exportName:s}]})));return{controls:{clear:e.clear,published:s=>e.read(s,"harness.topics.published","topic").map(({destination:y,...c})=>({...c,topic:y}))},surfaces:{topics:r}}},Ve=()=>{const t=new Oe(":memory:"),e=s=>({one(){if(s.length!==1)throw new N("INTERNAL",`expected exactly one row, received ${String(s.length)}`);const[y]=s;return y},[Symbol.iterator](){return s[Symbol.iterator]()},toArray(){return s}});return{close:()=>{t.close()},sql:{exec:(s,...y)=>{const b=t.prepare(s).all(...y);return e(b)}}}},L=new me,Ye=(t,e,r,s)=>(c,b)=>{let a=!1;const p=[];let u,d,E=0,l=0;const g=()=>L.run(s(),()=>K(c)?t("query",c,e,b,!1):Promise.resolve(c(e))),x=(n,f)=>{if(n<l)return;l=n;const S={done:!1,value:f};if(p.length===0)u=S,d=void 0;else{u=void 0,d=void 0;for(const T of p.splice(0))T.resolve(S)}},R=(n,f)=>{if(!(n<l))if(l=n,p.length===0)d={error:f},u=void 0;else{u=void 0,d=void 0;for(const S of p.splice(0))S.reject(f)}},o=n=>f=>{x(n,f)},w=n=>f=>{R(n,f)},h=()=>{if(a)return;E+=1;const n=E;g().then(o(n)).catch(w(n))};r.add(h);const m={[Symbol.asyncIterator](){return m},next:()=>{if(a)return Promise.resolve({done:!0,value:void 0});if(l===E){if(d!==void 0){const{error:n}=d;return d=void 0,Promise.reject(n)}if(u!==void 0){const n=u;return u=void 0,Promise.resolve(n)}}return l<E?new Promise((n,f)=>{p.push({reject:f,resolve:n})}):g().then(n=>{if(d!==void 0){const{error:f}=d;throw d=void 0,f}if(u!==void 0){const f=u;return u=void 0,f}return{done:!1,value:n}})},return:()=>{a=!0,r.delete(h);for(const n of p.splice(0))n.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return g().then(o(0)).catch(w(0)),m},ct=(t,e)=>{const{close:r,sql:s}=Ve();let y=!1;const c=()=>{y||(y=!0,r())},b=new Map(Object.entries(e?.functions??{}).map(([P,_])=>[P,_])),a=new Set,p=()=>{for(const P of a)P()},u=P=>(p(),P);let d,E,l;const g=()=>e?.now??Date.now(),x=g(),R=P=>((..._)=>L.run(g(),()=>P(..._))),o=Ce(),{controls:w,surfaces:h}=Ue(e?.queues),{controls:m,surfaces:n}=Qe(e?.topics),{controls:f,surfaces:S}=Fe(e?.notify,e?.env??{}),T=(P,_)=>{if(P===void 0)throw new N("INTERNAL",`[fake-scheduler] ${_} not yet available — scheduler.advance called before harness construction completed`);return P},{controls:oe,scheduler:se}=Ke(()=>T(d,"dispatch"),()=>T(E,"mutationContext"),()=>T(l,"actionContext"),()=>b,x),Y=ve(se),{createWriters:ce,resetHeadroom:F,runInTransaction:M}=pe(t,{enforceRls:e?.enforceRls,scheduler:Y,sql:s}),G=P=>{const _=ye(P),J={getIdentity:()=>Promise.resolve(_.claims),userId:_.userId},{database:W,rawDatabase:ie}=ce(_),k={...S,auth:J,db:W,env:e?.env,log:V,metrics:z,get now(){return L.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:o.handle,trace:Q,runQuery:((i,v)=>O("query",i,k,v)),secrets:A("secrets"),storage:A("storage"),vectors:A("vectors")},q={...S,...h,...n,auth:J,db:W,env:e?.env,log:V,metrics:z,get now(){return L.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:o.handle,trace:Q,runMutation:((i,v)=>O("mutation",i,q,v)),runQuery:((i,v)=>O("query",i,k,v)),scheduler:Y,secrets:A("secrets"),storage:A("storage"),vectors:A("vectors"),workflows:A("workflows")};E??=q;const ae=Object.defineProperties({},{...Object.getOwnPropertyDescriptors(q),db:{enumerable:!0,value:ie}}),j={...S,...h,...n,auth:J,db:W,env:e?.env,fetch:e?.fetch??A("fetch"),log:V,metrics:z,get now(){return L.getStore()??g()},newId:()=>crypto.randomUUID(),random:()=>Math.random(),shardKey:e?.shardKey??"__root__",span:o.handle,trace:Q,runAction:((i,v)=>O("action",i,j,v)),runMutation:((i,v)=>M(()=>O("mutation",i,q,v)).then(u)),runQuery:((i,v)=>O("query",i,k,v)),scheduler:Y,secrets:A("secrets"),services:qe(e?.services),storage:A("storage"),vectors:A("vectors"),workflows:A("workflows")};l??=j;const O=(i,v,$,B)=>C(i,v,$,B,!0);d??=(i,v,$,B)=>i==="mutation"?M(()=>C("mutation",v,$,B,!0)).then(u):(F(),O("action",v,$,B));const ue=((i,v)=>(F(),K(i)?C("query",i,k,v,!1):Promise.resolve(i(k)))),de=((i,v)=>{const $=K(i)?()=>C("mutation",i,q,v,!1):()=>i(q);return M($).then(u)}),le=((i,v)=>(F(),K(i)?C("action",i,j,v,!1):Promise.resolve(i(j)))),fe=Ye((...i)=>(F(),C(...i)),k,a,()=>e?.now??Date.now()),he=i=>M(()=>i(ae)).then(u);return{action:R(le),close:c,mutation:R(de),notify:f,query:R(ue),queues:w,run:R(he),scheduler:oe,subscribe:fe,topics:m,wideEvent:()=>o.recorded,withIdentity:i=>G(i)}};return G(null)};export{ct as lunoraTest};
|