@grest-ts/testkit 0.0.5

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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/src/GGBundleTest.d.ts +8 -0
  4. package/dist/src/GGBundleTest.d.ts.map +1 -0
  5. package/dist/src/GGBundleTest.js +75 -0
  6. package/dist/src/GGBundleTest.js.map +1 -0
  7. package/dist/src/GGTest.d.ts +131 -0
  8. package/dist/src/GGTest.d.ts.map +1 -0
  9. package/dist/src/GGTest.js +245 -0
  10. package/dist/src/GGTest.js.map +1 -0
  11. package/dist/src/GGTestContext.d.ts +36 -0
  12. package/dist/src/GGTestContext.d.ts.map +1 -0
  13. package/dist/src/GGTestContext.js +63 -0
  14. package/dist/src/GGTestContext.js.map +1 -0
  15. package/dist/src/GGTestRunner.d.ts +108 -0
  16. package/dist/src/GGTestRunner.d.ts.map +1 -0
  17. package/dist/src/GGTestRunner.js +242 -0
  18. package/dist/src/GGTestRunner.js.map +1 -0
  19. package/dist/src/GGTestRuntime.d.ts +103 -0
  20. package/dist/src/GGTestRuntime.d.ts.map +1 -0
  21. package/dist/src/GGTestRuntime.js +219 -0
  22. package/dist/src/GGTestRuntime.js.map +1 -0
  23. package/dist/src/GGTestRuntimeWorker.d.ts +41 -0
  24. package/dist/src/GGTestRuntimeWorker.d.ts.map +1 -0
  25. package/dist/src/GGTestRuntimeWorker.js +136 -0
  26. package/dist/src/GGTestRuntimeWorker.js.map +1 -0
  27. package/dist/src/GGTestSharedRef.d.ts +35 -0
  28. package/dist/src/GGTestSharedRef.d.ts.map +1 -0
  29. package/dist/src/GGTestSharedRef.js +126 -0
  30. package/dist/src/GGTestSharedRef.js.map +1 -0
  31. package/dist/src/GGTestkitExtensionsDiscovery.d.ts +21 -0
  32. package/dist/src/GGTestkitExtensionsDiscovery.d.ts.map +1 -0
  33. package/dist/src/GGTestkitExtensionsDiscovery.js +24 -0
  34. package/dist/src/GGTestkitExtensionsDiscovery.js.map +1 -0
  35. package/dist/src/IGGLocalDiscoveryServer.d.ts +16 -0
  36. package/dist/src/IGGLocalDiscoveryServer.d.ts.map +1 -0
  37. package/dist/src/IGGLocalDiscoveryServer.js +2 -0
  38. package/dist/src/IGGLocalDiscoveryServer.js.map +1 -0
  39. package/dist/src/callOn/GGCallOnSelector.d.ts +42 -0
  40. package/dist/src/callOn/GGCallOnSelector.d.ts.map +1 -0
  41. package/dist/src/callOn/GGCallOnSelector.js +35 -0
  42. package/dist/src/callOn/GGCallOnSelector.js.map +1 -0
  43. package/dist/src/callOn/GGContractClass.implement.d.ts +8 -0
  44. package/dist/src/callOn/GGContractClass.implement.d.ts.map +1 -0
  45. package/dist/src/callOn/GGContractClass.implement.js +31 -0
  46. package/dist/src/callOn/GGContractClass.implement.js.map +1 -0
  47. package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts +28 -0
  48. package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts.map +1 -0
  49. package/dist/src/callOn/GGTestActionForLocatorOnCall.js +118 -0
  50. package/dist/src/callOn/GGTestActionForLocatorOnCall.js.map +1 -0
  51. package/dist/src/callOn/TestableIPC.d.ts +72 -0
  52. package/dist/src/callOn/TestableIPC.d.ts.map +1 -0
  53. package/dist/src/callOn/TestableIPC.js +34 -0
  54. package/dist/src/callOn/TestableIPC.js.map +1 -0
  55. package/dist/src/callOn/callOn.d.ts +113 -0
  56. package/dist/src/callOn/callOn.d.ts.map +1 -0
  57. package/dist/src/callOn/callOn.js +122 -0
  58. package/dist/src/callOn/callOn.js.map +1 -0
  59. package/dist/src/callOn/registerOnCallHandler.d.ts +13 -0
  60. package/dist/src/callOn/registerOnCallHandler.d.ts.map +1 -0
  61. package/dist/src/callOn/registerOnCallHandler.js +111 -0
  62. package/dist/src/callOn/registerOnCallHandler.js.map +1 -0
  63. package/dist/src/index-node.d.ts +35 -0
  64. package/dist/src/index-node.d.ts.map +1 -0
  65. package/dist/src/index-node.js +50 -0
  66. package/dist/src/index-node.js.map +1 -0
  67. package/dist/src/mockable/GGMockable.d.ts +19 -0
  68. package/dist/src/mockable/GGMockable.d.ts.map +1 -0
  69. package/dist/src/mockable/GGMockable.js +2 -0
  70. package/dist/src/mockable/GGMockable.js.map +1 -0
  71. package/dist/src/mockable/GGMockableCall.d.ts +2 -0
  72. package/dist/src/mockable/GGMockableCall.d.ts.map +1 -0
  73. package/dist/src/mockable/GGMockableCall.js +41 -0
  74. package/dist/src/mockable/GGMockableCall.js.map +1 -0
  75. package/dist/src/mockable/GGMockableIPC.d.ts +17 -0
  76. package/dist/src/mockable/GGMockableIPC.d.ts.map +1 -0
  77. package/dist/src/mockable/GGMockableIPC.js +8 -0
  78. package/dist/src/mockable/GGMockableIPC.js.map +1 -0
  79. package/dist/src/mockable/GGMockableInterceptor.d.ts +24 -0
  80. package/dist/src/mockable/GGMockableInterceptor.d.ts.map +1 -0
  81. package/dist/src/mockable/GGMockableInterceptor.js +32 -0
  82. package/dist/src/mockable/GGMockableInterceptor.js.map +1 -0
  83. package/dist/src/mockable/GGMockableInterceptorsServer.d.ts +12 -0
  84. package/dist/src/mockable/GGMockableInterceptorsServer.d.ts.map +1 -0
  85. package/dist/src/mockable/GGMockableInterceptorsServer.js +55 -0
  86. package/dist/src/mockable/GGMockableInterceptorsServer.js.map +1 -0
  87. package/dist/src/mockable/mockable.d.ts +46 -0
  88. package/dist/src/mockable/mockable.d.ts.map +1 -0
  89. package/dist/src/mockable/mockable.js +47 -0
  90. package/dist/src/mockable/mockable.js.map +1 -0
  91. package/dist/src/runner/InlineRunner.d.ts +12 -0
  92. package/dist/src/runner/InlineRunner.d.ts.map +1 -0
  93. package/dist/src/runner/InlineRunner.js +42 -0
  94. package/dist/src/runner/InlineRunner.js.map +1 -0
  95. package/dist/src/runner/IsolatedRunner.d.ts +17 -0
  96. package/dist/src/runner/IsolatedRunner.d.ts.map +1 -0
  97. package/dist/src/runner/IsolatedRunner.js +155 -0
  98. package/dist/src/runner/IsolatedRunner.js.map +1 -0
  99. package/dist/src/runner/RuntimeRunner.d.ts +14 -0
  100. package/dist/src/runner/RuntimeRunner.d.ts.map +1 -0
  101. package/dist/src/runner/RuntimeRunner.js +2 -0
  102. package/dist/src/runner/RuntimeRunner.js.map +1 -0
  103. package/dist/src/runner/WorkerRunner.d.ts +17 -0
  104. package/dist/src/runner/WorkerRunner.d.ts.map +1 -0
  105. package/dist/src/runner/WorkerRunner.js +155 -0
  106. package/dist/src/runner/WorkerRunner.js.map +1 -0
  107. package/dist/src/runner/isolated-loader.mjs +91 -0
  108. package/dist/src/runner/worker-loader.mjs +49 -0
  109. package/dist/src/testers/GGCallInterceptor.d.ts +71 -0
  110. package/dist/src/testers/GGCallInterceptor.d.ts.map +1 -0
  111. package/dist/src/testers/GGCallInterceptor.js +170 -0
  112. package/dist/src/testers/GGCallInterceptor.js.map +1 -0
  113. package/dist/src/testers/GGMockWith.d.ts +30 -0
  114. package/dist/src/testers/GGMockWith.d.ts.map +1 -0
  115. package/dist/src/testers/GGMockWith.js +70 -0
  116. package/dist/src/testers/GGMockWith.js.map +1 -0
  117. package/dist/src/testers/GGSpyWith.d.ts +40 -0
  118. package/dist/src/testers/GGSpyWith.d.ts.map +1 -0
  119. package/dist/src/testers/GGSpyWith.js +90 -0
  120. package/dist/src/testers/GGSpyWith.js.map +1 -0
  121. package/dist/src/testers/GGTestAction.d.ts +126 -0
  122. package/dist/src/testers/GGTestAction.d.ts.map +1 -0
  123. package/dist/src/testers/GGTestAction.js +245 -0
  124. package/dist/src/testers/GGTestAction.js.map +1 -0
  125. package/dist/src/testers/GGTestComponent.d.ts +15 -0
  126. package/dist/src/testers/GGTestComponent.d.ts.map +1 -0
  127. package/dist/src/testers/GGTestComponent.js +2 -0
  128. package/dist/src/testers/GGTestComponent.js.map +1 -0
  129. package/dist/src/testers/GGTestSelector.d.ts +54 -0
  130. package/dist/src/testers/GGTestSelector.d.ts.map +1 -0
  131. package/dist/src/testers/GGTestSelector.js +179 -0
  132. package/dist/src/testers/GGTestSelector.js.map +1 -0
  133. package/dist/src/testers/IGGTestInterceptor.d.ts +8 -0
  134. package/dist/src/testers/IGGTestInterceptor.d.ts.map +1 -0
  135. package/dist/src/testers/IGGTestInterceptor.js +2 -0
  136. package/dist/src/testers/IGGTestInterceptor.js.map +1 -0
  137. package/dist/src/testers/IGGTestWith.d.ts +13 -0
  138. package/dist/src/testers/IGGTestWith.d.ts.map +1 -0
  139. package/dist/src/testers/IGGTestWith.js +2 -0
  140. package/dist/src/testers/IGGTestWith.js.map +1 -0
  141. package/dist/src/testers/RuntimeSelector.d.ts +117 -0
  142. package/dist/src/testers/RuntimeSelector.d.ts.map +1 -0
  143. package/dist/src/testers/RuntimeSelector.js +2 -0
  144. package/dist/src/testers/RuntimeSelector.js.map +1 -0
  145. package/dist/src/tsconfig.json +17 -0
  146. package/dist/src/utils/GGExpectations.d.ts +18 -0
  147. package/dist/src/utils/GGExpectations.d.ts.map +1 -0
  148. package/dist/src/utils/GGExpectations.js +59 -0
  149. package/dist/src/utils/GGExpectations.js.map +1 -0
  150. package/dist/src/utils/GGTestError.d.ts +13 -0
  151. package/dist/src/utils/GGTestError.d.ts.map +1 -0
  152. package/dist/src/utils/GGTestError.js +26 -0
  153. package/dist/src/utils/GGTestError.js.map +1 -0
  154. package/dist/src/utils/captureStack.d.ts +9 -0
  155. package/dist/src/utils/captureStack.d.ts.map +1 -0
  156. package/dist/src/utils/captureStack.js +51 -0
  157. package/dist/src/utils/captureStack.js.map +1 -0
  158. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  159. package/package.json +66 -0
  160. package/src/GGBundleTest.ts +89 -0
  161. package/src/GGTest.ts +318 -0
  162. package/src/GGTestContext.ts +74 -0
  163. package/src/GGTestRunner.ts +308 -0
  164. package/src/GGTestRuntime.ts +265 -0
  165. package/src/GGTestRuntimeWorker.ts +159 -0
  166. package/src/GGTestSharedRef.ts +116 -0
  167. package/src/GGTestkitExtensionsDiscovery.ts +26 -0
  168. package/src/IGGLocalDiscoveryServer.ts +16 -0
  169. package/src/callOn/GGCallOnSelector.ts +61 -0
  170. package/src/callOn/GGContractClass.implement.ts +43 -0
  171. package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -0
  172. package/src/callOn/TestableIPC.ts +81 -0
  173. package/src/callOn/callOn.ts +224 -0
  174. package/src/callOn/registerOnCallHandler.ts +123 -0
  175. package/src/index-node.ts +64 -0
  176. package/src/mockable/GGMockable.ts +22 -0
  177. package/src/mockable/GGMockableCall.ts +45 -0
  178. package/src/mockable/GGMockableIPC.ts +20 -0
  179. package/src/mockable/GGMockableInterceptor.ts +44 -0
  180. package/src/mockable/GGMockableInterceptorsServer.ts +69 -0
  181. package/src/mockable/mockable.ts +71 -0
  182. package/src/runner/InlineRunner.ts +47 -0
  183. package/src/runner/IsolatedRunner.ts +179 -0
  184. package/src/runner/RuntimeRunner.ts +15 -0
  185. package/src/runner/WorkerRunner.ts +179 -0
  186. package/src/runner/isolated-loader.mjs +91 -0
  187. package/src/runner/worker-loader.mjs +49 -0
  188. package/src/testers/GGCallInterceptor.ts +224 -0
  189. package/src/testers/GGMockWith.ts +92 -0
  190. package/src/testers/GGSpyWith.ts +115 -0
  191. package/src/testers/GGTestAction.ts +333 -0
  192. package/src/testers/GGTestComponent.ts +16 -0
  193. package/src/testers/GGTestSelector.ts +223 -0
  194. package/src/testers/IGGTestInterceptor.ts +11 -0
  195. package/src/testers/IGGTestWith.ts +15 -0
  196. package/src/testers/RuntimeSelector.ts +151 -0
  197. package/src/tsconfig.json +17 -0
  198. package/src/utils/GGExpectations.ts +78 -0
  199. package/src/utils/GGTestError.ts +37 -0
  200. package/src/utils/captureStack.ts +54 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Unified callOn function for invoking methods on runtime instances.
