@lunora/testing 1.0.0-alpha.83 → 1.0.0-alpha.84
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
|
@@ -289,6 +289,35 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
|
|
|
289
289
|
* clearly-throwing stubs for unsupported surfaces.
|
|
290
290
|
*/
|
|
291
291
|
interface LunoraTestOptions {
|
|
292
|
+
/**
|
|
293
|
+
* Enforce the secure-by-default RLS guard on the writer registered
|
|
294
|
+
* procedures dispatch through (`query`/`mutation`/`action`, via
|
|
295
|
+
* `reference.handler`) — the same `enforceRls: true` production's generated
|
|
296
|
+
* `buildCtx` always passes. Under a `.rls("required")` schema, a procedure
|
|
297
|
+
* that touches a known, non-`.public()` table without `.use(rls(...))` in
|
|
298
|
+
* its chain rejects with `RlsRequiredError`, exactly as it would on first
|
|
299
|
+
* dispatch in production. Defaults to `true` so a green suite means the
|
|
300
|
+
* deploy is RLS-safe; the harness's other surfaces — `t.run` and any
|
|
301
|
+
* `@lunora/seed` helper built on it — stay on the trusted, UNGUARDED writer
|
|
302
|
+
* regardless of this flag (mirroring production's admin/migration system
|
|
303
|
+
* paths).
|
|
304
|
+
*
|
|
305
|
+
* Set to `false` to opt back into the pre-guard permissive behaviour (every
|
|
306
|
+
* `lunoraTest` release before this option existed): every procedure's
|
|
307
|
+
* `ctx.db` goes unguarded even under a `.rls("required")` schema. This
|
|
308
|
+
* forfeits the "a passing suite means the deploy is safe" guarantee — a
|
|
309
|
+
* procedure that forgot `.use(rls(...))` will pass in tests and throw
|
|
310
|
+
* `RlsRequiredError` on its first production request. No effect when the
|
|
311
|
+
* schema does not declare `.rls("required")` (the guard is a no-op there
|
|
312
|
+
* either way).
|
|
313
|
+
* @default true
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* // Restores the old permissive behavior for a suite not yet migrated.
|
|
317
|
+
* const t = lunoraTest(schema, { enforceRls: false });
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
enforceRls?: boolean;
|
|
292
321
|
/**
|
|
293
322
|
* Injectable `ctx.env` for every context (query / mutation / action). When
|
|
294
323
|
* provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
|
|
@@ -368,7 +397,16 @@ interface TestHarness {
|
|
|
368
397
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
|
|
369
398
|
<R>(inline: InlineQueryFunction<R>): Promise<R>;
|
|
370
399
|
};
|
|
371
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Direct db access at mutation-level (read + write), mirroring `convexTest`'s
|
|
402
|
+
* `run`. This is the harness's trusted escape hatch: `ctx.db` here is always
|
|
403
|
+
* the UNGUARDED writer, regardless of `options.enforceRls` or the schema's
|
|
404
|
+
* RLS mode — seeding/asserting against a protected table never trips the
|
|
405
|
+
* secure-by-default guard. A `ctx.runMutation`/`ctx.runQuery` call from
|
|
406
|
+
* inside the body still dispatches the target as a real registered
|
|
407
|
+
* procedure, so it is guarded exactly as `t.mutation`/`t.query` would guard
|
|
408
|
+
* it.
|
|
409
|
+
*/
|
|
372
410
|
run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
|
|
373
411
|
/**
|
|
374
412
|
* Controls for the fake in-memory scheduler. Always present; scheduler
|
|
@@ -450,9 +488,14 @@ interface RecordedWideEvent {
|
|
|
450
488
|
*
|
|
451
489
|
* `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
|
|
452
490
|
* database, builds the same `ctx.db` writer the real Durable Object builds (via
|
|
453
|
-
* `@lunora/
|
|
454
|
-
*
|
|
455
|
-
*
|
|
491
|
+
* `@lunora/shard-engine`'s `createShardCtxDb`, with the same `enforceRls: true`
|
|
492
|
+
* production's generated `buildCtx` passes), and returns a harness whose
|
|
493
|
+
* `query` / `mutation` / `action` execute a registered function's `handler`
|
|
494
|
+
* directly — no Durable Object, no `wrangler`, no network. Under a
|
|
495
|
+
* `.rls("required")` schema a procedure missing `.use(rls(...))` therefore
|
|
496
|
+
* rejects here exactly as it would on its first production dispatch — see
|
|
497
|
+
* `LunoraTestOptions.enforceRls` to opt out, and `run`'s doc for the trusted
|
|
498
|
+
* escape hatch (always unguarded).
|
|
456
499
|
*
|
|
457
500
|
* **v1 surfaces now supported:**
|
|
458
501
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -289,6 +289,35 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
|
|
|
289
289
|
* clearly-throwing stubs for unsupported surfaces.
|
|
290
290
|
*/
|
|
291
291
|
interface LunoraTestOptions {
|
|
292
|
+
/**
|
|
293
|
+
* Enforce the secure-by-default RLS guard on the writer registered
|
|
294
|
+
* procedures dispatch through (`query`/`mutation`/`action`, via
|
|
295
|
+
* `reference.handler`) — the same `enforceRls: true` production's generated
|
|
296
|
+
* `buildCtx` always passes. Under a `.rls("required")` schema, a procedure
|
|
297
|
+
* that touches a known, non-`.public()` table without `.use(rls(...))` in
|
|
298
|
+
* its chain rejects with `RlsRequiredError`, exactly as it would on first
|
|
299
|
+
* dispatch in production. Defaults to `true` so a green suite means the
|
|
300
|
+
* deploy is RLS-safe; the harness's other surfaces — `t.run` and any
|
|
301
|
+
* `@lunora/seed` helper built on it — stay on the trusted, UNGUARDED writer
|
|
302
|
+
* regardless of this flag (mirroring production's admin/migration system
|
|
303
|
+
* paths).
|
|
304
|
+
*
|
|
305
|
+
* Set to `false` to opt back into the pre-guard permissive behaviour (every
|
|
306
|
+
* `lunoraTest` release before this option existed): every procedure's
|
|
307
|
+
* `ctx.db` goes unguarded even under a `.rls("required")` schema. This
|
|
308
|
+
* forfeits the "a passing suite means the deploy is safe" guarantee — a
|
|
309
|
+
* procedure that forgot `.use(rls(...))` will pass in tests and throw
|
|
310
|
+
* `RlsRequiredError` on its first production request. No effect when the
|
|
311
|
+
* schema does not declare `.rls("required")` (the guard is a no-op there
|
|
312
|
+
* either way).
|
|
313
|
+
* @default true
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* // Restores the old permissive behavior for a suite not yet migrated.
|
|
317
|
+
* const t = lunoraTest(schema, { enforceRls: false });
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
enforceRls?: boolean;
|
|
292
321
|
/**
|
|
293
322
|
* Injectable `ctx.env` for every context (query / mutation / action). When
|
|
294
323
|
* provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
|
|
@@ -368,7 +397,16 @@ interface TestHarness {
|
|
|
368
397
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
|
|
369
398
|
<R>(inline: InlineQueryFunction<R>): Promise<R>;
|
|
370
399
|
};
|
|
371
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Direct db access at mutation-level (read + write), mirroring `convexTest`'s
|
|
402
|
+
* `run`. This is the harness's trusted escape hatch: `ctx.db` here is always
|
|
403
|
+
* the UNGUARDED writer, regardless of `options.enforceRls` or the schema's
|
|
404
|
+
* RLS mode — seeding/asserting against a protected table never trips the
|
|
405
|
+
* secure-by-default guard. A `ctx.runMutation`/`ctx.runQuery` call from
|
|
406
|
+
* inside the body still dispatches the target as a real registered
|
|
407
|
+
* procedure, so it is guarded exactly as `t.mutation`/`t.query` would guard
|
|
408
|
+
* it.
|
|
409
|
+
*/
|
|
372
410
|
run: <R>(function_: InlineMutationFunction<R>) => Promise<R>;
|
|
373
411
|
/**
|
|
374
412
|
* Controls for the fake in-memory scheduler. Always present; scheduler
|
|
@@ -450,9 +488,14 @@ interface RecordedWideEvent {
|
|
|
450
488
|
*
|
|
451
489
|
* `lunoraTest(schema)` runs the migrations against a fresh `node:sqlite`
|
|
452
490
|
* database, builds the same `ctx.db` writer the real Durable Object builds (via
|
|
453
|
-
* `@lunora/
|
|
454
|
-
*
|
|
455
|
-
*
|
|
491
|
+
* `@lunora/shard-engine`'s `createShardCtxDb`, with the same `enforceRls: true`
|
|
492
|
+
* production's generated `buildCtx` passes), and returns a harness whose
|
|
493
|
+
* `query` / `mutation` / `action` execute a registered function's `handler`
|
|
494
|
+
* directly — no Durable Object, no `wrangler`, no network. Under a
|
|
495
|
+
* `.rls("required")` schema a procedure missing `.use(rls(...))` therefore
|
|
496
|
+
* rejects here exactly as it would on its first production dispatch — see
|
|
497
|
+
* `LunoraTestOptions.enforceRls` to opt out, and `run`'s doc for the trusted
|
|
498
|
+
* escape hatch (always unguarded).
|
|
456
499
|
*
|
|
457
500
|
* **v1 surfaces now supported:**
|
|
458
501
|
*
|
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-
|
|
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-BUsuJZQn.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{LunoraError as R}from"@lunora/errors";import{runShardMigrations as J,createShardCtxDb as W,RLS_UNWRAP_SYMBOL as z}from"@lunora/shard-engine";import{evaluationAttributes as X}from"./evaluationAttributes-GZvtFa1N.mjs";import{DatabaseSync as Z}from"node:sqlite";const ee=(s,i,n,l,E)=>{let f=E,g=1;const u=new Map,h=[],m=(t,a,d={})=>{const e=`fake-job-${String(g)}`;return g+=1,u.set(e,{args:d,enqueuedAt:f,functionPath:a,id:e,scheduledFor:t}),e},p=t=>typeof t=="string"?t:t.name??t.binding??"",x={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=m(f+t,p(a),d);return Promise.resolve(e)},runAt:(t,a,d)=>{const e=m(t,p(a),d);return Promise.resolve(e)}},T=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():i();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`)},j=async t=>{const a=[...u.values()].filter(o=>o.scheduledFor<=t).toSorted((o,v)=>o.scheduledFor-v.scheduledFor),d=[];let e=0;for(const o of a)if(u.has(o.id)){e+=1;try{await T(o)}catch(v){const A={args:o.args,error:v,functionPath:o.functionPath,id:o.id};d.push(A),h.push(A)}}return{executed:e,failed:d}},b=async(t,a)=>{const{executed:d,failed:e}=await j(t);if(e.length>0&&(a?.throwOnError??!0)){const[o]=e;throw e.length===1&&o!==void 0?o.error:new AggregateError(e.map(v=>v.error),`${String(e.length)} scheduled jobs failed: ${e.map(v=>v.functionPath).join(", ")}`)}return d};return{controls:{advance:(t,a)=>(f+=t,b(f,a)),failures:()=>[...h],list:()=>[...u.values()],runPending:t=>b(Number.POSITIVE_INFINITY,t)},scheduler:x}},te=()=>{const s=new Z(":memory:"),i=n=>({one(){if(n.length!==1)throw new R("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 E=s.prepare(n).all(...l);return i(E)}}}},q=s=>{if(typeof s!="object"||s===null)return;const{kind:i}=s;if(i==="query"||i==="mutation"||i==="action")return i},re=s=>typeof s=="object"&&s!==null&&s.visibility==="internal"?"internal":"public",Q=s=>{throw new R("INTERNAL",`ctx.${s} is not available in the in-memory @lunora/testing harness (v1)`)},w=s=>new Proxy((...i)=>Q(s),{apply:()=>Q(s),get:()=>Q(s)}),U={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},ne={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>U},oe=()=>{const s={attributes:{},events:[],links:[]},i={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,X(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),i.addEvent("exception",l)},setAttribute:(n,l)=>{s.attributes[n]=l},setAttributes:n=>{Object.assign(s.attributes,n)},spanContext:()=>U};return{handle:i,recorded:s}},M=async(s,i)=>await i(M,ne),B={count:()=>{},gauge:()=>{},record:()=>{}},O={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>O},se=(s,i,n)=>(l,E)=>{let f=!1;const g=[];let u,h,m=0,p=0;const x=()=>q(l)?s("query",l,i,E,!1):Promise.resolve(l(i)),T=(e,o)=>{if(e<p)return;p=e;const v={done:!1,value:o};if(g.length===0)u=v,h=void 0;else{u=void 0,h=void 0;for(const A of g.splice(0))A.resolve(v)}},j=(e,o)=>{if(!(e<p))if(p=e,g.length===0)h={error:o},u=void 0;else{u=void 0,h=void 0;for(const v of g.splice(0))v.reject(o)}},b=e=>o=>{T(e,o)},t=e=>o=>{j(e,o)},a=()=>{if(f)return;m+=1;const e=m;x().then(b(e)).catch(t(e))};n.add(a);const d={[Symbol.asyncIterator](){return d},next:()=>{if(f)return Promise.resolve({done:!0,value:void 0});if(p===m){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<m?new Promise((e,o)=>{g.push({reject:o,resolve:e})}):x().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:()=>{f=!0,n.delete(a);for(const e of g.splice(0))e.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return x().then(b(0)).catch(t(0)),d},le=(s,i)=>{const{close:n,sql:l}=te(),E=s;J(l,E);const f=W({enforceRls:i?.enforceRls??!0,schema:E,sql:l}),g=f[z]??f,u=y=>{l.exec.call(l,y)};let h=Promise.resolve();const m=y=>{const N=async()=>{u("BEGIN");try{const P=await y();return u("COMMIT"),P}catch(P){try{u("ROLLBACK")}catch{}throw P}},k=h.then(N);return h=k.then(()=>{},()=>{}),k};let p=!1;const x=()=>{p||(p=!0,n())},T=new Map(Object.entries(i?.functions??{}).map(([y,N])=>[y,N])),j=new Set,b=()=>{for(const y of j)y()};let t,a,d;const e=i?.now??Date.now(),o=oe(),{controls:v,scheduler:A}=ee(()=>{if(t===void 0)throw new R("INTERNAL","[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed");return t},()=>{if(a===void 0)throw new R("INTERNAL","[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed");return a},()=>{if(d===void 0)throw new R("INTERNAL","[fake-scheduler] actionContext not yet available — scheduler.advance called before harness construction completed");return d},()=>T,e),D=y=>{const N={getIdentity:()=>Promise.resolve(y??null),userId:y?.userId??null},k={auth:N,db:f,env:i?.env,log:O,metrics:B,now:e,span:o.handle,trace:M,runQuery:((r,c)=>$("query",r,k,c)),secrets:w("secrets"),storage:w("storage"),vectors:w("vectors")},P={auth:N,db:f,env:i?.env,log:O,metrics:B,now:e,span:o.handle,trace:M,runMutation:((r,c)=>$("mutation",r,P,c)),runQuery:((r,c)=>$("query",r,k,c)),scheduler:A,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};a??=P;const K={...P,db:g},C={auth:N,db:f,env:i?.env,fetch:i?.fetch??w("fetch"),log:O,metrics:B,now:e,span:o.handle,trace:M,runAction:((r,c)=>$("action",r,C,c)),runMutation:((r,c)=>$("mutation",r,P,c)),runQuery:((r,c)=>$("query",r,k,c)),scheduler:A,secrets:w("secrets"),storage:w("storage"),vectors:w("vectors"),workflows:w("workflows")};d??=C;const L=(r,c,I,S,F)=>{const _=q(c);if(_!==r)throw new R("INTERNAL",`expected a registered ${r}, received a ${_??"non-function"} reference`);if(!F&&re(c)==="internal")throw new R("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(c.handler(I,S??{}))},$=(r,c,I,S)=>L(r,c,I,S,!0);t??=(r,c,I,S)=>r==="mutation"?m(()=>L("mutation",c,I,S,!0)).then(F=>(b(),F)):$("action",c,I,S);const V=((r,c)=>q(r)?L("query",r,k,c,!1):Promise.resolve(r(k))),Y=((r,c)=>q(r)?m(()=>L("mutation",r,P,c,!1)).then(I=>(b(),I)):m(()=>r(P)).then(I=>(b(),I))),G=((r,c)=>q(r)?L("action",r,C,c,!1):Promise.resolve(r(C))),H=se(L,k,j);return{action:G,close:x,mutation:Y,query:V,run:r=>m(()=>r(K)).then(c=>(b(),c)),scheduler:v,subscribe:H,wideEvent:()=>o.recorded,withIdentity:r=>D(r)}};return D(null)};export{le 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.84",
|
|
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.36",
|
|
54
|
+
"@lunora/errors": "1.0.0-alpha.12",
|
|
55
|
+
"@lunora/mail": "1.0.0-alpha.39",
|
|
56
|
+
"@lunora/server": "1.0.0-alpha.57",
|
|
57
|
+
"@lunora/shard-engine": "1.0.0-alpha.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@playwright/test": "^1.61.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as S}from"@lunora/errors";import{runShardMigrations as H,createShardCtxDb as J}from"@lunora/shard-engine";import{evaluationAttributes as U}from"./evaluationAttributes-GZvtFa1N.mjs";import{DatabaseSync as W}from"node:sqlite";const Z=(o,i,n,l,x)=>{let m=x,v=1;const u=new Map,h=[],g=(t,a,d={})=>{const e=`fake-job-${String(v)}`;return v+=1,u.set(e,{args:d,enqueuedAt:m,functionPath:a,id:e,scheduledFor:t}),e},w=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=g(m+t,w(a),d);return Promise.resolve(e)},runAt:(t,a,d)=>{const e=g(t,w(a),d);return Promise.resolve(e)}},T=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=o(),e=a.kind==="action"?n():i();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`)},E=async t=>{const a=[...u.values()].filter(s=>s.scheduledFor<=t).toSorted((s,f)=>s.scheduledFor-f.scheduledFor),d=[];let e=0;for(const s of a)if(u.has(s.id)){e+=1;try{await T(s)}catch(f){const N={args:s.args,error:f,functionPath:s.functionPath,id:s.id};d.push(N),h.push(N)}}return{executed:e,failed:d}},I=async(t,a)=>{const{executed:d,failed:e}=await E(t);if(e.length>0&&(a?.throwOnError??!0)){const[s]=e;throw e.length===1&&s!==void 0?s.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,I(m,a)),failures:()=>[...h],list:()=>[...u.values()],runPending:t=>I(Number.POSITIVE_INFINITY,t)},scheduler:A}},z=()=>{const o=new W(":memory:"),i=n=>({one(){if(n.length!==1)throw new S("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:()=>{o.close()},sql:{exec:(n,...l)=>{const x=o.prepare(n).all(...l);return i(x)}}}},L=o=>{if(typeof o!="object"||o===null)return;const{kind:i}=o;if(i==="query"||i==="mutation"||i==="action")return i},X=o=>typeof o=="object"&&o!==null&&o.visibility==="internal"?"internal":"public",Q=o=>{throw new S("INTERNAL",`ctx.${o} is not available in the in-memory @lunora/testing harness (v1)`)},p=o=>new Proxy((...i)=>Q(o),{apply:()=>Q(o),get:()=>Q(o)}),K={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},ee={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>K},te=()=>{const o={attributes:{},events:[],links:[]},i={addEvent:(n,l)=>{o.events.push({...l===void 0?{}:{attributes:{...l}},name:n})},addLink:n=>{o.links.push({spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(o.attributes,U(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),i.addEvent("exception",l)},setAttribute:(n,l)=>{o.attributes[n]=l},setAttributes:n=>{Object.assign(o.attributes,n)},spanContext:()=>K};return{handle:i,recorded:o}},F=async(o,i)=>await i(F,ee),B={count:()=>{},gauge:()=>{},record:()=>{}},M={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>M},re=(o,i,n)=>(l,x)=>{let m=!1;const v=[];let u,h,g=0,w=0;const A=()=>L(l)?o("query",l,i,x,!1):Promise.resolve(l(i)),T=(e,s)=>{if(e<w)return;w=e;const f={done:!1,value:s};if(v.length===0)u=f,h=void 0;else{u=void 0,h=void 0;for(const N of v.splice(0))N.resolve(f)}},E=(e,s)=>{if(!(e<w))if(w=e,v.length===0)h={error:s},u=void 0;else{u=void 0,h=void 0;for(const f of v.splice(0))f.reject(s)}},I=e=>s=>{T(e,s)},t=e=>s=>{E(e,s)},a=()=>{if(m)return;g+=1;const e=g;A().then(I(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(w===g){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 w<g?new Promise((e,s)=>{v.push({reject:s,resolve:e})}):A().then(e=>{if(h!==void 0){const{error:s}=h;throw h=void 0,s}if(u!==void 0){const s=u;return u=void 0,s}return{done:!1,value:e}})},return:()=>{m=!0,n.delete(a);for(const e of v.splice(0))e.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return A().then(I(0)).catch(t(0)),d},ce=(o,i)=>{const{close:n,sql:l}=z(),x=o;H(l,x);const m=J({schema:x,sql:l}),v=b=>{l.exec.call(l,b)};let u=Promise.resolve();const h=b=>{const $=async()=>{v("BEGIN");try{const y=await b();return v("COMMIT"),y}catch(y){try{v("ROLLBACK")}catch{}throw y}},k=u.then($);return u=k.then(()=>{},()=>{}),k};let g=!1;const w=()=>{g||(g=!0,n())},A=new Map(Object.entries(i?.functions??{}).map(([b,$])=>[b,$])),T=new Set,E=()=>{for(const b of T)b()};let I,t,a;const d=i?.now??Date.now(),e=te(),{controls:s,scheduler:f}=Z(()=>{if(I===void 0)throw new S("INTERNAL","[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed");return I},()=>{if(t===void 0)throw new S("INTERNAL","[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed");return t},()=>{if(a===void 0)throw new S("INTERNAL","[fake-scheduler] actionContext not yet available — scheduler.advance called before harness construction completed");return a},()=>A,d),N=b=>{const $={getIdentity:()=>Promise.resolve(b??null),userId:b?.userId??null},k={auth:$,db:m,env:i?.env,log:M,metrics:B,now:d,span:e.handle,trace:F,runQuery:((r,c)=>j("query",r,k,c)),secrets:p("secrets"),storage:p("storage"),vectors:p("vectors")},y={auth:$,db:m,env:i?.env,log:M,metrics:B,now:d,span:e.handle,trace:F,runMutation:((r,c)=>j("mutation",r,y,c)),runQuery:((r,c)=>j("query",r,k,c)),scheduler:f,secrets:p("secrets"),storage:p("storage"),vectors:p("vectors"),workflows:p("workflows")};t??=y;const R={auth:$,db:m,env:i?.env,fetch:i?.fetch??p("fetch"),log:M,metrics:B,now:d,span:e.handle,trace:F,runAction:((r,c)=>j("action",r,R,c)),runMutation:((r,c)=>j("mutation",r,y,c)),runQuery:((r,c)=>j("query",r,k,c)),scheduler:f,secrets:p("secrets"),storage:p("storage"),vectors:p("vectors"),workflows:p("workflows")};a??=R;const q=(r,c,P,C,O)=>{const D=L(c);if(D!==r)throw new S("INTERNAL",`expected a registered ${r}, received a ${D??"non-function"} reference`);if(!O&&X(c)==="internal")throw new S("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(c.handler(P,C??{}))},j=(r,c,P,C)=>q(r,c,P,C,!0);I??=(r,c,P,C)=>r==="mutation"?h(()=>q("mutation",c,P,C,!0)).then(O=>(E(),O)):j("action",c,P,C);const V=((r,c)=>L(r)?q("query",r,k,c,!1):Promise.resolve(r(k))),Y=((r,c)=>L(r)?h(()=>q("mutation",r,y,c,!1)).then(P=>(E(),P)):h(()=>r(y)).then(P=>(E(),P))),_=((r,c)=>L(r)?q("action",r,R,c,!1):Promise.resolve(r(R))),G=re(q,k,T);return{action:_,close:w,mutation:Y,query:V,run:r=>h(()=>r(y)).then(c=>(E(),c)),scheduler:s,subscribe:G,wideEvent:()=>e.recorded,withIdentity:r=>N(r)}};return N(null)};export{ce as lunoraTest};
|