@memberjunction/server 5.3.1 → 5.4.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.
@@ -12,11 +12,11 @@ import {
12
12
  Int
13
13
  } from 'type-graphql';
14
14
  import { AppContext, UserPayload } from '../types.js';
15
- import { LogError, LogStatus } from '@memberjunction/core';
15
+ import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
16
16
  import { TestEngine } from '@memberjunction/testing-engine';
17
17
  import { ResolverBase } from '../generic/ResolverBase.js';
18
18
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
19
- import { TestRunVariables, TestLogMessage } from '@memberjunction/testing-engine-base';
19
+ import { TestRunVariables, TestLogMessage, TestRunResult as EngineTestRunResult } from '@memberjunction/testing-engine-base';
20
20
 
21
21
  // ===== GraphQL Types =====
22
22
 
@@ -89,7 +89,7 @@ export class TestExecutionStreamMessage {
89
89
  timestamp: Date;
90
90
 
91
91
  // Not a GraphQL field - used internally
92
- testRun?: any;
92
+ testRun?: Record<string, unknown>;
93
93
  }
94
94
 
95
95
  // ===== Resolver =====
@@ -98,7 +98,8 @@ export class TestExecutionStreamMessage {
98
98
  export class RunTestResolver extends ResolverBase {
99
99
 
100
100
  /**
101
- * Execute a single test
101
+ * Execute a single test.
102
+ * Supports fire-and-forget mode to avoid Azure proxy timeouts on long-running tests.
102
103
  */
103
104
  @Mutation(() => TestRunResult)
104
105
  async RunTest(
@@ -107,232 +108,307 @@ export class RunTestResolver extends ResolverBase {
107
108
  @Arg('environment', { nullable: true }) environment?: string,
108
109
  @Arg('tags', { nullable: true }) tags?: string,
109
110
  @Arg('variables', { nullable: true }) variables?: string,
111
+ @Arg('fireAndForget', { nullable: true }) fireAndForget?: boolean,
110
112
  @PubSub() pubSub?: PubSubEngine,
111
113
  @Ctx() { userPayload }: AppContext = {} as AppContext
114
+ ): Promise<TestRunResult> {
115
+ const user = this.GetUserFromPayload(userPayload);
116
+ if (!user) {
117
+ return { success: false, errorMessage: 'User context required', result: JSON.stringify({}) };
118
+ }
119
+
120
+ if (fireAndForget && pubSub) {
121
+ // Fire-and-forget: start in background, return immediately
122
+ this.executeTestInBackground(testId, verbose, environment, tags, variables, pubSub, userPayload, user);
123
+ LogStatus(`🔥 Fire-and-forget: Test ${testId} execution started in background`);
124
+ return {
125
+ success: true,
126
+ result: JSON.stringify({ accepted: true, fireAndForget: true })
127
+ };
128
+ }
129
+
130
+ // Synchronous mode (default): wait for execution to complete
131
+ return this.executeTest(testId, verbose, environment, tags, variables, pubSub, userPayload, user);
132
+ }
133
+
134
+ /**
135
+ * Execute a test suite.
136
+ * Supports fire-and-forget mode to avoid Azure proxy timeouts on long-running suites.
137
+ */
138
+ @Mutation(() => TestSuiteRunResult)
139
+ async RunTestSuite(
140
+ @Arg('suiteId') suiteId: string,
141
+ @Arg('verbose', { nullable: true }) verbose: boolean = true,
142
+ @Arg('environment', { nullable: true }) environment?: string,
143
+ @Arg('parallel', { nullable: true }) parallel: boolean = false,
144
+ @Arg('tags', { nullable: true }) tags?: string,
145
+ @Arg('variables', { nullable: true }) variables?: string,
146
+ @Arg('selectedTestIds', { nullable: true }) selectedTestIds?: string,
147
+ @Arg('sequenceStart', () => Int, { nullable: true }) sequenceStart?: number,
148
+ @Arg('sequenceEnd', () => Int, { nullable: true }) sequenceEnd?: number,
149
+ @Arg('fireAndForget', { nullable: true }) fireAndForget?: boolean,
150
+ @PubSub() pubSub?: PubSubEngine,
151
+ @Ctx() { userPayload }: AppContext = {} as AppContext
152
+ ): Promise<TestSuiteRunResult> {
153
+ const user = this.GetUserFromPayload(userPayload);
154
+ if (!user) {
155
+ return { success: false, errorMessage: 'User context required', result: JSON.stringify({}) };
156
+ }
157
+
158
+ if (fireAndForget && pubSub) {
159
+ // Fire-and-forget: start in background, return immediately
160
+ this.executeSuiteInBackground(
161
+ suiteId, verbose, environment, parallel, tags, variables,
162
+ selectedTestIds, sequenceStart, sequenceEnd, pubSub, userPayload, user
163
+ );
164
+ LogStatus(`🔥 Fire-and-forget: Suite ${suiteId} execution started in background`);
165
+ return {
166
+ success: true,
167
+ result: JSON.stringify({ accepted: true, fireAndForget: true })
168
+ };
169
+ }
170
+
171
+ // Synchronous mode (default): wait for execution to complete
172
+ return this.executeSuite(
173
+ suiteId, verbose, environment, parallel, tags, variables,
174
+ selectedTestIds, sequenceStart, sequenceEnd, pubSub, userPayload, user
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Query to check if a test is currently running
180
+ */
181
+ @Query(() => Boolean)
182
+ async IsTestRunning(
183
+ @Arg('testId') testId: string,
184
+ @Ctx() { userPayload }: AppContext = {} as AppContext
185
+ ): Promise<boolean> {
186
+ // TODO: Implement running test tracking
187
+ // For now, return false
188
+ return false;
189
+ }
190
+
191
+ // ===== Core Execution Methods =====
192
+
193
+ /**
194
+ * Execute a single test synchronously and return the result.
195
+ */
196
+ private async executeTest(
197
+ testId: string,
198
+ verbose: boolean,
199
+ environment: string | undefined,
200
+ tags: string | undefined,
201
+ variables: string | undefined,
202
+ pubSub: PubSubEngine | undefined,
203
+ userPayload: UserPayload,
204
+ user: UserInfo
112
205
  ): Promise<TestRunResult> {
113
206
  const startTime = Date.now();
114
207
 
115
208
  try {
116
- const user = this.GetUserFromPayload(userPayload);
117
- if (!user) {
118
- throw new Error('User context required');
119
- }
120
-
121
209
  LogStatus(`[RunTestResolver] Starting test execution: ${testId}`);
122
210
 
123
- // Get singleton instance
124
211
  const engine = TestEngine.Instance;
125
-
126
- // Configure engine (loads driver and oracle registries)
127
212
  await engine.Config(verbose, user);
128
213
 
129
- // Create progress callback if we have pubSub
130
- const progressCallback = pubSub ?
131
- this.createProgressCallback(pubSub, userPayload, testId) :
132
- undefined;
133
-
134
- // Create log callback to stream driver/engine logs to the UI in real-time
135
- const logCallback = pubSub ?
136
- this.createLogCallback(pubSub, userPayload, testId) :
137
- undefined;
138
-
139
- // Parse variables from JSON string if provided
140
- let parsedVariables: TestRunVariables | undefined;
141
- if (variables) {
142
- try {
143
- parsedVariables = JSON.parse(variables);
144
- } catch (e) {
145
- LogError(`[RunTestResolver] Failed to parse variables: ${variables}`);
146
- }
147
- }
148
-
149
- // Run the test
150
- const options = {
151
- verbose,
152
- environment,
153
- tags,
154
- variables: parsedVariables,
155
- progressCallback,
156
- logCallback
157
- };
214
+ const progressCallback = pubSub ? this.createProgressCallback(pubSub, userPayload, testId) : undefined;
215
+ const logCallback = pubSub ? this.createLogCallback(pubSub, userPayload, testId) : undefined;
216
+ const parsedVariables = this.parseVariablesJson(variables);
158
217
 
218
+ const options = { verbose, environment, tags, variables: parsedVariables, progressCallback, logCallback };
159
219
  const result = await engine.RunTest(testId, options, user);
160
220
 
161
- // Handle both single result and array of results (RepeatCount > 1)
162
- let finalResult;
163
- let allPassed = true;
164
-
165
- if (Array.isArray(result)) {
166
- // Multiple iterations - check if all passed
167
- allPassed = result.every(r => r.status === 'Passed');
168
-
169
- // For GraphQL, return summary information
170
- // The full array is serialized in the result field
171
- finalResult = {
172
- testRunId: result[0]?.testRunId || '',
173
- status: allPassed ? 'Passed' as const : 'Failed' as const
174
- };
175
-
176
- // Publish completion for each iteration
177
- if (pubSub) {
178
- for (const iterationResult of result) {
179
- this.publishComplete(pubSub, userPayload, iterationResult);
180
- }
181
- }
182
-
183
- LogStatus(`[RunTestResolver] Test completed: ${result.length} iterations, ${allPassed ? 'all passed' : 'some failed'} in ${Date.now() - startTime}ms`);
184
- } else {
185
- // Single result
186
- finalResult = result;
187
- allPassed = result.status === 'Passed';
221
+ const testRunResult = this.buildTestRunResult(result, startTime, pubSub, userPayload);
188
222
 
189
- // Publish completion
190
- if (pubSub && result.testRunId) {
191
- this.publishComplete(pubSub, userPayload, result);
192
- }
193
-
194
- LogStatus(`[RunTestResolver] Test completed: ${result.status} in ${Date.now() - startTime}ms`);
223
+ // Publish fire-and-forget completion event (used by background mode, harmless in sync mode)
224
+ if (pubSub) {
225
+ this.publishFireAndForgetComplete(pubSub, userPayload, testId, testRunResult);
195
226
  }
196
227
 
197
- const executionTime = Date.now() - startTime;
198
-
199
- return {
200
- success: allPassed,
201
- result: JSON.stringify(result), // Full result (single or array)
202
- executionTimeMs: executionTime
203
- };
228
+ return testRunResult;
204
229
 
205
230
  } catch (error) {
206
231
  const executionTime = Date.now() - startTime;
207
232
  const errorMsg = (error as Error).message;
208
-
209
233
  LogError(`[RunTestResolver] Test execution failed: ${errorMsg}`);
210
234
 
211
- // Publish error
212
235
  if (pubSub) {
213
236
  this.publishError(pubSub, userPayload, testId, errorMsg);
214
237
  }
215
238
 
216
- return {
217
- success: false,
218
- errorMessage: errorMsg,
219
- result: JSON.stringify({}),
220
- executionTimeMs: executionTime
221
- };
239
+ return { success: false, errorMessage: errorMsg, result: JSON.stringify({}), executionTimeMs: executionTime };
222
240
  }
223
241
  }
224
242
 
225
243
  /**
226
- * Execute a test suite
244
+ * Execute a test in background (fire-and-forget).
245
+ * Errors are published via PubSub, not propagated.
227
246
  */
228
- @Mutation(() => TestSuiteRunResult)
229
- async RunTestSuite(
230
- @Arg('suiteId') suiteId: string,
231
- @Arg('verbose', { nullable: true }) verbose: boolean = true,
232
- @Arg('environment', { nullable: true }) environment?: string,
233
- @Arg('parallel', { nullable: true }) parallel: boolean = false,
234
- @Arg('tags', { nullable: true }) tags?: string,
235
- @Arg('variables', { nullable: true }) variables?: string,
236
- @Arg('selectedTestIds', { nullable: true }) selectedTestIds?: string,
237
- @Arg('sequenceStart', () => Int, { nullable: true }) sequenceStart?: number,
238
- @Arg('sequenceEnd', () => Int, { nullable: true }) sequenceEnd?: number,
239
- @PubSub() pubSub?: PubSubEngine,
240
- @Ctx() { userPayload }: AppContext = {} as AppContext
247
+ private executeTestInBackground(
248
+ testId: string,
249
+ verbose: boolean,
250
+ environment: string | undefined,
251
+ tags: string | undefined,
252
+ variables: string | undefined,
253
+ pubSub: PubSubEngine,
254
+ userPayload: UserPayload,
255
+ user: UserInfo
256
+ ): void {
257
+ this.executeTest(testId, verbose, environment, tags, variables, pubSub, userPayload, user)
258
+ .catch((error: unknown) => {
259
+ const errorMessage = (error instanceof Error) ? error.message : 'Unknown background test execution error';
260
+ LogError(`🔥 Fire-and-forget test execution failed: ${errorMessage}`, undefined, error);
261
+ this.publishFireAndForgetError(pubSub, userPayload, testId, errorMessage);
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Execute a test suite synchronously and return the result.
267
+ */
268
+ private async executeSuite(
269
+ suiteId: string,
270
+ verbose: boolean,
271
+ environment: string | undefined,
272
+ parallel: boolean,
273
+ tags: string | undefined,
274
+ variables: string | undefined,
275
+ selectedTestIds: string | undefined,
276
+ sequenceStart: number | undefined,
277
+ sequenceEnd: number | undefined,
278
+ pubSub: PubSubEngine | undefined,
279
+ userPayload: UserPayload,
280
+ user: UserInfo
241
281
  ): Promise<TestSuiteRunResult> {
242
282
  const startTime = Date.now();
243
283
 
244
284
  try {
245
- const user = this.GetUserFromPayload(userPayload);
246
- if (!user) {
247
- throw new Error('User context required');
248
- }
249
-
250
285
  LogStatus(`[RunTestResolver] Starting suite execution: ${suiteId}`);
251
286
 
252
287
  const engine = TestEngine.Instance;
253
288
  await engine.Config(verbose, user);
254
289
 
255
- // Create progress callback
256
- const progressCallback = pubSub ?
257
- this.createProgressCallback(pubSub, userPayload, suiteId) :
258
- undefined;
259
-
260
- // Create log callback to stream driver/engine logs to the UI in real-time
261
- const logCallback = pubSub ?
262
- this.createLogCallback(pubSub, userPayload, suiteId) :
263
- undefined;
264
-
265
- // Parse selectedTestIds from JSON string if provided
266
- let parsedSelectedTestIds: string[] | undefined;
267
- if (selectedTestIds) {
268
- try {
269
- parsedSelectedTestIds = JSON.parse(selectedTestIds);
270
- } catch (e) {
271
- LogError(`[RunTestResolver] Failed to parse selectedTestIds: ${selectedTestIds}`);
272
- }
273
- }
274
-
275
- // Parse variables from JSON string if provided
276
- let parsedVariables: TestRunVariables | undefined;
277
- if (variables) {
278
- try {
279
- parsedVariables = JSON.parse(variables);
280
- } catch (e) {
281
- LogError(`[RunTestResolver] Failed to parse variables: ${variables}`);
282
- }
283
- }
290
+ const progressCallback = pubSub ? this.createProgressCallback(pubSub, userPayload, suiteId) : undefined;
291
+ const logCallback = pubSub ? this.createLogCallback(pubSub, userPayload, suiteId) : undefined;
292
+ const parsedSelectedTestIds = this.parseJsonArray(selectedTestIds, 'selectedTestIds');
293
+ const parsedVariables = this.parseVariablesJson(variables);
284
294
 
285
295
  const options = {
286
- verbose,
287
- environment,
288
- parallel,
289
- tags,
290
- variables: parsedVariables,
291
- selectedTestIds: parsedSelectedTestIds,
292
- sequenceStart,
293
- sequenceEnd,
294
- progressCallback,
295
- logCallback
296
+ verbose, environment, parallel, tags,
297
+ variables: parsedVariables, selectedTestIds: parsedSelectedTestIds,
298
+ sequenceStart, sequenceEnd, progressCallback, logCallback
296
299
  };
297
300
 
298
301
  const result = await engine.RunSuite(suiteId, options, user);
299
-
300
302
  const executionTime = Date.now() - startTime;
301
303
 
302
304
  LogStatus(`[RunTestResolver] Suite completed: ${result.totalTests} tests in ${executionTime}ms`);
303
305
 
304
- return {
306
+ const suiteRunResult: TestSuiteRunResult = {
305
307
  success: result.status === 'Completed',
306
308
  result: JSON.stringify(result),
307
309
  executionTimeMs: executionTime
308
310
  };
309
311
 
312
+ // Publish fire-and-forget completion event
313
+ if (pubSub) {
314
+ this.publishFireAndForgetSuiteComplete(pubSub, userPayload, suiteId, suiteRunResult);
315
+ }
316
+
317
+ return suiteRunResult;
318
+
310
319
  } catch (error) {
311
320
  const executionTime = Date.now() - startTime;
312
321
  const errorMsg = (error as Error).message;
313
-
314
322
  LogError(`[RunTestResolver] Suite execution failed: ${errorMsg}`);
315
323
 
316
- return {
317
- success: false,
318
- errorMessage: errorMsg,
319
- result: JSON.stringify({}),
320
- executionTimeMs: executionTime
321
- };
324
+ return { success: false, errorMessage: errorMsg, result: JSON.stringify({}), executionTimeMs: executionTime };
322
325
  }
323
326
  }
324
327
 
325
328
  /**
326
- * Query to check if a test is currently running
329
+ * Execute a test suite in background (fire-and-forget).
330
+ * Errors are published via PubSub, not propagated.
327
331
  */
328
- @Query(() => Boolean)
329
- async IsTestRunning(
330
- @Arg('testId') testId: string,
331
- @Ctx() { userPayload }: AppContext = {} as AppContext
332
- ): Promise<boolean> {
333
- // TODO: Implement running test tracking
334
- // For now, return false
335
- return false;
332
+ private executeSuiteInBackground(
333
+ suiteId: string,
334
+ verbose: boolean,
335
+ environment: string | undefined,
336
+ parallel: boolean,
337
+ tags: string | undefined,
338
+ variables: string | undefined,
339
+ selectedTestIds: string | undefined,
340
+ sequenceStart: number | undefined,
341
+ sequenceEnd: number | undefined,
342
+ pubSub: PubSubEngine,
343
+ userPayload: UserPayload,
344
+ user: UserInfo
345
+ ): void {
346
+ this.executeSuite(
347
+ suiteId, verbose, environment, parallel, tags, variables,
348
+ selectedTestIds, sequenceStart, sequenceEnd, pubSub, userPayload, user
349
+ ).catch((error: unknown) => {
350
+ const errorMessage = (error instanceof Error) ? error.message : 'Unknown background suite execution error';
351
+ LogError(`🔥 Fire-and-forget suite execution failed: ${errorMessage}`, undefined, error);
352
+ this.publishFireAndForgetSuiteError(pubSub, userPayload, suiteId, errorMessage);
353
+ });
354
+ }
355
+
356
+ // ===== Result Building =====
357
+
358
+ /**
359
+ * Build a TestRunResult from the engine result, handling both single and array results.
360
+ */
361
+ private buildTestRunResult(
362
+ result: EngineTestRunResult | EngineTestRunResult[],
363
+ startTime: number,
364
+ pubSub: PubSubEngine | undefined,
365
+ userPayload: UserPayload
366
+ ): TestRunResult {
367
+ const executionTime = Date.now() - startTime;
368
+
369
+ if (Array.isArray(result)) {
370
+ const allPassed = result.every((r) => r.status === 'Passed');
371
+
372
+ if (pubSub) {
373
+ for (const iterationResult of result) {
374
+ this.publishComplete(pubSub, userPayload, iterationResult);
375
+ }
376
+ }
377
+
378
+ LogStatus(`[RunTestResolver] Test completed: ${result.length} iterations, ${allPassed ? 'all passed' : 'some failed'} in ${executionTime}ms`);
379
+ return { success: allPassed, result: JSON.stringify(result), executionTimeMs: executionTime };
380
+ }
381
+
382
+ const passed = result.status === 'Passed';
383
+
384
+ if (pubSub && result.testRunId) {
385
+ this.publishComplete(pubSub, userPayload, result);
386
+ }
387
+
388
+ LogStatus(`[RunTestResolver] Test completed: ${result.status} in ${executionTime}ms`);
389
+ return { success: passed, result: JSON.stringify(result), executionTimeMs: executionTime };
390
+ }
391
+
392
+ // ===== Input Parsing =====
393
+
394
+ private parseVariablesJson(variables: string | undefined): TestRunVariables | undefined {
395
+ if (!variables) return undefined;
396
+ try {
397
+ return JSON.parse(variables) as TestRunVariables;
398
+ } catch (e) {
399
+ LogError(`[RunTestResolver] Failed to parse variables: ${variables}`);
400
+ return undefined;
401
+ }
402
+ }
403
+
404
+ private parseJsonArray(json: string | undefined, fieldName: string): string[] | undefined {
405
+ if (!json) return undefined;
406
+ try {
407
+ return JSON.parse(json) as string[];
408
+ } catch (e) {
409
+ LogError(`[RunTestResolver] Failed to parse ${fieldName}: ${json}`);
410
+ return undefined;
411
+ }
336
412
  }
337
413
 
338
414
  // ===== Progress Callbacks =====
@@ -349,25 +425,26 @@ export class RunTestResolver extends ResolverBase {
349
425
  step: string;
350
426
  percentage: number;
351
427
  message: string;
352
- metadata?: any;
428
+ metadata?: Record<string, unknown>;
353
429
  }) => {
354
430
  LogStatus(`[RunTestResolver] Progress: ${progress.step} - ${progress.percentage}%`);
355
431
 
356
- // Get test run from metadata
357
- const testRun = progress.metadata?.testRun;
432
+ const testRun = progress.metadata?.testRun as Record<string, unknown> | undefined;
358
433
 
359
434
  const progressMsg: TestExecutionStreamMessage = {
360
435
  sessionId: userPayload.sessionId || '',
361
- testRunId: testRun?.ID || testId,
436
+ testRunId: (testRun as { ID?: string })?.ID || testId,
362
437
  type: 'progress',
363
- testRun: testRun ? testRun.GetAll() : undefined,
438
+ testRun: testRun && typeof (testRun as { GetAll?: () => Record<string, unknown> }).GetAll === 'function'
439
+ ? (testRun as { GetAll: () => Record<string, unknown> }).GetAll()
440
+ : undefined,
364
441
  progress: {
365
442
  currentStep: progress.step,
366
443
  percentage: progress.percentage,
367
444
  message: progress.message,
368
- testName: progress.metadata?.testName,
369
- driverType: progress.metadata?.driverType,
370
- oracleEvaluation: progress.metadata?.oracleType
445
+ testName: progress.metadata?.testName as string | undefined,
446
+ driverType: progress.metadata?.driverType as string | undefined,
447
+ oracleEvaluation: progress.metadata?.oracleType as string | undefined
371
448
  },
372
449
  timestamp: new Date()
373
450
  };
@@ -402,6 +479,8 @@ export class RunTestResolver extends ResolverBase {
402
479
  };
403
480
  }
404
481
 
482
+ // ===== PubSub Publishing =====
483
+
405
484
  private publishProgress(pubSub: PubSubEngine, data: TestExecutionStreamMessage, userPayload: UserPayload) {
406
485
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
407
486
  message: JSON.stringify({
@@ -414,7 +493,7 @@ export class RunTestResolver extends ResolverBase {
414
493
  });
415
494
  }
416
495
 
417
- private publishComplete(pubSub: PubSubEngine, userPayload: UserPayload, result: any) {
496
+ private publishComplete(pubSub: PubSubEngine, userPayload: UserPayload, result: EngineTestRunResult) {
418
497
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
419
498
  message: JSON.stringify({
420
499
  resolver: 'RunTestResolver',
@@ -449,4 +528,120 @@ export class RunTestResolver extends ResolverBase {
449
528
  sessionId: userPayload.sessionId,
450
529
  });
451
530
  }
531
+
532
+ /**
533
+ * Publish a fire-and-forget completion event for a single test.
534
+ * Includes testId for client-side correlation and the full result.
535
+ */
536
+ private publishFireAndForgetComplete(
537
+ pubSub: PubSubEngine,
538
+ userPayload: UserPayload,
539
+ testId: string,
540
+ testRunResult: TestRunResult
541
+ ) {
542
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
543
+ message: JSON.stringify({
544
+ resolver: 'RunTestResolver',
545
+ type: 'FireAndForgetComplete',
546
+ status: 'ok',
547
+ data: {
548
+ sessionId: userPayload.sessionId,
549
+ testId,
550
+ type: 'complete',
551
+ success: testRunResult.success,
552
+ errorMessage: testRunResult.errorMessage,
553
+ executionTimeMs: testRunResult.executionTimeMs,
554
+ result: testRunResult.result,
555
+ timestamp: new Date()
556
+ },
557
+ }),
558
+ sessionId: userPayload.sessionId,
559
+ });
560
+ }
561
+
562
+ /**
563
+ * Publish a fire-and-forget error event for a single test.
564
+ */
565
+ private publishFireAndForgetError(
566
+ pubSub: PubSubEngine,
567
+ userPayload: UserPayload,
568
+ testId: string,
569
+ errorMessage: string
570
+ ) {
571
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
572
+ message: JSON.stringify({
573
+ resolver: 'RunTestResolver',
574
+ type: 'FireAndForgetComplete',
575
+ status: 'ok',
576
+ data: {
577
+ sessionId: userPayload.sessionId,
578
+ testId,
579
+ type: 'complete',
580
+ success: false,
581
+ errorMessage,
582
+ result: JSON.stringify({ success: false, errorMessage }),
583
+ timestamp: new Date()
584
+ },
585
+ }),
586
+ sessionId: userPayload.sessionId,
587
+ });
588
+ }
589
+
590
+ /**
591
+ * Publish a fire-and-forget completion event for a test suite.
592
+ * Includes suiteId for client-side correlation and the full result.
593
+ */
594
+ private publishFireAndForgetSuiteComplete(
595
+ pubSub: PubSubEngine,
596
+ userPayload: UserPayload,
597
+ suiteId: string,
598
+ suiteRunResult: TestSuiteRunResult
599
+ ) {
600
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
601
+ message: JSON.stringify({
602
+ resolver: 'RunTestResolver',
603
+ type: 'FireAndForgetSuiteComplete',
604
+ status: 'ok',
605
+ data: {
606
+ sessionId: userPayload.sessionId,
607
+ suiteId,
608
+ type: 'complete',
609
+ success: suiteRunResult.success,
610
+ errorMessage: suiteRunResult.errorMessage,
611
+ executionTimeMs: suiteRunResult.executionTimeMs,
612
+ result: suiteRunResult.result,
613
+ timestamp: new Date()
614
+ },
615
+ }),
616
+ sessionId: userPayload.sessionId,
617
+ });
618
+ }
619
+
620
+ /**
621
+ * Publish a fire-and-forget error event for a test suite.
622
+ */
623
+ private publishFireAndForgetSuiteError(
624
+ pubSub: PubSubEngine,
625
+ userPayload: UserPayload,
626
+ suiteId: string,
627
+ errorMessage: string
628
+ ) {
629
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
630
+ message: JSON.stringify({
631
+ resolver: 'RunTestResolver',
632
+ type: 'FireAndForgetSuiteComplete',
633
+ status: 'ok',
634
+ data: {
635
+ sessionId: userPayload.sessionId,
636
+ suiteId,
637
+ type: 'complete',
638
+ success: false,
639
+ errorMessage,
640
+ result: JSON.stringify({ success: false, errorMessage }),
641
+ timestamp: new Date()
642
+ },
643
+ }),
644
+ sessionId: userPayload.sessionId,
645
+ });
646
+ }
452
647
  }