@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/es/test-cli.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
2
2
|
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
3
|
-
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, isFatalDeviceError, normalizeSteps, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument } from "@midscene/core/internal/test-runner";
|
|
3
|
+
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText } from "@midscene/core/internal/test-runner";
|
|
4
4
|
import { inspect } from "node:util";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { require as api_require } from "tsx/cjs/api";
|
|
8
8
|
import { globSync } from "tinyglobby";
|
|
9
|
-
import { JSON_SCHEMA, load } from "js-yaml";
|
|
10
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { JSON_SCHEMA, load } from "js-yaml";
|
|
11
11
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
12
|
import { setMaxListeners } from "node:events";
|
|
13
13
|
import { TestRunReportAssembler, calculateTestRunHealth } from "@midscene/core/report";
|
|
@@ -20,7 +20,9 @@ var __webpack_modules__ = {
|
|
|
20
20
|
module.exports = __rspack_external_node_path_c5b9b54f;
|
|
21
21
|
},
|
|
22
22
|
"./package.json" (module) {
|
|
23
|
-
module.exports =
|
|
23
|
+
module.exports = {
|
|
24
|
+
rE: "1.15.2"
|
|
25
|
+
};
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
var __webpack_module_cache__ = {};
|
|
@@ -524,6 +526,69 @@ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=
|
|
|
524
526
|
] : []
|
|
525
527
|
])).digest('hex');
|
|
526
528
|
const normalize_normalizeSteps = normalizeSteps;
|
|
529
|
+
function parseWorkflowSource(source) {
|
|
530
|
+
const stack = [];
|
|
531
|
+
const nodes = [];
|
|
532
|
+
const sensitiveNodes = new Set();
|
|
533
|
+
const sensitiveValues = new Set();
|
|
534
|
+
const redactedPaths = new Set();
|
|
535
|
+
const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
|
|
536
|
+
const parsed = load(source, {
|
|
537
|
+
schema: JSON_SCHEMA,
|
|
538
|
+
listener (event, state) {
|
|
539
|
+
if ('open' === event) return void stack.push({
|
|
540
|
+
start: state.position + sourceOffset,
|
|
541
|
+
end: 0,
|
|
542
|
+
value: null,
|
|
543
|
+
children: []
|
|
544
|
+
});
|
|
545
|
+
const node = stack.pop();
|
|
546
|
+
node.end = state.position + sourceOffset;
|
|
547
|
+
node.value = state.result;
|
|
548
|
+
stack.at(-1)?.children.push(node);
|
|
549
|
+
nodes.push(node);
|
|
550
|
+
if ('mapping' !== state.kind) return;
|
|
551
|
+
for(let index = 0; index < node.children.length; index += 2){
|
|
552
|
+
const key = node.children[index].value;
|
|
553
|
+
const value = node.children[index + 1];
|
|
554
|
+
if ('string' == typeof key && isReportSecretKey(key) && value) {
|
|
555
|
+
sensitiveNodes.add(value);
|
|
556
|
+
if (null !== value.value) sensitiveValues.add(value.value);
|
|
557
|
+
redactedPaths.add(`$..[${JSON.stringify(key)}]`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
|
|
563
|
+
let offset = 0;
|
|
564
|
+
let redacted = '';
|
|
565
|
+
for (const range of ranges){
|
|
566
|
+
if (range.start < offset) continue;
|
|
567
|
+
const text = source.slice(range.start, range.end);
|
|
568
|
+
const leading = text.match(/^\s*/)?.[0] ?? '';
|
|
569
|
+
const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
|
|
570
|
+
redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
|
|
571
|
+
offset = range.end;
|
|
572
|
+
}
|
|
573
|
+
redacted += source.slice(offset);
|
|
574
|
+
const value = sanitizeReportText(redacted, 65536);
|
|
575
|
+
return {
|
|
576
|
+
parsed,
|
|
577
|
+
sourceYaml: {
|
|
578
|
+
value,
|
|
579
|
+
...redactedPaths.size ? {
|
|
580
|
+
redactedPaths: [
|
|
581
|
+
...redactedPaths
|
|
582
|
+
]
|
|
583
|
+
} : {},
|
|
584
|
+
...value !== redacted ? {
|
|
585
|
+
truncatedPaths: [
|
|
586
|
+
'$'
|
|
587
|
+
]
|
|
588
|
+
} : {}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
527
592
|
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
528
593
|
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
529
594
|
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
@@ -598,16 +663,15 @@ const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
|
598
663
|
const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
|
|
599
664
|
const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
|
|
600
665
|
function collectWorkflowDocument(source, options) {
|
|
601
|
-
let
|
|
666
|
+
let snapshot;
|
|
602
667
|
try {
|
|
603
|
-
|
|
604
|
-
schema: JSON_SCHEMA
|
|
605
|
-
});
|
|
668
|
+
snapshot = parseWorkflowSource((0, external_node_fs_.readFileSync)(source.absolutePath, 'utf8'));
|
|
606
669
|
} catch (error) {
|
|
607
670
|
throw new WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
608
671
|
sourcePath: source.sourcePath
|
|
609
672
|
}, error);
|
|
610
673
|
}
|
|
674
|
+
const { parsed, sourceYaml } = snapshot;
|
|
611
675
|
if (!isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
|
|
612
676
|
const format = classifyWorkflowDocumentFormat(parsed);
|
|
613
677
|
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
@@ -622,6 +686,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
622
686
|
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
623
687
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
624
688
|
...step,
|
|
689
|
+
templateInput: step.input,
|
|
625
690
|
input: resolveWorkflowVariables(step.input, {
|
|
626
691
|
variables: options.variables,
|
|
627
692
|
env: options.env ?? process.env,
|
|
@@ -707,6 +772,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
707
772
|
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
708
773
|
projectId: source.projectId,
|
|
709
774
|
sourcePath,
|
|
775
|
+
sourceYaml,
|
|
710
776
|
lifecycle,
|
|
711
777
|
cases
|
|
712
778
|
};
|
|
@@ -721,6 +787,15 @@ const writeJson = async (path, value)=>{
|
|
|
721
787
|
throw new WorkflowPublicationError('write-result', path, error);
|
|
722
788
|
}
|
|
723
789
|
};
|
|
790
|
+
const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_.join)(runDir, 'plan.json'), {
|
|
791
|
+
schemaVersion: 1,
|
|
792
|
+
createdAt: new Date().toISOString(),
|
|
793
|
+
projects: projects.map((item)=>({
|
|
794
|
+
projectId: item.project.projectId,
|
|
795
|
+
name: item.project.name,
|
|
796
|
+
documents: item.plans ?? []
|
|
797
|
+
}))
|
|
798
|
+
});
|
|
724
799
|
const toPosix = (value)=>value.split(external_node_path_.sep).join('/');
|
|
725
800
|
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
726
801
|
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
@@ -750,7 +825,7 @@ const relativeToSummary = (summaryPath, target)=>{
|
|
|
750
825
|
const value = (0, external_node_path_.relative)((0, external_node_path_.dirname)(summaryPath), target);
|
|
751
826
|
return toPosix(value || '.');
|
|
752
827
|
};
|
|
753
|
-
const errorJson =
|
|
828
|
+
const errorJson = sanitizeReportError;
|
|
754
829
|
const writeTestProjectRunResult = (options)=>{
|
|
755
830
|
const { result } = options;
|
|
756
831
|
const fact = (path)=>toPosix(path);
|
|
@@ -770,13 +845,17 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
770
845
|
configPath: options.configPath
|
|
771
846
|
} : {},
|
|
772
847
|
factsRoot: '.',
|
|
848
|
+
plan: 'plan.json',
|
|
849
|
+
...result.operationLog ? {
|
|
850
|
+
operationLog: {
|
|
851
|
+
...result.operationLog,
|
|
852
|
+
path: relativeToSummary(result.summaryPath, result.operationLog.path)
|
|
853
|
+
}
|
|
854
|
+
} : {},
|
|
773
855
|
reportDir: relativeToSummary(result.summaryPath, result.reportDir),
|
|
774
856
|
...result.reportPath ? {
|
|
775
857
|
report: relativeToSummary(result.summaryPath, result.reportPath)
|
|
776
858
|
} : {},
|
|
777
|
-
...result.markdownPath ? {
|
|
778
|
-
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
779
|
-
} : {},
|
|
780
859
|
summary: result.summary,
|
|
781
860
|
projects: result.projects.map((project)=>({
|
|
782
861
|
projectId: project.projectId,
|
|
@@ -794,6 +873,12 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
794
873
|
startedAt: project.lifecycle.startedAt,
|
|
795
874
|
endedAt: project.lifecycle.endedAt,
|
|
796
875
|
durationMs: project.lifecycle.durationMs,
|
|
876
|
+
...project.lifecycle.cleanup ? {
|
|
877
|
+
cleanup: project.lifecycle.cleanup
|
|
878
|
+
} : {},
|
|
879
|
+
...project.lifecycle.pendingOperations ? {
|
|
880
|
+
pendingOperations: project.lifecycle.pendingOperations
|
|
881
|
+
} : {},
|
|
797
882
|
...project.lifecycle.setupError ? {
|
|
798
883
|
setupError: errorJson(project.lifecycle.setupError)
|
|
799
884
|
} : {},
|
|
@@ -820,6 +905,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
820
905
|
attemptId: attempt.runId,
|
|
821
906
|
attemptIndex: attempt.attemptIndex,
|
|
822
907
|
status: attempt.status,
|
|
908
|
+
...attempt.cleanup ? {
|
|
909
|
+
cleanup: attempt.cleanup
|
|
910
|
+
} : {},
|
|
911
|
+
...attempt.pendingOperations ? {
|
|
912
|
+
pendingOperations: attempt.pendingOperations
|
|
913
|
+
} : {},
|
|
914
|
+
...attempt.executionStatus ? {
|
|
915
|
+
executionStatus: attempt.executionStatus
|
|
916
|
+
} : {},
|
|
917
|
+
...attempt.retryDecision ? {
|
|
918
|
+
retryDecision: attempt.retryDecision
|
|
919
|
+
} : {},
|
|
823
920
|
resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
|
|
824
921
|
...attempt.reportPaths?.length ? {
|
|
825
922
|
reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -835,6 +932,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
835
932
|
},
|
|
836
933
|
sourcePath: document.sourcePath,
|
|
837
934
|
status: document.status,
|
|
935
|
+
...document.cleanup ? {
|
|
936
|
+
cleanup: document.cleanup
|
|
937
|
+
} : {},
|
|
938
|
+
...document.pendingOperations ? {
|
|
939
|
+
pendingOperations: document.pendingOperations
|
|
940
|
+
} : {},
|
|
941
|
+
...document.executionStatus ? {
|
|
942
|
+
executionStatus: document.executionStatus
|
|
943
|
+
} : {},
|
|
944
|
+
...document.retryDecision ? {
|
|
945
|
+
retryDecision: document.retryDecision
|
|
946
|
+
} : {},
|
|
838
947
|
resultFile: fact(workflowDocumentResultPath(document)),
|
|
839
948
|
...document.reportPaths?.length ? {
|
|
840
949
|
reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -969,6 +1078,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
969
1078
|
});
|
|
970
1079
|
const collectionErrors = [];
|
|
971
1080
|
const invocations = [];
|
|
1081
|
+
const plans = [];
|
|
972
1082
|
let filteredCaseCount = 0;
|
|
973
1083
|
if (0 === sources.length) {
|
|
974
1084
|
const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
@@ -982,7 +1092,17 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
982
1092
|
const collected = await options.collectDocument?.(source, project);
|
|
983
1093
|
if (collected) {
|
|
984
1094
|
filteredCaseCount += collected.filteredCaseCount;
|
|
985
|
-
if (collected.invocation)
|
|
1095
|
+
if (collected.invocation) {
|
|
1096
|
+
const plan = buildTestRunReportPlan(collected.invocation.document, {
|
|
1097
|
+
defaultTimeoutMs: collected.invocation.defaultTimeoutMs,
|
|
1098
|
+
retry: collected.invocation.retry
|
|
1099
|
+
});
|
|
1100
|
+
invocations.push({
|
|
1101
|
+
...collected.invocation,
|
|
1102
|
+
plan
|
|
1103
|
+
});
|
|
1104
|
+
plans.push(plan);
|
|
1105
|
+
}
|
|
986
1106
|
continue;
|
|
987
1107
|
}
|
|
988
1108
|
const document = collectWorkflowDocument(source, {
|
|
@@ -994,8 +1114,18 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
994
1114
|
document,
|
|
995
1115
|
filtered: 0
|
|
996
1116
|
} : filterDocumentCases(document, project);
|
|
1117
|
+
const plan = buildTestRunReportPlan(document, {
|
|
1118
|
+
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
1119
|
+
defaultTimeoutMs,
|
|
1120
|
+
retry: {
|
|
1121
|
+
scope: 'case',
|
|
1122
|
+
count: project.retry
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
plans.push(plan);
|
|
997
1126
|
filteredCaseCount += filtered.filtered;
|
|
998
1127
|
if (filtered.document) invocations.push({
|
|
1128
|
+
plan,
|
|
999
1129
|
document: filtered.document,
|
|
1000
1130
|
retry: {
|
|
1001
1131
|
scope: 'case',
|
|
@@ -1028,6 +1158,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1028
1158
|
sources,
|
|
1029
1159
|
documents,
|
|
1030
1160
|
invocations,
|
|
1161
|
+
plans,
|
|
1031
1162
|
collectionErrors,
|
|
1032
1163
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
1033
1164
|
filteredCaseCount
|
|
@@ -1108,103 +1239,6 @@ async function prepareTestRun(options) {
|
|
|
1108
1239
|
onProgress: options.onProgress ?? (()=>{})
|
|
1109
1240
|
};
|
|
1110
1241
|
}
|
|
1111
|
-
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
1112
|
-
const duration = (ms)=>{
|
|
1113
|
-
const seconds = Math.round(ms / 1000);
|
|
1114
|
-
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1115
|
-
};
|
|
1116
|
-
const href = (from, target)=>(0, external_node_path_.relative)((0, external_node_path_.dirname)(from), target).split(external_node_path_.sep).map(encodeURIComponent).join('/');
|
|
1117
|
-
function renderTestResultMarkdown(dump, options = {}) {
|
|
1118
|
-
const { summary } = dump;
|
|
1119
|
-
const outcome = 'success' === dump.status ? `✅ **${summary.passed}/${summary.total} passed**` : `❌ **${summary.failed} failed** · ${summary.passed} passed · ${summary.notRun} not run`;
|
|
1120
|
-
const lines = [
|
|
1121
|
-
'## Midscene Test',
|
|
1122
|
-
'',
|
|
1123
|
-
`${outcome} · ${duration(dump.durationMs)}`,
|
|
1124
|
-
''
|
|
1125
|
-
];
|
|
1126
|
-
if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
|
|
1127
|
-
const infrastructure = [
|
|
1128
|
-
summary.collectionErrors ? `${summary.collectionErrors} collection errors` : '',
|
|
1129
|
-
summary.documentFailures ? `${summary.documentFailures} document failures` : '',
|
|
1130
|
-
summary.projectFailures ? `${summary.projectFailures} project failures` : ''
|
|
1131
|
-
].filter(Boolean);
|
|
1132
|
-
if (infrastructure.length) lines.push(`**Infrastructure:** ${infrastructure.join(' · ')}`, '');
|
|
1133
|
-
const groups = [];
|
|
1134
|
-
const errors = [];
|
|
1135
|
-
const recordSteps = (label, steps)=>{
|
|
1136
|
-
for (const step of steps)if ('failed' === step.status) errors.push(`${label} / ${step.node}: ${step.error?.message ?? 'Step failed'}`);
|
|
1137
|
-
};
|
|
1138
|
-
for (const project of dump.projects){
|
|
1139
|
-
if (project.lifecycle?.setupError) errors.push(`${project.name}: ${project.lifecycle.setupError.message}`);
|
|
1140
|
-
for (const error of project.lifecycle?.teardownErrors ?? [])errors.push(`${project.name}: ${error.message}`);
|
|
1141
|
-
for (const error of project.collectionErrors)errors.push(`${project.name} / ${error.sourcePath}: ${error.error.message}`);
|
|
1142
|
-
for (const document of project.documents){
|
|
1143
|
-
const name = document.sourcePath.replace(/\\/g, '/').replace(/\.(?:yaml|yml)$/, '');
|
|
1144
|
-
const group = {
|
|
1145
|
-
name: dump.projects.length > 1 ? `${project.name} / ${name}` : name,
|
|
1146
|
-
rows: []
|
|
1147
|
-
};
|
|
1148
|
-
groups.push(group);
|
|
1149
|
-
recordSteps(`${project.name} / ${document.sourcePath}`, [
|
|
1150
|
-
...document.beforeAll,
|
|
1151
|
-
...document.afterAll
|
|
1152
|
-
]);
|
|
1153
|
-
for (const error of document.teardownErrors ?? [])errors.push(`${document.sourcePath}: ${error.message}`);
|
|
1154
|
-
for (const failure of document.hostErrors ?? [])errors.push(`${document.sourcePath} / ${failure.phase}: ${failure.error.message}`);
|
|
1155
|
-
for (const testCase of document.cases){
|
|
1156
|
-
const attempt = testCase.attempts.at(-1);
|
|
1157
|
-
const steps = attempt ? [
|
|
1158
|
-
...attempt.beforeEach,
|
|
1159
|
-
...attempt.steps,
|
|
1160
|
-
...attempt.afterEach
|
|
1161
|
-
] : [];
|
|
1162
|
-
const failedSteps = steps.filter((step)=>'failed' === step.status);
|
|
1163
|
-
const step = failedSteps.at(-1) ?? [
|
|
1164
|
-
...steps
|
|
1165
|
-
].reverse().find((item)=>item.agentDetails?.length) ?? steps.at(-1);
|
|
1166
|
-
const route = new URLSearchParams({
|
|
1167
|
-
'runner-page': 'case',
|
|
1168
|
-
'runner-project': project.projectId,
|
|
1169
|
-
'runner-case': testCase.caseId,
|
|
1170
|
-
...step ? {
|
|
1171
|
-
'runner-step': step.id
|
|
1172
|
-
} : {}
|
|
1173
|
-
});
|
|
1174
|
-
const target = options.reportHref ? `${options.reportHref}#${route}` : void 0;
|
|
1175
|
-
const name = target ? `[${result_markdown_text(testCase.name)}](<${target}>)` : result_markdown_text(testCase.name);
|
|
1176
|
-
group.rows.push(` - ${'success' === testCase.status ? '✅' : 'not-run' === testCase.status ? '⏭️' : '❌'} ${name}`);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
for (const diagnostic of dump.diagnostics ?? [])if ('error' === diagnostic.level) errors.push(diagnostic.message);
|
|
1181
|
-
if (errors.length) lines.push('### Execution errors', '', ...errors.map((error)=>`- ${result_markdown_text(error)}`), '');
|
|
1182
|
-
for (const group of groups)if (group.rows.length) lines.push(`- ${result_markdown_text(group.name)}`, ...group.rows);
|
|
1183
|
-
if (!groups.some((group)=>group.rows.length)) lines.push('No cases were reported.');
|
|
1184
|
-
if (options.reportHref) lines.push('', `[Open the Midscene Test report ↗](<${options.reportHref}>)`);
|
|
1185
|
-
return `${lines.join('\n')}\n`;
|
|
1186
|
-
}
|
|
1187
|
-
const emptyIndex = ()=>({
|
|
1188
|
-
sources: [],
|
|
1189
|
-
metrics: {
|
|
1190
|
-
modelCallCount: 0,
|
|
1191
|
-
modelTimeMs: 0,
|
|
1192
|
-
promptTokens: 0,
|
|
1193
|
-
cachedInputTokens: 0,
|
|
1194
|
-
completionTokens: 0,
|
|
1195
|
-
totalTokens: 0
|
|
1196
|
-
}
|
|
1197
|
-
});
|
|
1198
|
-
async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
1199
|
-
const output = (0, external_node_path_.join)((0, external_node_path_.dirname)(result.summaryPath), 'result.md');
|
|
1200
|
-
const dump = buildTestRunReportDump(result, index);
|
|
1201
|
-
await writeFile(output, renderTestResultMarkdown(dump, {
|
|
1202
|
-
...result.reportPath ? {
|
|
1203
|
-
reportHref: href(output, result.reportPath)
|
|
1204
|
-
} : {}
|
|
1205
|
-
}));
|
|
1206
|
-
return output;
|
|
1207
|
-
}
|
|
1208
1242
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1209
1243
|
const formatStep = (info)=>{
|
|
1210
1244
|
const position = stepPosition(info);
|
|
@@ -1213,6 +1247,7 @@ const formatStep = (info)=>{
|
|
|
1213
1247
|
};
|
|
1214
1248
|
const formatStepResult = (info, result)=>{
|
|
1215
1249
|
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1250
|
+
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1216
1251
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1217
1252
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1218
1253
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
@@ -1222,11 +1257,11 @@ const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
|
1222
1257
|
...attempt.beforeEach,
|
|
1223
1258
|
...attempt.steps,
|
|
1224
1259
|
...attempt.afterEach
|
|
1225
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError));
|
|
1260
|
+
].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError) || !!attempt.executionErrors?.some(isResourceRecoveryError));
|
|
1226
1261
|
const documentHasFatalError = (result)=>[
|
|
1227
1262
|
...result.beforeAll,
|
|
1228
1263
|
...result.afterAll
|
|
1229
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError);
|
|
1264
|
+
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError) || !!result.executionErrors?.some(isResourceRecoveryError);
|
|
1230
1265
|
async function executeDocumentInvocation(options) {
|
|
1231
1266
|
const { invocation, project, sinks } = options;
|
|
1232
1267
|
const { document } = invocation;
|
|
@@ -1239,6 +1274,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1239
1274
|
}, async (documentAttemptIndex)=>{
|
|
1240
1275
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1241
1276
|
const execution = await runWorkflowDocument(document, {
|
|
1277
|
+
plan: invocation.plan,
|
|
1242
1278
|
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
1243
1279
|
...invocation.bindings,
|
|
1244
1280
|
project,
|
|
@@ -1276,6 +1312,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1276
1312
|
}).catch(async (error)=>{
|
|
1277
1313
|
if (error instanceof WorkflowExecutionFailure) {
|
|
1278
1314
|
const partial = error.result;
|
|
1315
|
+
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>isResourceRecoveryError(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
1316
|
+
if (resourceOnly) {
|
|
1317
|
+
if (error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
|
|
1318
|
+
return partial;
|
|
1319
|
+
}
|
|
1320
|
+
if (documentRetry) partial.document.retryDecision = decideRetry({
|
|
1321
|
+
scope: 'document',
|
|
1322
|
+
retryCount: invocation.retry.count,
|
|
1323
|
+
attemptIndex: documentAttemptIndex,
|
|
1324
|
+
status: 'failed',
|
|
1325
|
+
infrastructureError: true
|
|
1326
|
+
});
|
|
1279
1327
|
attempts.push(partial);
|
|
1280
1328
|
sinks.documents.push(partial.document);
|
|
1281
1329
|
sinks.cases.push(...partial.cases.map((outcome)=>({
|
|
@@ -1284,6 +1332,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1284
1332
|
documentRunId: partial.document.documentRunId
|
|
1285
1333
|
})));
|
|
1286
1334
|
try {
|
|
1335
|
+
for (const outcome of partial.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1287
1336
|
await writeWorkflowDocumentResult(options.runDir, partial.document);
|
|
1288
1337
|
} catch (publicationError) {
|
|
1289
1338
|
throw new WorkflowExecutionFailure(partial, [
|
|
@@ -1294,6 +1343,29 @@ async function executeDocumentInvocation(options) {
|
|
|
1294
1343
|
}
|
|
1295
1344
|
throw error;
|
|
1296
1345
|
});
|
|
1346
|
+
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1347
|
+
const owners = [
|
|
1348
|
+
execution.document,
|
|
1349
|
+
...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
|
|
1350
|
+
];
|
|
1351
|
+
const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
|
|
1352
|
+
const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
|
|
1353
|
+
const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
|
|
1354
|
+
if (cleanupPending || cleanupFailed || operationPending) options.markProjectFatal();
|
|
1355
|
+
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1356
|
+
const decision = decideRetry({
|
|
1357
|
+
scope: 'document',
|
|
1358
|
+
retryCount: invocation.retry.count,
|
|
1359
|
+
attemptIndex: documentAttemptIndex,
|
|
1360
|
+
status: failed ? 'failed' : 'success',
|
|
1361
|
+
cleanupPending,
|
|
1362
|
+
cleanupFailed,
|
|
1363
|
+
operationPending,
|
|
1364
|
+
fatalError: options.isProjectFatal(),
|
|
1365
|
+
interrupted: options.signal.aborted || options.rootSignal.aborted,
|
|
1366
|
+
shouldStop: options.shouldBail()
|
|
1367
|
+
});
|
|
1368
|
+
if (documentRetry) execution.document.retryDecision = decision;
|
|
1297
1369
|
attempts.push(execution);
|
|
1298
1370
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1299
1371
|
...outcome,
|
|
@@ -1301,15 +1373,30 @@ async function executeDocumentInvocation(options) {
|
|
|
1301
1373
|
documentRunId: execution.document.documentRunId
|
|
1302
1374
|
})));
|
|
1303
1375
|
sinks.documents.push(execution.document);
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1376
|
+
try {
|
|
1377
|
+
for (const outcome of execution.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1378
|
+
await writeWorkflowDocumentResult(options.runDir, execution.document);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
execution.document.status = 'failed';
|
|
1381
|
+
execution.document.hostErrors = [
|
|
1382
|
+
...execution.document.hostErrors ?? [],
|
|
1383
|
+
{
|
|
1384
|
+
phase: 'publication',
|
|
1385
|
+
error
|
|
1386
|
+
}
|
|
1387
|
+
];
|
|
1388
|
+
if (documentRetry) execution.document.retryDecision = decideRetry({
|
|
1389
|
+
scope: 'document',
|
|
1390
|
+
retryCount: invocation.retry.count,
|
|
1391
|
+
attemptIndex: documentAttemptIndex,
|
|
1392
|
+
status: 'failed',
|
|
1393
|
+
infrastructureError: true
|
|
1394
|
+
});
|
|
1395
|
+
throw new WorkflowExecutionFailure(execution, [
|
|
1396
|
+
error
|
|
1397
|
+
]);
|
|
1398
|
+
}
|
|
1399
|
+
return 'retry' === decision.decision ? 'failed' : void 0;
|
|
1313
1400
|
});
|
|
1314
1401
|
const final = attempts.at(-1);
|
|
1315
1402
|
if (!final) return;
|
|
@@ -1317,6 +1404,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1317
1404
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1318
1405
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1319
1406
|
}
|
|
1407
|
+
function createOperationJournal(runDir) {
|
|
1408
|
+
const path = (0, external_node_path_.join)(runDir, 'operations.jsonl');
|
|
1409
|
+
(0, external_node_fs_.writeFileSync)(path, '', {
|
|
1410
|
+
flag: 'wx'
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
path,
|
|
1414
|
+
append (event) {
|
|
1415
|
+
(0, external_node_fs_.appendFileSync)(path, `${JSON.stringify(event)}\n`);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1320
1419
|
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1321
1420
|
documentId,
|
|
1322
1421
|
caseId: collectedCase.caseId,
|
|
@@ -1375,6 +1474,8 @@ async function runPreparedTestProject(plan) {
|
|
|
1375
1474
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
1376
1475
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1377
1476
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrency, preparedProjects.length);
|
|
1477
|
+
await writeTestRunPlan(runDir, preparedProjects);
|
|
1478
|
+
const journal = createOperationJournal(runDir);
|
|
1378
1479
|
const rootController = new AbortController();
|
|
1379
1480
|
setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
1380
1481
|
const handleSignal = (signal)=>{
|
|
@@ -1404,6 +1505,16 @@ async function runPreparedTestProject(plan) {
|
|
|
1404
1505
|
throw error;
|
|
1405
1506
|
}
|
|
1406
1507
|
};
|
|
1508
|
+
const stopObservingOperations = observeResourceOperations(rootController.signal, (event)=>{
|
|
1509
|
+
try {
|
|
1510
|
+
journal.append(event);
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
recordInfrastructureError(new WorkflowPublicationError('write-result', journal.path, error));
|
|
1513
|
+
getDebug('test-runner:operations', {
|
|
1514
|
+
console: true
|
|
1515
|
+
})(`Could not persist operation event: ${String(error)}`);
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1407
1518
|
const announceProject = (prepared, projectIndex)=>{
|
|
1408
1519
|
const { project } = prepared;
|
|
1409
1520
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
@@ -1428,6 +1539,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1428
1539
|
lifecycle
|
|
1429
1540
|
} : {},
|
|
1430
1541
|
cases,
|
|
1542
|
+
plans: prepared.plans,
|
|
1431
1543
|
documents,
|
|
1432
1544
|
collectionErrors: prepared.collectionErrors
|
|
1433
1545
|
};
|
|
@@ -1546,6 +1658,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1546
1658
|
} catch (error) {
|
|
1547
1659
|
recordInfrastructureError(error);
|
|
1548
1660
|
} finally{
|
|
1661
|
+
stopObservingOperations();
|
|
1549
1662
|
process.off('SIGINT', sigint);
|
|
1550
1663
|
process.off('SIGTERM', sigterm);
|
|
1551
1664
|
}
|
|
@@ -1561,6 +1674,10 @@ async function runPreparedTestProject(plan) {
|
|
|
1561
1674
|
const endedAt = new Date();
|
|
1562
1675
|
const result = {
|
|
1563
1676
|
schemaVersion: 3,
|
|
1677
|
+
operationLog: {
|
|
1678
|
+
path: journal.path,
|
|
1679
|
+
snapshotAt: endedAt.toISOString()
|
|
1680
|
+
},
|
|
1564
1681
|
runId,
|
|
1565
1682
|
startedAt: startedAt.toISOString(),
|
|
1566
1683
|
endedAt: endedAt.toISOString(),
|
|
@@ -1580,7 +1697,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1580
1697
|
} : {}
|
|
1581
1698
|
};
|
|
1582
1699
|
let completedResult = result;
|
|
1583
|
-
let reportIndex;
|
|
1584
1700
|
const publish = async (operation, path, callback)=>{
|
|
1585
1701
|
try {
|
|
1586
1702
|
await callback();
|
|
@@ -1611,7 +1727,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1611
1727
|
overwrite: false,
|
|
1612
1728
|
sources: collectTestRunReportSources(completedResult),
|
|
1613
1729
|
buildRunnerDump: (index)=>{
|
|
1614
|
-
reportIndex = index;
|
|
1615
1730
|
const dump = buildTestRunReportDump(completedResult, index);
|
|
1616
1731
|
const warn = getDebug('test-runner:report-assembler', {
|
|
1617
1732
|
console: true
|
|
@@ -1629,13 +1744,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1629
1744
|
for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
|
|
1630
1745
|
await publication.publish(completedResult);
|
|
1631
1746
|
});
|
|
1632
|
-
await publish('write-result', (0, external_node_path_.join)((0, external_node_path_.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
1633
|
-
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1634
|
-
completedResult = {
|
|
1635
|
-
...completedResult,
|
|
1636
|
-
markdownPath
|
|
1637
|
-
};
|
|
1638
|
-
});
|
|
1639
1747
|
await writeSummary();
|
|
1640
1748
|
if (hasInfrastructureError) throw new WorkflowExecutionFailure(completedResult, infrastructureErrors);
|
|
1641
1749
|
return completedResult;
|
|
@@ -1822,7 +1930,6 @@ const reportTestProjectResult = (result, io, infrastructureErrors = result.error
|
|
|
1822
1930
|
io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
|
|
1823
1931
|
io.log(`Results: ${result.resultDir}`);
|
|
1824
1932
|
io.log(`Summary: ${result.summaryPath}`);
|
|
1825
|
-
if (result.markdownPath) io.log(`Markdown: ${result.markdownPath}`);
|
|
1826
1933
|
if (result.reportPath) io.log(`Report: ${result.reportPath}`);
|
|
1827
1934
|
return result.exitCode;
|
|
1828
1935
|
};
|