@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.
- package/dist/generated/generated.d.ts +111 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +631 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +2 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +15 -2
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts +49 -5
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +249 -141
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +3 -2
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/package.json +53 -53
- package/src/generated/generated.ts +433 -1
- package/src/index.ts +3 -0
- package/src/resolvers/RunAIAgentResolver.ts +23 -1
- package/src/resolvers/RunTestResolver.ts +369 -174
- package/src/rest/setupRESTEndpoints.ts +3 -2
|
@@ -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?:
|
|
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
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
|
244
|
+
* Execute a test in background (fire-and-forget).
|
|
245
|
+
* Errors are published via PubSub, not propagated.
|
|
227
246
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
329
|
+
* Execute a test suite in background (fire-and-forget).
|
|
330
|
+
* Errors are published via PubSub, not propagated.
|
|
327
331
|
*/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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?:
|
|
428
|
+
metadata?: Record<string, unknown>;
|
|
353
429
|
}) => {
|
|
354
430
|
LogStatus(`[RunTestResolver] Progress: ${progress.step} - ${progress.percentage}%`);
|
|
355
431
|
|
|
356
|
-
|
|
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
|
|
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:
|
|
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
|
}
|