@voltro/testing 0.80.0 → 0.82.0

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.
@@ -4551,7 +4551,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4551
4551
  SOFTWARE.
4552
4552
  ```
4553
4553
 
4554
- ## undici@8.10.0
4554
+ ## undici@8.11.2
4555
4555
 
4556
4556
  License: MIT
4557
4557
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AnonymousPolicy } from '@voltro/protocol';
1
2
  import { AppContext } from '@voltro/runtime';
2
3
  import { AuthStrategy } from '@voltro/protocol';
3
4
  import { changeDelete } from '@voltro/database';
@@ -27,6 +28,7 @@ import { RpcInterceptor } from '@voltro/protocol';
27
28
  import { RpcKind } from '@voltro/protocol';
28
29
  import { Schema } from 'effect';
29
30
  import { Scope } from 'effect';
31
+ import { ServiceApi } from '@voltro/protocol';
30
32
  import { Subject } from '@voltro/protocol';
31
33
  import { SubscribeContext } from '@voltro/runtime';
32
34
  import { SyncLogger } from '@voltro/logger';
@@ -223,6 +225,17 @@ export declare const fixtureRow: (table: TableLike, overrides?: Row) => Row;
223
225
  */
224
226
  export declare const frozenTime: (at: Date | number | string | MockClock) => Effect.Effect<MockClock, never, Scope.Scope>;
225
227
 
228
+ declare type HandlerFor<D> = D extends {
229
+ readonly input: infer I;
230
+ readonly output: infer O;
231
+ } ? (input: I extends {
232
+ readonly Type: infer T;
233
+ } ? T : never) => Promise<O extends {
234
+ readonly Type: infer T;
235
+ } ? T : never> | (O extends {
236
+ readonly Type: infer T;
237
+ } ? T : never) : never;
238
+
226
239
  /**
227
240
  * What `invoke` resolves to for a handler returning `Result`: an `Effect`'s
228
241
  * SUCCESS value, an awaited `Promise`, or the value itself.
@@ -287,6 +300,10 @@ export declare const invoke: <D extends Pick<ProcedureDescriptor, "input">, Resu
287
300
  /** Can we open a TCP connection to `host:port` within `timeoutMs`? */
288
301
  export declare const isTcpReachable: (target: ReachableTarget) => Promise<boolean>;
289
302
 
303
+ export declare const makeServiceApiMock: <P>(descriptors: Readonly<Record<string, MockDescriptor>>, handlers: ServiceApiMockHandlers<P>, options?: {
304
+ readonly name?: string;
305
+ }) => ServiceApiMock<P>;
306
+
290
307
  /**
291
308
  * A `SubscribeContext` typed as the real one.
292
309
  *
@@ -352,15 +369,30 @@ export declare interface MakeTestAppOptions {
352
369
  * itself the behaviour most worth testing, and the one `invoke` cannot reach.
353
370
  */
354
371
  readonly strategies?: ReadonlyArray<AuthStrategy>;
355
- /** Refuse an anonymous caller that carries no tenant (`Unauthenticated`).
356
- * Passed straight to `composeAuthStrategies`. */
357
- readonly anonymousTenantRequired?: boolean;
372
+ /** What an unauthenticated caller is — the app's `auth.anonymous`
373
+ * (`'refuse' | 'no-tenant' | 'header' | { tenantId }`), passed straight to
374
+ * `composeAuthStrategies`. Default: the `voltro dev` convenience — the
375
+ * `x-tenant` header's tenant, else `acme` — so a test sees what the dev
376
+ * server would; pass the app's own decision to see what serve would. */
377
+ readonly anonymous?: AnonymousPolicy;
358
378
  /** HTTP idempotency binding — the same `{ store, header, ttlMs }` the serve
359
379
  * path builds from `app.config.ts`'s `idempotency` field. */
360
380
  readonly idempotency?: RestServeBindings['idempotency'];
361
381
  /** The api's `publicApi:` config — the REST profile the projections speak,
362
382
  * the same object `app.config.ts` declares. Absent → `'rpc'`. */
363
383
  readonly publicApiProfile?: PublicApiProfileConfig;
384
+ /**
385
+ * The api's `security` settings that decide the caller's address — the same
386
+ * fields `app.config.ts` declares. `ctx.remoteAddr` is resolved from a
387
+ * request's `remoteAddr` and headers through them, by the function the
388
+ * server's listener calls, so a forwarding header is believed here exactly
389
+ * when it would be in production. `VOLTRO_TRUSTED_PROXIES` applies as it does
390
+ * there. Absent → no proxy is trusted.
391
+ */
392
+ readonly security?: {
393
+ readonly trustedProxies?: ReadonlyArray<string>;
394
+ readonly clientAddressHeader?: string;
395
+ };
364
396
  }
365
397
 
366
398
  export declare const makeTestContext: <RowFilterCtx = unknown>(options?: MakeTestContextOptions<RowFilterCtx>) => TestContext;
@@ -369,6 +401,13 @@ export declare interface MakeTestContextOptions<RowFilterCtx = unknown> {
369
401
  /** The acting subject. Default: anonymous, no tenant. Read it back at
370
402
  * `ctx.request.subject` (the same place a live handler reads it). */
371
403
  readonly subject?: Subject;
404
+ /**
405
+ * `ctx.apis` — the apps this handler calls. Pass a fake per binding, usually
406
+ * `makeServiceApiMock<ServiceProcedures>({ … })` from `@voltro/testing`, which
407
+ * decodes every input and output against the callee's real schemas. Absent →
408
+ * the empty object, as for an app with no `apis:`.
409
+ */
410
+ readonly apis?: AppContext['apis'];
372
411
  /** Optional connection fixture, or the actual RuntimeContext.connection from
373
412
  * a listener test. Shared by transaction/subject re-scopes, never invented by
374
413
  * the harness; omitting it models an internal invocation. */
@@ -517,7 +556,7 @@ export declare interface MakeWorkflowRunnerOptions {
517
556
  readonly workflows?: ReadonlyArray<WorkflowEntry>;
518
557
  }
519
558
 
520
- export declare const markRestInvocation: (ctx: TestContext, publicApi: PublicApiSpec) => void;
559
+ export declare const markRestInvocation: (ctx: TestContext, publicApi: PublicApiSpec, remoteAddr?: string) => void;
521
560
 
522
561
  /** Minimal AI mock surface. The full `mockAi({...})` helper is owned by
523
562
  * the AI package (plan 05); typed here as an optional injected interface
@@ -560,6 +599,12 @@ export declare class MockClock {
560
599
  uninstall(): void;
561
600
  }
562
601
 
602
+ declare interface MockDescriptor {
603
+ readonly kind: string;
604
+ readonly input: unknown;
605
+ readonly output: unknown;
606
+ }
607
+
563
608
  export declare class MockLLM {
564
609
  private readonly queue;
565
610
  readonly calls: MockLLMCall[];
@@ -600,10 +645,16 @@ export declare class MockWebhooks {
600
645
  * testable. */
601
646
  emit<P>(event: string | {
602
647
  readonly id?: string;
648
+ readonly name?: string;
603
649
  readonly event?: string;
604
650
  }, payload: P): Promise<{
605
651
  readonly delivered: number;
606
652
  }>;
653
+ /** Mirrors `WebhooksServiceShape.listTargets`. No targets are subscribed
654
+ * here, so a handler that reads them — a status page, a health check — gets
655
+ * the empty list the real service gives an app that has none, instead of
656
+ * throwing because the recorder lacked the method. */
657
+ listTargets(_event?: string, _scope?: Readonly<Record<string, unknown>>): Promise<ReadonlyArray<never>>;
607
658
  /** Every payload emitted for one event, oldest first. */
608
659
  payloadsFor(event: string): ReadonlyArray<unknown>;
609
660
  /** The most recent emission of an event, or undefined. */
@@ -702,9 +753,19 @@ export declare const reportEngineVersion: (label: string, probe: () => Promise<s
702
753
  */
703
754
  export declare const resetAfterCommit: (ctx: TestContext) => void;
704
755
 
705
- export declare const restInvocationOf: (ctx: TestContext) => {
756
+ /** A context that is running ONE `publicApi`-projected request — `invoke`
757
+ * reads it to put `surface: 'rest'` and the spec on the interceptor meta,
758
+ * exactly as the serve pipeline does for a `/v1` call. Set by `makeTestApp`
759
+ * on the per-request scoped context; never on the one a test holds. */
760
+ export declare interface RestInvocation {
706
761
  readonly publicApi: PublicApiSpec;
707
- } | undefined;
762
+ /** The request's resolved address — the serve pipeline hands it to the
763
+ * interceptors of a projected call (`projectedRequest`), so a per-address
764
+ * limit reads it there and must read it here. */
765
+ readonly remoteAddr?: string | undefined;
766
+ }
767
+
768
+ export declare const restInvocationOf: (ctx: TestContext) => RestInvocation | undefined;
708
769
 
709
770
  export { Row }
710
771
 
@@ -752,6 +813,27 @@ export declare const runInStoreTransaction: <T>(ctx: TestContext, work: (txCtx:
752
813
  * `storeForTenant` view). NOT the cross-tenant `system()` below. */
753
814
  export declare const serviceAccount: (id: string, options?: TestSubjectOptions) => Subject;
754
815
 
816
+ export declare interface ServiceApiMock<P> {
817
+ /** Pass this as `makeTestContext({ apis: { <name>: mock.api } })`. */
818
+ readonly api: ServiceApi<P>;
819
+ /** Every call made, in order — optionally for one tag. */
820
+ readonly calls: (tag?: keyof P & string) => ReadonlyArray<ServiceApiMockCall>;
821
+ /** Push a new value to every open subscription of a query. */
822
+ readonly emit: (tag: keyof P & string, input: unknown, value: unknown) => void;
823
+ }
824
+
825
+ export declare interface ServiceApiMockCall {
826
+ readonly tag: string;
827
+ readonly input: unknown;
828
+ readonly idempotencyKey: string | undefined;
829
+ }
830
+
831
+ /** Per tag: what the fake callee answers. A tag with no handler answers
832
+ * `ApiUnavailable` — the honest outcome of calling an app that is not there. */
833
+ export declare type ServiceApiMockHandlers<P> = {
834
+ readonly [K in keyof P]?: HandlerFor<P[K]>;
835
+ };
836
+
755
837
  /**
756
838
  * The handler service layer for this context — every registered plugin's
757
839
  * `services:` layer, then the app's own `layers:`, merged in that order.
@@ -774,6 +856,45 @@ export declare const serviceAccount: (id: string, options?: TestSubjectOptions)
774
856
  */
775
857
  export declare const serviceLayerFor: (ctx: TestContext) => Layer.Layer<never, never, never> | undefined;
776
858
 
859
+ export declare interface StartedTestApp {
860
+ readonly name: string;
861
+ readonly port: number;
862
+ /** `http://127.0.0.1:<port>` — the app's HTTP origin. */
863
+ readonly url: string;
864
+ /** `ws://127.0.0.1:<port>/ws` — the socket a service client connects to. */
865
+ readonly wsUrl: string;
866
+ /** Everything the process printed so far. */
867
+ readonly log: () => string;
868
+ readonly stop: () => Promise<void>;
869
+ }
870
+
871
+ export declare interface StartedTestApps<Name extends string> {
872
+ readonly apps: Readonly<Record<Name, StartedTestApp>>;
873
+ /** The credential of an edge — what a test presents to call the callee AS the caller. */
874
+ readonly token: (caller: Name, callee: Name) => string;
875
+ /** Stops every app; safe to call twice. */
876
+ readonly stop: () => Promise<void>;
877
+ }
878
+
879
+ /**
880
+ * Boot the apps — every callee before its callers — wait until each answers
881
+ * `/internal/readiness`, and return their ports, logs and edge credentials. A
882
+ * process that exits or does not become ready in time stops every other one
883
+ * and rejects with its log.
884
+ */
885
+ export declare const startTestApps: <Name extends string>(options: StartTestAppsOptions<Name>) => Promise<StartedTestApps<Name>>;
886
+
887
+ export declare interface StartTestAppsOptions<Name extends string> {
888
+ readonly apps: Readonly<Record<Name, TestAppSpec>>;
889
+ readonly edges?: ReadonlyArray<TestAppEdge<Name>>;
890
+ /** `dev` (codegen, migrations, minted secrets — the default) or `serve`. */
891
+ readonly mode?: 'dev' | 'serve';
892
+ /** Path to `voltro.mjs`. Default: `@voltro/cli` as the first app resolves it. */
893
+ readonly cli?: string;
894
+ /** How long EACH app may take to answer `/internal/readiness`. Default 5 minutes. */
895
+ readonly bootTimeoutMs?: number;
896
+ }
897
+
777
898
  /**
778
899
  * The CROSS-TENANT machine actor — cron, workflows, backfills. `tenantId` is
779
900
  * `null` by construction and the tenant read-scope mixin reads that as "every
@@ -811,6 +932,22 @@ export declare interface TestApp {
811
932
  readonly paths: ReadonlyArray<string>;
812
933
  }
813
934
 
935
+ export declare interface TestAppEdge<Name extends string> {
936
+ /** The calling app (its `name`). */
937
+ readonly caller: Name;
938
+ /** The called app (its `name`). */
939
+ readonly callee: Name;
940
+ /** The caller's `apis:` key for the callee. */
941
+ readonly binding: string;
942
+ }
943
+
944
+ export declare interface TestAppSpec {
945
+ /** The app's directory (the one holding `app.config.ts`). */
946
+ readonly root: string;
947
+ /** Extra environment for this app — its `DB_URL`, feature flags, … */
948
+ readonly env?: Readonly<Record<string, string>>;
949
+ }
950
+
814
951
  /** The test context IS an `AppContext` (store + request + cache), plus the
815
952
  * deterministic doubles and the subject/tenant re-scopers. Because it
816
953
  * extends `AppContext`, you can pass it straight into an executor typed
@@ -939,6 +1076,14 @@ export declare interface TestRequestOptions {
939
1076
  * `withHeaders`) headers. Names are lowercased, as the transport delivers
940
1077
  * them. */
941
1078
  readonly headers?: Readonly<Record<string, string>>;
1079
+ /**
1080
+ * The address the connection comes from — the socket peer. With no trusted
1081
+ * proxy declared (the default) it is `ctx.remoteAddr`; behind a declared
1082
+ * proxy, `x-forwarded-for` from that peer decides, as on the server.
1083
+ * Defaults to `127.0.0.1`. Two values are two clients, which is how a route
1084
+ * test shows a per-address limit.
1085
+ */
1086
+ readonly remoteAddr?: string;
942
1087
  }
943
1088
 
944
1089
  export declare interface TestResponse {