@midscene/test 1.15.2-beta-20261012123219.0 → 1.15.2
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/es/cli/index.mjs +235 -129
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +72 -106
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +54 -30
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +237 -130
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +233 -127
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +70 -110
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +53 -29
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +236 -129
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +10 -6
- package/dist/types/index.d.ts +1 -85
- package/dist/types/midscene/index.d.ts +3 -1
- package/package.json +13 -13
package/dist/lib/cli/index.js
CHANGED
|
@@ -318,7 +318,6 @@ async function loadTestProject(configPath) {
|
|
|
318
318
|
}
|
|
319
319
|
const external_node_fs_namespaceObject = require("node:fs");
|
|
320
320
|
const external_tinyglobby_namespaceObject = require("tinyglobby");
|
|
321
|
-
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
322
321
|
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
323
322
|
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
324
323
|
projectId,
|
|
@@ -336,6 +335,70 @@ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=
|
|
|
336
335
|
] : []
|
|
337
336
|
])).digest('hex');
|
|
338
337
|
const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
|
|
338
|
+
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
339
|
+
function parseWorkflowSource(source) {
|
|
340
|
+
const stack = [];
|
|
341
|
+
const nodes = [];
|
|
342
|
+
const sensitiveNodes = new Set();
|
|
343
|
+
const sensitiveValues = new Set();
|
|
344
|
+
const redactedPaths = new Set();
|
|
345
|
+
const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
|
|
346
|
+
const parsed = (0, external_js_yaml_namespaceObject.load)(source, {
|
|
347
|
+
schema: external_js_yaml_namespaceObject.JSON_SCHEMA,
|
|
348
|
+
listener (event, state) {
|
|
349
|
+
if ('open' === event) return void stack.push({
|
|
350
|
+
start: state.position + sourceOffset,
|
|
351
|
+
end: 0,
|
|
352
|
+
value: null,
|
|
353
|
+
children: []
|
|
354
|
+
});
|
|
355
|
+
const node = stack.pop();
|
|
356
|
+
node.end = state.position + sourceOffset;
|
|
357
|
+
node.value = state.result;
|
|
358
|
+
stack.at(-1)?.children.push(node);
|
|
359
|
+
nodes.push(node);
|
|
360
|
+
if ('mapping' !== state.kind) return;
|
|
361
|
+
for(let index = 0; index < node.children.length; index += 2){
|
|
362
|
+
const key = node.children[index].value;
|
|
363
|
+
const value = node.children[index + 1];
|
|
364
|
+
if ('string' == typeof key && (0, test_runner_namespaceObject.isReportSecretKey)(key) && value) {
|
|
365
|
+
sensitiveNodes.add(value);
|
|
366
|
+
if (null !== value.value) sensitiveValues.add(value.value);
|
|
367
|
+
redactedPaths.add(`$..[${JSON.stringify(key)}]`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
|
|
373
|
+
let offset = 0;
|
|
374
|
+
let redacted = '';
|
|
375
|
+
for (const range of ranges){
|
|
376
|
+
if (range.start < offset) continue;
|
|
377
|
+
const text = source.slice(range.start, range.end);
|
|
378
|
+
const leading = text.match(/^\s*/)?.[0] ?? '';
|
|
379
|
+
const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
|
|
380
|
+
redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
|
|
381
|
+
offset = range.end;
|
|
382
|
+
}
|
|
383
|
+
redacted += source.slice(offset);
|
|
384
|
+
const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
|
|
385
|
+
return {
|
|
386
|
+
parsed,
|
|
387
|
+
sourceYaml: {
|
|
388
|
+
value,
|
|
389
|
+
...redactedPaths.size ? {
|
|
390
|
+
redactedPaths: [
|
|
391
|
+
...redactedPaths
|
|
392
|
+
]
|
|
393
|
+
} : {},
|
|
394
|
+
...value !== redacted ? {
|
|
395
|
+
truncatedPaths: [
|
|
396
|
+
'$'
|
|
397
|
+
]
|
|
398
|
+
} : {}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
339
402
|
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
340
403
|
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
341
404
|
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
@@ -410,16 +473,15 @@ const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
|
410
473
|
const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
|
|
411
474
|
const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
|
|
412
475
|
function collectWorkflowDocument(source, options) {
|
|
413
|
-
let
|
|
476
|
+
let snapshot;
|
|
414
477
|
try {
|
|
415
|
-
|
|
416
|
-
schema: external_js_yaml_namespaceObject.JSON_SCHEMA
|
|
417
|
-
});
|
|
478
|
+
snapshot = parseWorkflowSource((0, external_node_fs_namespaceObject.readFileSync)(source.absolutePath, 'utf8'));
|
|
418
479
|
} catch (error) {
|
|
419
480
|
throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
420
481
|
sourcePath: source.sourcePath
|
|
421
482
|
}, error);
|
|
422
483
|
}
|
|
484
|
+
const { parsed, sourceYaml } = snapshot;
|
|
423
485
|
if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
|
|
424
486
|
const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
|
|
425
487
|
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
@@ -434,6 +496,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
434
496
|
const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
|
|
435
497
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
436
498
|
...step,
|
|
499
|
+
templateInput: step.input,
|
|
437
500
|
input: resolveWorkflowVariables(step.input, {
|
|
438
501
|
variables: options.variables,
|
|
439
502
|
env: options.env ?? process.env,
|
|
@@ -519,6 +582,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
519
582
|
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
520
583
|
projectId: source.projectId,
|
|
521
584
|
sourcePath,
|
|
585
|
+
sourceYaml,
|
|
522
586
|
lifecycle,
|
|
523
587
|
cases
|
|
524
588
|
};
|
|
@@ -534,6 +598,15 @@ const writeJson = async (path, value)=>{
|
|
|
534
598
|
throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
|
|
535
599
|
}
|
|
536
600
|
};
|
|
601
|
+
const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, 'plan.json'), {
|
|
602
|
+
schemaVersion: 1,
|
|
603
|
+
createdAt: new Date().toISOString(),
|
|
604
|
+
projects: projects.map((item)=>({
|
|
605
|
+
projectId: item.project.projectId,
|
|
606
|
+
name: item.project.name,
|
|
607
|
+
documents: item.plans ?? []
|
|
608
|
+
}))
|
|
609
|
+
});
|
|
537
610
|
const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
|
|
538
611
|
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
539
612
|
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
@@ -563,7 +636,7 @@ const relativeToSummary = (summaryPath, target)=>{
|
|
|
563
636
|
const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
|
|
564
637
|
return toPosix(value || '.');
|
|
565
638
|
};
|
|
566
|
-
const errorJson =
|
|
639
|
+
const errorJson = test_runner_namespaceObject.sanitizeReportError;
|
|
567
640
|
const writeTestProjectRunResult = (options)=>{
|
|
568
641
|
const { result } = options;
|
|
569
642
|
const fact = (path)=>toPosix(path);
|
|
@@ -583,13 +656,17 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
583
656
|
configPath: options.configPath
|
|
584
657
|
} : {},
|
|
585
658
|
factsRoot: '.',
|
|
659
|
+
plan: 'plan.json',
|
|
660
|
+
...result.operationLog ? {
|
|
661
|
+
operationLog: {
|
|
662
|
+
...result.operationLog,
|
|
663
|
+
path: relativeToSummary(result.summaryPath, result.operationLog.path)
|
|
664
|
+
}
|
|
665
|
+
} : {},
|
|
586
666
|
reportDir: relativeToSummary(result.summaryPath, result.reportDir),
|
|
587
667
|
...result.reportPath ? {
|
|
588
668
|
report: relativeToSummary(result.summaryPath, result.reportPath)
|
|
589
669
|
} : {},
|
|
590
|
-
...result.markdownPath ? {
|
|
591
|
-
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
592
|
-
} : {},
|
|
593
670
|
summary: result.summary,
|
|
594
671
|
projects: result.projects.map((project)=>({
|
|
595
672
|
projectId: project.projectId,
|
|
@@ -607,6 +684,12 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
607
684
|
startedAt: project.lifecycle.startedAt,
|
|
608
685
|
endedAt: project.lifecycle.endedAt,
|
|
609
686
|
durationMs: project.lifecycle.durationMs,
|
|
687
|
+
...project.lifecycle.cleanup ? {
|
|
688
|
+
cleanup: project.lifecycle.cleanup
|
|
689
|
+
} : {},
|
|
690
|
+
...project.lifecycle.pendingOperations ? {
|
|
691
|
+
pendingOperations: project.lifecycle.pendingOperations
|
|
692
|
+
} : {},
|
|
610
693
|
...project.lifecycle.setupError ? {
|
|
611
694
|
setupError: errorJson(project.lifecycle.setupError)
|
|
612
695
|
} : {},
|
|
@@ -633,6 +716,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
633
716
|
attemptId: attempt.runId,
|
|
634
717
|
attemptIndex: attempt.attemptIndex,
|
|
635
718
|
status: attempt.status,
|
|
719
|
+
...attempt.cleanup ? {
|
|
720
|
+
cleanup: attempt.cleanup
|
|
721
|
+
} : {},
|
|
722
|
+
...attempt.pendingOperations ? {
|
|
723
|
+
pendingOperations: attempt.pendingOperations
|
|
724
|
+
} : {},
|
|
725
|
+
...attempt.executionStatus ? {
|
|
726
|
+
executionStatus: attempt.executionStatus
|
|
727
|
+
} : {},
|
|
728
|
+
...attempt.retryDecision ? {
|
|
729
|
+
retryDecision: attempt.retryDecision
|
|
730
|
+
} : {},
|
|
636
731
|
resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
|
|
637
732
|
...attempt.reportPaths?.length ? {
|
|
638
733
|
reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -648,6 +743,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
648
743
|
},
|
|
649
744
|
sourcePath: document.sourcePath,
|
|
650
745
|
status: document.status,
|
|
746
|
+
...document.cleanup ? {
|
|
747
|
+
cleanup: document.cleanup
|
|
748
|
+
} : {},
|
|
749
|
+
...document.pendingOperations ? {
|
|
750
|
+
pendingOperations: document.pendingOperations
|
|
751
|
+
} : {},
|
|
752
|
+
...document.executionStatus ? {
|
|
753
|
+
executionStatus: document.executionStatus
|
|
754
|
+
} : {},
|
|
755
|
+
...document.retryDecision ? {
|
|
756
|
+
retryDecision: document.retryDecision
|
|
757
|
+
} : {},
|
|
651
758
|
resultFile: fact(workflowDocumentResultPath(document)),
|
|
652
759
|
...document.reportPaths?.length ? {
|
|
653
760
|
reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -787,6 +894,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
787
894
|
});
|
|
788
895
|
const collectionErrors = [];
|
|
789
896
|
const invocations = [];
|
|
897
|
+
const plans = [];
|
|
790
898
|
let filteredCaseCount = 0;
|
|
791
899
|
if (0 === sources.length) {
|
|
792
900
|
const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
@@ -800,7 +908,17 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
800
908
|
const collected = await options.collectDocument?.(source, project);
|
|
801
909
|
if (collected) {
|
|
802
910
|
filteredCaseCount += collected.filteredCaseCount;
|
|
803
|
-
if (collected.invocation)
|
|
911
|
+
if (collected.invocation) {
|
|
912
|
+
const plan = (0, test_runner_namespaceObject.buildTestRunReportPlan)(collected.invocation.document, {
|
|
913
|
+
defaultTimeoutMs: collected.invocation.defaultTimeoutMs,
|
|
914
|
+
retry: collected.invocation.retry
|
|
915
|
+
});
|
|
916
|
+
invocations.push({
|
|
917
|
+
...collected.invocation,
|
|
918
|
+
plan
|
|
919
|
+
});
|
|
920
|
+
plans.push(plan);
|
|
921
|
+
}
|
|
804
922
|
continue;
|
|
805
923
|
}
|
|
806
924
|
const document = collectWorkflowDocument(source, {
|
|
@@ -812,8 +930,18 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
812
930
|
document,
|
|
813
931
|
filtered: 0
|
|
814
932
|
} : filterDocumentCases(document, project);
|
|
933
|
+
const plan = (0, test_runner_namespaceObject.buildTestRunReportPlan)(document, {
|
|
934
|
+
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
935
|
+
defaultTimeoutMs,
|
|
936
|
+
retry: {
|
|
937
|
+
scope: 'case',
|
|
938
|
+
count: project.retry
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
plans.push(plan);
|
|
815
942
|
filteredCaseCount += filtered.filtered;
|
|
816
943
|
if (filtered.document) invocations.push({
|
|
944
|
+
plan,
|
|
817
945
|
document: filtered.document,
|
|
818
946
|
retry: {
|
|
819
947
|
scope: 'case',
|
|
@@ -846,6 +974,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
846
974
|
sources,
|
|
847
975
|
documents,
|
|
848
976
|
invocations,
|
|
977
|
+
plans,
|
|
849
978
|
collectionErrors,
|
|
850
979
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
851
980
|
filteredCaseCount
|
|
@@ -929,103 +1058,6 @@ async function prepareTestRun(options) {
|
|
|
929
1058
|
const external_node_events_namespaceObject = require("node:events");
|
|
930
1059
|
const report_namespaceObject = require("@midscene/core/report");
|
|
931
1060
|
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
932
|
-
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
933
|
-
const duration = (ms)=>{
|
|
934
|
-
const seconds = Math.round(ms / 1000);
|
|
935
|
-
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
936
|
-
};
|
|
937
|
-
const href = (from, target)=>(0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(from), target).split(external_node_path_namespaceObject.sep).map(encodeURIComponent).join('/');
|
|
938
|
-
function renderTestResultMarkdown(dump, options = {}) {
|
|
939
|
-
const { summary } = dump;
|
|
940
|
-
const outcome = 'success' === dump.status ? `✅ **${summary.passed}/${summary.total} passed**` : `❌ **${summary.failed} failed** · ${summary.passed} passed · ${summary.notRun} not run`;
|
|
941
|
-
const lines = [
|
|
942
|
-
'## Midscene Test',
|
|
943
|
-
'',
|
|
944
|
-
`${outcome} · ${duration(dump.durationMs)}`,
|
|
945
|
-
''
|
|
946
|
-
];
|
|
947
|
-
if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
|
|
948
|
-
const infrastructure = [
|
|
949
|
-
summary.collectionErrors ? `${summary.collectionErrors} collection errors` : '',
|
|
950
|
-
summary.documentFailures ? `${summary.documentFailures} document failures` : '',
|
|
951
|
-
summary.projectFailures ? `${summary.projectFailures} project failures` : ''
|
|
952
|
-
].filter(Boolean);
|
|
953
|
-
if (infrastructure.length) lines.push(`**Infrastructure:** ${infrastructure.join(' · ')}`, '');
|
|
954
|
-
const groups = [];
|
|
955
|
-
const errors = [];
|
|
956
|
-
const recordSteps = (label, steps)=>{
|
|
957
|
-
for (const step of steps)if ('failed' === step.status) errors.push(`${label} / ${step.node}: ${step.error?.message ?? 'Step failed'}`);
|
|
958
|
-
};
|
|
959
|
-
for (const project of dump.projects){
|
|
960
|
-
if (project.lifecycle?.setupError) errors.push(`${project.name}: ${project.lifecycle.setupError.message}`);
|
|
961
|
-
for (const error of project.lifecycle?.teardownErrors ?? [])errors.push(`${project.name}: ${error.message}`);
|
|
962
|
-
for (const error of project.collectionErrors)errors.push(`${project.name} / ${error.sourcePath}: ${error.error.message}`);
|
|
963
|
-
for (const document of project.documents){
|
|
964
|
-
const name = document.sourcePath.replace(/\\/g, '/').replace(/\.(?:yaml|yml)$/, '');
|
|
965
|
-
const group = {
|
|
966
|
-
name: dump.projects.length > 1 ? `${project.name} / ${name}` : name,
|
|
967
|
-
rows: []
|
|
968
|
-
};
|
|
969
|
-
groups.push(group);
|
|
970
|
-
recordSteps(`${project.name} / ${document.sourcePath}`, [
|
|
971
|
-
...document.beforeAll,
|
|
972
|
-
...document.afterAll
|
|
973
|
-
]);
|
|
974
|
-
for (const error of document.teardownErrors ?? [])errors.push(`${document.sourcePath}: ${error.message}`);
|
|
975
|
-
for (const failure of document.hostErrors ?? [])errors.push(`${document.sourcePath} / ${failure.phase}: ${failure.error.message}`);
|
|
976
|
-
for (const testCase of document.cases){
|
|
977
|
-
const attempt = testCase.attempts.at(-1);
|
|
978
|
-
const steps = attempt ? [
|
|
979
|
-
...attempt.beforeEach,
|
|
980
|
-
...attempt.steps,
|
|
981
|
-
...attempt.afterEach
|
|
982
|
-
] : [];
|
|
983
|
-
const failedSteps = steps.filter((step)=>'failed' === step.status);
|
|
984
|
-
const step = failedSteps.at(-1) ?? [
|
|
985
|
-
...steps
|
|
986
|
-
].reverse().find((item)=>item.agentDetails?.length) ?? steps.at(-1);
|
|
987
|
-
const route = new URLSearchParams({
|
|
988
|
-
'runner-page': 'case',
|
|
989
|
-
'runner-project': project.projectId,
|
|
990
|
-
'runner-case': testCase.caseId,
|
|
991
|
-
...step ? {
|
|
992
|
-
'runner-step': step.id
|
|
993
|
-
} : {}
|
|
994
|
-
});
|
|
995
|
-
const target = options.reportHref ? `${options.reportHref}#${route}` : void 0;
|
|
996
|
-
const name = target ? `[${result_markdown_text(testCase.name)}](<${target}>)` : result_markdown_text(testCase.name);
|
|
997
|
-
group.rows.push(` - ${'success' === testCase.status ? '✅' : 'not-run' === testCase.status ? '⏭️' : '❌'} ${name}`);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
for (const diagnostic of dump.diagnostics ?? [])if ('error' === diagnostic.level) errors.push(diagnostic.message);
|
|
1002
|
-
if (errors.length) lines.push('### Execution errors', '', ...errors.map((error)=>`- ${result_markdown_text(error)}`), '');
|
|
1003
|
-
for (const group of groups)if (group.rows.length) lines.push(`- ${result_markdown_text(group.name)}`, ...group.rows);
|
|
1004
|
-
if (!groups.some((group)=>group.rows.length)) lines.push('No cases were reported.');
|
|
1005
|
-
if (options.reportHref) lines.push('', `[Open the Midscene Test report ↗](<${options.reportHref}>)`);
|
|
1006
|
-
return `${lines.join('\n')}\n`;
|
|
1007
|
-
}
|
|
1008
|
-
const emptyIndex = ()=>({
|
|
1009
|
-
sources: [],
|
|
1010
|
-
metrics: {
|
|
1011
|
-
modelCallCount: 0,
|
|
1012
|
-
modelTimeMs: 0,
|
|
1013
|
-
promptTokens: 0,
|
|
1014
|
-
cachedInputTokens: 0,
|
|
1015
|
-
completionTokens: 0,
|
|
1016
|
-
totalTokens: 0
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
|
-
async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
1020
|
-
const output = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(result.summaryPath), 'result.md');
|
|
1021
|
-
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(result, index);
|
|
1022
|
-
await (0, promises_namespaceObject.writeFile)(output, renderTestResultMarkdown(dump, {
|
|
1023
|
-
...result.reportPath ? {
|
|
1024
|
-
reportHref: href(output, result.reportPath)
|
|
1025
|
-
} : {}
|
|
1026
|
-
}));
|
|
1027
|
-
return output;
|
|
1028
|
-
}
|
|
1029
1061
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1030
1062
|
const formatStep = (info)=>{
|
|
1031
1063
|
const position = stepPosition(info);
|
|
@@ -1034,6 +1066,7 @@ const formatStep = (info)=>{
|
|
|
1034
1066
|
};
|
|
1035
1067
|
const formatStepResult = (info, result)=>{
|
|
1036
1068
|
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1069
|
+
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1037
1070
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1038
1071
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1039
1072
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
@@ -1043,11 +1076,11 @@ const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
|
1043
1076
|
...attempt.beforeEach,
|
|
1044
1077
|
...attempt.steps,
|
|
1045
1078
|
...attempt.afterEach
|
|
1046
|
-
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (attempt.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError));
|
|
1079
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (attempt.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError) || !!attempt.executionErrors?.some(test_runner_namespaceObject.isResourceRecoveryError));
|
|
1047
1080
|
const documentHasFatalError = (result)=>[
|
|
1048
1081
|
...result.beforeAll,
|
|
1049
1082
|
...result.afterAll
|
|
1050
|
-
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (result.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError);
|
|
1083
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (result.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError) || !!result.executionErrors?.some(test_runner_namespaceObject.isResourceRecoveryError);
|
|
1051
1084
|
async function executeDocumentInvocation(options) {
|
|
1052
1085
|
const { invocation, project, sinks } = options;
|
|
1053
1086
|
const { document } = invocation;
|
|
@@ -1060,6 +1093,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1060
1093
|
}, async (documentAttemptIndex)=>{
|
|
1061
1094
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1062
1095
|
const execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(document, {
|
|
1096
|
+
plan: invocation.plan,
|
|
1063
1097
|
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
1064
1098
|
...invocation.bindings,
|
|
1065
1099
|
project,
|
|
@@ -1097,6 +1131,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1097
1131
|
}).catch(async (error)=>{
|
|
1098
1132
|
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) {
|
|
1099
1133
|
const partial = error.result;
|
|
1134
|
+
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>(0, test_runner_namespaceObject.isResourceRecoveryError)(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
1135
|
+
if (resourceOnly) {
|
|
1136
|
+
if (error.errors.some(test_runner_namespaceObject.isResourceRecoveryError)) options.markProjectFatal();
|
|
1137
|
+
return partial;
|
|
1138
|
+
}
|
|
1139
|
+
if (documentRetry) partial.document.retryDecision = (0, test_runner_namespaceObject.decideRetry)({
|
|
1140
|
+
scope: 'document',
|
|
1141
|
+
retryCount: invocation.retry.count,
|
|
1142
|
+
attemptIndex: documentAttemptIndex,
|
|
1143
|
+
status: 'failed',
|
|
1144
|
+
infrastructureError: true
|
|
1145
|
+
});
|
|
1100
1146
|
attempts.push(partial);
|
|
1101
1147
|
sinks.documents.push(partial.document);
|
|
1102
1148
|
sinks.cases.push(...partial.cases.map((outcome)=>({
|
|
@@ -1105,6 +1151,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1105
1151
|
documentRunId: partial.document.documentRunId
|
|
1106
1152
|
})));
|
|
1107
1153
|
try {
|
|
1154
|
+
for (const outcome of partial.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1108
1155
|
await writeWorkflowDocumentResult(options.runDir, partial.document);
|
|
1109
1156
|
} catch (publicationError) {
|
|
1110
1157
|
throw new test_runner_namespaceObject.WorkflowExecutionFailure(partial, [
|
|
@@ -1115,6 +1162,29 @@ async function executeDocumentInvocation(options) {
|
|
|
1115
1162
|
}
|
|
1116
1163
|
throw error;
|
|
1117
1164
|
});
|
|
1165
|
+
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1166
|
+
const owners = [
|
|
1167
|
+
execution.document,
|
|
1168
|
+
...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
|
|
1169
|
+
];
|
|
1170
|
+
const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
|
|
1171
|
+
const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
|
|
1172
|
+
const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
|
|
1173
|
+
if (cleanupPending || cleanupFailed || operationPending) options.markProjectFatal();
|
|
1174
|
+
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1175
|
+
const decision = (0, test_runner_namespaceObject.decideRetry)({
|
|
1176
|
+
scope: 'document',
|
|
1177
|
+
retryCount: invocation.retry.count,
|
|
1178
|
+
attemptIndex: documentAttemptIndex,
|
|
1179
|
+
status: failed ? 'failed' : 'success',
|
|
1180
|
+
cleanupPending,
|
|
1181
|
+
cleanupFailed,
|
|
1182
|
+
operationPending,
|
|
1183
|
+
fatalError: options.isProjectFatal(),
|
|
1184
|
+
interrupted: options.signal.aborted || options.rootSignal.aborted,
|
|
1185
|
+
shouldStop: options.shouldBail()
|
|
1186
|
+
});
|
|
1187
|
+
if (documentRetry) execution.document.retryDecision = decision;
|
|
1118
1188
|
attempts.push(execution);
|
|
1119
1189
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1120
1190
|
...outcome,
|
|
@@ -1122,15 +1192,30 @@ async function executeDocumentInvocation(options) {
|
|
|
1122
1192
|
documentRunId: execution.document.documentRunId
|
|
1123
1193
|
})));
|
|
1124
1194
|
sinks.documents.push(execution.document);
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1195
|
+
try {
|
|
1196
|
+
for (const outcome of execution.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1197
|
+
await writeWorkflowDocumentResult(options.runDir, execution.document);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
execution.document.status = 'failed';
|
|
1200
|
+
execution.document.hostErrors = [
|
|
1201
|
+
...execution.document.hostErrors ?? [],
|
|
1202
|
+
{
|
|
1203
|
+
phase: 'publication',
|
|
1204
|
+
error
|
|
1205
|
+
}
|
|
1206
|
+
];
|
|
1207
|
+
if (documentRetry) execution.document.retryDecision = (0, test_runner_namespaceObject.decideRetry)({
|
|
1208
|
+
scope: 'document',
|
|
1209
|
+
retryCount: invocation.retry.count,
|
|
1210
|
+
attemptIndex: documentAttemptIndex,
|
|
1211
|
+
status: 'failed',
|
|
1212
|
+
infrastructureError: true
|
|
1213
|
+
});
|
|
1214
|
+
throw new test_runner_namespaceObject.WorkflowExecutionFailure(execution, [
|
|
1215
|
+
error
|
|
1216
|
+
]);
|
|
1217
|
+
}
|
|
1218
|
+
return 'retry' === decision.decision ? 'failed' : void 0;
|
|
1134
1219
|
});
|
|
1135
1220
|
const final = attempts.at(-1);
|
|
1136
1221
|
if (!final) return;
|
|
@@ -1138,6 +1223,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1138
1223
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1139
1224
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1140
1225
|
}
|
|
1226
|
+
function createOperationJournal(runDir) {
|
|
1227
|
+
const path = (0, external_node_path_namespaceObject.join)(runDir, 'operations.jsonl');
|
|
1228
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(path, '', {
|
|
1229
|
+
flag: 'wx'
|
|
1230
|
+
});
|
|
1231
|
+
return {
|
|
1232
|
+
path,
|
|
1233
|
+
append (event) {
|
|
1234
|
+
(0, external_node_fs_namespaceObject.appendFileSync)(path, `${JSON.stringify(event)}\n`);
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1141
1238
|
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1142
1239
|
documentId,
|
|
1143
1240
|
caseId: collectedCase.caseId,
|
|
@@ -1196,6 +1293,8 @@ async function runPreparedTestProject(plan) {
|
|
|
1196
1293
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
1197
1294
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1198
1295
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrency, preparedProjects.length);
|
|
1296
|
+
await writeTestRunPlan(runDir, preparedProjects);
|
|
1297
|
+
const journal = createOperationJournal(runDir);
|
|
1199
1298
|
const rootController = new AbortController();
|
|
1200
1299
|
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
1201
1300
|
const handleSignal = (signal)=>{
|
|
@@ -1225,6 +1324,16 @@ async function runPreparedTestProject(plan) {
|
|
|
1225
1324
|
throw error;
|
|
1226
1325
|
}
|
|
1227
1326
|
};
|
|
1327
|
+
const stopObservingOperations = (0, test_runner_namespaceObject.observeResourceOperations)(rootController.signal, (event)=>{
|
|
1328
|
+
try {
|
|
1329
|
+
journal.append(event);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
recordInfrastructureError(new test_runner_namespaceObject.WorkflowPublicationError('write-result', journal.path, error));
|
|
1332
|
+
(0, logger_namespaceObject.getDebug)('test-runner:operations', {
|
|
1333
|
+
console: true
|
|
1334
|
+
})(`Could not persist operation event: ${String(error)}`);
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1228
1337
|
const announceProject = (prepared, projectIndex)=>{
|
|
1229
1338
|
const { project } = prepared;
|
|
1230
1339
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
@@ -1249,6 +1358,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1249
1358
|
lifecycle
|
|
1250
1359
|
} : {},
|
|
1251
1360
|
cases,
|
|
1361
|
+
plans: prepared.plans,
|
|
1252
1362
|
documents,
|
|
1253
1363
|
collectionErrors: prepared.collectionErrors
|
|
1254
1364
|
};
|
|
@@ -1367,6 +1477,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1367
1477
|
} catch (error) {
|
|
1368
1478
|
recordInfrastructureError(error);
|
|
1369
1479
|
} finally{
|
|
1480
|
+
stopObservingOperations();
|
|
1370
1481
|
process.off('SIGINT', sigint);
|
|
1371
1482
|
process.off('SIGTERM', sigterm);
|
|
1372
1483
|
}
|
|
@@ -1382,6 +1493,10 @@ async function runPreparedTestProject(plan) {
|
|
|
1382
1493
|
const endedAt = new Date();
|
|
1383
1494
|
const result = {
|
|
1384
1495
|
schemaVersion: 3,
|
|
1496
|
+
operationLog: {
|
|
1497
|
+
path: journal.path,
|
|
1498
|
+
snapshotAt: endedAt.toISOString()
|
|
1499
|
+
},
|
|
1385
1500
|
runId,
|
|
1386
1501
|
startedAt: startedAt.toISOString(),
|
|
1387
1502
|
endedAt: endedAt.toISOString(),
|
|
@@ -1401,7 +1516,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1401
1516
|
} : {}
|
|
1402
1517
|
};
|
|
1403
1518
|
let completedResult = result;
|
|
1404
|
-
let reportIndex;
|
|
1405
1519
|
const publish = async (operation, path, callback)=>{
|
|
1406
1520
|
try {
|
|
1407
1521
|
await callback();
|
|
@@ -1432,7 +1546,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1432
1546
|
overwrite: false,
|
|
1433
1547
|
sources: (0, test_runner_namespaceObject.collectTestRunReportSources)(completedResult),
|
|
1434
1548
|
buildRunnerDump: (index)=>{
|
|
1435
|
-
reportIndex = index;
|
|
1436
1549
|
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, index);
|
|
1437
1550
|
const warn = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
|
|
1438
1551
|
console: true
|
|
@@ -1450,13 +1563,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1450
1563
|
for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
|
|
1451
1564
|
await publication.publish(completedResult);
|
|
1452
1565
|
});
|
|
1453
|
-
await publish('write-result', (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
1454
|
-
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1455
|
-
completedResult = {
|
|
1456
|
-
...completedResult,
|
|
1457
|
-
markdownPath
|
|
1458
|
-
};
|
|
1459
|
-
});
|
|
1460
1566
|
await writeSummary();
|
|
1461
1567
|
if (hasInfrastructureError) throw new test_runner_namespaceObject.WorkflowExecutionFailure(completedResult, infrastructureErrors);
|
|
1462
1568
|
return completedResult;
|