@lunora/testing 1.0.0-alpha.247 → 1.0.0-alpha.248
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -122,15 +122,16 @@ Pass a `services` option with a fake per `lunora.config` service an action calls
|
|
|
122
122
|
(`{ documentParser: { fetch } }`). See the [package docs](https://lunora.sh/docs/packages/testing#injectable-ctxservices)
|
|
123
123
|
for an example.
|
|
124
124
|
|
|
125
|
-
#### Recording `ctx.queues` and `ctx.notify` / `ctx.push`
|
|
125
|
+
#### Recording `ctx.queues`, `ctx.topics` and `ctx.notify` / `ctx.push`
|
|
126
126
|
|
|
127
|
-
Pass `queues` (the export names from
|
|
128
|
-
`lunora/notify.ts` default export).
|
|
129
|
-
|
|
127
|
+
Pass `queues` / `topics` (the queue and topic export names from
|
|
128
|
+
`lunora/queues.ts`) and/or `notify` (the `lunora/notify.ts` default export).
|
|
129
|
+
Sends are recorded instead of delivered, and read back with
|
|
130
|
+
`t.queues.sent(name)` / `t.topics.published(name)` / `t.notify.sent(channel)`. See the
|
|
130
131
|
[package docs](https://lunora.sh/docs/packages/testing#queues) for details.
|
|
131
132
|
|
|
132
133
|
```ts
|
|
133
|
-
const t = lunoraTest(schema, { notify, queues: ["jobs"] });
|
|
134
|
+
const t = lunoraTest(schema, { notify, queues: ["jobs"], topics: ["signups"] });
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
#### Fixed `ctx.now`
|
package/dist/index.d.mts
CHANGED
|
@@ -427,6 +427,24 @@ interface FakeQueueControls {
|
|
|
427
427
|
/** Recorded messages in send order, optionally only those sent to `queue`; an undeclared `queue` throws. */
|
|
428
428
|
sent: (queue?: string) => SentQueueMessage[];
|
|
429
429
|
}
|
|
430
|
+
/** One message a handler published through `ctx.topics.<name>`, as a subscriber decodes it. */
|
|
431
|
+
interface PublishedTopicMessage {
|
|
432
|
+
body: unknown;
|
|
433
|
+
contentType?: QueueContentType;
|
|
434
|
+
delaySeconds?: number;
|
|
435
|
+
/** The `lunora/queues.ts` topic export name the message was published to. */
|
|
436
|
+
topic: string;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Inspect what handlers published. Like queue sends, a publish is not
|
|
440
|
+
* transactional, so one made by a mutation that then threw stays recorded.
|
|
441
|
+
*/
|
|
442
|
+
interface FakeTopicControls {
|
|
443
|
+
/** Forget every recorded message. */
|
|
444
|
+
clear: () => void;
|
|
445
|
+
/** Recorded messages in publish order, optionally only those published to `topic`; an undeclared `topic` throws. */
|
|
446
|
+
published: (topic?: string) => PublishedTopicMessage[];
|
|
447
|
+
}
|
|
430
448
|
/** The schema value produced by `@lunora/server`'s `defineSchema`. */
|
|
431
449
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
432
450
|
/**
|
|
@@ -606,6 +624,24 @@ interface LunoraTestOptions {
|
|
|
606
624
|
* ```
|
|
607
625
|
*/
|
|
608
626
|
services?: Record<string, object>;
|
|
627
|
+
/**
|
|
628
|
+
* The export names of the topics in `lunora/queues.ts` (the keys of
|
|
629
|
+
* `ctx.topics`). Each gets a recording publisher on mutation and action
|
|
630
|
+
* contexts, built by `@lunora/queue`'s own `createTopicContext` — so the
|
|
631
|
+
* batch cap, delay ceiling and reserved-key check reject as in production,
|
|
632
|
+
* and a name not listed rejects on use. Bodies are recorded and size-checked
|
|
633
|
+
* as `queues` records them, once per message however many subscriptions the
|
|
634
|
+
* app declares; no subscription handler runs. Assert with
|
|
635
|
+
* `harness.topics.published(name)`. Left unset, touching `ctx.topics` — or
|
|
636
|
+
* calling `harness.topics.published()` — throws, naming this option.
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* const t = lunoraTest(schema, { topics: ["signups"] });
|
|
640
|
+
* await t.mutation(signUp, { email: "a@example.test" });
|
|
641
|
+
* expect(t.topics.published("signups")).toHaveLength(1);
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
topics?: ReadonlyArray<string>;
|
|
609
645
|
}
|
|
610
646
|
/**
|
|
611
647
|
* The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
|
|
@@ -697,6 +733,13 @@ interface TestHarness {
|
|
|
697
733
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
|
|
698
734
|
<R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
|
|
699
735
|
};
|
|
736
|
+
/**
|
|
737
|
+
* What handlers published through `ctx.topics` (declared by `options.topics`):
|
|
738
|
+
* `published(name?)` in publish order, `clear()` to reset. Like queue sends,
|
|
739
|
+
* publishes are not transactional. Both throw when the option was not passed.
|
|
740
|
+
* Shared with any `withIdentity` view.
|
|
741
|
+
*/
|
|
742
|
+
topics: FakeTopicControls;
|
|
700
743
|
/**
|
|
701
744
|
* What handlers attached to `ctx.span` — the **wide event** — during this
|
|
702
745
|
* harness's runs, so a test can assert the instrumentation itself:
|
|
@@ -738,6 +781,8 @@ interface TestHarness {
|
|
|
738
781
|
* that then throws never becomes pending.
|
|
739
782
|
* - `ctx.queues` (mutations + actions): recording producers for `options.queues`;
|
|
740
783
|
* inspect via `harness.queues.sent(name)`.
|
|
784
|
+
* - `ctx.topics` (mutations + actions): recording publishers for `options.topics`;
|
|
785
|
+
* inspect via `harness.topics.published(name)`.
|
|
741
786
|
* - `ctx.notify` / `ctx.push` (all contexts): built from `options.notify` with
|
|
742
787
|
* recorded deliveries and in-memory subscriptions; inspect via `harness.notify.sent()`.
|
|
743
788
|
* - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
|
|
@@ -897,4 +942,4 @@ declare const groundednessScorer: (options: {
|
|
|
897
942
|
judge: (prompt: string) => Promise<string>;
|
|
898
943
|
name?: string;
|
|
899
944
|
}) => Scorer;
|
|
900
|
-
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 FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
|
|
945
|
+
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 FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
|
package/dist/index.d.ts
CHANGED
|
@@ -427,6 +427,24 @@ interface FakeQueueControls {
|
|
|
427
427
|
/** Recorded messages in send order, optionally only those sent to `queue`; an undeclared `queue` throws. */
|
|
428
428
|
sent: (queue?: string) => SentQueueMessage[];
|
|
429
429
|
}
|
|
430
|
+
/** One message a handler published through `ctx.topics.<name>`, as a subscriber decodes it. */
|
|
431
|
+
interface PublishedTopicMessage {
|
|
432
|
+
body: unknown;
|
|
433
|
+
contentType?: QueueContentType;
|
|
434
|
+
delaySeconds?: number;
|
|
435
|
+
/** The `lunora/queues.ts` topic export name the message was published to. */
|
|
436
|
+
topic: string;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Inspect what handlers published. Like queue sends, a publish is not
|
|
440
|
+
* transactional, so one made by a mutation that then threw stays recorded.
|
|
441
|
+
*/
|
|
442
|
+
interface FakeTopicControls {
|
|
443
|
+
/** Forget every recorded message. */
|
|
444
|
+
clear: () => void;
|
|
445
|
+
/** Recorded messages in publish order, optionally only those published to `topic`; an undeclared `topic` throws. */
|
|
446
|
+
published: (topic?: string) => PublishedTopicMessage[];
|
|
447
|
+
}
|
|
430
448
|
/** The schema value produced by `@lunora/server`'s `defineSchema`. */
|
|
431
449
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
432
450
|
/**
|
|
@@ -606,6 +624,24 @@ interface LunoraTestOptions {
|
|
|
606
624
|
* ```
|
|
607
625
|
*/
|
|
608
626
|
services?: Record<string, object>;
|
|
627
|
+
/**
|
|
628
|
+
* The export names of the topics in `lunora/queues.ts` (the keys of
|
|
629
|
+
* `ctx.topics`). Each gets a recording publisher on mutation and action
|
|
630
|
+
* contexts, built by `@lunora/queue`'s own `createTopicContext` — so the
|
|
631
|
+
* batch cap, delay ceiling and reserved-key check reject as in production,
|
|
632
|
+
* and a name not listed rejects on use. Bodies are recorded and size-checked
|
|
633
|
+
* as `queues` records them, once per message however many subscriptions the
|
|
634
|
+
* app declares; no subscription handler runs. Assert with
|
|
635
|
+
* `harness.topics.published(name)`. Left unset, touching `ctx.topics` — or
|
|
636
|
+
* calling `harness.topics.published()` — throws, naming this option.
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* const t = lunoraTest(schema, { topics: ["signups"] });
|
|
640
|
+
* await t.mutation(signUp, { email: "a@example.test" });
|
|
641
|
+
* expect(t.topics.published("signups")).toHaveLength(1);
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
topics?: ReadonlyArray<string>;
|
|
609
645
|
}
|
|
610
646
|
/**
|
|
611
647
|
* The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
|
|
@@ -697,6 +733,13 @@ interface TestHarness {
|
|
|
697
733
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): TestSubscription<R>;
|
|
698
734
|
<R>(inline: InlineQueryFunction<R>): TestSubscription<R>;
|
|
699
735
|
};
|
|
736
|
+
/**
|
|
737
|
+
* What handlers published through `ctx.topics` (declared by `options.topics`):
|
|
738
|
+
* `published(name?)` in publish order, `clear()` to reset. Like queue sends,
|
|
739
|
+
* publishes are not transactional. Both throw when the option was not passed.
|
|
740
|
+
* Shared with any `withIdentity` view.
|
|
741
|
+
*/
|
|
742
|
+
topics: FakeTopicControls;
|
|
700
743
|
/**
|
|
701
744
|
* What handlers attached to `ctx.span` — the **wide event** — during this
|
|
702
745
|
* harness's runs, so a test can assert the instrumentation itself:
|
|
@@ -738,6 +781,8 @@ interface TestHarness {
|
|
|
738
781
|
* that then throws never becomes pending.
|
|
739
782
|
* - `ctx.queues` (mutations + actions): recording producers for `options.queues`;
|
|
740
783
|
* inspect via `harness.queues.sent(name)`.
|
|
784
|
+
* - `ctx.topics` (mutations + actions): recording publishers for `options.topics`;
|
|
785
|
+
* inspect via `harness.topics.published(name)`.
|
|
741
786
|
* - `ctx.notify` / `ctx.push` (all contexts): built from `options.notify` with
|
|
742
787
|
* recorded deliveries and in-memory subscriptions; inspect via `harness.notify.sent()`.
|
|
743
788
|
* - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
|
|
@@ -897,4 +942,4 @@ declare const groundednessScorer: (options: {
|
|
|
897
942
|
judge: (prompt: string) => Promise<string>;
|
|
898
943
|
name?: string;
|
|
899
944
|
}) => Scorer;
|
|
900
|
-
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 FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
|
|
945
|
+
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 FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, 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-DA9onGVO.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{createArtifactsFake as s}from"./packem_shared/createArtifactsFake-L0tjki11.mjs";import{lunoraTest as p}from"./packem_shared/lunoraTest-
|
|
1
|
+
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{createArtifactsFake as s}from"./packem_shared/createArtifactsFake-L0tjki11.mjs";import{lunoraTest as p}from"./packem_shared/lunoraTest-XYylSvj1.mjs";import{groundednessScorer as f,mrrScorer as u,ndcgAtK as S,precisionAtK as d,recallAtK as A}from"./packem_shared/groundednessScorer-kgXJCZDg.mjs";import{containsScorer as k,evaluate as v,exactMatchScorer as K,keywordScorer as M,llmScorer as T,regexScorer as w,scoreSample as C}from"./packem_shared/containsScorer-DoLfFOs6.mjs";import{extractLink as b,listCapturedMail as h,waitForMail as y}from"@lunora/mail/testing";export{o as agentHarness,k as containsScorer,s as createArtifactsFake,v as evaluate,l as evaluationAttributes,K as exactMatchScorer,b as extractLink,t as finalTurn,f as groundednessScorer,M as keywordScorer,h as listCapturedMail,T as llmScorer,p as lunoraTest,u as mrrScorer,S as ndcgAtK,d as precisionAtK,A as recallAtK,n as recordEvaluation,w as regexScorer,C as scoreSample,a as toolCallTurn,y as waitForMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as R}from"@lunora/errors";import{withDeferredSchedules as we}from"@lunora/server";import{createInProcessRuntime as ve,resolveInProcessIdentity as Ae,registeredFunctionKind as V,runRegisteredFunction as D}from"@lunora/server/in-process";import{evaluationAttributes as Ee}from"./evaluationAttributes-CYLgTP1P.mjs";import{memorySubscriptionStore as Se,createNotify as Re}from"@lunora/notify";import{createNotification as Te}from"@visulima/notification";import{serialize as Ne,deserialize as Oe}from"node:v8";import{createQueues as Pe,createTopicContext as xe}from"@lunora/queue";import{assertScheduleDelay as Ie,MAX_RETRY_ATTEMPTS as ke,RETRY_BASE_DELAY_MS as _e}from"@lunora/scheduler";import{DatabaseSync as $e}from"node:sqlite";const I=(e,r)=>{throw new R("INTERNAL",r===void 0?`ctx.${e} is not available in the in-memory @lunora/testing harness (v1)`:`ctx.${e} is not enabled in the @lunora/testing harness — pass lunoraTest(schema, { ${r} })`)},je=new Set(["$$typeof","asymmetricMatch","then","toJSON"]),qe=e=>new Proxy({...e},{get(r,n){if(!(je.has(n)&&!Object.hasOwn(r,n))){if(typeof n=="symbol"||Object.hasOwn(r,n)||n in Object.prototype)return Reflect.get(r,n);throw new R("INTERNAL",`ctx.services.${n} has no fake in the @lunora/testing harness — pass lunoraTest(schema, { services: { ${n}: … } })`)}}}),E=(e,r)=>new Proxy((...n)=>I(e,r),{apply:()=>I(e,r),get:()=>I(e,r)}),de={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},Ce={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>de},Le=()=>{const e={attributes:{},events:[],links:[]},r={addEvent:(n,o)=>{e.events.push({...o===void 0?{}:{attributes:{...o}},name:n})},addLink:n=>{e.links.push({spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(e.attributes,Ee(n))},recordException:n=>{const o={"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&&(o["exception.stacktrace"]=n.stack),r.addEvent("exception",o)},setAttribute:(n,o)=>{e.attributes[n]=o},setAttributes:n=>{Object.assign(e.attributes,n)},spanContext:()=>de};return{handle:r,recorded:e}},J=async(e,r)=>await r(J,Ce),ee={count:()=>{},gauge:()=>{},record:()=>{}},W={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>W},Q="notify",De=e=>{let r=e;if(typeof e=="string"){if(!e.startsWith("{"))return;try{r=JSON.parse(e)}catch{return}}const n=r?.endpoint;return typeof n=="string"?n:void 0},F=(e,r)=>typeof e=="function"?e(r):e,Fe=(e,r)=>{const n=[],o=(b,v)=>(n.push({channel:b,payload:structuredClone(v)}),{data:{messageId:`${b}-${String(n.length)}`,sent:!0,timestamp:new Date},success:!0}),f=b=>({channel:b,id:`lunora-testing-${b}`,initialize:()=>{},isAvailable:()=>!0,send:v=>o(b,v)}),t=F(e.webPush,r)!==void 0,s=F(e.fcm,r)!==void 0,c={...f("push"),send:b=>{const v=Array.isArray(b.to)?b.to:[b.to];if(v.length===0)throw new R("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");for(const T of v){const k=De(T)!==void 0;if(k?!t:!s)throw new Error(k?"@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 o("push",b)}},d={...t||s?{push:c}:{},...F(e.chat,r)===void 0?{}:{chat:f("chat")},...F(e.inApp,r)===void 0?{}:{inapp:f("inapp")},...F(e.webhook,r)===void 0?{}:{webhook:f("webhook")}},l=Se(),{notify:h,push:S}=Re({...e,store:()=>l},r,{engine:Te(d),silent:!0});return{controls:{clear:()=>{n.length=0},sent:b=>b===void 0?[...n]:n.filter(v=>v.channel===b)},surfaces:{notify:h,push:S}}},Be=(e,r)=>e!==void 0?Fe(e,r):{controls:{clear:()=>I("notify",Q),sent:()=>I("notify",Q)},surfaces:{notify:E("notify",Q),push:E("push",Q)}},te="queues: [...]",se=128*1024,ce=256*1024,Me="json",ie=new TextEncoder,Ue=(e,r)=>{if(r==="v8"){const o=Ne(e);return{body:Oe(o),bytes:o.byteLength}}if(r==="text"){if(typeof e!="string")throw new R("VALIDATION_ERROR",`@lunora/queue: a "text" message body must be a string, got ${typeof e}`);return{body:e,bytes:ie.encode(e).byteLength}}if(r==="bytes"){if(!(e instanceof ArrayBuffer)&&!ArrayBuffer.isView(e))throw new R("VALIDATION_ERROR",'@lunora/queue: a "bytes" message body must be an ArrayBuffer or an ArrayBufferView');return{body:e,bytes:e.byteLength}}const n=JSON.stringify(e);if(typeof n!="string")throw new R("VALIDATION_ERROR",`@lunora/queue: a "json" message body must be JSON-serializable, got ${typeof e}`);return{body:JSON.parse(n),bytes:ie.encode(n).byteLength}},fe=e=>{const r=new Set(e),n=[],o=(t,s,c)=>{const d=c?.contentType??Me,l=Ue(s,d);if(l.bytes>se)throw new R("VALIDATION_ERROR",`@lunora/queue: ${t} message is ${String(l.bytes)} bytes, over the Cloudflare Queues limit of ${String(se)} (128 KB)`);return{bytes:l.bytes,message:{body:l.body,contentType:d,...c?.delaySeconds===void 0?{}:{delaySeconds:c.delaySeconds},destination:t}}},f=t=>({send:(s,c)=>(n.push(o(t,s,c).message),Promise.resolve()),sendBatch:(s,c)=>{const d=[...s].map(h=>o(t,h.body,{contentType:h.contentType,delaySeconds:h.delaySeconds??c?.delaySeconds})),l=d.reduce((h,S)=>h+S.bytes,0);if(l>ce)throw new R("VALIDATION_ERROR",`@lunora/queue: ${t} batch is ${String(l)} bytes, over the Cloudflare Queues limit of ${String(ce)} (256 KB)`);return n.push(...d.map(h=>h.message)),Promise.resolve()}});return{bindings:Object.fromEntries([...r].map(t=>[t,f(t)])),clear:()=>{n.length=0},read:(t,s,c)=>{if(t===void 0)return[...n];if(!r.has(t))throw new R("INTERNAL",`${s}("${t}"): no such ${c} — declared: ${[...r].join(", ")||"(none)"}`);return n.filter(d=>d.destination===t)}}},Ye=e=>{const r=fe(e);return{controls:{clear:r.clear,sent:n=>r.read(n,"harness.queues.sent","queue").map(({destination:o,...f})=>({...f,queue:o}))},surfaces:{queues:Pe({bindings:r.bindings})}}},Qe=e=>e!==void 0?Ye(e):{controls:{clear:()=>I("queues",te),sent:()=>I("queues",te)},surfaces:{queues:E("queues",te)}},X=e=>{let r="";for(let o=0;o<e.length;o+=32768)r+=String.fromCharCode(...e.subarray(o,o+32768));return btoa(r)},Ve=e=>{const r=atob(e),n=new Uint8Array(r.length);for(let o=0;o<r.length;o+=1)n[o]=r.codePointAt(o)??0;return n},A="$lunora.wire$",H=64,K=1024,ne="__proto__",ae={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},ue={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},Je=e=>{if(e===null||typeof e!="object")return!1;const r=Object.getPrototypeOf(e);return r===null||r===Object.prototype},x=(e,r=0)=>{if(r>H)throw new RangeError(`wire-codec: value nesting exceeds the ${H}-level limit`);if(e===void 0)return[A,"undefined"];if(e===null)return null;const n=typeof e;if(n==="bigint"){const t=e.toString();if(t.length>K)throw new RangeError(`wire-codec: over-long bigint (max ${K} digits)`);return[A,"bigint",t]}if(n==="number"){const t=e;return Number.isNaN(t)?[A,"nan"]:t===1/0?[A,"inf"]:t===-1/0?[A,"-inf"]:t}if(n!=="object")return e;if(e instanceof Date)return[A,"date",x(e.getTime(),r+1)];if(e instanceof Error){const t=e,s=x(Object.fromEntries(Object.keys(t).map(d=>[d,t[d]])),r+1),c=[A,"error",String(t.name),String(t.message),s];return t.cause!==void 0&&c.push(x(t.cause,r+1)),c}if(e instanceof URL)return[A,"url",e.href];if(e instanceof Map)return[A,"map",[...e.entries()].map(([t,s])=>[x(t,r+1),x(s,r+1)])];if(e instanceof Set)return[A,"set",[...e].map(t=>x(t,r+1))];if(e instanceof ArrayBuffer)return[A,"bytes",X(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const t=e,s=t.constructor.name,c=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return s==="Uint8Array"?[A,"bytes",X(c)]:[A,"bytes",X(c),s]}if(Array.isArray(e)){const t=e.map(s=>x(s,r+1));return t.length>0&&t[0]===A?[A,"arr",t]:t}if(!Je(e)){const t=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${t} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const o=e,f={};for(const t of Object.keys(o)){const s=o[t];if(s===void 0)continue;const c=x(s,r+1);t===ne?Object.defineProperty(f,t,{configurable:!0,enumerable:!0,value:c,writable:!0}):f[t]=c}return f},O=(e,r=0)=>{if(r>H)throw new RangeError(`wire-codec: value nesting exceeds the ${H}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===A)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(t=>O(t,r+1));case"bigint":{const t=e[2];if(typeof t!="string"||t.length>K||!/^-?\d+$/.test(t))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${K} digits)`);return BigInt(t)}case"date":{const t=O(e[2],r+1);if(typeof t!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(t)}case"map":{const t=e[2];return new Map(t.map(s=>{if(!Array.isArray(s)||s.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[O(s[0],r+1),O(s[1],r+1)]}))}case"set":return new Set(e[2].map(t=>O(t,r+1)));case"url":{const t=e[2];if(typeof t!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(t)}case"error":{const t=e[2],s=e[3];if(typeof t!="string"||typeof s!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const c=(Object.hasOwn(ue,t)?ue[t]:void 0)??Error,d=new c(s);d.name!==t&&Object.defineProperty(d,"name",{configurable:!0,value:t,writable:!0});const l=O(e[4],r+1);if(l===null||typeof l!="object"||Array.isArray(l))throw new TypeError("wire-codec: malformed error — props must be an object");for(const h of Object.keys(l))h===ne?Object.defineProperty(d,h,{configurable:!0,enumerable:!0,value:l[h],writable:!0}):d[h]=l[h];return e.length>5&&Object.defineProperty(d,"cause",{configurable:!0,value:O(e[5],r+1),writable:!0}),d}case"bytes":{const t=e[2];if(typeof t!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const s=Ve(t);if(X(s)!==t)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const c=e[3]??"Uint8Array";if(c==="ArrayBuffer")return s.buffer.byteLength===s.byteLength?s.buffer:s.slice().buffer;const d=Object.hasOwn(ae,c)?ae[c]:void 0;return d?new d(s.slice().buffer):s}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(t=>O(t,r+1))}return e.map(f=>O(f,r+1))}const n=e,o={};for(const f of Object.keys(n)){const t=O(n[f],r+1);f===ne?Object.defineProperty(o,f,{configurable:!0,enumerable:!0,value:t,writable:!0}):o[f]=t}return o},We=(e,r,n,o,f)=>{let t=f,s=1;const c=new Map,d=[],l=(u,g,m={},a)=>{const i=x(m);JSON.stringify(i);const w=O(i),N=a??`fake-job-${String(s)}`;return s+=1,c.set(N,{args:w,enqueuedAt:t,functionPath:g,id:N,scheduledFor:u}),N},h=u=>typeof u=="string"?u:"__lunoraRef"in u?u.__lunoraRef:u.name??u.className??"",S={cancel:u=>{const g=c.has(u);return c.delete(u),Promise.resolve({cancelled:g})},get:u=>Promise.resolve(c.get(u)??null),list:()=>Promise.resolve([...c.values()]),runAfter:(u,g,m,a)=>{Ie(u,"ctx.scheduler.runAfter");const i=l(t+u,h(g),m,a?.id);return Promise.resolve(i)},runAt:(u,g,m,a)=>{const i=l(u,h(g),m,a?.id);return Promise.resolve(i)}},b=async u=>{c.delete(u.id);const g=o(),m=u.functionPath===void 0?void 0:g.get(u.functionPath);if(m===void 0)throw new R("FUNCTION_NOT_FOUND",`unknown functionPath "${u.functionPath??"(workflow-targeted)"}"`,{status:404});if(m.kind!=="mutation"&&m.kind!=="action")throw new R("FUNCTION_NOT_FOUND",`functionPath "${u.functionPath??"(workflow-targeted)"}" is a ${m.kind} — only mutations and actions can be scheduled`,{status:404});const a=e(),i=m.kind==="action"?n():r();await a(m.kind,m,{...i,now:t},u.args)},v=async u=>{const g=[...c.values()].filter(i=>i.scheduledFor<=u).toSorted((i,w)=>i.scheduledFor-w.scheduledFor),m=[];let a=0;for(const i of g)if(c.has(i.id)){a+=1;try{await b(i)}catch(w){const N=(i.attempts??0)+1;if(N>ke){const j={args:i.args,error:w,functionPath:i.functionPath??"",id:i.id};m.push(j),d.push(j)}else{const j=_e*2**(N-1);c.set(i.id,{...i,attempts:N,scheduledFor:t+j})}}}return{executed:a,failed:m}},T=async(u,g)=>{const{executed:m,failed:a}=await v(u);if(a.length>0&&(g?.throwOnError??!0)){const[i]=a;throw a.length===1&&i!==void 0?i.error:new AggregateError(a.map(w=>w.error),`${String(a.length)} scheduled jobs failed: ${a.map(w=>w.functionPath).join(", ")}`)}return m};return{controls:{advance:(u,g)=>(t+=u,T(t,g)),failures:()=>[...d],list:()=>[...c.values()],runPending:u=>T(Number.POSITIVE_INFINITY,u)},scheduler:S}},re="topics: [...]",Xe=e=>{if(e===void 0)return{controls:{clear:()=>I("topics",re),published:()=>I("topics",re)},surfaces:{topics:E("topics",re)}};const r=fe(e),n=xe(r.bindings,e.map(o=>({exportName:o,subscriptions:[{binding:o,exportName:o}]})));return{controls:{clear:r.clear,published:o=>r.read(o,"harness.topics.published","topic").map(({destination:f,...t})=>({...t,topic:f}))},surfaces:{topics:n}}},He=()=>{const e=new $e(":memory:"),r=o=>({one(){if(o.length!==1)throw new R("INTERNAL",`expected exactly one row, received ${String(o.length)}`);const[f]=o;return f},[Symbol.iterator](){return o[Symbol.iterator]()},toArray(){return o}});return{close:()=>{e.close()},sql:{exec:(o,...f)=>{const s=e.prepare(o).all(...f);return r(s)}}}},Ke=(e,r,n)=>(f,t)=>{let s=!1;const c=[];let d,l,h=0,S=0;const b=()=>V(f)?e("query",f,r,t,!1):Promise.resolve(f(r)),v=(a,i)=>{if(a<S)return;S=a;const w={done:!1,value:i};if(c.length===0)d=w,l=void 0;else{d=void 0,l=void 0;for(const N of c.splice(0))N.resolve(w)}},T=(a,i)=>{if(!(a<S))if(S=a,c.length===0)l={error:i},d=void 0;else{d=void 0,l=void 0;for(const w of c.splice(0))w.reject(i)}},k=a=>i=>{v(a,i)},u=a=>i=>{T(a,i)},g=()=>{if(s)return;h+=1;const a=h;b().then(k(a)).catch(u(a))};n.add(g);const m={[Symbol.asyncIterator](){return m},next:()=>{if(s)return Promise.resolve({done:!0,value:void 0});if(S===h){if(l!==void 0){const{error:a}=l;return l=void 0,Promise.reject(a)}if(d!==void 0){const a=d;return d=void 0,Promise.resolve(a)}}return S<h?new Promise((a,i)=>{c.push({reject:i,resolve:a})}):b().then(a=>{if(l!==void 0){const{error:i}=l;throw l=void 0,i}if(d!==void 0){const i=d;return d=void 0,i}return{done:!1,value:a}})},return:()=>{s=!0,n.delete(g);for(const a of c.splice(0))a.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return b().then(k(0)).catch(u(0)),m},at=(e,r)=>{const{close:n,sql:o}=He();let f=!1;const t=()=>{f||(f=!0,n())},s=new Map(Object.entries(r?.functions??{}).map(([P,_])=>[P,_])),c=new Set,d=()=>{for(const P of c)P()},l=P=>(d(),P);let h,S,b;const v=r?.now??Date.now(),T=Le(),{controls:k,surfaces:u}=Qe(r?.queues),{controls:g,surfaces:m}=Xe(r?.topics),{controls:a,surfaces:i}=Be(r?.notify,r?.env??{}),w=(P,_)=>{if(P===void 0)throw new R("INTERNAL",`[fake-scheduler] ${_} not yet available — scheduler.advance called before harness construction completed`);return P},{controls:N,scheduler:j}=We(()=>w(h,"dispatch"),()=>w(S,"mutationContext"),()=>w(b,"actionContext"),()=>s,v),z=we(j),{createWriters:le,resetHeadroom:B,runInTransaction:M}=ve(e,{enforceRls:r?.enforceRls,scheduler:z,sql:o}),oe=P=>{const _=Ae(P),G={getIdentity:()=>Promise.resolve(_.claims),userId:_.userId},{database:Z,rawDatabase:ye}=le(_),q={...i,auth:G,db:Z,env:r?.env,log:W,metrics:ee,now:v,span:T.handle,trace:J,runQuery:((y,p)=>$("query",y,q,p)),secrets:E("secrets"),storage:E("storage"),vectors:E("vectors")},C={...i,...u,...m,auth:G,db:Z,env:r?.env,log:W,metrics:ee,now:v,span:T.handle,trace:J,runMutation:((y,p)=>$("mutation",y,C,p)),runQuery:((y,p)=>$("query",y,q,p)),scheduler:z,secrets:E("secrets"),storage:E("storage"),vectors:E("vectors"),workflows:E("workflows")};S??=C;const he={...C,db:ye},U={...i,...u,...m,auth:G,db:Z,env:r?.env,fetch:r?.fetch??E("fetch"),log:W,metrics:ee,now:v,span:T.handle,trace:J,runAction:((y,p)=>$("action",y,U,p)),runMutation:((y,p)=>M(()=>$("mutation",y,C,p)).then(l)),runQuery:((y,p)=>$("query",y,q,p)),scheduler:z,secrets:E("secrets"),services:qe(r?.services),storage:E("storage"),vectors:E("vectors"),workflows:E("workflows")};b??=U;const $=(y,p,L,Y)=>D(y,p,L,Y,!0);h??=(y,p,L,Y)=>y==="mutation"?M(()=>D("mutation",p,L,Y,!0)).then(l):(B(),$("action",p,L,Y));const me=((y,p)=>(B(),V(y)?D("query",y,q,p,!1):Promise.resolve(y(q)))),be=((y,p)=>{const L=V(y)?()=>D("mutation",y,C,p,!1):()=>y(C);return M(L).then(l)}),pe=((y,p)=>(B(),V(y)?D("action",y,U,p,!1):Promise.resolve(y(U)))),ge=Ke((...y)=>(B(),D(...y)),q,c);return{action:pe,close:t,mutation:be,notify:a,query:me,queues:k,run:y=>M(()=>y(he)).then(l),scheduler:N,subscribe:ge,topics:g,wideEvent:()=>T.recorded,withIdentity:y=>oe(y)}};return oe(null)};export{at as lunoraTest};
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as T}from"@lunora/errors";import{withDeferredSchedules as me}from"@lunora/server";import{createInProcessRuntime as be,resolveInProcessIdentity as we,registeredFunctionKind as V,runRegisteredFunction as q}from"@lunora/server/in-process";import{evaluationAttributes as ge}from"./evaluationAttributes-CYLgTP1P.mjs";import{memorySubscriptionStore as pe,createNotify as ve}from"@lunora/notify";import{createNotification as Ae}from"@visulima/notification";import{serialize as Ee,deserialize as Se}from"node:v8";import{createQueues as Re}from"@lunora/queue";import{assertScheduleDelay as Te,MAX_RETRY_ATTEMPTS as Ne,RETRY_BASE_DELAY_MS as Oe}from"@lunora/scheduler";import{DatabaseSync as Pe}from"node:sqlite";const $=(e,t)=>{throw new T("INTERNAL",t===void 0?`ctx.${e} is not available in the in-memory @lunora/testing harness (v1)`:`ctx.${e} is not enabled in the @lunora/testing harness — pass lunoraTest(schema, { ${t} })`)},Ie=new Set(["$$typeof","asymmetricMatch","then","toJSON"]),xe=e=>new Proxy({...e},{get(t,n){if(!(Ie.has(n)&&!Object.hasOwn(t,n))){if(typeof n=="symbol"||Object.hasOwn(t,n)||n in Object.prototype)return Reflect.get(t,n);throw new T("INTERNAL",`ctx.services.${n} has no fake in the @lunora/testing harness — pass lunoraTest(schema, { services: { ${n}: … } })`)}}}),S=(e,t)=>new Proxy((...n)=>$(e,t),{apply:()=>$(e,t),get:()=>$(e,t)}),ae={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},_e={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>ae},ke=()=>{const e={attributes:{},events:[],links:[]},t={addEvent:(n,s)=>{e.events.push({...s===void 0?{}:{attributes:{...s}},name:n})},addLink:n=>{e.links.push({spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(e.attributes,ge(n))},recordException:n=>{const s={"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&&(s["exception.stacktrace"]=n.stack),t.addEvent("exception",s)},setAttribute:(n,s)=>{e.attributes[n]=s},setAttributes:n=>{Object.assign(e.attributes,n)},spanContext:()=>ae};return{handle:t,recorded:e}},J=async(e,t)=>await t(J,_e),Z={count:()=>{},gauge:()=>{},record:()=>{}},W={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>W},Q="notify",$e=e=>{let t=e;if(typeof e=="string"){if(!e.startsWith("{"))return;try{t=JSON.parse(e)}catch{return}}const n=t?.endpoint;return typeof n=="string"?n:void 0},B=(e,t)=>typeof e=="function"?e(t):e,je=(e,t)=>{const n=[],s=(h,v)=>(n.push({channel:h,payload:structuredClone(v)}),{data:{messageId:`${h}-${String(n.length)}`,sent:!0,timestamp:new Date},success:!0}),y=h=>({channel:h,id:`lunora-testing-${h}`,initialize:()=>{},isAvailable:()=>!0,send:v=>s(h,v)}),r=B(e.webPush,t)!==void 0,c=B(e.fcm,t)!==void 0,o={...y("push"),send:h=>{const v=Array.isArray(h.to)?h.to:[h.to];if(v.length===0)throw new T("BAD_REQUEST","@lunora/notify: push send has no recipients — `to` is an empty array");for(const N of v){const x=$e(N)!==void 0;if(x?!r:!c)throw new Error(x?"@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",h)}},d={...r||c?{push:o}:{},...B(e.chat,t)===void 0?{}:{chat:y("chat")},...B(e.inApp,t)===void 0?{}:{inapp:y("inapp")},...B(e.webhook,t)===void 0?{}:{webhook:y("webhook")}},f=pe(),{notify:b,push:g}=ve({...e,store:()=>f},t,{engine:Ae(d),silent:!0});return{controls:{clear:()=>{n.length=0},sent:h=>h===void 0?[...n]:n.filter(v=>v.channel===h)},surfaces:{notify:b,push:g}}},Ce=(e,t)=>e!==void 0?je(e,t):{controls:{clear:()=>$("notify",Q),sent:()=>$("notify",Q)},surfaces:{notify:S("notify",Q),push:S("push",Q)}},ee="queues: [...]",ne=128*1024,oe=256*1024,Le="json",se=new TextEncoder,De=(e,t)=>{if(t==="v8"){const s=Ee(e);return{body:Se(s),bytes:s.byteLength}}if(t==="text"){if(typeof e!="string")throw new T("VALIDATION_ERROR",`@lunora/queue: a "text" message body must be a string, got ${typeof e}`);return{body:e,bytes:se.encode(e).byteLength}}if(t==="bytes"){if(!(e instanceof ArrayBuffer)&&!ArrayBuffer.isView(e))throw new T("VALIDATION_ERROR",'@lunora/queue: a "bytes" message body must be an ArrayBuffer or an ArrayBufferView');return{body:e,bytes:e.byteLength}}const n=JSON.stringify(e);if(typeof n!="string")throw new T("VALIDATION_ERROR",`@lunora/queue: a "json" message body must be JSON-serializable, got ${typeof e}`);return{body:JSON.parse(n),bytes:se.encode(n).byteLength}},qe=e=>{const t=new Set(e),n=[],s=(o,d,f)=>{const b=f?.contentType??Le,g=De(d,b);if(g.bytes>ne)throw new T("VALIDATION_ERROR",`@lunora/queue: ${o} message is ${String(g.bytes)} bytes, over the Cloudflare Queues limit of ${String(ne)} (128 KB)`);return{bytes:g.bytes,message:{body:g.body,contentType:b,...f?.delaySeconds===void 0?{}:{delaySeconds:f.delaySeconds},queue:o}}},y=o=>({send:(d,f)=>(n.push(s(o,d,f).message),Promise.resolve()),sendBatch:(d,f)=>{const b=[...d].map(h=>s(o,h.body,{contentType:h.contentType,delaySeconds:h.delaySeconds??f?.delaySeconds})),g=b.reduce((h,v)=>h+v.bytes,0);if(g>oe)throw new T("VALIDATION_ERROR",`@lunora/queue: ${o} batch is ${String(g)} bytes, over the Cloudflare Queues limit of ${String(oe)} (256 KB)`);return n.push(...b.map(h=>h.message)),Promise.resolve()}}),r=Re({bindings:Object.fromEntries([...t].map(o=>[o,y(o)]))});return{controls:{clear:()=>{n.length=0},sent:o=>{if(o===void 0)return[...n];if(!t.has(o))throw new T("INTERNAL",`harness.queues.sent("${o}"): no such queue — declared: ${[...t].join(", ")||"(none)"}`);return n.filter(d=>d.queue===o)}},surfaces:{queues:r}}},Be=e=>e!==void 0?qe(e):{controls:{clear:()=>$("queues",ee),sent:()=>$("queues",ee)},surfaces:{queues:S("queues",ee)}},X=e=>{let t="";for(let s=0;s<e.length;s+=32768)t+=String.fromCharCode(...e.subarray(s,s+32768));return btoa(t)},Fe=e=>{const t=atob(e),n=new Uint8Array(t.length);for(let s=0;s<t.length;s+=1)n[s]=t.codePointAt(s)??0;return n},E="$lunora.wire$",H=64,K=1024,te="__proto__",ie={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},ce={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},Me=e=>{if(e===null||typeof e!="object")return!1;const t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},I=(e,t=0)=>{if(t>H)throw new RangeError(`wire-codec: value nesting exceeds the ${H}-level limit`);if(e===void 0)return[E,"undefined"];if(e===null)return null;const n=typeof e;if(n==="bigint"){const r=e.toString();if(r.length>K)throw new RangeError(`wire-codec: over-long bigint (max ${K} digits)`);return[E,"bigint",r]}if(n==="number"){const r=e;return Number.isNaN(r)?[E,"nan"]:r===1/0?[E,"inf"]:r===-1/0?[E,"-inf"]:r}if(n!=="object")return e;if(e instanceof Date)return[E,"date",I(e.getTime(),t+1)];if(e instanceof Error){const r=e,c=I(Object.fromEntries(Object.keys(r).map(d=>[d,r[d]])),t+1),o=[E,"error",String(r.name),String(r.message),c];return r.cause!==void 0&&o.push(I(r.cause,t+1)),o}if(e instanceof URL)return[E,"url",e.href];if(e instanceof Map)return[E,"map",[...e.entries()].map(([r,c])=>[I(r,t+1),I(c,t+1)])];if(e instanceof Set)return[E,"set",[...e].map(r=>I(r,t+1))];if(e instanceof ArrayBuffer)return[E,"bytes",X(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const r=e,c=r.constructor.name,o=new Uint8Array(r.buffer,r.byteOffset,r.byteLength);return c==="Uint8Array"?[E,"bytes",X(o)]:[E,"bytes",X(o),c]}if(Array.isArray(e)){const r=e.map(c=>I(c,t+1));return r.length>0&&r[0]===E?[E,"arr",r]:r}if(!Me(e)){const r=e.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${r} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const s=e,y={};for(const r of Object.keys(s)){const c=s[r];if(c===void 0)continue;const o=I(c,t+1);r===te?Object.defineProperty(y,r,{configurable:!0,enumerable:!0,value:o,writable:!0}):y[r]=o}return y},O=(e,t=0)=>{if(t>H)throw new RangeError(`wire-codec: value nesting exceeds the ${H}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===E)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(r=>O(r,t+1));case"bigint":{const r=e[2];if(typeof r!="string"||r.length>K||!/^-?\d+$/.test(r))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${K} digits)`);return BigInt(r)}case"date":{const r=O(e[2],t+1);if(typeof r!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(r)}case"map":{const r=e[2];return new Map(r.map(c=>{if(!Array.isArray(c)||c.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[O(c[0],t+1),O(c[1],t+1)]}))}case"set":return new Set(e[2].map(r=>O(r,t+1)));case"url":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(r)}case"error":{const r=e[2],c=e[3];if(typeof r!="string"||typeof c!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const o=(Object.hasOwn(ce,r)?ce[r]:void 0)??Error,d=new o(c);d.name!==r&&Object.defineProperty(d,"name",{configurable:!0,value:r,writable:!0});const f=O(e[4],t+1);if(f===null||typeof f!="object"||Array.isArray(f))throw new TypeError("wire-codec: malformed error — props must be an object");for(const b of Object.keys(f))b===te?Object.defineProperty(d,b,{configurable:!0,enumerable:!0,value:f[b],writable:!0}):d[b]=f[b];return e.length>5&&Object.defineProperty(d,"cause",{configurable:!0,value:O(e[5],t+1),writable:!0}),d}case"bytes":{const r=e[2];if(typeof r!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const c=Fe(r);if(X(c)!==r)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const o=e[3]??"Uint8Array";if(o==="ArrayBuffer")return c.buffer.byteLength===c.byteLength?c.buffer:c.slice().buffer;const d=Object.hasOwn(ie,o)?ie[o]:void 0;return d?new d(c.slice().buffer):c}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(r=>O(r,t+1))}return e.map(y=>O(y,t+1))}const n=e,s={};for(const y of Object.keys(n)){const r=O(n[y],t+1);y===te?Object.defineProperty(s,y,{configurable:!0,enumerable:!0,value:r,writable:!0}):s[y]=r}return s},Ue=(e,t,n,s,y)=>{let r=y,c=1;const o=new Map,d=[],f=(u,p,m={},i)=>{const a=I(m);JSON.stringify(a);const A=O(a),R=i??`fake-job-${String(c)}`;return c+=1,o.set(R,{args:A,enqueuedAt:r,functionPath:p,id:R,scheduledFor:u}),R},b=u=>typeof u=="string"?u:"__lunoraRef"in u?u.__lunoraRef:u.name??u.className??"",g={cancel:u=>{const p=o.has(u);return o.delete(u),Promise.resolve({cancelled:p})},get:u=>Promise.resolve(o.get(u)??null),list:()=>Promise.resolve([...o.values()]),runAfter:(u,p,m,i)=>{Te(u,"ctx.scheduler.runAfter");const a=f(r+u,b(p),m,i?.id);return Promise.resolve(a)},runAt:(u,p,m,i)=>{const a=f(u,b(p),m,i?.id);return Promise.resolve(a)}},h=async u=>{o.delete(u.id);const p=s(),m=u.functionPath===void 0?void 0:p.get(u.functionPath);if(m===void 0)throw new T("FUNCTION_NOT_FOUND",`unknown functionPath "${u.functionPath??"(workflow-targeted)"}"`,{status:404});if(m.kind!=="mutation"&&m.kind!=="action")throw new T("FUNCTION_NOT_FOUND",`functionPath "${u.functionPath??"(workflow-targeted)"}" is a ${m.kind} — only mutations and actions can be scheduled`,{status:404});const i=e(),a=m.kind==="action"?n():t();await i(m.kind,m,{...a,now:r},u.args)},v=async u=>{const p=[...o.values()].filter(a=>a.scheduledFor<=u).toSorted((a,A)=>a.scheduledFor-A.scheduledFor),m=[];let i=0;for(const a of p)if(o.has(a.id)){i+=1;try{await h(a)}catch(A){const R=(a.attempts??0)+1;if(R>Ne){const j={args:a.args,error:A,functionPath:a.functionPath??"",id:a.id};m.push(j),d.push(j)}else{const j=Oe*2**(R-1);o.set(a.id,{...a,attempts:R,scheduledFor:r+j})}}}return{executed:i,failed:m}},N=async(u,p)=>{const{executed:m,failed:i}=await v(u);if(i.length>0&&(p?.throwOnError??!0)){const[a]=i;throw i.length===1&&a!==void 0?a.error:new AggregateError(i.map(A=>A.error),`${String(i.length)} scheduled jobs failed: ${i.map(A=>A.functionPath).join(", ")}`)}return m};return{controls:{advance:(u,p)=>(r+=u,N(r,p)),failures:()=>[...d],list:()=>[...o.values()],runPending:u=>N(Number.POSITIVE_INFINITY,u)},scheduler:g}},Ye=()=>{const e=new Pe(":memory:"),t=s=>({one(){if(s.length!==1)throw new T("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:()=>{e.close()},sql:{exec:(s,...y)=>{const c=e.prepare(s).all(...y);return t(c)}}}},Qe=(e,t,n)=>(y,r)=>{let c=!1;const o=[];let d,f,b=0,g=0;const h=()=>V(y)?e("query",y,t,r,!1):Promise.resolve(y(t)),v=(i,a)=>{if(i<g)return;g=i;const A={done:!1,value:a};if(o.length===0)d=A,f=void 0;else{d=void 0,f=void 0;for(const R of o.splice(0))R.resolve(A)}},N=(i,a)=>{if(!(i<g))if(g=i,o.length===0)f={error:a},d=void 0;else{d=void 0,f=void 0;for(const A of o.splice(0))A.reject(a)}},x=i=>a=>{v(i,a)},u=i=>a=>{N(i,a)},p=()=>{if(c)return;b+=1;const i=b;h().then(x(i)).catch(u(i))};n.add(p);const m={[Symbol.asyncIterator](){return m},next:()=>{if(c)return Promise.resolve({done:!0,value:void 0});if(g===b){if(f!==void 0){const{error:i}=f;return f=void 0,Promise.reject(i)}if(d!==void 0){const i=d;return d=void 0,Promise.resolve(i)}}return g<b?new Promise((i,a)=>{o.push({reject:a,resolve:i})}):h().then(i=>{if(f!==void 0){const{error:a}=f;throw f=void 0,a}if(d!==void 0){const a=d;return d=void 0,a}return{done:!1,value:i}})},return:()=>{c=!0,n.delete(p);for(const i of o.splice(0))i.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return h().then(x(0)).catch(u(0)),m},rt=(e,t)=>{const{close:n,sql:s}=Ye();let y=!1;const r=()=>{y||(y=!0,n())},c=new Map(Object.entries(t?.functions??{}).map(([P,_])=>[P,_])),o=new Set,d=()=>{for(const P of o)P()},f=P=>(d(),P);let b,g,h;const v=t?.now??Date.now(),N=ke(),{controls:x,surfaces:u}=Be(t?.queues),{controls:p,surfaces:m}=Ce(t?.notify,t?.env??{}),i=(P,_)=>{if(P===void 0)throw new T("INTERNAL",`[fake-scheduler] ${_} not yet available — scheduler.advance called before harness construction completed`);return P},{controls:a,scheduler:A}=Ue(()=>i(b,"dispatch"),()=>i(g,"mutationContext"),()=>i(h,"actionContext"),()=>c,v),R=me(A),{createWriters:j,resetHeadroom:F,runInTransaction:M}=be(e,{enforceRls:t?.enforceRls,scheduler:R,sql:s}),re=P=>{const _=we(P),z={getIdentity:()=>Promise.resolve(_.claims),userId:_.userId},{database:G,rawDatabase:ue}=j(_),C={...m,auth:z,db:G,env:t?.env,log:W,metrics:Z,now:v,span:N.handle,trace:J,runQuery:((l,w)=>k("query",l,C,w)),secrets:S("secrets"),storage:S("storage"),vectors:S("vectors")},L={...m,...u,auth:z,db:G,env:t?.env,log:W,metrics:Z,now:v,span:N.handle,trace:J,runMutation:((l,w)=>k("mutation",l,L,w)),runQuery:((l,w)=>k("query",l,C,w)),scheduler:R,secrets:S("secrets"),storage:S("storage"),vectors:S("vectors"),workflows:S("workflows")};g??=L;const de={...L,db:ue},U={...m,...u,auth:z,db:G,env:t?.env,fetch:t?.fetch??S("fetch"),log:W,metrics:Z,now:v,span:N.handle,trace:J,runAction:((l,w)=>k("action",l,U,w)),runMutation:((l,w)=>M(()=>k("mutation",l,L,w)).then(f)),runQuery:((l,w)=>k("query",l,C,w)),scheduler:R,secrets:S("secrets"),services:xe(t?.services),storage:S("storage"),vectors:S("vectors"),workflows:S("workflows")};h??=U;const k=(l,w,D,Y)=>q(l,w,D,Y,!0);b??=(l,w,D,Y)=>l==="mutation"?M(()=>q("mutation",w,D,Y,!0)).then(f):(F(),k("action",w,D,Y));const fe=((l,w)=>(F(),V(l)?q("query",l,C,w,!1):Promise.resolve(l(C)))),le=((l,w)=>{const D=V(l)?()=>q("mutation",l,L,w,!1):()=>l(L);return M(D).then(f)}),ye=((l,w)=>(F(),V(l)?q("action",l,U,w,!1):Promise.resolve(l(U)))),he=Qe((...l)=>(F(),q(...l)),C,o);return{action:ye,close:r,mutation:le,notify:p,query:fe,queues:x,run:l=>M(()=>l(de)).then(f),scheduler:a,subscribe:he,wideEvent:()=>N.recorded,withIdentity:l=>re(l)}};return re(null)};export{rt as lunoraTest};
|