@grest-ts/testkit 0.0.5 → 0.0.7

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.
Files changed (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +418 -413
  3. package/dist/src/runner/isolated-loader.mjs +91 -91
  4. package/dist/src/runner/worker-loader.mjs +49 -49
  5. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  6. package/package.json +12 -12
  7. package/src/GGBundleTest.ts +89 -89
  8. package/src/GGTest.ts +318 -318
  9. package/src/GGTestContext.ts +74 -74
  10. package/src/GGTestRunner.ts +308 -308
  11. package/src/GGTestRuntime.ts +265 -265
  12. package/src/GGTestRuntimeWorker.ts +159 -159
  13. package/src/GGTestSharedRef.ts +116 -116
  14. package/src/GGTestkitExtensionsDiscovery.ts +26 -26
  15. package/src/IGGLocalDiscoveryServer.ts +16 -16
  16. package/src/callOn/GGCallOnSelector.ts +61 -61
  17. package/src/callOn/GGContractClass.implement.ts +43 -43
  18. package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -134
  19. package/src/callOn/TestableIPC.ts +81 -81
  20. package/src/callOn/callOn.ts +224 -224
  21. package/src/callOn/registerOnCallHandler.ts +123 -123
  22. package/src/index-node.ts +64 -64
  23. package/src/mockable/GGMockable.ts +22 -22
  24. package/src/mockable/GGMockableCall.ts +45 -45
  25. package/src/mockable/GGMockableIPC.ts +20 -20
  26. package/src/mockable/GGMockableInterceptor.ts +44 -44
  27. package/src/mockable/GGMockableInterceptorsServer.ts +69 -69
  28. package/src/mockable/mockable.ts +71 -71
  29. package/src/runner/InlineRunner.ts +47 -47
  30. package/src/runner/IsolatedRunner.ts +179 -179
  31. package/src/runner/RuntimeRunner.ts +15 -15
  32. package/src/runner/WorkerRunner.ts +179 -179
  33. package/src/runner/isolated-loader.mjs +91 -91
  34. package/src/runner/worker-loader.mjs +49 -49
  35. package/src/testers/GGCallInterceptor.ts +224 -224
  36. package/src/testers/GGMockWith.ts +92 -92
  37. package/src/testers/GGSpyWith.ts +115 -115
  38. package/src/testers/GGTestAction.ts +332 -332
  39. package/src/testers/GGTestComponent.ts +16 -16
  40. package/src/testers/GGTestSelector.ts +223 -223
  41. package/src/testers/IGGTestInterceptor.ts +10 -10
  42. package/src/testers/IGGTestWith.ts +15 -15
  43. package/src/testers/RuntimeSelector.ts +151 -151
  44. package/src/utils/GGExpectations.ts +78 -78
  45. package/src/utils/GGTestError.ts +36 -36
  46. package/src/utils/captureStack.ts +53 -53
package/src/GGTest.ts CHANGED
@@ -1,318 +1,318 @@
1
- import {GG_TEST_RESOURCE, GG_TEST_RUNNER, GGTestRunner, TestResource} from "./GGTestRunner";
2
- import {RuntimeConstructor, RuntimeInput, StartResult, Selector} from "./testers/RuntimeSelector";
3
- import {GGTestMode, GGTestRuntime, GGTestRuntimeConfig} from "./GGTestRuntime";
4
- import {createStartResult} from "./testers/GGTestSelector";
5
- import {IGGTestWith} from "./testers/IGGTestWith";
6
-
7
- // -------------------------------------------------
8
- // GGTest - Static API for test setup
9
- // -------------------------------------------------
10
-
11
- /**
12
- * Make a result awaitable by adding a then() method.
13
- */
14
- type Awaitable<T> = T & PromiseLike<T>;
15
-
16
- /**
17
- * Static API for test setup operations.
18
- * Use GGTest.startWorker(), GGTest.with(), etc. in test files.
19
- *
20
- * @example
21
- * // Single runtime
22
- * const t = GGTest.startWorker(MainRuntime);
23
- * t.logs.cursor();
24
- *
25
- * // Multiple instances
26
- * const t = GGTest.startWorker([MainRuntime, MainRuntime]);
27
- * t[0].logs.cursor();
28
- *
29
- * // Named runtimes
30
- * const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
31
- * t.main.logs.cursor();
32
- * t.sub.config.update();
33
- */
34
- export class GGTest {
35
-
36
- // -------------------------------------------------
37
- // Private state for runtime management
38
- // -------------------------------------------------
39
-
40
- // When GG_COVERAGE_MODE is set, force INLINE mode for all runtimes
41
- // This is needed because v8 coverage doesn't capture worker_threads or child_process
42
- private static readonly forceCoverageInline = process.env.GG_COVERAGE_MODE === '1';
43
-
44
- // -------------------------------------------------
45
- // Public static methods
46
- // -------------------------------------------------
47
-
48
- /**
49
- * Get a resource wrapper for calling test operations.
50
- * Returns an object with methods defined by the resource's [GG_TEST_RESOURCE].
51
- *
52
- * @example
53
- * GGTest.with(MyConfig.mysql).clone();
54
- * GGTest.with(MyConfig.mysql).clone("seed.sql");
55
- */
56
- public static with<T extends TestResource>(resource: T): T[typeof GG_TEST_RESOURCE] {
57
- return resource[GG_TEST_RESOURCE];
58
- }
59
-
60
- /**
61
- * Start runtimes in inline mode.
62
- * Inline mode runs the service code in the same process as the test.
63
- *
64
- * @example
65
- * // Single runtime
66
- * const t = GGTest.startInline(MainRuntime);
67
- *
68
- * // Array of runtimes (multiple instances)
69
- * const t = GGTest.startInline([MainRuntime, MainRuntime]);
70
- *
71
- * // Object with named runtimes
72
- * const t = GGTest.startInline({main: MainRuntime, sub: SubRuntime});
73
- */
74
- public static startInline<const T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
75
- return this.startWithMode(input, GGTestMode.INLINE);
76
- }
77
-
78
- /**
79
- * Start runtimes in worker mode.
80
- * Worker mode runs the service code in a worker thread.
81
- *
82
- * @example
83
- * // Single runtime
84
- * const t = GGTest.startWorker(MainRuntime);
85
- *
86
- * // Array of runtimes (multiple instances)
87
- * const t = GGTest.startWorker([MainRuntime, MainRuntime]);
88
- *
89
- * // Object with named runtimes
90
- * const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
91
- */
92
- public static startWorker<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
93
- return this.startWithMode(input, GGTestMode.WORKER);
94
- }
95
-
96
- /**
97
- * Start runtimes in isolated mode.
98
- * Isolated mode runs the service code in a separate process.
99
- *
100
- * @example
101
- * // Single runtime
102
- * const t = GGTest.startIsolated(MainRuntime);
103
- *
104
- * // Array of runtimes (multiple instances)
105
- * const t = GGTest.startIsolated([MainRuntime, MainRuntime]);
106
- *
107
- * // Object with named runtimes
108
- * const t = GGTest.startIsolated({main: MainRuntime, sub: SubRuntime});
109
- */
110
- public static startIsolated<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
111
- return this.startWithMode(input, GGTestMode.ISOLATED);
112
- }
113
-
114
- /**
115
- * Wait for an async expectation to be satisfied.
116
- * Use this for standalone async expectations outside of action chains.
117
- *
118
- * Polls the expectation every 20ms until it's satisfied or timeout is reached.
119
- *
120
- * @param expectation - The expectation to wait for
121
- * @param timeout - Max time to wait in ms (default: 5000)
122
- *
123
- * @example
124
- * // Wait for a log after injecting an event
125
- * await UserEventsPublisher.inject.registered({...});
126
- * await GGTest.waitFor(t.logs.expect(/validation failed/i));
127
- *
128
- * @example
129
- * // Wait for a metric to be incremented
130
- * await GGTest.waitFor(t.main.metrics.expect(SomeMetric).inc({label: 'value'}));
131
- */
132
- public static async waitFor(expectation: IGGTestWith, timeout: number = 5000): Promise<void> {
133
- const interceptor = expectation.createInterceptor();
134
- const checkInterval = 20;
135
- const startTime = Date.now();
136
-
137
- await interceptor.register();
138
-
139
- try {
140
- await new Promise<void>((resolve, reject) => {
141
- const check = () => {
142
- const elapsed = Date.now() - startTime;
143
-
144
- if (interceptor.isCalled()) {
145
- resolve();
146
- return;
147
- }
148
-
149
- if (elapsed > timeout) {
150
- reject(new Error(
151
- `[GGTest.waitFor] Timeout after ${timeout}ms waiting for expectation`
152
- ));
153
- return;
154
- }
155
-
156
- setTimeout(check, checkInterval);
157
- };
158
-
159
- check();
160
- });
161
-
162
- await interceptor.validate();
163
- const error = interceptor.getMockValidationError();
164
- if (error) {
165
- throw error;
166
- }
167
- } finally {
168
- await interceptor.unregister();
169
- }
170
- }
171
-
172
- // -------------------------------------------------
173
- // Private helpers
174
- // -------------------------------------------------
175
-
176
- private static startWithMode<T extends RuntimeInput>(
177
- input: T,
178
- mode: GGTestMode
179
- ): Awaitable<StartResult<T>> {
180
- const test = GG_TEST_RUNNER.get();
181
- const constructors = this.extractConstructors(input);
182
- const runtimes = this.createRuntimes(test, constructors, {mode: this.getCoverageMode(mode)});
183
- const result = createStartResult(input, runtimes);
184
-
185
- if (test.started) {
186
- // We're inside a test block - start immediately
187
- // Cleanup is handled by the global afterEach registered in initInTestCleanup
188
- const startupPromise = this.startRuntimesInTestBlock(runtimes);
189
- return this.makeAwaitable(result, startupPromise);
190
- } else {
191
- // We're in describe block - normal flow, runtimes start in beforeAll
192
- // Return immediately-resolved thenable for consistency
193
- return this.makeAwaitable(result, Promise.resolve());
194
- }
195
- }
196
-
197
- /**
198
- * Extract runtime constructors from the input in the order they should be created.
199
- */
200
- private static extractConstructors(input: RuntimeInput): RuntimeConstructor[] {
201
- // Single runtime
202
- if ('NAME' in input) {
203
- return [input as RuntimeConstructor];
204
- }
205
-
206
- // Array of runtimes
207
- if (Array.isArray(input)) {
208
- return input;
209
- }
210
-
211
- // Object: { main: MainRuntime, sub: [SubRuntime, SubRuntime] }
212
- const constructors: RuntimeConstructor[] = [];
213
- for (const value of Object.values(input)) {
214
- if (Array.isArray(value)) {
215
- constructors.push(...value);
216
- } else {
217
- constructors.push(value);
218
- }
219
- }
220
- return constructors;
221
- }
222
-
223
- /**
224
- * Start runtimes immediately (for in-test usage).
225
- * Runtimes are automatically tracked by the runner for afterEach cleanup.
226
- */
227
- private static async startRuntimesInTestBlock(runtimes: GGTestRuntime[]): Promise<void> {
228
- for (const runtime of runtimes) {
229
- await runtime.start();
230
- }
231
- }
232
-
233
- /**
234
- * Makes a result thenable by adding a then() method.
235
- * This allows both sync and async usage patterns.
236
- *
237
- * IMPORTANT: The then() method removes itself after being called to prevent
238
- * infinite thenable resolution. When onfulfilled returns the result,
239
- * JavaScript would try to resolve it again if then() still existed.
240
- */
241
- private static makeAwaitable<T>(result: T, startupPromise: Promise<void>): Awaitable<T> {
242
- const awaitable = result as Awaitable<T>;
243
-
244
- awaitable.then = <TResult1 = T, TResult2 = never>(
245
- onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
246
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
247
- ): Promise<TResult1 | TResult2> => {
248
- // Remove then() to prevent infinite thenable resolution
249
- delete (awaitable as any).then;
250
-
251
- return startupPromise.then(
252
- () => onfulfilled ? onfulfilled(result) : result as unknown as TResult1,
253
- onrejected
254
- );
255
- };
256
- return awaitable;
257
- }
258
-
259
- private static getCoverageMode(requestedMode: GGTestMode): GGTestMode {
260
- if (this.forceCoverageInline && requestedMode !== GGTestMode.INLINE) {
261
- return GGTestMode.INLINE;
262
- }
263
- return requestedMode;
264
- }
265
-
266
- private static createRuntimes(
267
- test: GGTestRunner,
268
- services: RuntimeConstructor[],
269
- config: GGTestRuntimeConfig
270
- ): GGTestRuntime[] {
271
- const createdRuntimes: GGTestRuntime[] = []
272
-
273
- for (const service of services) {
274
- const runtimeConstructor = service as RuntimeConstructor;
275
-
276
- // Validate that runtime has NAME property
277
- if (!runtimeConstructor.NAME) {
278
- throw new Error(
279
- `Runtime '${service.name}' must define a static NAME property. ` +
280
- `Add 'public static readonly NAME = "yourname"' to the class.`
281
- );
282
- }
283
-
284
- // Validate that runtime has SOURCE_MODULE_URL (set by GGRuntime.cli())
285
- if (!runtimeConstructor.SOURCE_MODULE_URL) {
286
- throw new Error(
287
- `Runtime '${service.name}' has no source path. ` +
288
- `Make sure the runtime file calls: ${service.name}.cli(import.meta.url)`
289
- );
290
- }
291
-
292
- const className = service.name;
293
- const name = runtimeConstructor.NAME;
294
- const sourcePath = runtimeConstructor.SOURCE_MODULE_URL;
295
-
296
- const runtime = new GGTestRuntime(test, sourcePath, className, name, config);
297
- // Store factory for inline mode — avoids dynamic import which causes
298
- // duplicate module loading in Vite/vitest environments
299
- runtime.runtimeFactory = () => new (runtimeConstructor as any)();
300
- createdRuntimes.push(runtime)
301
- }
302
- return createdRuntimes;
303
- }
304
- }
305
-
306
- // -------------------------------------------------
307
- // Legacy exports for backwards compatibility
308
- // -------------------------------------------------
309
-
310
- /**
311
- * @deprecated Use StartResult instead
312
- */
313
- export type {RuntimeResult} from "./testers/RuntimeSelector";
314
-
315
- /**
316
- * @deprecated Use Awaitable<StartResult<T>> instead
317
- */
318
- export type AwaitableRuntimeResult<R extends RuntimeConstructor[]> = Awaitable<Selector<R>>;
1
+ import {GG_TEST_RESOURCE, GG_TEST_RUNNER, GGTestRunner, TestResource} from "./GGTestRunner";
2
+ import {RuntimeConstructor, RuntimeInput, StartResult, Selector} from "./testers/RuntimeSelector";
3
+ import {GGTestMode, GGTestRuntime, GGTestRuntimeConfig} from "./GGTestRuntime";
4
+ import {createStartResult} from "./testers/GGTestSelector";
5
+ import {IGGTestWith} from "./testers/IGGTestWith";
6
+
7
+ // -------------------------------------------------
8
+ // GGTest - Static API for test setup
9
+ // -------------------------------------------------
10
+
11
+ /**
12
+ * Make a result awaitable by adding a then() method.
13
+ */
14
+ type Awaitable<T> = T & PromiseLike<T>;
15
+
16
+ /**
17
+ * Static API for test setup operations.
18
+ * Use GGTest.startWorker(), GGTest.with(), etc. in test files.
19
+ *
20
+ * @example
21
+ * // Single runtime
22
+ * const t = GGTest.startWorker(MainRuntime);
23
+ * t.logs.cursor();
24
+ *
25
+ * // Multiple instances
26
+ * const t = GGTest.startWorker([MainRuntime, MainRuntime]);
27
+ * t[0].logs.cursor();
28
+ *
29
+ * // Named runtimes
30
+ * const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
31
+ * t.main.logs.cursor();
32
+ * t.sub.config.update();
33
+ */
34
+ export class GGTest {
35
+
36
+ // -------------------------------------------------
37
+ // Private state for runtime management
38
+ // -------------------------------------------------
39
+
40
+ // When GG_COVERAGE_MODE is set, force INLINE mode for all runtimes
41
+ // This is needed because v8 coverage doesn't capture worker_threads or child_process
42
+ private static readonly forceCoverageInline = process.env.GG_COVERAGE_MODE === '1';
43
+
44
+ // -------------------------------------------------
45
+ // Public static methods
46
+ // -------------------------------------------------
47
+
48
+ /**
49
+ * Get a resource wrapper for calling test operations.
50
+ * Returns an object with methods defined by the resource's [GG_TEST_RESOURCE].
51
+ *
52
+ * @example
53
+ * GGTest.with(MyConfig.mysql).clone();
54
+ * GGTest.with(MyConfig.mysql).clone("seed.sql");
55
+ */
56
+ public static with<T extends TestResource>(resource: T): T[typeof GG_TEST_RESOURCE] {
57
+ return resource[GG_TEST_RESOURCE];
58
+ }
59
+
60
+ /**
61
+ * Start runtimes in inline mode.
62
+ * Inline mode runs the service code in the same process as the test.
63
+ *
64
+ * @example
65
+ * // Single runtime
66
+ * const t = GGTest.startInline(MainRuntime);
67
+ *
68
+ * // Array of runtimes (multiple instances)
69
+ * const t = GGTest.startInline([MainRuntime, MainRuntime]);
70
+ *
71
+ * // Object with named runtimes
72
+ * const t = GGTest.startInline({main: MainRuntime, sub: SubRuntime});
73
+ */
74
+ public static startInline<const T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
75
+ return this.startWithMode(input, GGTestMode.INLINE);
76
+ }
77
+
78
+ /**
79
+ * Start runtimes in worker mode.
80
+ * Worker mode runs the service code in a worker thread.
81
+ *
82
+ * @example
83
+ * // Single runtime
84
+ * const t = GGTest.startWorker(MainRuntime);
85
+ *
86
+ * // Array of runtimes (multiple instances)
87
+ * const t = GGTest.startWorker([MainRuntime, MainRuntime]);
88
+ *
89
+ * // Object with named runtimes
90
+ * const t = GGTest.startWorker({main: MainRuntime, sub: SubRuntime});
91
+ */
92
+ public static startWorker<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
93
+ return this.startWithMode(input, GGTestMode.WORKER);
94
+ }
95
+
96
+ /**
97
+ * Start runtimes in isolated mode.
98
+ * Isolated mode runs the service code in a separate process.
99
+ *
100
+ * @example
101
+ * // Single runtime
102
+ * const t = GGTest.startIsolated(MainRuntime);
103
+ *
104
+ * // Array of runtimes (multiple instances)
105
+ * const t = GGTest.startIsolated([MainRuntime, MainRuntime]);
106
+ *
107
+ * // Object with named runtimes
108
+ * const t = GGTest.startIsolated({main: MainRuntime, sub: SubRuntime});
109
+ */
110
+ public static startIsolated<T extends RuntimeInput>(input: T): Awaitable<StartResult<T>> {
111
+ return this.startWithMode(input, GGTestMode.ISOLATED);
112
+ }
113
+
114
+ /**
115
+ * Wait for an async expectation to be satisfied.
116
+ * Use this for standalone async expectations outside of action chains.
117
+ *
118
+ * Polls the expectation every 20ms until it's satisfied or timeout is reached.
119
+ *
120
+ * @param expectation - The expectation to wait for
121
+ * @param timeout - Max time to wait in ms (default: 5000)
122
+ *
123
+ * @example
124
+ * // Wait for a log after injecting an event
125
+ * await UserEventsPublisher.inject.registered({...});
126
+ * await GGTest.waitFor(t.logs.expect(/validation failed/i));
127
+ *
128
+ * @example
129
+ * // Wait for a metric to be incremented
130
+ * await GGTest.waitFor(t.main.metrics.expect(SomeMetric).inc({label: 'value'}));
131
+ */
132
+ public static async waitFor(expectation: IGGTestWith, timeout: number = 5000): Promise<void> {
133
+ const interceptor = expectation.createInterceptor();
134
+ const checkInterval = 20;
135
+ const startTime = Date.now();
136
+
137
+ await interceptor.register();
138
+
139
+ try {
140
+ await new Promise<void>((resolve, reject) => {
141
+ const check = () => {
142
+ const elapsed = Date.now() - startTime;
143
+
144
+ if (interceptor.isCalled()) {
145
+ resolve();
146
+ return;
147
+ }
148
+
149
+ if (elapsed > timeout) {
150
+ reject(new Error(
151
+ `[GGTest.waitFor] Timeout after ${timeout}ms waiting for expectation`
152
+ ));
153
+ return;
154
+ }
155
+
156
+ setTimeout(check, checkInterval);
157
+ };
158
+
159
+ check();
160
+ });
161
+
162
+ await interceptor.validate();
163
+ const error = interceptor.getMockValidationError();
164
+ if (error) {
165
+ throw error;
166
+ }
167
+ } finally {
168
+ await interceptor.unregister();
169
+ }
170
+ }
171
+
172
+ // -------------------------------------------------
173
+ // Private helpers
174
+ // -------------------------------------------------
175
+
176
+ private static startWithMode<T extends RuntimeInput>(
177
+ input: T,
178
+ mode: GGTestMode
179
+ ): Awaitable<StartResult<T>> {
180
+ const test = GG_TEST_RUNNER.get();
181
+ const constructors = this.extractConstructors(input);
182
+ const runtimes = this.createRuntimes(test, constructors, {mode: this.getCoverageMode(mode)});
183
+ const result = createStartResult(input, runtimes);
184
+
185
+ if (test.started) {
186
+ // We're inside a test block - start immediately
187
+ // Cleanup is handled by the global afterEach registered in initInTestCleanup
188
+ const startupPromise = this.startRuntimesInTestBlock(runtimes);
189
+ return this.makeAwaitable(result, startupPromise);
190
+ } else {
191
+ // We're in describe block - normal flow, runtimes start in beforeAll
192
+ // Return immediately-resolved thenable for consistency
193
+ return this.makeAwaitable(result, Promise.resolve());
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Extract runtime constructors from the input in the order they should be created.
199
+ */
200
+ private static extractConstructors(input: RuntimeInput): RuntimeConstructor[] {
201
+ // Single runtime
202
+ if ('NAME' in input) {
203
+ return [input as RuntimeConstructor];
204
+ }
205
+
206
+ // Array of runtimes
207
+ if (Array.isArray(input)) {
208
+ return input;
209
+ }
210
+
211
+ // Object: { main: MainRuntime, sub: [SubRuntime, SubRuntime] }
212
+ const constructors: RuntimeConstructor[] = [];
213
+ for (const value of Object.values(input)) {
214
+ if (Array.isArray(value)) {
215
+ constructors.push(...value);
216
+ } else {
217
+ constructors.push(value);
218
+ }
219
+ }
220
+ return constructors;
221
+ }
222
+
223
+ /**
224
+ * Start runtimes immediately (for in-test usage).
225
+ * Runtimes are automatically tracked by the runner for afterEach cleanup.
226
+ */
227
+ private static async startRuntimesInTestBlock(runtimes: GGTestRuntime[]): Promise<void> {
228
+ for (const runtime of runtimes) {
229
+ await runtime.start();
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Makes a result thenable by adding a then() method.
235
+ * This allows both sync and async usage patterns.
236
+ *
237
+ * IMPORTANT: The then() method removes itself after being called to prevent
238
+ * infinite thenable resolution. When onfulfilled returns the result,
239
+ * JavaScript would try to resolve it again if then() still existed.
240
+ */
241
+ private static makeAwaitable<T>(result: T, startupPromise: Promise<void>): Awaitable<T> {
242
+ const awaitable = result as Awaitable<T>;
243
+
244
+ awaitable.then = <TResult1 = T, TResult2 = never>(
245
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
246
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
247
+ ): Promise<TResult1 | TResult2> => {
248
+ // Remove then() to prevent infinite thenable resolution
249
+ delete (awaitable as any).then;
250
+
251
+ return startupPromise.then(
252
+ () => onfulfilled ? onfulfilled(result) : result as unknown as TResult1,
253
+ onrejected
254
+ );
255
+ };
256
+ return awaitable;
257
+ }
258
+
259
+ private static getCoverageMode(requestedMode: GGTestMode): GGTestMode {
260
+ if (this.forceCoverageInline && requestedMode !== GGTestMode.INLINE) {
261
+ return GGTestMode.INLINE;
262
+ }
263
+ return requestedMode;
264
+ }
265
+
266
+ private static createRuntimes(
267
+ test: GGTestRunner,
268
+ services: RuntimeConstructor[],
269
+ config: GGTestRuntimeConfig
270
+ ): GGTestRuntime[] {
271
+ const createdRuntimes: GGTestRuntime[] = []
272
+
273
+ for (const service of services) {
274
+ const runtimeConstructor = service as RuntimeConstructor;
275
+
276
+ // Validate that runtime has NAME property
277
+ if (!runtimeConstructor.NAME) {
278
+ throw new Error(
279
+ `Runtime '${service.name}' must define a static NAME property. ` +
280
+ `Add 'public static readonly NAME = "yourname"' to the class.`
281
+ );
282
+ }
283
+
284
+ // Validate that runtime has SOURCE_MODULE_URL (set by GGRuntime.cli())
285
+ if (!runtimeConstructor.SOURCE_MODULE_URL) {
286
+ throw new Error(
287
+ `Runtime '${service.name}' has no source path. ` +
288
+ `Make sure the runtime file calls: ${service.name}.cli(import.meta.url)`
289
+ );
290
+ }
291
+
292
+ const className = service.name;
293
+ const name = runtimeConstructor.NAME;
294
+ const sourcePath = runtimeConstructor.SOURCE_MODULE_URL;
295
+
296
+ const runtime = new GGTestRuntime(test, sourcePath, className, name, config);
297
+ // Store factory for inline mode — avoids dynamic import which causes
298
+ // duplicate module loading in Vite/vitest environments
299
+ runtime.runtimeFactory = () => new (runtimeConstructor as any)();
300
+ createdRuntimes.push(runtime)
301
+ }
302
+ return createdRuntimes;
303
+ }
304
+ }
305
+
306
+ // -------------------------------------------------
307
+ // Legacy exports for backwards compatibility
308
+ // -------------------------------------------------
309
+
310
+ /**
311
+ * @deprecated Use StartResult instead
312
+ */
313
+ export type {RuntimeResult} from "./testers/RuntimeSelector";
314
+
315
+ /**
316
+ * @deprecated Use Awaitable<StartResult<T>> instead
317
+ */
318
+ export type AwaitableRuntimeResult<R extends RuntimeConstructor[]> = Awaitable<Selector<R>>;