@lunora/testing 1.0.0-alpha.246 → 1.0.0-alpha.247
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,6 +122,17 @@ 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`
|
|
126
|
+
|
|
127
|
+
Pass `queues` (the export names from `lunora/queues.ts`) and/or `notify` (the
|
|
128
|
+
`lunora/notify.ts` default export). Sends are recorded instead of delivered, and
|
|
129
|
+
read back with `t.queues.sent(name)` / `t.notify.sent(channel)`. See the
|
|
130
|
+
[package docs](https://lunora.sh/docs/packages/testing#queues) for details.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const t = lunoraTest(schema, { notify, queues: ["jobs"] });
|
|
134
|
+
```
|
|
135
|
+
|
|
125
136
|
#### Fixed `ctx.now`
|
|
126
137
|
|
|
127
138
|
Pass a `now` option (epoch ms) to pin `ctx.now` across every context, so
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
2
|
import { ArtifactsBindingLike, ArtifactsErrorCode, ArtifactsCommitMetadata, ArtifactsTreeEntry } from '@lunora/bindings/artifacts';
|
|
3
3
|
import { ScheduledJob, LogFields, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
4
|
+
import { NotifyDefinition } from '@lunora/notify';
|
|
5
|
+
import { ChannelType, ChannelPayloadMap } from '@visulima/notification';
|
|
6
|
+
import { QueueContentType } from '@lunora/queue';
|
|
4
7
|
export {
|
|
5
8
|
/**
|
|
6
9
|
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
@@ -386,6 +389,44 @@ interface RecordedWideEvent {
|
|
|
386
389
|
traceId: string;
|
|
387
390
|
}[];
|
|
388
391
|
}
|
|
392
|
+
/** The channels `ctx.notify` can deliver on. */
|
|
393
|
+
type NotifyChannel = Extract<ChannelType, "chat" | "inapp" | "push" | "webhook">;
|
|
394
|
+
/**
|
|
395
|
+
* One delivery a handler made through `ctx.notify` / `ctx.push`. For push,
|
|
396
|
+
* `payload.to` is the device target: the FCM token, or for Web Push the
|
|
397
|
+
* JSON-stringified subscription (keys included).
|
|
398
|
+
*/
|
|
399
|
+
type SentNotification = { [C in NotifyChannel]: {
|
|
400
|
+
channel: C;
|
|
401
|
+
payload: ChannelPayloadMap[C];
|
|
402
|
+
}; }[NotifyChannel];
|
|
403
|
+
/** Inspect what handlers delivered through `ctx.notify` / `ctx.push`. */
|
|
404
|
+
interface FakeNotifyControls {
|
|
405
|
+
/** Forget every recorded delivery (registered subscriptions are kept). */
|
|
406
|
+
clear: () => void;
|
|
407
|
+
/** Recorded deliveries in send order, optionally only those on `channel`. Throws when `options.notify` was not passed. */
|
|
408
|
+
sent: (channel?: NotifyChannel) => SentNotification[];
|
|
409
|
+
}
|
|
410
|
+
/** One message a handler enqueued through `ctx.queues.<name>`, as the consumer decodes it. */
|
|
411
|
+
interface SentQueueMessage {
|
|
412
|
+
body: unknown;
|
|
413
|
+
contentType?: QueueContentType;
|
|
414
|
+
delaySeconds?: number;
|
|
415
|
+
/** The `lunora/queues.ts` export name the message was sent through. */
|
|
416
|
+
queue: string;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Inspect what handlers enqueued. Production sends are not transactional — a
|
|
420
|
+
* mutation that throws after `ctx.queues.x.send(...)` has still sent it — so the
|
|
421
|
+
* record keeps those too. Nothing is consumed: a test asserts on the messages and
|
|
422
|
+
* drives the consumer itself if it wants to.
|
|
423
|
+
*/
|
|
424
|
+
interface FakeQueueControls {
|
|
425
|
+
/** Forget every recorded message. */
|
|
426
|
+
clear: () => void;
|
|
427
|
+
/** Recorded messages in send order, optionally only those sent to `queue`; an undeclared `queue` throws. */
|
|
428
|
+
sent: (queue?: string) => SentQueueMessage[];
|
|
429
|
+
}
|
|
389
430
|
/** The schema value produced by `@lunora/server`'s `defineSchema`. */
|
|
390
431
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
391
432
|
/**
|
|
@@ -503,6 +544,28 @@ interface LunoraTestOptions {
|
|
|
503
544
|
* ```
|
|
504
545
|
*/
|
|
505
546
|
functions?: FunctionRegistry;
|
|
547
|
+
/**
|
|
548
|
+
* The app's `lunora/notify.ts` default export. Enables `ctx.notify` and its
|
|
549
|
+
* `ctx.push` alias on every context, built by `@lunora/notify`'s own
|
|
550
|
+
* `createNotify` — so register / list / unregister / broadcast behave as in
|
|
551
|
+
* production, and channels are wired exactly when production wires them
|
|
552
|
+
* (each factory is called with `options.env`; a push target for a transport
|
|
553
|
+
* that resolved to nothing fails). Differences: deliveries are recorded and
|
|
554
|
+
* report accepted instead of reaching a service (no send-time DNS re-check,
|
|
555
|
+
* retry or circuit breaker), and subscriptions live in an in-memory store —
|
|
556
|
+
* the definition's `store` is never called. Assert on deliveries with
|
|
557
|
+
* `harness.notify.sent()`. Left unset, touching `ctx.notify` / `ctx.push` —
|
|
558
|
+
* or calling `harness.notify.sent()` — throws, naming this option.
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* import notify from "../lunora/notify";
|
|
562
|
+
*
|
|
563
|
+
* const t = lunoraTest(schema, { notify });
|
|
564
|
+
* await t.action(alertOwner, { orderId });
|
|
565
|
+
* expect(t.notify.sent("push")).toHaveLength(1);
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
notify?: NotifyDefinition;
|
|
506
569
|
/**
|
|
507
570
|
* Fixed value for `ctx.now` (epoch ms) in every context. Production captures
|
|
508
571
|
* `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
|
|
@@ -513,6 +576,23 @@ interface LunoraTestOptions {
|
|
|
513
576
|
* ```
|
|
514
577
|
*/
|
|
515
578
|
now?: number;
|
|
579
|
+
/**
|
|
580
|
+
* The export names of the queues in `lunora/queues.ts` (the keys of
|
|
581
|
+
* `ctx.queues`). Each gets a recording producer on mutation and action
|
|
582
|
+
* contexts, behind `@lunora/queue`'s own validation (batch cap, delay
|
|
583
|
+
* ceiling), so a send the platform would refuse rejects here too. A name not
|
|
584
|
+
* listed rejects on use, as an undeclared queue does in production. Assert
|
|
585
|
+
* on sends with `harness.queues.sent(name)`. Left unset, touching
|
|
586
|
+
* `ctx.queues` — or calling `harness.queues.sent()` — throws, naming this
|
|
587
|
+
* option.
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* const t = lunoraTest(schema, { queues: ["jobs"] });
|
|
591
|
+
* await t.mutation(enqueueJob, { id: "a" });
|
|
592
|
+
* expect(t.queues.sent("jobs")).toEqual([{ body: { id: "a" }, queue: "jobs" }]);
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
queues?: ReadonlyArray<string>;
|
|
516
596
|
/**
|
|
517
597
|
* Fakes for `ctx.services` (the `lunora.config` `services` an app calls),
|
|
518
598
|
* keyed like `ctx.services`. Set on action contexts only — queries and
|
|
@@ -547,11 +627,25 @@ interface TestHarness {
|
|
|
547
627
|
<A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
|
|
548
628
|
<R>(inline: InlineMutationFunction<R>): Promise<R>;
|
|
549
629
|
};
|
|
630
|
+
/**
|
|
631
|
+
* What handlers delivered through `ctx.notify` / `ctx.push` (enabled by
|
|
632
|
+
* `options.notify`): `sent(channel?)` in send order, `clear()` to reset.
|
|
633
|
+
* Both throw when the option was not passed. Shared with any `withIdentity` view.
|
|
634
|
+
*/
|
|
635
|
+
notify: FakeNotifyControls;
|
|
550
636
|
/** Run a registered `query` (or an inline `async (context) => …`) against the harness. */
|
|
551
637
|
query: {
|
|
552
638
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
|
|
553
639
|
<R>(inline: InlineQueryFunction<R>): Promise<R>;
|
|
554
640
|
};
|
|
641
|
+
/**
|
|
642
|
+
* What handlers enqueued through `ctx.queues` (declared by `options.queues`):
|
|
643
|
+
* `sent(name?)` in send order, `clear()` to reset. Sends are not
|
|
644
|
+
* transactional in production, so one made by a mutation that then threw is
|
|
645
|
+
* recorded too. Both throw when the option was not passed. Shared with any
|
|
646
|
+
* `withIdentity` view.
|
|
647
|
+
*/
|
|
648
|
+
queues: FakeQueueControls;
|
|
555
649
|
/**
|
|
556
650
|
* Direct db access at mutation-level (read + write), mirroring `convexTest`'s
|
|
557
651
|
* `run`. This is the harness's trusted escape hatch: `ctx.db` here is always
|
|
@@ -642,6 +736,10 @@ interface TestHarness {
|
|
|
642
736
|
* control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`. As in
|
|
643
737
|
* production it is transactional inside a mutation: a job scheduled by a mutation
|
|
644
738
|
* that then throws never becomes pending.
|
|
739
|
+
* - `ctx.queues` (mutations + actions): recording producers for `options.queues`;
|
|
740
|
+
* inspect via `harness.queues.sent(name)`.
|
|
741
|
+
* - `ctx.notify` / `ctx.push` (all contexts): built from `options.notify` with
|
|
742
|
+
* recorded deliveries and in-memory subscriptions; inspect via `harness.notify.sent()`.
|
|
645
743
|
* - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
|
|
646
744
|
*
|
|
647
745
|
* **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
|
|
@@ -799,4 +897,4 @@ declare const groundednessScorer: (options: {
|
|
|
799
897
|
judge: (prompt: string) => Promise<string>;
|
|
800
898
|
name?: string;
|
|
801
899
|
}) => Scorer;
|
|
802
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, 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 };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
2
|
import { ArtifactsBindingLike, ArtifactsErrorCode, ArtifactsCommitMetadata, ArtifactsTreeEntry } from '@lunora/bindings/artifacts';
|
|
3
3
|
import { ScheduledJob, LogFields, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
4
|
+
import { NotifyDefinition } from '@lunora/notify';
|
|
5
|
+
import { ChannelType, ChannelPayloadMap } from '@visulima/notification';
|
|
6
|
+
import { QueueContentType } from '@lunora/queue';
|
|
4
7
|
export {
|
|
5
8
|
/**
|
|
6
9
|
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
@@ -386,6 +389,44 @@ interface RecordedWideEvent {
|
|
|
386
389
|
traceId: string;
|
|
387
390
|
}[];
|
|
388
391
|
}
|
|
392
|
+
/** The channels `ctx.notify` can deliver on. */
|
|
393
|
+
type NotifyChannel = Extract<ChannelType, "chat" | "inapp" | "push" | "webhook">;
|
|
394
|
+
/**
|
|
395
|
+
* One delivery a handler made through `ctx.notify` / `ctx.push`. For push,
|
|
396
|
+
* `payload.to` is the device target: the FCM token, or for Web Push the
|
|
397
|
+
* JSON-stringified subscription (keys included).
|
|
398
|
+
*/
|
|
399
|
+
type SentNotification = { [C in NotifyChannel]: {
|
|
400
|
+
channel: C;
|
|
401
|
+
payload: ChannelPayloadMap[C];
|
|
402
|
+
}; }[NotifyChannel];
|
|
403
|
+
/** Inspect what handlers delivered through `ctx.notify` / `ctx.push`. */
|
|
404
|
+
interface FakeNotifyControls {
|
|
405
|
+
/** Forget every recorded delivery (registered subscriptions are kept). */
|
|
406
|
+
clear: () => void;
|
|
407
|
+
/** Recorded deliveries in send order, optionally only those on `channel`. Throws when `options.notify` was not passed. */
|
|
408
|
+
sent: (channel?: NotifyChannel) => SentNotification[];
|
|
409
|
+
}
|
|
410
|
+
/** One message a handler enqueued through `ctx.queues.<name>`, as the consumer decodes it. */
|
|
411
|
+
interface SentQueueMessage {
|
|
412
|
+
body: unknown;
|
|
413
|
+
contentType?: QueueContentType;
|
|
414
|
+
delaySeconds?: number;
|
|
415
|
+
/** The `lunora/queues.ts` export name the message was sent through. */
|
|
416
|
+
queue: string;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Inspect what handlers enqueued. Production sends are not transactional — a
|
|
420
|
+
* mutation that throws after `ctx.queues.x.send(...)` has still sent it — so the
|
|
421
|
+
* record keeps those too. Nothing is consumed: a test asserts on the messages and
|
|
422
|
+
* drives the consumer itself if it wants to.
|
|
423
|
+
*/
|
|
424
|
+
interface FakeQueueControls {
|
|
425
|
+
/** Forget every recorded message. */
|
|
426
|
+
clear: () => void;
|
|
427
|
+
/** Recorded messages in send order, optionally only those sent to `queue`; an undeclared `queue` throws. */
|
|
428
|
+
sent: (queue?: string) => SentQueueMessage[];
|
|
429
|
+
}
|
|
389
430
|
/** The schema value produced by `@lunora/server`'s `defineSchema`. */
|
|
390
431
|
type TestSchema = Schema<Record<string, TableDefinition>>;
|
|
391
432
|
/**
|
|
@@ -503,6 +544,28 @@ interface LunoraTestOptions {
|
|
|
503
544
|
* ```
|
|
504
545
|
*/
|
|
505
546
|
functions?: FunctionRegistry;
|
|
547
|
+
/**
|
|
548
|
+
* The app's `lunora/notify.ts` default export. Enables `ctx.notify` and its
|
|
549
|
+
* `ctx.push` alias on every context, built by `@lunora/notify`'s own
|
|
550
|
+
* `createNotify` — so register / list / unregister / broadcast behave as in
|
|
551
|
+
* production, and channels are wired exactly when production wires them
|
|
552
|
+
* (each factory is called with `options.env`; a push target for a transport
|
|
553
|
+
* that resolved to nothing fails). Differences: deliveries are recorded and
|
|
554
|
+
* report accepted instead of reaching a service (no send-time DNS re-check,
|
|
555
|
+
* retry or circuit breaker), and subscriptions live in an in-memory store —
|
|
556
|
+
* the definition's `store` is never called. Assert on deliveries with
|
|
557
|
+
* `harness.notify.sent()`. Left unset, touching `ctx.notify` / `ctx.push` —
|
|
558
|
+
* or calling `harness.notify.sent()` — throws, naming this option.
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* import notify from "../lunora/notify";
|
|
562
|
+
*
|
|
563
|
+
* const t = lunoraTest(schema, { notify });
|
|
564
|
+
* await t.action(alertOwner, { orderId });
|
|
565
|
+
* expect(t.notify.sent("push")).toHaveLength(1);
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
notify?: NotifyDefinition;
|
|
506
569
|
/**
|
|
507
570
|
* Fixed value for `ctx.now` (epoch ms) in every context. Production captures
|
|
508
571
|
* `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
|
|
@@ -513,6 +576,23 @@ interface LunoraTestOptions {
|
|
|
513
576
|
* ```
|
|
514
577
|
*/
|
|
515
578
|
now?: number;
|
|
579
|
+
/**
|
|
580
|
+
* The export names of the queues in `lunora/queues.ts` (the keys of
|
|
581
|
+
* `ctx.queues`). Each gets a recording producer on mutation and action
|
|
582
|
+
* contexts, behind `@lunora/queue`'s own validation (batch cap, delay
|
|
583
|
+
* ceiling), so a send the platform would refuse rejects here too. A name not
|
|
584
|
+
* listed rejects on use, as an undeclared queue does in production. Assert
|
|
585
|
+
* on sends with `harness.queues.sent(name)`. Left unset, touching
|
|
586
|
+
* `ctx.queues` — or calling `harness.queues.sent()` — throws, naming this
|
|
587
|
+
* option.
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* const t = lunoraTest(schema, { queues: ["jobs"] });
|
|
591
|
+
* await t.mutation(enqueueJob, { id: "a" });
|
|
592
|
+
* expect(t.queues.sent("jobs")).toEqual([{ body: { id: "a" }, queue: "jobs" }]);
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
queues?: ReadonlyArray<string>;
|
|
516
596
|
/**
|
|
517
597
|
* Fakes for `ctx.services` (the `lunora.config` `services` an app calls),
|
|
518
598
|
* keyed like `ctx.services`. Set on action contexts only — queries and
|
|
@@ -547,11 +627,25 @@ interface TestHarness {
|
|
|
547
627
|
<A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
|
|
548
628
|
<R>(inline: InlineMutationFunction<R>): Promise<R>;
|
|
549
629
|
};
|
|
630
|
+
/**
|
|
631
|
+
* What handlers delivered through `ctx.notify` / `ctx.push` (enabled by
|
|
632
|
+
* `options.notify`): `sent(channel?)` in send order, `clear()` to reset.
|
|
633
|
+
* Both throw when the option was not passed. Shared with any `withIdentity` view.
|
|
634
|
+
*/
|
|
635
|
+
notify: FakeNotifyControls;
|
|
550
636
|
/** Run a registered `query` (or an inline `async (context) => …`) against the harness. */
|
|
551
637
|
query: {
|
|
552
638
|
<A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>): Promise<R>;
|
|
553
639
|
<R>(inline: InlineQueryFunction<R>): Promise<R>;
|
|
554
640
|
};
|
|
641
|
+
/**
|
|
642
|
+
* What handlers enqueued through `ctx.queues` (declared by `options.queues`):
|
|
643
|
+
* `sent(name?)` in send order, `clear()` to reset. Sends are not
|
|
644
|
+
* transactional in production, so one made by a mutation that then threw is
|
|
645
|
+
* recorded too. Both throw when the option was not passed. Shared with any
|
|
646
|
+
* `withIdentity` view.
|
|
647
|
+
*/
|
|
648
|
+
queues: FakeQueueControls;
|
|
555
649
|
/**
|
|
556
650
|
* Direct db access at mutation-level (read + write), mirroring `convexTest`'s
|
|
557
651
|
* `run`. This is the harness's trusted escape hatch: `ctx.db` here is always
|
|
@@ -642,6 +736,10 @@ interface TestHarness {
|
|
|
642
736
|
* control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`. As in
|
|
643
737
|
* production it is transactional inside a mutation: a job scheduled by a mutation
|
|
644
738
|
* that then throws never becomes pending.
|
|
739
|
+
* - `ctx.queues` (mutations + actions): recording producers for `options.queues`;
|
|
740
|
+
* inspect via `harness.queues.sent(name)`.
|
|
741
|
+
* - `ctx.notify` / `ctx.push` (all contexts): built from `options.notify` with
|
|
742
|
+
* recorded deliveries and in-memory subscriptions; inspect via `harness.notify.sent()`.
|
|
645
743
|
* - `harness.subscribe(query, args)`: async iterable that re-emits after mutations.
|
|
646
744
|
*
|
|
647
745
|
* **v1 stubs (still throwing):** `ctx.storage`, `ctx.vectors`, `ctx.workflows`.
|
|
@@ -799,4 +897,4 @@ declare const groundednessScorer: (options: {
|
|
|
799
897
|
judge: (prompt: string) => Promise<string>;
|
|
800
898
|
name?: string;
|
|
801
899
|
}) => Scorer;
|
|
802
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, 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 };
|
|
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 };
|
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-B5xt8phy.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 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};
|
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.247",
|
|
4
4
|
"description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -54,8 +54,11 @@
|
|
|
54
54
|
"@lunora/bindings": "1.0.0-alpha.103",
|
|
55
55
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
56
56
|
"@lunora/mail": "1.0.0-alpha.103",
|
|
57
|
+
"@lunora/notify": "1.0.0-alpha.77",
|
|
58
|
+
"@lunora/queue": "1.0.0-alpha.97",
|
|
57
59
|
"@lunora/scheduler": "1.0.0-alpha.116",
|
|
58
|
-
"@lunora/server": "1.0.0-alpha.187"
|
|
60
|
+
"@lunora/server": "1.0.0-alpha.187",
|
|
61
|
+
"@visulima/notification": "1.2.0"
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|
|
61
64
|
"@playwright/test": "^1.61.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as U}from"@lunora/errors";import{withDeferredSchedules as se}from"@lunora/server";import{createInProcessRuntime as ie,resolveInProcessIdentity as ce,registeredFunctionKind as L,runRegisteredFunction as $}from"@lunora/server/in-process";import{evaluationAttributes as ae}from"./evaluationAttributes-CYLgTP1P.mjs";import{assertScheduleDelay as ue,MAX_RETRY_ATTEMPTS as de,RETRY_BASE_DELAY_MS as fe}from"@lunora/scheduler";import{DatabaseSync as le}from"node:sqlite";const X=e=>{throw new U("INTERNAL",`ctx.${e} is not available in the in-memory @lunora/testing harness (v1)`)},me=new Set(["$$typeof","asymmetricMatch","then","toJSON"]),ye=e=>new Proxy({...e},{get(r,n){if(!(me.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 U("INTERNAL",`ctx.services.${n} has no fake in the @lunora/testing harness — pass lunoraTest(schema, { services: { ${n}: … } })`)}}}),A=e=>new Proxy((...r)=>X(e),{apply:()=>X(e),get:()=>X(e)}),z={spanId:"0000000000000001",traceId:"00000000000000000000000000000001"},we={addEvent:()=>{},addLink:()=>{},recordEvaluation:()=>{},recordException:()=>{},setAttribute:()=>{},setAttributes:()=>{},spanContext:()=>z},be=()=>{const e={attributes:{},events:[],links:[]},r={addEvent:(n,a)=>{e.events.push({...a===void 0?{}:{attributes:{...a}},name:n})},addLink:n=>{e.links.push({spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(e.attributes,ae(n))},recordException:n=>{const a={"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&&(a["exception.stacktrace"]=n.stack),r.addEvent("exception",a)},setAttribute:(n,a)=>{e.attributes[n]=a},setAttributes:n=>{Object.assign(e.attributes,n)},spanContext:()=>z};return{handle:r,recorded:e}},F=async(e,r)=>await r(F,we),G={count:()=>{},gauge:()=>{},record:()=>{}},q={debug:()=>{},error:()=>{},event:()=>{},fatal:()=>{},info:()=>{},log:()=>{},trace:()=>{},warn:()=>{},with:()=>q},B=e=>{let r="";for(let a=0;a<e.length;a+=32768)r+=String.fromCharCode(...e.subarray(a,a+32768));return btoa(r)},he=e=>{const r=atob(e),n=new Uint8Array(r.length);for(let a=0;a<r.length;a+=1)n[a]=r.codePointAt(a)??0;return n},p="$lunora.wire$",Y=64,H=1024,J="__proto__",K={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},V={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},ge=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>Y)throw new RangeError(`wire-codec: value nesting exceeds the ${Y}-level limit`);if(e===void 0)return[p,"undefined"];if(e===null)return null;const n=typeof e;if(n==="bigint"){const t=e.toString();if(t.length>H)throw new RangeError(`wire-codec: over-long bigint (max ${H} digits)`);return[p,"bigint",t]}if(n==="number"){const t=e;return Number.isNaN(t)?[p,"nan"]:t===1/0?[p,"inf"]:t===-1/0?[p,"-inf"]:t}if(n!=="object")return e;if(e instanceof Date)return[p,"date",x(e.getTime(),r+1)];if(e instanceof Error){const t=e,i=x(Object.fromEntries(Object.keys(t).map(f=>[f,t[f]])),r+1),u=[p,"error",String(t.name),String(t.message),i];return t.cause!==void 0&&u.push(x(t.cause,r+1)),u}if(e instanceof URL)return[p,"url",e.href];if(e instanceof Map)return[p,"map",[...e.entries()].map(([t,i])=>[x(t,r+1),x(i,r+1)])];if(e instanceof Set)return[p,"set",[...e].map(t=>x(t,r+1))];if(e instanceof ArrayBuffer)return[p,"bytes",B(new Uint8Array(e)),"ArrayBuffer"];if(ArrayBuffer.isView(e)){const t=e,i=t.constructor.name,u=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return i==="Uint8Array"?[p,"bytes",B(u)]:[p,"bytes",B(u),i]}if(Array.isArray(e)){const t=e.map(i=>x(i,r+1));return t.length>0&&t[0]===p?[p,"arr",t]:t}if(!ge(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 a=e,y={};for(const t of Object.keys(a)){const i=a[t];if(i===void 0)continue;const u=x(i,r+1);t===J?Object.defineProperty(y,t,{configurable:!0,enumerable:!0,value:u,writable:!0}):y[t]=u}return y},P=(e,r=0)=>{if(r>Y)throw new RangeError(`wire-codec: value nesting exceeds the ${Y}-level limit`);if(e===null||typeof e!="object")return e;if(Array.isArray(e)){if(e[0]===p)switch(e[1]){case"-inf":return-1/0;case"arr":return e[2].map(t=>P(t,r+1));case"bigint":{const t=e[2];if(typeof t!="string"||t.length>H||!/^-?\d+$/.test(t))throw new RangeError(`wire-codec: invalid or over-long bigint (max ${H} digits)`);return BigInt(t)}case"date":{const t=P(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(i=>{if(!Array.isArray(i)||i.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[P(i[0],r+1),P(i[1],r+1)]}))}case"set":return new Set(e[2].map(t=>P(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],i=e[3];if(typeof t!="string"||typeof i!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const u=(Object.hasOwn(V,t)?V[t]:void 0)??Error,f=new u(i);f.name!==t&&Object.defineProperty(f,"name",{configurable:!0,value:t,writable:!0});const l=P(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 g of Object.keys(l))g===J?Object.defineProperty(f,g,{configurable:!0,enumerable:!0,value:l[g],writable:!0}):f[g]=l[g];return e.length>5&&Object.defineProperty(f,"cause",{configurable:!0,value:P(e[5],r+1),writable:!0}),f}case"bytes":{const t=e[2];if(typeof t!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const i=he(t);if(B(i)!==t)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const u=e[3]??"Uint8Array";if(u==="ArrayBuffer")return i.buffer.byteLength===i.byteLength?i.buffer:i.slice().buffer;const f=Object.hasOwn(K,u)?K[u]:void 0;return f?new f(i.slice().buffer):i}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return e.map(t=>P(t,r+1))}return e.map(y=>P(y,r+1))}const n=e,a={};for(const y of Object.keys(n)){const t=P(n[y],r+1);y===J?Object.defineProperty(a,y,{configurable:!0,enumerable:!0,value:t,writable:!0}):a[y]=t}return a},pe=(e,r,n,a,y)=>{let t=y,i=1;const u=new Map,f=[],l=(c,b,m={},s)=>{const o=x(m);JSON.stringify(o);const h=P(o),E=s??`fake-job-${String(i)}`;return i+=1,u.set(E,{args:h,enqueuedAt:t,functionPath:b,id:E,scheduledFor:c}),E},g=c=>typeof c=="string"?c:"__lunoraRef"in c?c.__lunoraRef:c.name??c.className??"",S={cancel:c=>{const b=u.has(c);return u.delete(c),Promise.resolve({cancelled:b})},get:c=>Promise.resolve(u.get(c)??null),list:()=>Promise.resolve([...u.values()]),runAfter:(c,b,m,s)=>{ue(c,"ctx.scheduler.runAfter");const o=l(t+c,g(b),m,s?.id);return Promise.resolve(o)},runAt:(c,b,m,s)=>{const o=l(c,g(b),m,s?.id);return Promise.resolve(o)}},T=async c=>{u.delete(c.id);const b=a(),m=c.functionPath===void 0?void 0:b.get(c.functionPath);if(m===void 0)throw new U("FUNCTION_NOT_FOUND",`unknown functionPath "${c.functionPath??"(workflow-targeted)"}"`,{status:404});if(m.kind!=="mutation"&&m.kind!=="action")throw new U("FUNCTION_NOT_FOUND",`functionPath "${c.functionPath??"(workflow-targeted)"}" is a ${m.kind} — only mutations and actions can be scheduled`,{status:404});const s=e(),o=m.kind==="action"?n():r();await s(m.kind,m,{...o,now:t},c.args)},N=async c=>{const b=[...u.values()].filter(o=>o.scheduledFor<=c).toSorted((o,h)=>o.scheduledFor-h.scheduledFor),m=[];let s=0;for(const o of b)if(u.has(o.id)){s+=1;try{await T(o)}catch(h){const E=(o.attempts??0)+1;if(E>de){const v={args:o.args,error:h,functionPath:o.functionPath??"",id:o.id};m.push(v),f.push(v)}else{const v=fe*2**(E-1);u.set(o.id,{...o,attempts:E,scheduledFor:t+v})}}}return{executed:s,failed:m}},R=async(c,b)=>{const{executed:m,failed:s}=await N(c);if(s.length>0&&(b?.throwOnError??!0)){const[o]=s;throw s.length===1&&o!==void 0?o.error:new AggregateError(s.map(h=>h.error),`${String(s.length)} scheduled jobs failed: ${s.map(h=>h.functionPath).join(", ")}`)}return m};return{controls:{advance:(c,b)=>(t+=c,R(t,b)),failures:()=>[...f],list:()=>[...u.values()],runPending:c=>R(Number.POSITIVE_INFINITY,c)},scheduler:S}},ve=()=>{const e=new le(":memory:"),r=a=>({one(){if(a.length!==1)throw new U("INTERNAL",`expected exactly one row, received ${String(a.length)}`);const[y]=a;return y},[Symbol.iterator](){return a[Symbol.iterator]()},toArray(){return a}});return{close:()=>{e.close()},sql:{exec:(a,...y)=>{const i=e.prepare(a).all(...y);return r(i)}}}},Ee=(e,r,n)=>(y,t)=>{let i=!1;const u=[];let f,l,g=0,S=0;const T=()=>L(y)?e("query",y,r,t,!1):Promise.resolve(y(r)),N=(s,o)=>{if(s<S)return;S=s;const h={done:!1,value:o};if(u.length===0)f=h,l=void 0;else{f=void 0,l=void 0;for(const E of u.splice(0))E.resolve(h)}},R=(s,o)=>{if(!(s<S))if(S=s,u.length===0)l={error:o},f=void 0;else{f=void 0,l=void 0;for(const h of u.splice(0))h.reject(o)}},k=s=>o=>{N(s,o)},c=s=>o=>{R(s,o)},b=()=>{if(i)return;g+=1;const s=g;T().then(k(s)).catch(c(s))};n.add(b);const m={[Symbol.asyncIterator](){return m},next:()=>{if(i)return Promise.resolve({done:!0,value:void 0});if(S===g){if(l!==void 0){const{error:s}=l;return l=void 0,Promise.reject(s)}if(f!==void 0){const s=f;return f=void 0,Promise.resolve(s)}}return S<g?new Promise((s,o)=>{u.push({reject:o,resolve:s})}):T().then(s=>{if(l!==void 0){const{error:o}=l;throw l=void 0,o}if(f!==void 0){const o=f;return f=void 0,o}return{done:!1,value:s}})},return:()=>{i=!0,n.delete(b);for(const s of u.splice(0))s.resolve({done:!0,value:void 0});return Promise.resolve({done:!0,value:void 0})}};return T().then(k(0)).catch(c(0)),m},ke=(e,r)=>{const{close:n,sql:a}=ve();let y=!1;const t=()=>{y||(y=!0,n())},i=new Map(Object.entries(r?.functions??{}).map(([v,I])=>[v,I])),u=new Set,f=()=>{for(const v of u)v()},l=v=>(f(),v);let g,S,T;const N=r?.now??Date.now(),R=be(),k=(v,I)=>{if(v===void 0)throw new U("INTERNAL",`[fake-scheduler] ${I} not yet available — scheduler.advance called before harness construction completed`);return v},{controls:c,scheduler:b}=pe(()=>k(g,"dispatch"),()=>k(S,"mutationContext"),()=>k(T,"actionContext"),()=>i,N),m=se(b),{createWriters:s,resetHeadroom:o,runInTransaction:h}=ie(e,{enforceRls:r?.enforceRls,scheduler:m,sql:a}),E=v=>{const I=ce(v),Q={getIdentity:()=>Promise.resolve(I.claims),userId:I.userId},{database:W,rawDatabase:Z}=s(I),_={auth:Q,db:W,env:r?.env,log:q,metrics:G,now:N,span:R.handle,trace:F,runQuery:((d,w)=>O("query",d,_,w)),secrets:A("secrets"),storage:A("storage"),vectors:A("vectors")},j={auth:Q,db:W,env:r?.env,log:q,metrics:G,now:N,span:R.handle,trace:F,runMutation:((d,w)=>O("mutation",d,j,w)),runQuery:((d,w)=>O("query",d,_,w)),scheduler:m,secrets:A("secrets"),storage:A("storage"),vectors:A("vectors"),workflows:A("workflows")};S??=j;const ee={...j,db:Z},D={auth:Q,db:W,env:r?.env,fetch:r?.fetch??A("fetch"),log:q,metrics:G,now:N,span:R.handle,trace:F,runAction:((d,w)=>O("action",d,D,w)),runMutation:((d,w)=>h(()=>O("mutation",d,j,w)).then(l)),runQuery:((d,w)=>O("query",d,_,w)),scheduler:m,secrets:A("secrets"),services:ye(r?.services),storage:A("storage"),vectors:A("vectors"),workflows:A("workflows")};T??=D;const O=(d,w,C,M)=>$(d,w,C,M,!0);g??=(d,w,C,M)=>d==="mutation"?h(()=>$("mutation",w,C,M,!0)).then(l):(o(),O("action",w,C,M));const te=((d,w)=>(o(),L(d)?$("query",d,_,w,!1):Promise.resolve(d(_)))),re=((d,w)=>{const C=L(d)?()=>$("mutation",d,j,w,!1):()=>d(j);return h(C).then(l)}),ne=((d,w)=>(o(),L(d)?$("action",d,D,w,!1):Promise.resolve(d(D)))),oe=Ee((...d)=>(o(),$(...d)),_,u);return{action:ne,close:t,mutation:re,query:te,run:d=>h(()=>d(ee)).then(l),scheduler:c,subscribe:oe,wideEvent:()=>R.recorded,withIdentity:d=>E(d)}};return E(null)};export{ke as lunoraTest};
|