@grest-ts/testkit 0.0.6 → 0.0.8

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 +413 -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 +13 -13
  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
@@ -1,159 +1,159 @@
1
- import {IPCClient, IPCClientRequest} from "@grest-ts/ipc";
2
- import {GGLog} from "@grest-ts/logger";
3
- import {GGLoggerConsole} from "@grest-ts/logger-console";
4
- import {GGRuntime} from "@grest-ts/runtime";
5
- import {GGLocatorKey, GGLocatorScope} from "@grest-ts/locator";
6
- import {pathToFileURL} from "url";
7
- import {GGTestCommand, GGTestEnvConfig} from "./GGTestRuntime";
8
- import {GGExtensionDiscovery} from "@grest-ts/common";
9
- import {type MockableTestContext, runWithMockableContext} from "@grest-ts/testkit-runtime";
10
- import {CALL_THROUGH} from "./mockable/GGMockableInterceptorsServer";
11
- import {GGMockableIPC} from "./mockable/GGMockableIPC";
12
- import {registerOnCallHandler} from "./callOn/registerOnCallHandler";
13
- import {TestableIPC} from "./callOn/TestableIPC";
14
-
15
- export const GG_TEST_RUNTIME_WORKER = new GGLocatorKey<GGTestRuntimeWorker>("GGTestRuntimeWorker");
16
-
17
- export class GGTestRuntimeWorker {
18
-
19
- private static beforeRuntimeStartHandlers: (() => void)[] = [];
20
- private static beforeRuntimeStartExecuted = false;
21
-
22
- public readonly ipcClient: IPCClient;
23
- public readonly config: GGTestEnvConfig;
24
- public runtime: GGRuntime = undefined
25
- private runtimeStopped = false;
26
-
27
- private readonly scope: GGLocatorScope;
28
-
29
- constructor(config: GGTestEnvConfig) {
30
- this.ipcClient = new IPCClient(config.testRouterPort);
31
- this.config = config;
32
- this.scope = new GGLocatorScope("GGTestRuntimeWorker").enter();
33
- this.scope.set(GG_TEST_RUNTIME_WORKER, this);
34
- GGLog.init();
35
- GGLog.add(new GGLoggerConsole({showData: true}));
36
- }
37
-
38
- /**
39
- * Register a function to be called before the runtime starts.
40
- * Safe to call at module load time - just adds to array.
41
- * Handlers are executed during start() after extensions are loaded but before runtime creation.
42
- */
43
- public static onBeforeRuntimeStart(handler: () => void): void {
44
- if (this.beforeRuntimeStartExecuted) {
45
- throw new Error("Cannot register beforeRuntimeStart handler after worker has started");
46
- }
47
- this.beforeRuntimeStartHandlers.push(handler);
48
- }
49
-
50
- public async start(createRuntime?: () => GGRuntime): Promise<void> {
51
- process.env.GG_LOCAL_ROUTER_PORT = String(this.config.testRouterPort);
52
-
53
- await new GGExtensionDiscovery('testkit').load();
54
-
55
- // Create mockable context that bridges to IPC
56
- const mockableContext: MockableTestContext = {
57
- CALL_THROUGH,
58
- sendCall: async (className, methodName, callArgs) => {
59
- return this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.call, {
60
- className,
61
- methodName,
62
- callArgs
63
- });
64
- },
65
- sendSpyResult: async (className, methodName, callResult) => {
66
- await this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.spyResult, {
67
- className,
68
- methodName,
69
- callResult
70
- });
71
- }
72
- };
73
-
74
- // Wrap in mockable context so @mockable decorators can intercept
75
- await runWithMockableContext(mockableContext, async () => {
76
- await this.ipcClient.connect(this.config.runtimeId);
77
- GGLog.debug(this, 'Connected to test router');
78
-
79
- // Register testable handler for direct service invocation from tests
80
- registerOnCallHandler(this);
81
-
82
- GGTestRuntimeWorker.beforeRuntimeStartExecuted = true;
83
- GGTestRuntimeWorker.beforeRuntimeStartHandlers.forEach(handler => handler());
84
-
85
- if (createRuntime) {
86
- this.runtime = createRuntime();
87
- } else {
88
- // Dynamic import of the runtime source file.
89
- // Always use file:// URL — required by Node ESM on Windows.
90
- const moduleUrl = this.config.executablePath.startsWith('file:')
91
- ? this.config.executablePath
92
- : pathToFileURL(this.config.executablePath).href;
93
- const module = await import(moduleUrl);
94
- const RuntimeClass = module[this.config.className];
95
- if (!RuntimeClass) {
96
- throw new Error(
97
- `Runtime class '${this.config.className}' not found in module '${this.config.executablePath}'. ` +
98
- `Make sure the class is exported.`
99
- );
100
- }
101
- this.runtime = new RuntimeClass();
102
- }
103
- await this.runtime!.start();
104
-
105
- // Send registered locator keys to test runner for callOn routing
106
- const keys = this.runtime!.scope.getKeys();
107
- await this.ipcClient.sendFrameworkRequest(TestableIPC.server.registerKeys, {
108
- runtimeId: this.config.runtimeId,
109
- keys
110
- });
111
- GGLog.debug(this, `Sent ${keys.length} locator keys to test runner`);
112
- });
113
- }
114
-
115
- /**
116
- * Stop the GGRuntime (teardown services) but keep IPC alive.
117
- * The runtime reference is kept so IPC handlers can still access
118
- * async contexts (logs, metrics, config) via runInContext().
119
- */
120
- public async stopRuntime(): Promise<void> {
121
- if (this.runtimeStopped) return;
122
- this.runtimeStopped = true;
123
- if (this.runtime) {
124
- await this.runtime.scope.run(() => this.runtime!.teardown());
125
- }
126
- }
127
-
128
- /**
129
- * Register an IPC request handler that automatically runs within the runtime's context.
130
- * Use this instead of ipcClient.onFrameworkRequest() for handlers that need access to
131
- * runtime services (logs, metrics, config, etc.) even after runtime.stop().
132
- */
133
- public onIpcRequest<Req, Res>(
134
- type: IPCClientRequest<Req, Res>,
135
- handler: (payload: Req) => Res | Promise<Res>
136
- ): void {
137
- this.ipcClient.onFrameworkRequest(type, async (payload) => {
138
- if (this.runtime) {
139
- return this.runtime.scope.run(() => handler(payload));
140
- }
141
- return handler(payload);
142
- });
143
- }
144
-
145
- /**
146
- * Fully shutdown the worker, including IPC disconnection.
147
- * Calls stopRuntime() if not already stopped.
148
- */
149
- public async shutdown(): Promise<void> {
150
- if (!this.runtimeStopped) {
151
- await this.stopRuntime();
152
- }
153
- this.ipcClient.disconnect();
154
- }
155
-
156
- public getInitialCommandsFor<Payload>(type: IPCClientRequest<Payload, any>): GGTestCommand<Payload>[] {
157
- return this.config.initialCommands.filter(cmd => cmd.method === type) as GGTestCommand<Payload>[];
158
- }
159
- }
1
+ import {IPCClient, IPCClientRequest} from "@grest-ts/ipc";
2
+ import {GGLog} from "@grest-ts/logger";
3
+ import {GGLoggerConsole} from "@grest-ts/logger-console";
4
+ import {GGRuntime} from "@grest-ts/runtime";
5
+ import {GGLocatorKey, GGLocatorScope} from "@grest-ts/locator";
6
+ import {pathToFileURL} from "url";
7
+ import {GGTestCommand, GGTestEnvConfig} from "./GGTestRuntime";
8
+ import {GGExtensionDiscovery} from "@grest-ts/common";
9
+ import {type MockableTestContext, runWithMockableContext} from "@grest-ts/testkit-runtime";
10
+ import {CALL_THROUGH} from "./mockable/GGMockableInterceptorsServer";
11
+ import {GGMockableIPC} from "./mockable/GGMockableIPC";
12
+ import {registerOnCallHandler} from "./callOn/registerOnCallHandler";
13
+ import {TestableIPC} from "./callOn/TestableIPC";
14
+
15
+ export const GG_TEST_RUNTIME_WORKER = new GGLocatorKey<GGTestRuntimeWorker>("GGTestRuntimeWorker");
16
+
17
+ export class GGTestRuntimeWorker {
18
+
19
+ private static beforeRuntimeStartHandlers: (() => void)[] = [];
20
+ private static beforeRuntimeStartExecuted = false;
21
+
22
+ public readonly ipcClient: IPCClient;
23
+ public readonly config: GGTestEnvConfig;
24
+ public runtime: GGRuntime = undefined
25
+ private runtimeStopped = false;
26
+
27
+ private readonly scope: GGLocatorScope;
28
+
29
+ constructor(config: GGTestEnvConfig) {
30
+ this.ipcClient = new IPCClient(config.testRouterPort);
31
+ this.config = config;
32
+ this.scope = new GGLocatorScope("GGTestRuntimeWorker").enter();
33
+ this.scope.set(GG_TEST_RUNTIME_WORKER, this);
34
+ GGLog.init();
35
+ GGLog.add(new GGLoggerConsole({showData: true}));
36
+ }
37
+
38
+ /**
39
+ * Register a function to be called before the runtime starts.
40
+ * Safe to call at module load time - just adds to array.
41
+ * Handlers are executed during start() after extensions are loaded but before runtime creation.
42
+ */
43
+ public static onBeforeRuntimeStart(handler: () => void): void {
44
+ if (this.beforeRuntimeStartExecuted) {
45
+ throw new Error("Cannot register beforeRuntimeStart handler after worker has started");
46
+ }
47
+ this.beforeRuntimeStartHandlers.push(handler);
48
+ }
49
+
50
+ public async start(createRuntime?: () => GGRuntime): Promise<void> {
51
+ process.env.GG_LOCAL_ROUTER_PORT = String(this.config.testRouterPort);
52
+
53
+ await new GGExtensionDiscovery('testkit').load();
54
+
55
+ // Create mockable context that bridges to IPC
56
+ const mockableContext: MockableTestContext = {
57
+ CALL_THROUGH,
58
+ sendCall: async (className, methodName, callArgs) => {
59
+ return this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.call, {
60
+ className,
61
+ methodName,
62
+ callArgs
63
+ });
64
+ },
65
+ sendSpyResult: async (className, methodName, callResult) => {
66
+ await this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.spyResult, {
67
+ className,
68
+ methodName,
69
+ callResult
70
+ });
71
+ }
72
+ };
73
+
74
+ // Wrap in mockable context so @mockable decorators can intercept
75
+ await runWithMockableContext(mockableContext, async () => {
76
+ await this.ipcClient.connect(this.config.runtimeId);
77
+ GGLog.debug(this, 'Connected to test router');
78
+
79
+ // Register testable handler for direct service invocation from tests
80
+ registerOnCallHandler(this);
81
+
82
+ GGTestRuntimeWorker.beforeRuntimeStartExecuted = true;
83
+ GGTestRuntimeWorker.beforeRuntimeStartHandlers.forEach(handler => handler());
84
+
85
+ if (createRuntime) {
86
+ this.runtime = createRuntime();
87
+ } else {
88
+ // Dynamic import of the runtime source file.
89
+ // Always use file:// URL — required by Node ESM on Windows.
90
+ const moduleUrl = this.config.executablePath.startsWith('file:')
91
+ ? this.config.executablePath
92
+ : pathToFileURL(this.config.executablePath).href;
93
+ const module = await import(moduleUrl);
94
+ const RuntimeClass = module[this.config.className];
95
+ if (!RuntimeClass) {
96
+ throw new Error(
97
+ `Runtime class '${this.config.className}' not found in module '${this.config.executablePath}'. ` +
98
+ `Make sure the class is exported.`
99
+ );
100
+ }
101
+ this.runtime = new RuntimeClass();
102
+ }
103
+ await this.runtime!.start();
104
+
105
+ // Send registered locator keys to test runner for callOn routing
106
+ const keys = this.runtime!.scope.getKeys();
107
+ await this.ipcClient.sendFrameworkRequest(TestableIPC.server.registerKeys, {
108
+ runtimeId: this.config.runtimeId,
109
+ keys
110
+ });
111
+ GGLog.debug(this, `Sent ${keys.length} locator keys to test runner`);
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Stop the GGRuntime (teardown services) but keep IPC alive.
117
+ * The runtime reference is kept so IPC handlers can still access
118
+ * async contexts (logs, metrics, config) via runInContext().
119
+ */
120
+ public async stopRuntime(): Promise<void> {
121
+ if (this.runtimeStopped) return;
122
+ this.runtimeStopped = true;
123
+ if (this.runtime) {
124
+ await this.runtime.scope.run(() => this.runtime!.teardown());
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Register an IPC request handler that automatically runs within the runtime's context.
130
+ * Use this instead of ipcClient.onFrameworkRequest() for handlers that need access to
131
+ * runtime services (logs, metrics, config, etc.) even after runtime.stop().
132
+ */
133
+ public onIpcRequest<Req, Res>(
134
+ type: IPCClientRequest<Req, Res>,
135
+ handler: (payload: Req) => Res | Promise<Res>
136
+ ): void {
137
+ this.ipcClient.onFrameworkRequest(type, async (payload) => {
138
+ if (this.runtime) {
139
+ return this.runtime.scope.run(() => handler(payload));
140
+ }
141
+ return handler(payload);
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Fully shutdown the worker, including IPC disconnection.
147
+ * Calls stopRuntime() if not already stopped.
148
+ */
149
+ public async shutdown(): Promise<void> {
150
+ if (!this.runtimeStopped) {
151
+ await this.stopRuntime();
152
+ }
153
+ this.ipcClient.disconnect();
154
+ }
155
+
156
+ public getInitialCommandsFor<Payload>(type: IPCClientRequest<Payload, any>): GGTestCommand<Payload>[] {
157
+ return this.config.initialCommands.filter(cmd => cmd.method === type) as GGTestCommand<Payload>[];
158
+ }
159
+ }
@@ -1,116 +1,116 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
-
5
- /**
6
- * Reference counting for shared test resources across workers.
7
- *
8
- * Used to coordinate shared resources (e.g. database schemas)
9
- * that multiple workers use simultaneously. Handles file-system
10
- * based locking so callers don't need their own locking mechanism.
11
- *
12
- * - acquire(key, onCreate): first caller runs onCreate, others wait and skip
13
- * - release(key, onLast): last caller runs onLast (e.g. cleanup)
14
- */
15
- export class GGTestSharedRef {
16
-
17
- private static getRefDir(): string {
18
- const runId = process.env.GG_TEST_RUN_ID;
19
- if (!runId) {
20
- throw new Error("GG_TEST_RUN_ID not set. Add globalSetup '@grest-ts/testkit-vitest/globalSetup' to vitest.config.ts.");
21
- }
22
- const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
23
- fs.mkdirSync(dir, {recursive: true});
24
- return dir;
25
- }
26
-
27
- private static getRefFile(key: string): string {
28
- return path.join(this.getRefDir(), `${key}.ref`);
29
- }
30
-
31
- private static getLockPath(key: string): string {
32
- return path.join(this.getRefDir(), `${key}.lock`);
33
- }
34
-
35
- private static async acquireLock(key: string): Promise<void> {
36
- const lockPath = this.getLockPath(key);
37
- const timeout = 60000;
38
- const start = Date.now();
39
- while (true) {
40
- try {
41
- fs.mkdirSync(lockPath);
42
- return;
43
- } catch {
44
- if (Date.now() - start > timeout) {
45
- throw new Error(`[GGTestSharedRef] Failed to acquire lock for '${key}' after ${timeout}ms`);
46
- }
47
- await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 50));
48
- }
49
- }
50
- }
51
-
52
- private static releaseLock(key: string): void {
53
- try { fs.rmdirSync(this.getLockPath(key)); } catch {}
54
- }
55
-
56
- /**
57
- * Acquire a shared reference. If this is the first reference,
58
- * the onCreate callback is called (e.g. to create a shared resource).
59
- * Other callers wait for the lock and skip onCreate.
60
- */
61
- static async acquire(key: string, onCreate: () => Promise<void>): Promise<void> {
62
- await this.acquireLock(key);
63
- try {
64
- const file = this.getRefFile(key);
65
- let count = 0;
66
- try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
67
-
68
- if (count === 0) {
69
- await onCreate();
70
- }
71
-
72
- fs.writeFileSync(file, (count + 1).toString());
73
- } finally {
74
- this.releaseLock(key);
75
- }
76
- }
77
-
78
- /**
79
- * Release a shared reference. If this is the last reference,
80
- * the onLast callback is called (e.g. to drop a shared resource).
81
- * Errors in onLast are caught and logged (cleanup is best-effort).
82
- */
83
- static async release(key: string, onLast: () => Promise<void>): Promise<void> {
84
- await this.acquireLock(key);
85
- try {
86
- const file = this.getRefFile(key);
87
- let count = 0;
88
- try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
89
- count -= 1;
90
-
91
- if (count <= 0) {
92
- try { fs.unlinkSync(file); } catch {}
93
- try {
94
- await onLast();
95
- } catch (err) {
96
- console.error('[GGTestSharedRef] Cleanup failed:', err);
97
- }
98
- } else {
99
- fs.writeFileSync(file, count.toString());
100
- }
101
- } finally {
102
- this.releaseLock(key);
103
- }
104
- }
105
-
106
- /**
107
- * Remove the ref counting temp directory for the current test run.
108
- * Called by globalSetup teardown after all workers have finished.
109
- */
110
- static cleanup(): void {
111
- const runId = process.env.GG_TEST_RUN_ID;
112
- if (!runId) return;
113
- const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
114
- try { fs.rmSync(dir, {recursive: true}); } catch {}
115
- }
116
- }
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ /**
6
+ * Reference counting for shared test resources across workers.
7
+ *
8
+ * Used to coordinate shared resources (e.g. database schemas)
9
+ * that multiple workers use simultaneously. Handles file-system
10
+ * based locking so callers don't need their own locking mechanism.
11
+ *
12
+ * - acquire(key, onCreate): first caller runs onCreate, others wait and skip
13
+ * - release(key, onLast): last caller runs onLast (e.g. cleanup)
14
+ */
15
+ export class GGTestSharedRef {
16
+
17
+ private static getRefDir(): string {
18
+ const runId = process.env.GG_TEST_RUN_ID;
19
+ if (!runId) {
20
+ throw new Error("GG_TEST_RUN_ID not set. Add globalSetup '@grest-ts/testkit-vitest/globalSetup' to vitest.config.ts.");
21
+ }
22
+ const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
23
+ fs.mkdirSync(dir, {recursive: true});
24
+ return dir;
25
+ }
26
+
27
+ private static getRefFile(key: string): string {
28
+ return path.join(this.getRefDir(), `${key}.ref`);
29
+ }
30
+
31
+ private static getLockPath(key: string): string {
32
+ return path.join(this.getRefDir(), `${key}.lock`);
33
+ }
34
+
35
+ private static async acquireLock(key: string): Promise<void> {
36
+ const lockPath = this.getLockPath(key);
37
+ const timeout = 60000;
38
+ const start = Date.now();
39
+ while (true) {
40
+ try {
41
+ fs.mkdirSync(lockPath);
42
+ return;
43
+ } catch {
44
+ if (Date.now() - start > timeout) {
45
+ throw new Error(`[GGTestSharedRef] Failed to acquire lock for '${key}' after ${timeout}ms`);
46
+ }
47
+ await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 50));
48
+ }
49
+ }
50
+ }
51
+
52
+ private static releaseLock(key: string): void {
53
+ try { fs.rmdirSync(this.getLockPath(key)); } catch {}
54
+ }
55
+
56
+ /**
57
+ * Acquire a shared reference. If this is the first reference,
58
+ * the onCreate callback is called (e.g. to create a shared resource).
59
+ * Other callers wait for the lock and skip onCreate.
60
+ */
61
+ static async acquire(key: string, onCreate: () => Promise<void>): Promise<void> {
62
+ await this.acquireLock(key);
63
+ try {
64
+ const file = this.getRefFile(key);
65
+ let count = 0;
66
+ try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
67
+
68
+ if (count === 0) {
69
+ await onCreate();
70
+ }
71
+
72
+ fs.writeFileSync(file, (count + 1).toString());
73
+ } finally {
74
+ this.releaseLock(key);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Release a shared reference. If this is the last reference,
80
+ * the onLast callback is called (e.g. to drop a shared resource).
81
+ * Errors in onLast are caught and logged (cleanup is best-effort).
82
+ */
83
+ static async release(key: string, onLast: () => Promise<void>): Promise<void> {
84
+ await this.acquireLock(key);
85
+ try {
86
+ const file = this.getRefFile(key);
87
+ let count = 0;
88
+ try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
89
+ count -= 1;
90
+
91
+ if (count <= 0) {
92
+ try { fs.unlinkSync(file); } catch {}
93
+ try {
94
+ await onLast();
95
+ } catch (err) {
96
+ console.error('[GGTestSharedRef] Cleanup failed:', err);
97
+ }
98
+ } else {
99
+ fs.writeFileSync(file, count.toString());
100
+ }
101
+ } finally {
102
+ this.releaseLock(key);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Remove the ref counting temp directory for the current test run.
108
+ * Called by globalSetup teardown after all workers have finished.
109
+ */
110
+ static cleanup(): void {
111
+ const runId = process.env.GG_TEST_RUN_ID;
112
+ if (!runId) return;
113
+ const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
114
+ try { fs.rmSync(dir, {recursive: true}); } catch {}
115
+ }
116
+ }
@@ -1,26 +1,26 @@
1
- import {GGExtensionDiscovery} from '@grest-ts/common';
2
-
3
- /**
4
- * Discovers testkit extensions by scanning node_modules for packages
5
- * that follow the convention of having a testkit/index-testkit.ts file.
6
- *
7
- * @deprecated Use GGExtensionDiscovery from @grest-ts/common directly:
8
- * ```typescript
9
- * const discovery = new GGExtensionDiscovery('testkit');
10
- * await discovery.load();
11
- * ```
12
- */
13
- export class GGTestkitExtensionsDiscovery {
14
-
15
- private static discovery = new GGExtensionDiscovery('testkit');
16
-
17
- /**
18
- * Discover and load all testkits.
19
- * - Scans for testkit packages
20
- * - Generates .d.ts file for IDE support
21
- * - Dynamically imports testkits for runtime
22
- */
23
- public static async load(): Promise<void> {
24
- await this.discovery.load();
25
- }
26
- }
1
+ import {GGExtensionDiscovery} from '@grest-ts/common';
2
+
3
+ /**
4
+ * Discovers testkit extensions by scanning node_modules for packages
5
+ * that follow the convention of having a testkit/index-testkit.ts file.
6
+ *
7
+ * @deprecated Use GGExtensionDiscovery from @grest-ts/common directly:
8
+ * ```typescript
9
+ * const discovery = new GGExtensionDiscovery('testkit');
10
+ * await discovery.load();
11
+ * ```
12
+ */
13
+ export class GGTestkitExtensionsDiscovery {
14
+
15
+ private static discovery = new GGExtensionDiscovery('testkit');
16
+
17
+ /**
18
+ * Discover and load all testkits.
19
+ * - Scans for testkit packages
20
+ * - Generates .d.ts file for IDE support
21
+ * - Dynamically imports testkits for runtime
22
+ */
23
+ public static async load(): Promise<void> {
24
+ await this.discovery.load();
25
+ }
26
+ }
@@ -1,16 +1,16 @@
1
- /**
2
- * Interface for discovery server used by GGTestRunner.
3
- * Implemented by GGLocalDiscoveryServer in @grest-ts/discovery.
4
- * This interface allows @grest-ts/testkit to not depend on @grest-ts/discovery.
5
- */
6
- export interface IGGLocalDiscoveryServer {
7
- start(): Promise<boolean>;
8
- teardown(): Promise<void>;
9
- getRoutingUrl(api: string): string;
10
- }
11
-
12
- export interface IServiceRoute {
13
- api: string;
14
- baseUrl: string;
15
- pathPrefix: string;
16
- }
1
+ /**
2
+ * Interface for discovery server used by GGTestRunner.
3
+ * Implemented by GGLocalDiscoveryServer in @grest-ts/discovery.
4
+ * This interface allows @grest-ts/testkit to not depend on @grest-ts/discovery.
5
+ */
6
+ export interface IGGLocalDiscoveryServer {
7
+ start(): Promise<boolean>;
8
+ teardown(): Promise<void>;
9
+ getRoutingUrl(api: string): string;
10
+ }
11
+
12
+ export interface IServiceRoute {
13
+ api: string;
14
+ baseUrl: string;
15
+ pathPrefix: string;
16
+ }