@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.
- package/CHANGELOG.md +1610 -6
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/index.d.ts +151 -6
- package/dist/index.js +358 -186
- package/dist/platform.d.ts +14 -0
- package/dist/platform.js +46 -0
- package/package.json +13 -8
package/THIRD-PARTY-NOTICES.md
CHANGED
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
|
-
/**
|
|
356
|
-
*
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|