Skip to content

Commit bf2ef90

Browse files
Copilotquanru
andauthored
feat(cli): categorize execution results with resultType for better failure tracking (#1265)
* Initial plan * fix(cli): track assertion failures correctly with continueOnError=true * feat(cli): add resultType to categorize execution results - Added 'resultType' field to MidsceneYamlConfigResult to distinguish between: - 'success': All tasks completed successfully - 'failed': Complete execution failure (player error) - 'partialFailed': Some tasks failed with continueOnError=true - 'notExecuted': Not executed due to previous failures - Updated BatchRunner to track and display partial failures separately - Added 'Partial failed' count to execution summary - Updated tests to verify correct categorization This provides better visibility into what actually happened during execution, distinguishing between complete failures and partial failures where tasks failed but execution continued due to continueOnError setting. Co-authored-by: quanru <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: quanru <[email protected]>
1 parent de314e3 commit bf2ef90

File tree

3 files changed

+146
-11
lines changed

3 files changed

+146
-11
lines changed

packages/cli/src/batch-runner.ts

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,28 @@ class BatchRunner {
355355

356356
for (const context of executedContexts) {
357357
const { file, player, duration } = context;
358-
const success = player.status !== 'error';
358+
// Determine result type based on player and task statuses
359+
const hasFailedTasks =
360+
player.taskStatusList?.some((task) => task.status === 'error') ?? false;
361+
const hasPlayerError = player.status === 'error';
362+
363+
let success: boolean;
364+
let resultType: 'success' | 'failed' | 'partialFailed';
365+
366+
if (hasPlayerError) {
367+
// Complete failure - player itself failed
368+
success = false;
369+
resultType = 'failed';
370+
} else if (hasFailedTasks) {
371+
// Partial failure - some tasks failed but execution continued (continueOnError)
372+
success = false;
373+
resultType = 'partialFailed';
374+
} else {
375+
// Success - all tasks completed successfully
376+
success = true;
377+
resultType = 'success';
378+
}
379+
359380
let reportFile: string | undefined;
360381

361382
if (player.reportFile) {
@@ -375,9 +396,11 @@ class BatchRunner {
375396
output: outputPath,
376397
report: reportFile,
377398
duration,
399+
resultType,
378400
error:
379401
player.errorInSetup?.message ||
380-
(player.status === 'error' ? 'Execution failed' : undefined),
402+
(hasPlayerError ? 'Execution failed' : undefined) ||
403+
(hasFailedTasks ? 'Some tasks failed' : undefined),
381404
});
382405
}
383406

@@ -389,6 +412,7 @@ class BatchRunner {
389412
output: undefined,
390413
report: undefined,
391414
duration: 0,
415+
resultType: 'notExecuted',
392416
error: 'Not executed (previous task failed)',
393417
});
394418
}
@@ -435,8 +459,15 @@ class BatchRunner {
435459
const indexData = {
436460
summary: {
437461
total: this.results.length,
438-
successful: this.results.filter((r) => r.success).length,
439-
failed: this.results.filter((r) => !r.success).length,
462+
successful: this.results.filter((r) => r.resultType === 'success')
463+
.length,
464+
failed: this.results.filter((r) => r.resultType === 'failed').length,
465+
partialFailed: this.results.filter(
466+
(r) => r.resultType === 'partialFailed',
467+
).length,
468+
notExecuted: this.results.filter(
469+
(r) => r.resultType === 'notExecuted',
470+
).length,
440471
totalDuration: this.results.reduce(
441472
(sum, r) => sum + (r.duration || 0),
442473
0,
@@ -446,6 +477,7 @@ class BatchRunner {
446477
results: this.results.map((result) => ({
447478
script: relative(outputDir, result.file),
448479
success: result.success,
480+
resultType: result.resultType,
449481
output: result.output
450482
? (() => {
451483
const relativePath = relative(outputDir, result.output);
@@ -473,17 +505,26 @@ class BatchRunner {
473505
total: number;
474506
successful: number;
475507
failed: number;
508+
partialFailed: number;
476509
notExecuted: number;
477510
totalDuration: number;
478511
} {
479-
const successful = this.results.filter((r) => r.success).length;
480-
const notExecuted = this.results.filter((r) => !r.executed).length;
481-
const failed = this.results.filter((r) => r.executed && !r.success).length;
512+
const successful = this.results.filter(
513+
(r) => r.resultType === 'success',
514+
).length;
515+
const failed = this.results.filter((r) => r.resultType === 'failed').length;
516+
const partialFailed = this.results.filter(
517+
(r) => r.resultType === 'partialFailed',
518+
).length;
519+
const notExecuted = this.results.filter(
520+
(r) => r.resultType === 'notExecuted',
521+
).length;
482522

483523
return {
484524
total: this.results.length,
485525
successful,
486526
failed,
527+
partialFailed,
487528
notExecuted,
488529
totalDuration: this.results.reduce(
489530
(sum, r) => sum + (r.duration || 0),
@@ -494,16 +535,26 @@ class BatchRunner {
494535

495536
getFailedFiles(): string[] {
496537
return this.results
497-
.filter((r) => r.executed && !r.success)
538+
.filter((r) => r.resultType === 'failed')
539+
.map((r) => r.file);
540+
}
541+
542+
getPartialFailedFiles(): string[] {
543+
return this.results
544+
.filter((r) => r.resultType === 'partialFailed')
498545
.map((r) => r.file);
499546
}
500547

501548
getNotExecutedFiles(): string[] {
502-
return this.results.filter((r) => !r.executed).map((r) => r.file);
549+
return this.results
550+
.filter((r) => r.resultType === 'notExecuted')
551+
.map((r) => r.file);
503552
}
504553

505554
getSuccessfulFiles(): string[] {
506-
return this.results.filter((r) => r.success).map((r) => r.file);
555+
return this.results
556+
.filter((r) => r.resultType === 'success')
557+
.map((r) => r.file);
507558
}
508559

509560
getResults(): MidsceneYamlConfigResult[] {
@@ -512,12 +563,16 @@ class BatchRunner {
512563

513564
printExecutionSummary(): boolean {
514565
const summary = this.getExecutionSummary();
515-
const success = summary.failed === 0 && summary.notExecuted === 0;
566+
const success =
567+
summary.failed === 0 &&
568+
summary.partialFailed === 0 &&
569+
summary.notExecuted === 0;
516570

517571
console.log('\n📊 Execution Summary:');
518572
console.log(` Total files: ${summary.total}`);
519573
console.log(` Successful: ${summary.successful}`);
520574
console.log(` Failed: ${summary.failed}`);
575+
console.log(` Partial failed: ${summary.partialFailed}`);
521576
console.log(` Not executed: ${summary.notExecuted}`);
522577
console.log(` Duration: ${(summary.totalDuration / 1000).toFixed(2)}s`);
523578
console.log(` Summary: ${this.getSummaryAbsolutePath()}`);
@@ -536,6 +591,15 @@ class BatchRunner {
536591
});
537592
}
538593

594+
if (summary.partialFailed > 0) {
595+
console.log(
596+
'\n⚠️ Partial failed files (some tasks failed with continueOnError)',
597+
);
598+
this.getPartialFailedFiles().forEach((file) => {
599+
console.log(` ${file}`);
600+
});
601+
}
602+
539603
if (summary.notExecuted > 0) {
540604
console.log('\n⏸️ Not executed files');
541605
this.getNotExecutedFiles().forEach((file) => {

packages/cli/tests/unit-test/batch-runner.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,69 @@ describe('BatchRunner', () => {
369369
);
370370
consoleSpy.mockRestore();
371371
});
372+
373+
test('continueOnError: failed tasks should be counted as failed files', async () => {
374+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
375+
376+
// Create a mock player that simulates continueOnError behavior:
377+
// - player.status = 'done' (execution completed)
378+
// - but taskStatusList contains failed tasks
379+
const createMockPlayerWithFailedTasks = (
380+
fileName: string,
381+
): ScriptPlayer<MidsceneYamlScriptEnv> => {
382+
const isFile1 = fileName === 'file1.yml';
383+
const mockPlayer = {
384+
status: 'done' as ScriptPlayerStatusValue, // Always 'done' with continueOnError
385+
output: '/test/output/file.json',
386+
reportFile: '/test/report.html',
387+
result: { test: 'data' },
388+
errorInSetup: null,
389+
taskStatusList: isFile1
390+
? [
391+
{
392+
status: 'error',
393+
error: new Error(
394+
'Assertion failed: this is not a search engine',
395+
),
396+
},
397+
{ status: 'done' },
398+
]
399+
: [{ status: 'done' }],
400+
run: vi.fn().mockImplementation(async () => {
401+
return undefined;
402+
}),
403+
script: mockYamlScript,
404+
setupAgent: vi.fn(),
405+
unnamedResultIndex: 0,
406+
pageAgent: null,
407+
currentTaskIndex: undefined,
408+
agentStatusTip: '',
409+
};
410+
return mockPlayer as unknown as ScriptPlayer<MidsceneYamlScriptEnv>;
411+
};
412+
413+
vi.mocked(createYamlPlayer).mockImplementation(async (file) =>
414+
createMockPlayerWithFailedTasks(file),
415+
);
416+
417+
const config = { ...mockBatchConfig, continueOnError: true };
418+
const executor = new BatchRunner(config);
419+
await executor.run();
420+
421+
const summary = executor.getExecutionSummary();
422+
const success = executor.printExecutionSummary();
423+
424+
// Files with failed tasks and continueOnError should be counted as partialFailed
425+
expect(summary.partialFailed).toBe(1);
426+
expect(summary.failed).toBe(0); // No complete failures
427+
expect(summary.successful).toBe(2); // The other two files succeeded
428+
expect(success).toBe(false); // Overall should still be false due to partial failure
429+
expect(consoleSpy).toHaveBeenCalledWith(
430+
expect.stringContaining('⚠️ Partial failed files'),
431+
);
432+
433+
consoleSpy.mockRestore();
434+
});
372435
});
373436

374437
describe('BatchRunner output file existence check', () => {

packages/core/src/yaml.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,12 @@ export interface MidsceneYamlConfigResult {
235235
report?: string | null;
236236
error?: string;
237237
duration?: number;
238+
/**
239+
* Type of result:
240+
* - 'success': All tasks completed successfully
241+
* - 'failed': Execution failed (player error)
242+
* - 'partialFailed': Some tasks failed but execution continued (continueOnError)
243+
* - 'notExecuted': Not executed due to previous failures
244+
*/
245+
resultType?: 'success' | 'failed' | 'partialFailed' | 'notExecuted';
238246
}

0 commit comments

Comments
 (0)