@memberjunction/server 2.116.0 → 2.118.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.
@@ -0,0 +1,369 @@
1
+ import {
2
+ Resolver,
3
+ Mutation,
4
+ Query,
5
+ Arg,
6
+ Ctx,
7
+ ObjectType,
8
+ Field,
9
+ PubSub,
10
+ PubSubEngine,
11
+ ID,
12
+ Int
13
+ } from 'type-graphql';
14
+ import { AppContext, UserPayload } from '../types.js';
15
+ import { LogError, LogStatus } from '@memberjunction/core';
16
+ import { TestEngine } from '@memberjunction/testing-engine';
17
+ import { ResolverBase } from '../generic/ResolverBase.js';
18
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
19
+
20
+ // ===== GraphQL Types =====
21
+
22
+ @ObjectType()
23
+ export class TestRunResult {
24
+ @Field()
25
+ success: boolean;
26
+
27
+ @Field({ nullable: true })
28
+ errorMessage?: string;
29
+
30
+ @Field({ nullable: true })
31
+ executionTimeMs?: number;
32
+
33
+ @Field()
34
+ result: string; // JSON serialized TestRunResult
35
+ }
36
+
37
+ @ObjectType()
38
+ export class TestSuiteRunResult {
39
+ @Field()
40
+ success: boolean;
41
+
42
+ @Field({ nullable: true })
43
+ errorMessage?: string;
44
+
45
+ @Field({ nullable: true })
46
+ executionTimeMs?: number;
47
+
48
+ @Field()
49
+ result: string; // JSON serialized TestSuiteRunResult
50
+ }
51
+
52
+ @ObjectType()
53
+ export class TestExecutionProgress {
54
+ @Field()
55
+ currentStep: string;
56
+
57
+ @Field(() => Int)
58
+ percentage: number;
59
+
60
+ @Field()
61
+ message: string;
62
+
63
+ @Field({ nullable: true })
64
+ testName?: string;
65
+
66
+ @Field({ nullable: true })
67
+ driverType?: string;
68
+
69
+ @Field({ nullable: true })
70
+ oracleEvaluation?: string;
71
+ }
72
+
73
+ @ObjectType()
74
+ export class TestExecutionStreamMessage {
75
+ @Field(() => ID)
76
+ sessionId: string;
77
+
78
+ @Field(() => ID)
79
+ testRunId: string;
80
+
81
+ @Field()
82
+ type: 'progress' | 'oracle_eval' | 'complete' | 'error';
83
+
84
+ @Field({ nullable: true })
85
+ progress?: TestExecutionProgress;
86
+
87
+ @Field()
88
+ timestamp: Date;
89
+
90
+ // Not a GraphQL field - used internally
91
+ testRun?: any;
92
+ }
93
+
94
+ // ===== Resolver =====
95
+
96
+ @Resolver()
97
+ export class RunTestResolver extends ResolverBase {
98
+
99
+ /**
100
+ * Execute a single test
101
+ */
102
+ @Mutation(() => TestRunResult)
103
+ async RunTest(
104
+ @Arg('testId') testId: string,
105
+ @Arg('verbose', { nullable: true }) verbose: boolean = true,
106
+ @Arg('environment', { nullable: true }) environment?: string,
107
+ @PubSub() pubSub?: PubSubEngine,
108
+ @Ctx() { userPayload }: AppContext = {} as AppContext
109
+ ): Promise<TestRunResult> {
110
+ const startTime = Date.now();
111
+
112
+ try {
113
+ const user = this.GetUserFromPayload(userPayload);
114
+ if (!user) {
115
+ throw new Error('User context required');
116
+ }
117
+
118
+ LogStatus(`[RunTestResolver] Starting test execution: ${testId}`);
119
+
120
+ // Get singleton instance
121
+ const engine = TestEngine.Instance;
122
+
123
+ // Configure engine (loads driver and oracle registries)
124
+ await engine.Config(verbose, user);
125
+
126
+ // Create progress callback if we have pubSub
127
+ const progressCallback = pubSub ?
128
+ this.createProgressCallback(pubSub, userPayload, testId) :
129
+ undefined;
130
+
131
+ // Run the test
132
+ const options = {
133
+ verbose,
134
+ environment,
135
+ progressCallback
136
+ };
137
+
138
+ const result = await engine.RunTest(testId, options, user);
139
+
140
+ // Handle both single result and array of results (RepeatCount > 1)
141
+ let finalResult;
142
+ let allPassed = true;
143
+
144
+ if (Array.isArray(result)) {
145
+ // Multiple iterations - check if all passed
146
+ allPassed = result.every(r => r.status === 'Passed');
147
+
148
+ // For GraphQL, return summary information
149
+ // The full array is serialized in the result field
150
+ finalResult = {
151
+ testRunId: result[0]?.testRunId || '',
152
+ status: allPassed ? 'Passed' as const : 'Failed' as const
153
+ };
154
+
155
+ // Publish completion for each iteration
156
+ if (pubSub) {
157
+ for (const iterationResult of result) {
158
+ this.publishComplete(pubSub, userPayload, iterationResult);
159
+ }
160
+ }
161
+
162
+ LogStatus(`[RunTestResolver] Test completed: ${result.length} iterations, ${allPassed ? 'all passed' : 'some failed'} in ${Date.now() - startTime}ms`);
163
+ } else {
164
+ // Single result
165
+ finalResult = result;
166
+ allPassed = result.status === 'Passed';
167
+
168
+ // Publish completion
169
+ if (pubSub && result.testRunId) {
170
+ this.publishComplete(pubSub, userPayload, result);
171
+ }
172
+
173
+ LogStatus(`[RunTestResolver] Test completed: ${result.status} in ${Date.now() - startTime}ms`);
174
+ }
175
+
176
+ const executionTime = Date.now() - startTime;
177
+
178
+ return {
179
+ success: allPassed,
180
+ result: JSON.stringify(result), // Full result (single or array)
181
+ executionTimeMs: executionTime
182
+ };
183
+
184
+ } catch (error) {
185
+ const executionTime = Date.now() - startTime;
186
+ const errorMsg = (error as Error).message;
187
+
188
+ LogError(`[RunTestResolver] Test execution failed: ${errorMsg}`);
189
+
190
+ // Publish error
191
+ if (pubSub) {
192
+ this.publishError(pubSub, userPayload, testId, errorMsg);
193
+ }
194
+
195
+ return {
196
+ success: false,
197
+ errorMessage: errorMsg,
198
+ result: JSON.stringify({}),
199
+ executionTimeMs: executionTime
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Execute a test suite
206
+ */
207
+ @Mutation(() => TestSuiteRunResult)
208
+ async RunTestSuite(
209
+ @Arg('suiteId') suiteId: string,
210
+ @Arg('verbose', { nullable: true }) verbose: boolean = true,
211
+ @Arg('environment', { nullable: true }) environment?: string,
212
+ @Arg('parallel', { nullable: true }) parallel: boolean = false,
213
+ @PubSub() pubSub?: PubSubEngine,
214
+ @Ctx() { userPayload }: AppContext = {} as AppContext
215
+ ): Promise<TestSuiteRunResult> {
216
+ const startTime = Date.now();
217
+
218
+ try {
219
+ const user = this.GetUserFromPayload(userPayload);
220
+ if (!user) {
221
+ throw new Error('User context required');
222
+ }
223
+
224
+ LogStatus(`[RunTestResolver] Starting suite execution: ${suiteId}`);
225
+
226
+ const engine = TestEngine.Instance;
227
+ await engine.Config(verbose, user);
228
+
229
+ // Create progress callback
230
+ const progressCallback = pubSub ?
231
+ this.createProgressCallback(pubSub, userPayload, suiteId) :
232
+ undefined;
233
+
234
+ const options = {
235
+ verbose,
236
+ environment,
237
+ parallel,
238
+ progressCallback
239
+ };
240
+
241
+ const result = await engine.RunSuite(suiteId, options, user);
242
+
243
+ const executionTime = Date.now() - startTime;
244
+
245
+ LogStatus(`[RunTestResolver] Suite completed: ${result.totalTests} tests in ${executionTime}ms`);
246
+
247
+ return {
248
+ success: result.status === 'Completed',
249
+ result: JSON.stringify(result),
250
+ executionTimeMs: executionTime
251
+ };
252
+
253
+ } catch (error) {
254
+ const executionTime = Date.now() - startTime;
255
+ const errorMsg = (error as Error).message;
256
+
257
+ LogError(`[RunTestResolver] Suite execution failed: ${errorMsg}`);
258
+
259
+ return {
260
+ success: false,
261
+ errorMessage: errorMsg,
262
+ result: JSON.stringify({}),
263
+ executionTimeMs: executionTime
264
+ };
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Query to check if a test is currently running
270
+ */
271
+ @Query(() => Boolean)
272
+ async IsTestRunning(
273
+ @Arg('testId') testId: string,
274
+ @Ctx() { userPayload }: AppContext = {} as AppContext
275
+ ): Promise<boolean> {
276
+ // TODO: Implement running test tracking
277
+ // For now, return false
278
+ return false;
279
+ }
280
+
281
+ // ===== Progress Callbacks =====
282
+
283
+ /**
284
+ * Create progress callback for test execution
285
+ */
286
+ private createProgressCallback(
287
+ pubSub: PubSubEngine,
288
+ userPayload: UserPayload,
289
+ testId: string
290
+ ) {
291
+ return (progress: {
292
+ step: string;
293
+ percentage: number;
294
+ message: string;
295
+ metadata?: any;
296
+ }) => {
297
+ LogStatus(`[RunTestResolver] Progress: ${progress.step} - ${progress.percentage}%`);
298
+
299
+ // Get test run from metadata
300
+ const testRun = progress.metadata?.testRun;
301
+
302
+ const progressMsg: TestExecutionStreamMessage = {
303
+ sessionId: userPayload.sessionId || '',
304
+ testRunId: testRun?.ID || testId,
305
+ type: 'progress',
306
+ testRun: testRun ? testRun.GetAll() : undefined,
307
+ progress: {
308
+ currentStep: progress.step,
309
+ percentage: progress.percentage,
310
+ message: progress.message,
311
+ testName: progress.metadata?.testName,
312
+ driverType: progress.metadata?.driverType,
313
+ oracleEvaluation: progress.metadata?.oracleType
314
+ },
315
+ timestamp: new Date()
316
+ };
317
+
318
+ this.publishProgress(pubSub, progressMsg, userPayload);
319
+ };
320
+ }
321
+
322
+ private publishProgress(pubSub: PubSubEngine, data: TestExecutionStreamMessage, userPayload: UserPayload) {
323
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
324
+ message: JSON.stringify({
325
+ resolver: 'RunTestResolver',
326
+ type: 'TestExecutionProgress',
327
+ status: 'ok',
328
+ data,
329
+ }),
330
+ sessionId: userPayload.sessionId,
331
+ });
332
+ }
333
+
334
+ private publishComplete(pubSub: PubSubEngine, userPayload: UserPayload, result: any) {
335
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
336
+ message: JSON.stringify({
337
+ resolver: 'RunTestResolver',
338
+ type: 'TestExecutionComplete',
339
+ status: 'ok',
340
+ data: {
341
+ sessionId: userPayload.sessionId,
342
+ testRunId: result.testRunId,
343
+ type: 'complete',
344
+ result: JSON.stringify(result),
345
+ timestamp: new Date()
346
+ },
347
+ }),
348
+ sessionId: userPayload.sessionId,
349
+ });
350
+ }
351
+
352
+ private publishError(pubSub: PubSubEngine, userPayload: UserPayload, testId: string, errorMsg: string) {
353
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
354
+ message: JSON.stringify({
355
+ resolver: 'RunTestResolver',
356
+ type: 'TestExecutionError',
357
+ status: 'error',
358
+ data: {
359
+ sessionId: userPayload.sessionId,
360
+ testRunId: testId,
361
+ type: 'error',
362
+ errorMessage: errorMsg,
363
+ timestamp: new Date()
364
+ },
365
+ }),
366
+ sessionId: userPayload.sessionId,
367
+ });
368
+ }
369
+ }