3
+ *
4
+ * Works with:
5
+ * - @testable classes: Direct service invocation via IPC
6
+ * - GGLocatorKey: Direct instance lookup via IPC
7
+ * - GGContractClass: Contract implementation lookup via IPC
8
+ * - Contract holders (HTTP APIs, etc.): Delegated to their own factory
9
+ *
10
+ * @example
11
+ * // Call @testable service
12
+ * await callOn(MyService).doSomething("hello")
13
+ *
14
+ * // Call HTTP API (goes through HTTP transport)
15
+ * await callOn(ChainApi).quickWeatherCheck({city: "NYC"})
16
+ *
17
+ * // Call contract directly (skips HTTP, uses IPC)
18
+ * await callOn(ChainApiContract).quickWeatherCheck({city: "NYC"})
19
+ *
20
+ * // With context
21
+ * await callOn(MyService, ctx).doSomething("hello")
22
+ */
23
+
24
+ import {GGLocatorKey} from "@grest-ts/locator";
25
+ import {GGTestActionForLocatorOnCall} from "./GGTestActionForLocatorOnCall";
26
+ import {LOCATOR_KEY_PREFIX_FOR_TESTABLE} from "@grest-ts/testkit-runtime";
27
+ import {GGContractApiDefinition, GGContractClass, GGContractMethod} from "@grest-ts/schema";
28
+ import {GGContext} from "@grest-ts/context";
29
+ import {LOCATOR_KEY_PREFIX_FOR_CONTRACT} from "./GGContractClass.implement";
30
+ import type {GGTestRuntime} from "../GGTestRuntime";
31
+
32
+ // ============================================================================
33
+ // Factory symbol for protocol-specific callOn support
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Symbol used by contract holders (HTTP APIs, WebSocket APIs, etc.) to provide
38
+ * their own callOn proxy. This allows protocols to control how calls are made
39
+ * without callOn needing to know about specific protocols.
40
+ *
41
+ * The factory can return anything - there's no constraint on the return type.
42
+ * Each protocol fully controls what methods and types are available.
43
+ */
44
+ export const CALL_ON_FACTORY = Symbol.for("gg:callOnFactory");
45
+
46
+ /**
47
+ * Marker interface for targets that provide their own callOn proxy.
48
+ * The factory can return any type - protocols have full control.
49
+ */
50
+ export interface GGCallOnFactory {
51
+ [CALL_ON_FACTORY](ctx?: GGContext): unknown;
52
+ }
53
+
54
+ // ============================================================================
55
+ // Type definitions
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Maps a class's async methods to GGLocatorLookupTestAction calls.
60
+ * Used for @testable classes and GGLocatorKey lookups.
61
+ */
62
+ export type LocatorLookupAccess<T> = {
63
+ [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
64
+ ? (...args: A) => GGTestActionForLocatorOnCall<R>
65
+ : never
66
+ };
67
+
68
+ /**
69
+ * Maps contract methods to GGLocatorLookupTestAction calls.
70
+ * Used for direct GGContractClass lookups via IPC.
71
+ */
72
+ export type ContractLocatorAccess<TContract> = {
73
+ [K in keyof TContract]: TContract[K] extends GGContractMethod<infer Input, infer Output>
74
+ ? Input extends undefined ? () => GGTestActionForLocatorOnCall<Output> : (data: Input) => GGTestActionForLocatorOnCall<Output>
75
+ : never
76
+ };
77
+
78
+ export type GGTestCallOnCollection<T> = { [K in keyof T]: GGTestCallOn<T[K]> }
79
+
80
+ /**
81
+ * Resolves the return type for callOn(target):
82
+ * - Factory targets → extract factory's return type (protocol controls everything)
83
+ * - GGContractClass → ContractLocatorAccess (IPC)
84
+ * - Constructor/GGLocatorKey → LocatorLookupAccess (IPC)
85
+ * - Plain objects → recurse into properties
86
+ */
87
+ export type GGTestCallOn<T> =
88
+ T extends { [CALL_ON_FACTORY]: (ctx?: GGContext) => infer R } ? R
89
+ : T extends GGContractClass<infer TContract> ? ContractLocatorAccess<TContract>
90
+ : T extends GGLocatorKey<infer Instance> ? LocatorLookupAccess<Instance>
91
+ : T extends new (...args: any[]) => infer Instance ? LocatorLookupAccess<Instance>
92
+ : T extends object ? { [K in keyof T]: GGTestCallOn<T[K]> }
93
+ : never;
94
+
95
+ // ============================================================================
96
+ // The callOn function
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Check if target implements the CALL_ON_FACTORY interface.
101
+ */
102
+ function hasCallOnFactory(target: unknown): target is GGCallOnFactory {
103
+ return target != null && typeof (target as any)[CALL_ON_FACTORY] === 'function';
104
+ }
105
+
106
+ /**
107
+ * Check if target is a simple class (constructor function).
108
+ */
109
+ function isSimpleClass(value: unknown): value is new (...args: any[]) => any {
110
+ return typeof value === 'function' && value.prototype !== undefined && value.prototype.constructor === value;
111
+ }
112
+
113
+ /**
114
+ * Invoke methods on runtime instances via a unified interface.
115
+ *
116
+ * Resolution order:
117
+ * 1. Factory symbol (HTTP APIs, WebSocket APIs provide their own)
118
+ * 2. GGContractClass → IPC lookup via @contract: prefix
119
+ * 3. GGLocatorKey → IPC lookup via key name
120
+ * 4. Simple class → IPC lookup via @testable: prefix
121
+ *
122
+ * @example
123
+ * await callOn(MyService, ctx).doSomething("hello")
124
+ * await callOn(ChainApi, ctx).quickWeatherCheck({city: "NYC"})
125
+ * await callOn(ChainApiContract, ctx).quickWeatherCheck({city: "NYC"})
126
+ */
127
+ export function callOn<T extends GGCallOnFactory>(target: T, ctx?: GGContext): GGTestCallOn<T>;
128
+ export function callOn<T extends GGContractApiDefinition>(target: GGContractClass<T>, ctx?: GGContext): ContractLocatorAccess<T>;
129
+ export function callOn<T>(target: GGLocatorKey<T>, ctx?: GGContext): LocatorLookupAccess<T>;
130
+ export function callOn<T extends new (...args: any[]) => any>(target: T, ctx?: GGContext): LocatorLookupAccess<InstanceType<T>>;
131
+ export function callOn<T extends object>(target: T, ctx?: GGContext): GGTestCallOn<T>;
132
+ export function callOn(target: any, ctx?: GGContext): any {
133
+ ctx ??= new GGContext("Test")
134
+
135
+ if (hasCallOnFactory(target)) {
136
+ return target[CALL_ON_FACTORY](ctx);
137
+ }
138
+
139
+ const keyName = resolveKeyName(target);
140
+ return createCallOnProxy(keyName, ctx);
141
+ }
142
+
143
+ // ============================================================================
144
+ // Helper functions for key resolution
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Resolve a target to its locator key name.
149
+ * Handles GGContractClass, GGLocatorKey, and simple classes.
150
+ */
151
+ function resolveKeyName(target: any): string {
152
+ if (target instanceof GGContractClass) {
153
+ return LOCATOR_KEY_PREFIX_FOR_CONTRACT + target.name;
154
+ }
155
+ if (target instanceof GGLocatorKey) {
156
+ return target.name;
157
+ }
158
+ if (isSimpleClass(target)) {
159
+ return LOCATOR_KEY_PREFIX_FOR_TESTABLE + target.name;
160
+ }
161
+ throw new Error(`Unknown callOn target: ${target?.name ?? target?.constructor?.name ?? typeof target}`);
162
+ }
163
+
164
+ /**
165
+ * Create a proxy that intercepts method calls and returns GGTestActionForLocatorOnCall.
166
+ */
167
+ function createCallOnProxy(keyName: string, ctx: GGContext, targetRuntimes?: GGTestRuntime[]): any {
168
+ return new Proxy({}, {
169
+ get(_, methodName: string) {
170
+ return (...args: any[]) => new GGTestActionForLocatorOnCall<any>(ctx, keyName, methodName, args, targetRuntimes);
171
+ }
172
+ });
173
+ }
174
+
175
+ // ============================================================================
176
+ // Targeted callOn - for explicit runtime selection
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Invoke methods on runtime instances, targeting specific runtimes.
181
+ * Use this when you need to explicitly select which runtime to call
182
+ * when multiple different runtime classes have the same service.
183
+ *
184
+ * @param target - The target to call (class, GGLocatorKey, or GGContractClass)
185
+ * @param runtimes - The specific runtimes to target
186
+ * @param ctx - Optional context for the call
187
+ *
188
+ * @example
189
+ * const f = GGTest.startWorker({chain: ChainRuntime, weather: WeatherOnlyRuntime});
190
+ * // Both have WeatherService - explicitly target one:
191
+ * await f.chain.callOn(WeatherService).getWeather("Test")
192
+ */
193
+ export function callOnTargeted<T>(target: T, runtimes: GGTestRuntime[], ctx?: GGContext): GGTestCallOn<T>;
194
+ export function callOnTargeted(target: any, runtimes: GGTestRuntime[], ctx?: GGContext): any {
195
+ ctx ??= new GGContext("Test");
196
+
197
+ // Factory targets don't support runtime targeting - they control their own routing
198
+ if (hasCallOnFactory(target)) {
199
+ return target[CALL_ON_FACTORY](ctx);
200
+ }
201
+
202
+ const keyName = resolveKeyName(target);
203
+ return createCallOnProxy(keyName, ctx, runtimes);
204
+ }
205
+
206
+ /**
207
+ * Process a collection of targets and create callOn proxies for each.
208
+ * It can be a recursive object where all "testable things" are replaced with callOn(X) handlers.
209
+ * {serviceA: ApiA, sub: { serviceB: ApiB}} -> {serviceA: callOn(ApiA), sub: { serviceB: callOn(ApiB)}}
210
+ */
211
+ export function callOnCollection<T extends object | any[]>(target: T, ctx?: GGContext): GGTestCallOnCollection<T> {
212
+ const result: any = Array.isArray(target) ? [] : {};
213
+ for (const key in target) {
214
+ const value = target[key];
215
+ if (value != null) {
216
+ if (hasCallOnFactory(value) || value instanceof GGContractClass || value instanceof GGLocatorKey || isSimpleClass(value)) {
217
+ result[key] = callOn(value as any, ctx);
218
+ } else if (typeof value === 'object') {
219
+ result[key] = callOnCollection(value, ctx);
220
+ }
221
+ }
222
+ }
223
+ return result;
224
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Worker-side handler for GGLocator-based service invocation.
3
+ *
4
+ * Receives IPC requests from tests to invoke methods on registered instances
5
+ * (@testable, @contract, etc.) and returns the results.
6
+ */
7
+
8
+ import {GGLocator, GGLocatorKey} from "@grest-ts/locator";
9
+ import {GGLog} from "@grest-ts/logger";
10
+ import {SerializedContext, TestableIPC, TestableInvokePayload, TestableInvokeResult} from "./TestableIPC";
11
+ import {GGTestRuntimeWorker} from "../GGTestRuntimeWorker";
12
+ import {GGContext} from "@grest-ts/context";
13
+
14
+ /**
15
+ * Context for logging
16
+ */
17
+ const LOG_CONTEXT = {name: "GGLocatorWorkerHandler"};
18
+
19
+ /**
20
+ * Deserialize context data into a GGContext.
21
+ * Directly populates the internal values map.
22
+ */
23
+ function deserializeContext(data: SerializedContext): GGContext {
24
+ const ctx = new GGContext("ipc-context");
25
+ // Access internal values map and populate directly
26
+ const ctxAny = ctx as any;
27
+ for (const [keyName, value] of Object.entries(data)) {
28
+ ctxAny.values.set(keyName, value);
29
+ }
30
+ return ctx;
31
+ }
32
+
33
+ /**
34
+ * Register the locator lookup handler on the worker.
35
+ * Called during worker initialization.
36
+ */
37
+ export function registerOnCallHandler(worker: GGTestRuntimeWorker): void {
38
+ worker.onIpcRequest(TestableIPC.invoke, async (payload: TestableInvokePayload): Promise<TestableInvokeResult> => {
39
+ const {keyName, methodName, args, context} = payload;
40
+
41
+ GGLog.debug(LOG_CONTEXT, `Invoking ${keyName}.${methodName}`, {args, hasContext: !!context});
42
+
43
+ try {
44
+ // Look up the instance in the current scope
45
+ const scope = GGLocator.tryGetScope();
46
+ if (!scope) {
47
+ return {
48
+ success: false,
49
+ error: `No GGLocator scope available - is the runtime running?`
50
+ };
51
+ }
52
+
53
+ const key = new GGLocatorKey<any>(keyName);
54
+ const instance = scope.tryGet(key);
55
+
56
+ if (!instance) {
57
+ return {
58
+ success: false,
59
+ error: `Instance '${keyName}' not found in GGLocator. ` +
60
+ `Make sure the class is decorated with @testable or registered during compose().`
61
+ };
62
+ }
63
+
64
+ // Check if method exists
65
+ const method = instance[methodName];
66
+ if (typeof method !== 'function') {
67
+ return {
68
+ success: false,
69
+ error: `Method '${methodName}' not found on instance '${keyName}'. ` +
70
+ `Available methods: ${getMethodNames(instance).join(', ')}`
71
+ };
72
+ }
73
+
74
+ // Invoke the method, optionally within the provided context
75
+ let result: any;
76
+ if (context) {
77
+ const ctx = deserializeContext(context);
78
+ result = await ctx.run(() => method.apply(instance, args));
79
+ } else {
80
+ result = await method.apply(instance, args);
81
+ }
82
+
83
+ GGLog.debug(LOG_CONTEXT, `Invocation ${keyName}.${methodName} completed`, {result});
84
+
85
+ return {
86
+ success: true,
87
+ result
88
+ };
89
+ } catch (error: any) {
90
+ GGLog.error(LOG_CONTEXT, `Error invoking ${keyName}.${methodName}`, error);
91
+
92
+ return {
93
+ success: false,
94
+ error: error.message || String(error),
95
+ stack: error.stack
96
+ };
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Get method names from an instance for error messages.
103
+ */
104
+ function getMethodNames(instance: any): string[] {
105
+ const names: string[] = [];
106
+ let proto = Object.getPrototypeOf(instance);
107
+
108
+ while (proto && proto !== Object.prototype) {
109
+ const propNames = Object.getOwnPropertyNames(proto)
110
+ .filter(name => {
111
+ if (name === 'constructor') return false;
112
+ try {
113
+ return typeof proto[name] === 'function';
114
+ } catch {
115
+ return false;
116
+ }
117
+ });
118
+ names.push(...propNames);
119
+ proto = Object.getPrototypeOf(proto);
120
+ }
121
+
122
+ return [...new Set(names)].sort();
123
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @grest-ts/testkit - Component testing library
3
+ */
4
+
5
+ // Core test framework
6
+ import path from "path";
7
+ import {fileURLToPath} from "url";
8
+ import {WorkerRunner} from "./runner/WorkerRunner";
9
+ import {IsolatedRunner} from "./runner/IsolatedRunner";
10
+
11
+ export * from './GGTest'
12
+ export * from './GGTestRunner'
13
+ export * from './IGGLocalDiscoveryServer'
14
+ export * from './GGTestRuntime'
15
+ export * from './testers/GGTestComponent'
16
+ export * from './mockable/GGMockable'
17
+ export * from './mockable/GGMockableCall'
18
+ export * from './mockable/mockable'
19
+ export * from './GGTestContext'
20
+
21
+ // Mockable component server (import triggers factory registration)
22
+ export * from './mockable/GGMockableInterceptorsServer'
23
+
24
+ // Testable - direct service invocation from tests
25
+ export * from './callOn/callOn'
26
+ export * from './callOn/GGTestActionForLocatorOnCall'
27
+ export * from './callOn/GGCallOnSelector'
28
+
29
+ // Contract registration - patches GGContractClass.implement() to auto-register
30
+ import './callOn/GGContractClass.implement'
31
+
32
+ // Control channel for runtime config updates
33
+ export * from './GGTestRuntimeWorker'
34
+
35
+ // Test utilities - core infrastructure
36
+ export * from './testers/IGGTestWith'
37
+ export * from './testers/GGTestAction'
38
+ export * from './testers/GGMockWith'
39
+ export * from './testers/GGSpyWith'
40
+ export * from './testers/GGCallInterceptor'
41
+ export * from './utils/GGExpectations'
42
+ export * from './utils/GGTestError'
43
+ export {captureStackSourceFile} from './utils/captureStack'
44
+ export * from './GGTestSharedRef'
45
+
46
+ // Production bundle DCE verification
47
+ export * from './GGBundleTest'
48
+
49
+ // Mockable interceptor
50
+ export * from './mockable/GGMockableInterceptor'
51
+
52
+ // Selector system for runtime access
53
+ export * from './testers/GGTestSelector'
54
+ export * from './testers/RuntimeSelector'
55
+
56
+ // Worker runner (path configured by @grest-ts/testkit-vitest)
57
+ export * from './runner/WorkerRunner'
58
+
59
+ export type * from "./testers/IGGTestWith";
60
+ export type * from "./testers/IGGTestInterceptor";
61
+
62
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
63
+ WorkerRunner.setWorkerLoaderPath(path.join(__dirname, 'runner', 'worker-loader.mjs'));
64
+ IsolatedRunner.setIsolatedLoaderPath(path.join(__dirname, 'runner', 'isolated-loader.mjs'));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Marker interface for external API services that can be mocked in tests.
3
+ * When a class implements this interface, the test framework can intercept
4
+ * all method calls and provide mock responses.
5
+ *
6
+ * Examples: REST APIs, GraphQL clients, third-party SDKs
7
+ */
8
+ export interface GGMockableExternalApi {
9
+ // Marker interface - no methods required
10
+ }
11
+
12
+ /**
13
+ * Marker interface for database/storage services that can be mocked in tests.
14
+ * When a class implements this interface, the test framework can intercept
15
+ * all method calls and provide mock responses.
16
+ *
17
+ * Examples: Database clients, cache services, file storage
18
+ */
19
+ export interface GGMockableDatabaseApi {
20
+ // Marker interface - no methods required
21
+ }
22
+
@@ -0,0 +1,45 @@
1
+ import {GG_TEST_RUNTIME_WORKER} from "../GGTestRuntimeWorker";
2
+ import {CALL_THROUGH} from "./GGMockableInterceptorsServer";
3
+ import {GGMockableIPC} from "./GGMockableIPC";
4
+
5
+ const MOCKABLE_WRAPPED = Symbol('GGMockableWrapped');
6
+
7
+ export function GGMockableCall(cls: any, methodName: string, nameMapping: string[]): void {
8
+ // Skip if already wrapped (happens in INLINE mode with multiple runtime instances)
9
+ if (cls.prototype[methodName]?.[MOCKABLE_WRAPPED]) {
10
+ return;
11
+ }
12
+
13
+ const originalMethod = cls.prototype[methodName];
14
+ const wrappedMethod = async function (this: any, ...inputArgs: any[]) {
15
+ const worker = GG_TEST_RUNTIME_WORKER.get();
16
+
17
+ const args: any = {}
18
+ for (let i = 0; i < inputArgs.length; i++) {
19
+ if (!nameMapping[i]) break;
20
+ args[nameMapping[i]] = inputArgs[i];
21
+ }
22
+
23
+ const result = await worker.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.call, {
24
+ className: cls.name,
25
+ methodName: methodName,
26
+ callArgs: args
27
+ });
28
+
29
+ if (result === CALL_THROUGH) {
30
+ const realResult = await originalMethod.apply(this, inputArgs);
31
+ await worker.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.spyResult, {
32
+ className: cls.name,
33
+ methodName: methodName,
34
+ callResult: realResult
35
+ });
36
+ return realResult;
37
+ } else {
38
+ return result;
39
+ }
40
+ };
41
+
42
+ // Mark as wrapped and assign to prototype
43
+ (wrappedMethod as any)[MOCKABLE_WRAPPED] = true;
44
+ cls.prototype[methodName] = wrappedMethod;
45
+ }
@@ -0,0 +1,20 @@
1
+ import {IPCServer} from "@grest-ts/ipc";
2
+
3
+ export interface MockableCallPayload {
4
+ className: string;
5
+ methodName: string;
6
+ callArgs: any;
7
+ }
8
+
9
+ export interface MockableSpyResultPayload {
10
+ className: string;
11
+ methodName: string;
12
+ callResult: any;
13
+ }
14
+
15
+ export const GGMockableIPC = {
16
+ testServer: {
17
+ call: IPCServer.defineRequest<MockableCallPayload, any>("mockable/call"),
18
+ spyResult: IPCServer.defineRequest<MockableSpyResultPayload, any>("mockable/spy-result"),
19
+ }
20
+ }
@@ -0,0 +1,44 @@
1
+ import type {GGTestRunner} from "../GGTestRunner";
2
+ import {GGCallInterceptor, GGCallInterceptorConfig} from "../testers/GGCallInterceptor";
3
+ import {GGMockableInterceptorsServer} from "./GGMockableInterceptorsServer";
4
+
5
+ export interface MockableInterceptorConfig extends GGCallInterceptorConfig {
6
+ className: string;
7
+ methodName: string;
8
+ }
9
+
10
+ /**
11
+ * Interceptor for mocking/spying on mockable class methods.
12
+ * Registers with GGMockableInterceptorsServer which handles the IPC communication.
13
+ *
14
+ * For spy mode, the flow is two-phase:
15
+ * 1. mockable/call → onRequest() validates input, returns undefined (CALL_THROUGH)
16
+ * 2. mockable/spy-result → onResponse() validates output
17
+ */
18
+ export class GGMockableInterceptor extends GGCallInterceptor {
19
+
20
+ public readonly className: string;
21
+ public readonly methodName: string;
22
+
23
+ constructor(test: GGTestRunner, config: MockableInterceptorConfig) {
24
+ super(test, config);
25
+ this.className = config.className;
26
+ this.methodName = config.methodName;
27
+ }
28
+
29
+ public getKey(): string {
30
+ return `${this.className}.${this.methodName}`;
31
+ }
32
+
33
+ protected doRegister(): void {
34
+ this.test.getExtensionInstance(GGMockableInterceptorsServer).addInterceptor(this);
35
+ }
36
+
37
+ protected doUnregister(): void {
38
+ this.test.getExtensionInstance(GGMockableInterceptorsServer).deleteInterceptor(this);
39
+ }
40
+
41
+ protected parseResponseData(result: any): any {
42
+ return result;
43
+ }
44
+ }
@@ -0,0 +1,69 @@
1
+ import type {GGMockableInterceptor} from "./GGMockableInterceptor";
2
+ import {GGTestComponent} from "../testers/GGTestComponent";
3
+ import {GGTestRunner} from "../GGTestRunner";
4
+ import {GGMockableIPC} from "./GGMockableIPC";
5
+
6
+ export const CALL_THROUGH = "__spyCallThrough|migo0am5g0htea";
7
+
8
+ export class GGMockableInterceptorsServer implements GGTestComponent {
9
+
10
+ private readonly interceptors: Map<string, GGMockableInterceptor> = new Map();
11
+
12
+ constructor(runner: GGTestRunner) {
13
+ const server = runner.ipcServer;
14
+ server.onFrameworkMessage(GGMockableIPC.testServer.call, async (body) => {
15
+ const key = body.className + "." + body.methodName;
16
+ const handler = this.interceptors.get(key);
17
+
18
+ if (!handler) {
19
+ // No mock configured - call through to real implementation
20
+ // This allows testable() to invoke real methods on @mockable services
21
+ // throw new Error(
22
+ // `Expected handler to be set for mockable '${key}'!\n` +
23
+ // "Did you forget to call .with(...)?"
24
+ // );
25
+ return CALL_THROUGH;
26
+ }
27
+
28
+ // onRequest validates input and returns mock data (or undefined for spy)
29
+ const result = await handler.onRequest(body.callArgs);
30
+
31
+ if (handler.passThrough) {
32
+ // Spy mode - signal worker to call through
33
+ return CALL_THROUGH;
34
+ } else {
35
+ // Mock mode - return the mock data
36
+ return result;
37
+ }
38
+ });
39
+
40
+ server.onFrameworkMessage(GGMockableIPC.testServer.spyResult, async (body) => {
41
+ const key = body.className + "." + body.methodName;
42
+ const handler = this.interceptors.get(key);
43
+
44
+ if (!handler || !handler.passThrough) {
45
+ // No spy handler configured - just ignore the result
46
+ // This allows testable() to call through without requiring spyOn()
47
+ // throw new Error(`Expected spy handler for '${key}'`);
48
+ return;
49
+ }
50
+
51
+ // Validate the response from the real implementation
52
+ await handler.onResponse(body.callResult);
53
+ });
54
+ }
55
+
56
+ public addInterceptor(interceptor: GGMockableInterceptor) {
57
+ this.interceptors.set(interceptor.getKey(), interceptor);
58
+ }
59
+
60
+ public deleteInterceptor(interceptor: GGMockableInterceptor) {
61
+ this.interceptors.delete(interceptor.getKey());
62
+ }
63
+
64
+ public async teardown(): Promise<void> {
65
+ this.interceptors.clear();
66
+ }
67
+ }
68
+
69
+ GGTestRunner.registerExtension(GGMockableInterceptorsServer);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Mockable decorator and test helpers.
3
+ *
4
+ * The @mockable decorator is re-exported from mockable-runtime.ts for production use.
5
+ * mockBy() and spyOn() are test-only utilities that require the full testkit.
6
+ */
7
+
8
+ // Test-only imports - these pull in testkit infrastructure
9
+ import {GGMockableInterceptor} from "./GGMockableInterceptor";
10
+ import {GGMockWith} from "../testers/GGMockWith";
11
+ import {GGSpyWith} from "../testers/GGSpyWith";
12
+
13
+ // ============================================================================
14
+ // Type helpers for mockBy/spyOn
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Type for mock access - maps class methods to GGMockWith
19
+ */
20
+ type MockAccess<T> = {
21
+ [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
22
+ ? GGMockWith<A extends [infer Single] ? (Single extends object ? Single : Record<string, Single>) : Record<string, any>, Awaited<R>, never>
23
+ : never
24
+ };
25
+
26
+ /**
27
+ * Type for spy access - maps class methods to GGSpyWith
28
+ */
29
+ type SpyAccess<T> = {
30
+ [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>
31
+ ? GGSpyWith<A extends [infer Single] ? (Single extends object ? Single : Record<string, Single>) : Record<string, any>, Awaited<R>, never>
32
+ : never
33
+ };
34
+
35
+ /**
36
+ * Get mock access for a @mockable class.
37
+ * Use this to create mock expectations in tests.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * .with(mockOf(AddressResolverService).resolveAddress
42
+ * .toEqual({address: "123 Main St"})
43
+ * .andReturn({lat: 40.7, lng: -74.0}))
44
+ * ```
45
+ */
46
+ export function mockOf<T>(cls: new (...args: any[]) => T): MockAccess<T> {
47
+ return new Proxy({} as any, {
48
+ get(_, methodName: string) {
49
+ return new GGMockWith(GGMockableInterceptor, {className: cls.name, methodName})
50
+ }
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Get spy access for a @mockable class.
56
+ * Use this to create spy expectations in tests - the real method will be called.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * .with(spyOn(AddressResolverService).resolveAddress
61
+ * .toEqual({address: "123 Main St"})
62
+ * .responseToMatchObject({lat: 40.7}))
63
+ * ```
64
+ */
65
+ export function spyOn<T>(cls: new (...args: any[]) => T): SpyAccess<T> {
66
+ return new Proxy({} as any, {
67
+ get(_, methodName: string) {
68
+ return new GGSpyWith(GGMockableInterceptor, {className: cls.name, methodName})
69
+ }
70
+ });
71
+ }