@justscale/testing 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JustScale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @justscale/testing
2
+
3
+ Test utilities for JustScale: a `createTestKit()` harness that spawns one or more app instances with lifecycle auto-drain, plus DI mocks and spy helpers built on `node:test`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @justscale/testing
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { test } from 'node:test';
15
+ import { createTestKit } from '@justscale/testing';
16
+
17
+ test('user service', async (t) => {
18
+ const kit = createTestKit(t);
19
+
20
+ const app = await kit.spawn(builder =>
21
+ builder.register(UserRepository, myInMemoryRepo),
22
+ );
23
+
24
+ const result = await app.service(UserService).findAll();
25
+ assert.ok(result.length > 0);
26
+ });
27
+ ```
28
+
29
+ `createTestKit(t)` registers `afterEach` automatically and drains all lifecycle handles (timers, locks, channels) on teardown — no manual cleanup required.
30
+
31
+ ## API
32
+
33
+ - `createTestKit(t)` — primary harness.
34
+ - `kit.spawn(builderFn)` — single in-process app.
35
+ - `kit.spawnHttp(builderFn, opts?)` — app with real HTTP listener; returns typed client for controller endpoints.
36
+ - `kit.spawnCluster(n, builderFn)` — `n` coordinated instances for distributed-invariant tests.
37
+ - `TestContainer` — low-level DI container that accepts mocks in place of concrete services.
38
+ - `createTestApp` — compiles an app without opening real sockets; use `.get` / `.post` / etc.
39
+ - `mockService` / `spyOn` / `mockFn` / `mockResolves` — thin wrappers over `node:test` mocks with DI-aware typings.
40
+ - `InMemoryLockProvider` — lock primitive without Postgres, useful for unit tests.
41
+
42
+ ## Docs
43
+
44
+ https://justscale.sh/testing/overview
package/dist/app.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Test App
3
+ *
4
+ * Create a testable app instance with access to internals.
5
+ */
6
+ import type { App, ServiceToken, InstanceOf, ControllerDef } from '@justscale/core';
7
+ import { TestContainer } from './container.js';
8
+ /**
9
+ * Extended App interface for testing with access to container and helpers.
10
+ */
11
+ export interface TestApp extends App {
12
+ /** The test container - allows mocking services */
13
+ readonly testContainer: TestContainer;
14
+ /**
15
+ * Get typed access to a service from the container.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const userService = await app.service(UserService);
20
+ * ```
21
+ */
22
+ service<T extends ServiceToken>(token: T): Promise<InstanceOf<T>>;
23
+ /**
24
+ * Mock a service with a partial implementation.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * app.mock(UserRepository, {
29
+ * findById: mockFn().returns(Promise.resolve(null)),
30
+ * });
31
+ * ```
32
+ */
33
+ mock<T>(token: ServiceToken<T>, mockInstance: Partial<T>): TestApp;
34
+ /**
35
+ * Clear all mocks.
36
+ */
37
+ clearMocks(): TestApp;
38
+ }
39
+ /**
40
+ * Configuration for creating a test app.
41
+ */
42
+ export interface TestAppConfig {
43
+ services?: ServiceToken[];
44
+ controllers: ControllerDef<any>[];
45
+ }
46
+ /**
47
+ * Create a test app with mock support.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const app = createTestApp({
52
+ * services: [UserRepository, UserService],
53
+ * controllers: [UsersController],
54
+ * });
55
+ *
56
+ * // Mock a dependency
57
+ * app.mock(UserRepository, {
58
+ * findById: mockFn().returns(Promise.resolve({ id: '1' })),
59
+ * });
60
+ *
61
+ * // Access services
62
+ * const userService = app.service(UserService);
63
+ *
64
+ * // Use the app for testing
65
+ * const matched = app.match('GET', '/users/1');
66
+ * ```
67
+ */
68
+ export declare function createTestApp(config: TestAppConfig): Promise<TestApp>;
69
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAgB,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAElG,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,OAAQ,SAAQ,GAAG;IAClC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IAEtC;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAElE;;;;;;;;;OASG;IACH,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAEnE;;OAEG;IACH,UAAU,IAAI,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,WAAW,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA0F3E"}
package/dist/app.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Test App
3
+ *
4
+ * Create a testable app instance with access to internals.
5
+ */
6
+ import { executeSteps } from '@justscale/core';
7
+ import { TestContainer } from './container.js';
8
+ /**
9
+ * Create a test app with mock support.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const app = createTestApp({
14
+ * services: [UserRepository, UserService],
15
+ * controllers: [UsersController],
16
+ * });
17
+ *
18
+ * // Mock a dependency
19
+ * app.mock(UserRepository, {
20
+ * findById: mockFn().returns(Promise.resolve({ id: '1' })),
21
+ * });
22
+ *
23
+ * // Access services
24
+ * const userService = app.service(UserService);
25
+ *
26
+ * // Use the app for testing
27
+ * const matched = app.match('GET', '/users/1');
28
+ * ```
29
+ */
30
+ export async function createTestApp(config) {
31
+ const container = new TestContainer();
32
+ const controllers = [];
33
+ // Register all services
34
+ for (const service of config.services ?? []) {
35
+ if (typeof service === 'function') {
36
+ container.registerClass(service);
37
+ }
38
+ else {
39
+ container.register(service);
40
+ }
41
+ }
42
+ // Register and resolve controllers
43
+ for (const controllerDef of config.controllers) {
44
+ container.register(controllerDef);
45
+ const instance = await container.resolve(controllerDef);
46
+ controllers.push(instance);
47
+ }
48
+ function match(method, pathname) {
49
+ for (const controller of controllers) {
50
+ for (const route of controller.routes) {
51
+ if (route.method !== method)
52
+ continue;
53
+ const routeMatch = pathname.match(route.pattern);
54
+ if (!routeMatch)
55
+ continue;
56
+ const params = {};
57
+ route.paramNames.forEach((name, i) => {
58
+ params[name] = routeMatch[i + 1] ?? '';
59
+ });
60
+ return { route, deps: controller.deps, params };
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ async function execute(matched, contextAdditions) {
66
+ const { route, deps, params } = matched;
67
+ const ctx = {
68
+ deps,
69
+ params,
70
+ ...contextAdditions,
71
+ };
72
+ // Run steps (middleware + guards) via the canonical path that reads route.steps.
73
+ // The old route.middlewares/route.guards fields are always [] after compilation.
74
+ const passed = await executeSteps(route, ctx);
75
+ if (!passed) {
76
+ return undefined; // Guard denied
77
+ }
78
+ return route.handler(ctx);
79
+ }
80
+ const app = {
81
+ container,
82
+ controllers,
83
+ adapters: [],
84
+ subApps: [],
85
+ match,
86
+ execute,
87
+ ready: Promise.resolve(), // Test app is always ready immediately
88
+ get testContainer() {
89
+ return container;
90
+ },
91
+ service(token) {
92
+ return container.get(token);
93
+ },
94
+ mock(token, mockInstance) {
95
+ container.mock(token, mockInstance);
96
+ return app;
97
+ },
98
+ clearMocks() {
99
+ container.clearMocks();
100
+ return app;
101
+ },
102
+ };
103
+ return app;
104
+ }
105
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Test Client
3
+ *
4
+ * Provides a base test client for testing JustScale applications.
5
+ * Transport-specific functionality (HTTP, CLI, etc.) is added via explicit
6
+ * transport configuration.
7
+ */
8
+ import type { App, ServiceDef, Service, ControllerDef, ResponseEntry } from '@justscale/core';
9
+ /**
10
+ * Drain an app's background work without going through BuiltApp.stop().
11
+ * Tests use `JustScale().add(...).build().compile()` which returns the
12
+ * raw App (no stop method); we replicate the kernel's teardown order so
13
+ * the event loop drains and the test process can exit cleanly.
14
+ *
15
+ * Use this in tests that build an app directly (no TestClient) to avoid
16
+ * the "Promise resolution still pending" file-level timeout.
17
+ */
18
+ export declare function teardownApp(app: App): Promise<void>;
19
+ import type { Ref } from '@justscale/core/models';
20
+ /**
21
+ * Response from a test request.
22
+ * Transports extend this with their specific fields.
23
+ */
24
+ export interface TestResponse<T = unknown, TStatus extends number = number> {
25
+ /** HTTP status code */
26
+ status: TStatus;
27
+ /** Parsed response data */
28
+ data: T;
29
+ /** Whether the request was successful (2xx status) */
30
+ ok: TStatus extends 200 | 201 | 202 | 203 | 204 | 205 | 206 ? true : false;
31
+ }
32
+ type ServiceInstance<T> = T extends ServiceDef<infer S, any> ? S : T extends Service<infer S, any> ? S : never;
33
+ type AnyServiceDef = ServiceDef<any, any> | Service<any, any>;
34
+ type BuildServiceAPI<T extends Record<string, AnyServiceDef>> = {
35
+ [K in keyof T]: ServiceInstance<T[K]>;
36
+ };
37
+ type ExtractPathParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}` ? {
38
+ [K in Param]: string;
39
+ } & ExtractPathParams<`/${Rest}`> : Path extends `${string}:${infer Param}` ? {
40
+ [K in Param]: string;
41
+ } : {};
42
+ /**
43
+ * Resolve the model class for a given path param, supporting lowercase matching.
44
+ * `.types({ Campaign })` matches path param `:campaign` (lowercased key).
45
+ */
46
+ type ResolveParamModel<ParamName extends string, TParamTypes extends Record<string, abstract new (...args: any[]) => any>> = {
47
+ [K in keyof TParamTypes]: Lowercase<ParamName> extends Lowercase<K & string> ? TParamTypes[K] : never;
48
+ }[keyof TParamTypes];
49
+ /**
50
+ * The type a client may pass for a path param.
51
+ * If `.types({ Model })` declared the model for this param, accept a string OR
52
+ * any Ref<Model> (Reference / Persistent / Locked) - the transport extracts
53
+ * the identifier at runtime via refId().
54
+ */
55
+ type ClientPathParamValue<ParamName extends string, TParamTypes extends Record<string, abstract new (...args: any[]) => any>> = [
56
+ ResolveParamModel<ParamName, TParamTypes>
57
+ ] extends [never] ? string : string | Ref<InstanceType<Extract<ResolveParamModel<ParamName, TParamTypes>, abstract new (...args: any[]) => any>>>;
58
+ type ClientPathParams<Path extends string, TParamTypes extends Record<string, abstract new (...args: any[]) => any>> = {
59
+ [K in keyof ExtractPathParams<Path>]: ClientPathParamValue<K & string, TParamTypes>;
60
+ };
61
+ type JoinPath<TPrefix extends string, TPath extends string> = TPath extends '/' ? TPrefix extends '/' ? '/' : TPrefix : TPrefix extends '/' ? TPath : `${TPrefix}${TPath}`;
62
+ type Prettify<T> = {
63
+ [K in keyof T]: T[K];
64
+ } & {};
65
+ type IsEmptyObject<T> = keyof T extends never ? true : false;
66
+ type ResponseEntryToTestResponse<T> = T extends ResponseEntry<infer Status extends number, infer Body, any> ? TestResponse<Body, Status> : TestResponse<unknown>;
67
+ type HasKnownBody<T> = unknown extends T ? false : true;
68
+ type ExtractRouteReturns<T> = T extends {
69
+ _types?: {
70
+ returns: infer R;
71
+ };
72
+ } ? R : unknown;
73
+ type ExtractRouteBody<T> = T extends {
74
+ _types?: {
75
+ body: infer B;
76
+ };
77
+ } ? B : unknown;
78
+ type ExtractRouteParamTypes<T> = T extends {
79
+ _types?: {
80
+ paramTypes: infer P;
81
+ };
82
+ } ? P extends Record<string, abstract new (...args: any[]) => any> ? P : {} : {};
83
+ type TypedRouteMethodV2<TPath extends string, TReturns, TBody, TParamTypes extends Record<string, abstract new (...args: any[]) => any> = {}> = IsEmptyObject<ExtractPathParams<TPath>> extends true ? HasKnownBody<TBody> extends true ? (input: TBody) => Promise<ResponseEntryToTestResponse<TReturns>> : (input?: Record<string, unknown>) => Promise<ResponseEntryToTestResponse<TReturns>> : HasKnownBody<TBody> extends true ? (input: Prettify<ClientPathParams<TPath, TParamTypes> & TBody>) => Promise<ResponseEntryToTestResponse<TReturns>> : (input: Prettify<ClientPathParams<TPath, TParamTypes>> & Record<string, unknown>) => Promise<ResponseEntryToTestResponse<TReturns>>;
84
+ type RouteToMethod<TRoute, TPrefix extends string> = TRoute extends {
85
+ path: infer Path extends string;
86
+ steps: any[];
87
+ } ? TypedRouteMethodV2<JoinPath<TPrefix, Path>, ExtractRouteReturns<TRoute>, ExtractRouteBody<TRoute>, ExtractRouteParamTypes<TRoute>> : never;
88
+ type ExtractPrefix<TSettings> = TSettings extends {
89
+ prefix: infer P extends string;
90
+ } ? P : '/';
91
+ type ControllerMethods<TController> = TController extends ControllerDef<any, infer Routes, infer Settings> ? {
92
+ [K in keyof Routes]: RouteToMethod<Routes[K], ExtractPrefix<Settings>>;
93
+ } : never;
94
+ /**
95
+ * Build typed API from a controller mapping
96
+ */
97
+ export type BuildControllerAPI<T extends Record<string, ControllerDef<any, any, any>>> = {
98
+ [K in keyof T]: ControllerMethods<T[K]>;
99
+ };
100
+ /**
101
+ * Internal state passed to transports
102
+ */
103
+ export interface TransportState {
104
+ app: App<any>;
105
+ cleanupFns: (() => Promise<void> | void)[];
106
+ }
107
+ /**
108
+ * Base interface for a test transport.
109
+ * Transports implement this to provide their functionality.
110
+ */
111
+ export interface TestTransport<TTransportClient = unknown, TOptions = unknown> {
112
+ /** Called during client creation to set up the transport */
113
+ setup(state: TransportState, options?: TOptions): Promise<TTransportClient> | TTransportClient;
114
+ /** Called during cleanup */
115
+ cleanup?(transportClient: TTransportClient): Promise<void> | void;
116
+ }
117
+ /**
118
+ * Extract the client type from a transport
119
+ */
120
+ export type TransportClient<T> = T extends TestTransport<infer C, any> ? C : never;
121
+ /**
122
+ * Extract options type from a transport
123
+ */
124
+ export type TransportOptions<T> = T extends TestTransport<any, infer O> ? O : never;
125
+ /**
126
+ * Base test client interface.
127
+ */
128
+ export interface TestClient<_TApp extends App<any> = App<any>> {
129
+ /**
130
+ * Get typed access to services from the app.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const { game, playerRepo } = await client.services({
135
+ * game: GameService,
136
+ * playerRepo: PlayerRepository,
137
+ * });
138
+ * ```
139
+ */
140
+ services<T extends Record<string, AnyServiceDef>>(mapping: T): Promise<BuildServiceAPI<T>>;
141
+ /** Close the test client and clean up resources */
142
+ close(): Promise<void>;
143
+ }
144
+ /**
145
+ * Test client with transport clients attached.
146
+ * Each transport is accessible by its configured name.
147
+ */
148
+ export type TestClientWithTransports<TApp extends App<any>, TTransports extends Record<string, TestTransport<any, any>>> = TestClient<TApp> & {
149
+ [K in keyof TTransports]: TransportClient<TTransports[K]>;
150
+ };
151
+ /**
152
+ * Options for creating a test client.
153
+ */
154
+ export interface TestClientOptions<TTransports extends Record<string, TestTransport<any, any>> = {}> {
155
+ /** Named transports to use */
156
+ transports?: TTransports;
157
+ /** Options passed to each transport (keyed by transport name) */
158
+ transportOptions?: {
159
+ [K in keyof TTransports]?: TransportOptions<TTransports[K]>;
160
+ };
161
+ }
162
+ /**
163
+ * Create a test client for testing JustScale applications.
164
+ *
165
+ * @example Without transports (services only)
166
+ * ```typescript
167
+ * const client = await createTestClient(app);
168
+ * const { game } = await client.services({ game: GameService });
169
+ * ```
170
+ *
171
+ * @example With HTTP transport
172
+ * ```typescript
173
+ * import { httpTransport } from '@justscale/http/testing';
174
+ *
175
+ * const client = await createTestClient(app, {
176
+ * transports: { http: httpTransport },
177
+ * transportOptions: { http: { port: 0 } }
178
+ * });
179
+ *
180
+ * // Access typed controllers via HTTP
181
+ * const { api } = client.http.useControllers({ player: PlayersController });
182
+ * await api.player.list();
183
+ *
184
+ * // Raw HTTP calls
185
+ * await client.http.get('/health');
186
+ * ```
187
+ */
188
+ export declare function createTestClient<TApp extends App<any>, TTransports extends Record<string, TestTransport<any, any>> = {}>(app: TApp, options?: TestClientOptions<TTransports>): Promise<TestClientWithTransports<TApp, TTransports>>;
189
+ export {};
190
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Test Client
3
+ *
4
+ * Provides a base test client for testing JustScale applications.
5
+ * Transport-specific functionality (HTTP, CLI, etc.) is added via explicit
6
+ * transport configuration.
7
+ */
8
+ import { Lifecycle, AbstractLockProvider, AbstractChannelBackend } from '@justscale/core';
9
+ /**
10
+ * Drain an app's background work without going through BuiltApp.stop().
11
+ * Tests use `JustScale().add(...).build().compile()` which returns the
12
+ * raw App (no stop method); we replicate the kernel's teardown order so
13
+ * the event loop drains and the test process can exit cleanly.
14
+ *
15
+ * Use this in tests that build an app directly (no TestClient) to avoid
16
+ * the "Promise resolution still pending" file-level timeout.
17
+ */
18
+ export async function teardownApp(app) {
19
+ try {
20
+ const lifecycle = await app.container.resolve(Lifecycle);
21
+ if (lifecycle && typeof lifecycle.runHook === 'function') {
22
+ await lifecycle.runHook('stop');
23
+ }
24
+ }
25
+ catch {
26
+ // Lifecycle not registered — nothing to drain at this layer.
27
+ }
28
+ for (const token of [AbstractLockProvider, AbstractChannelBackend]) {
29
+ try {
30
+ const svc = await app.container.resolve(token);
31
+ if (svc && typeof svc.close === 'function')
32
+ await svc.close();
33
+ }
34
+ catch {
35
+ // Provider not registered — skip.
36
+ }
37
+ }
38
+ }
39
+ // ============================================================================
40
+ // Implementation
41
+ // ============================================================================
42
+ /**
43
+ * Create a test client for testing JustScale applications.
44
+ *
45
+ * @example Without transports (services only)
46
+ * ```typescript
47
+ * const client = await createTestClient(app);
48
+ * const { game } = await client.services({ game: GameService });
49
+ * ```
50
+ *
51
+ * @example With HTTP transport
52
+ * ```typescript
53
+ * import { httpTransport } from '@justscale/http/testing';
54
+ *
55
+ * const client = await createTestClient(app, {
56
+ * transports: { http: httpTransport },
57
+ * transportOptions: { http: { port: 0 } }
58
+ * });
59
+ *
60
+ * // Access typed controllers via HTTP
61
+ * const { api } = client.http.useControllers({ player: PlayersController });
62
+ * await api.player.list();
63
+ *
64
+ * // Raw HTTP calls
65
+ * await client.http.get('/health');
66
+ * ```
67
+ */
68
+ export async function createTestClient(app, options = {}) {
69
+ const state = {
70
+ app,
71
+ cleanupFns: [],
72
+ };
73
+ const transports = options.transports ?? {};
74
+ const transportOptions = options.transportOptions ?? {};
75
+ const transportClients = {};
76
+ // Set up each transport
77
+ for (const [name, transport] of Object.entries(transports)) {
78
+ const opts = transportOptions[name];
79
+ const transportClient = await transport.setup(state, opts);
80
+ transportClients[name] = transportClient;
81
+ // Register cleanup if transport has it
82
+ if (transport.cleanup) {
83
+ const cleanup = transport.cleanup.bind(transport, transportClient);
84
+ state.cleanupFns.push(cleanup);
85
+ }
86
+ }
87
+ const client = {
88
+ async services(mapping) {
89
+ const services = {};
90
+ for (const [name, serviceDef] of Object.entries(mapping)) {
91
+ services[name] = await app.container.resolve(serviceDef);
92
+ }
93
+ return services;
94
+ },
95
+ async close() {
96
+ // Run transport cleanups first (close listening sockets, etc.)
97
+ for (const cleanup of state.cleanupFns.reverse()) {
98
+ await cleanup();
99
+ }
100
+ // Then stop the app — runs lifecycle stop hooks, drains channels,
101
+ // releases lock-provider connections, stops durable processes.
102
+ // Without this, every test leaks an app's worth of background
103
+ // work; node:test's harness then waits for the event loop to
104
+ // drain and reports "Promise resolution still pending" at the
105
+ // file level.
106
+ // Drain the app's background work. compile() returns App (no stop)
107
+ // — we replicate the kernel teardown sequence inline:
108
+ // 1. lifecycle stop hooks (cancels durable-process timers, etc.)
109
+ // 2. close the lock provider (clears TTL setTimeouts)
110
+ // 3. close the channel backend (closes pubsub LISTEN connection)
111
+ // Without this, every test leaves an app's worth of timers and
112
+ // connections holding the event loop open; node:test waits up to
113
+ // its file-level timeout and reports "Promise resolution still
114
+ // pending" even though the assertions all passed.
115
+ await teardownApp(app);
116
+ },
117
+ // Spread transport clients
118
+ ...transportClients,
119
+ };
120
+ return client;
121
+ }
122
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Adapter conformance suite - cluster primitives.
3
+ *
4
+ * Every cluster-capable adapter (Postgres, Redis, future) must pass this
5
+ * suite to claim conformance. Shape follows the well-trod JDBC/ODBC
6
+ * pattern: one abstract spec, multiple concrete runners.
7
+ *
8
+ * ## How to use
9
+ *
10
+ * ```typescript
11
+ * import { describeClusterConformance } from '@justscale/testing/conformance';
12
+ * import { PostgresFeature, PostgresChannelFeature, PostgresLockFeature,
13
+ * PostgresProcessFeature } from '@justscale/postgres';
14
+ *
15
+ * describeClusterConformance('postgres (real docker pg)', {
16
+ * makeInstance: () => JustScale()
17
+ * .add(env)
18
+ * .add(PostgresFeature)
19
+ * .add(PostgresChannelFeature)
20
+ * .add(PostgresLockFeature)
21
+ * .add(PostgresProcessFeature),
22
+ * skipIfUnavailable: async () => !(await hasDockerPg()),
23
+ * });
24
+ * ```
25
+ *
26
+ * ## What it asserts
27
+ *
28
+ * 1. Channel pub/sub cross-instance - publish on A, subscribe on B, B receives within 500ms.
29
+ * 2. Channel isolation - channels keyed differently don't cross-talk.
30
+ * 3. Lock mutex - two instances racing for the same advisory lock, only one wins; release lets the other acquire.
31
+ * 4. Lock hand-off - kill lock-holder, another instance picks it up.
32
+ * 5. Process signal resume - process suspended at race(), signal fires, process wakes and advances pc.
33
+ * 6. Process cross-instance signal - process running on A, emit from B, A resumes.
34
+ * 7. Process advisory lock - spawn same process path on two instances, only one runs.
35
+ *
36
+ * Each scenario is scoped tight enough to pinpoint the failing primitive.
37
+ */
38
+ import type JustScale from '@justscale/core';
39
+ /** The builder shape returned by `JustScale()`. */
40
+ type JustScaleBuilder = ReturnType<typeof JustScale>;
41
+ export interface ClusterConformanceOptions {
42
+ /**
43
+ * Factory that produces a fresh JustScale builder wired to the adapter
44
+ * under test. Called per scenario (and per instance within scenarios
45
+ * that need multiple). Must be totally independent - no shared state
46
+ * between calls beyond the adapter's natural backend (e.g. the same
47
+ * Postgres database, the same Redis instance).
48
+ */
49
+ makeInstance: (instanceId: number) => JustScaleBuilder | Promise<JustScaleBuilder>;
50
+ /**
51
+ * Optional: skip the whole suite if the adapter isn't available in the
52
+ * current environment. For pg this means "docker compose up -d"; for
53
+ * redis similar. Return true to skip, false to run.
54
+ */
55
+ skipIfUnavailable?: () => boolean | Promise<boolean>;
56
+ /**
57
+ * Optional: called once before any scenario to prepare the backend
58
+ * (e.g., create the test database + run migrations). Runs outside any
59
+ * instance factory.
60
+ */
61
+ beforeAll?: () => Promise<void>;
62
+ /**
63
+ * Optional: called once after all scenarios finish.
64
+ */
65
+ afterAll?: () => Promise<void>;
66
+ }
67
+ /**
68
+ * Run the cluster conformance suite against an adapter.
69
+ *
70
+ * @param adapterName - Shows up in test output: "cluster-conformance/postgres"
71
+ * @param opts - How to spin up instances + setup/teardown hooks
72
+ */
73
+ export declare function describeClusterConformance(adapterName: string, opts: ClusterConformanceOptions): void;
74
+ export {};
75
+ //# sourceMappingURL=cluster-primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-primitives.d.ts","sourceRoot":"","sources":["../../src/conformance/cluster-primitives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAE7C,mDAAmD;AACnD,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAErD,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEnF;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAErD;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,yBAAyB,GAC9B,IAAI,CAON"}