@midscene/test 1.12.5 → 1.12.6-beta-20260909034232.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.
Files changed (40) hide show
  1. package/dist/es/1~891.mjs +1 -3
  2. package/dist/es/1~891.mjs.map +1 -1
  3. package/dist/es/android/index.mjs +71 -4
  4. package/dist/es/android/index.mjs.map +1 -1
  5. package/dist/es/cli/index.mjs +499 -25
  6. package/dist/es/cli/index.mjs.map +1 -1
  7. package/dist/es/harmony/index.mjs +71 -4
  8. package/dist/es/harmony/index.mjs.map +1 -1
  9. package/dist/es/index.mjs +478 -25
  10. package/dist/es/index.mjs.map +1 -1
  11. package/dist/es/ios/index.mjs +71 -4
  12. package/dist/es/ios/index.mjs.map +1 -1
  13. package/dist/es/midscene/index.mjs +71 -4
  14. package/dist/es/midscene/index.mjs.map +1 -1
  15. package/dist/es/test-cli.mjs +500 -25
  16. package/dist/es/test-cli.mjs.map +1 -1
  17. package/dist/lib/0~458.js +1 -3
  18. package/dist/lib/0~458.js.map +1 -1
  19. package/dist/lib/android/index.js +72 -4
  20. package/dist/lib/android/index.js.map +1 -1
  21. package/dist/lib/cli/index.js +499 -25
  22. package/dist/lib/cli/index.js.map +1 -1
  23. package/dist/lib/harmony/index.js +72 -4
  24. package/dist/lib/harmony/index.js.map +1 -1
  25. package/dist/lib/index.js +494 -26
  26. package/dist/lib/index.js.map +1 -1
  27. package/dist/lib/ios/index.js +72 -4
  28. package/dist/lib/ios/index.js.map +1 -1
  29. package/dist/lib/midscene/index.js +72 -4
  30. package/dist/lib/midscene/index.js.map +1 -1
  31. package/dist/lib/test-cli.js +500 -25
  32. package/dist/lib/test-cli.js.map +1 -1
  33. package/dist/types/android/index.d.ts +13 -0
  34. package/dist/types/cli/index.d.ts +25 -1
  35. package/dist/types/harmony/index.d.ts +13 -0
  36. package/dist/types/index.d.ts +91 -0
  37. package/dist/types/ios/index.d.ts +13 -0
  38. package/dist/types/midscene/index.d.ts +20 -1
  39. package/dist/types/playwright/index.d.ts +13 -0
  40. package/package.json +11 -10
@@ -6,8 +6,10 @@ import { require as api_require } from "tsx/cjs/api";
6
6
  import { tsImport } from "tsx/esm/api";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { setMaxListeners } from "node:events";
9
+ import { TestRunReportAssembler } from "@midscene/core/report";
9
10
  import { globSync } from "tinyglobby";
10
11
  import { JSON_SCHEMA, load } from "js-yaml";
12
+ import { getDebug } from "@midscene/shared/logger";
11
13
  var __webpack_modules__ = {
12
14
  "node:fs" (module) {
13
15
  module.exports = __rspack_external_node_fs_5ea92f0c;
@@ -985,6 +987,30 @@ async function executeNode(step, execute, phase, stepIndex, options) {
985
987
  parentSignal?.removeEventListener('abort', abortFromParent);
986
988
  }
987
989
  }
990
+ function createNodeReportCollector(node) {
991
+ const traces = new Map();
992
+ let accepting = true;
993
+ return {
994
+ collector: Object.freeze({
995
+ addTrace (trace) {
996
+ if (!accepting) throw new NodeExecutionError(node, new Error('Report traces can only be added while the current Step is running.'));
997
+ if (!trace || 'object' != typeof trace || 'midscene-execution' !== trace.type) throw new NodeExecutionError(node, new TypeError('Report trace type must be "midscene-execution".'));
998
+ if ('string' != typeof trace.executionId || 0 === trace.executionId.trim().length) throw new NodeExecutionError(node, new TypeError('Report trace executionId must be non-empty.'));
999
+ const normalized = Object.freeze({
1000
+ type: 'midscene-execution',
1001
+ executionId: trace.executionId
1002
+ });
1003
+ traces.set(`${normalized.type}:${normalized.executionId}`, normalized);
1004
+ }
1005
+ }),
1006
+ close () {
1007
+ accepting = false;
1008
+ return Object.freeze([
1009
+ ...traces.values()
1010
+ ]);
1011
+ }
1012
+ };
1013
+ }
988
1014
  async function parseNodeInput(node, input) {
989
1015
  if (!node.inputSchema) return input;
990
1016
  const parsed = await node.inputSchema.safeParseAsync(input);
@@ -994,32 +1020,47 @@ async function parseNodeInput(node, input) {
994
1020
  async function executeStep(step, node, target, context, execution = {}) {
995
1021
  const phase = 'case' === target.scope ? target.case.phase : target.document.phase;
996
1022
  const stepIndex = 'case' === target.scope ? target.case.stepIndex : target.document.stepIndex;
997
- return executeNode(step, async (signal)=>{
998
- const input = await parseNodeInput(node, step.input);
999
- signal.throwIfAborted();
1000
- const common = {
1001
- input,
1002
- $: step.meta,
1003
- signal,
1004
- context,
1005
- onTeardown: (teardown)=>{
1006
- if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
1007
- execution.onTeardown(step.node, teardown);
1023
+ const reportCollector = createNodeReportCollector(step.node);
1024
+ let result;
1025
+ try {
1026
+ result = await executeNode(step, async (signal)=>{
1027
+ const input = await parseNodeInput(node, step.input);
1028
+ signal.throwIfAborted();
1029
+ const common = {
1030
+ input,
1031
+ $: step.meta,
1032
+ signal,
1033
+ context,
1034
+ report: reportCollector.collector,
1035
+ onTeardown: (teardown)=>{
1036
+ if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
1037
+ execution.onTeardown(step.node, teardown);
1038
+ }
1039
+ };
1040
+ return 'case' === target.scope ? node.execute({
1041
+ ...common,
1042
+ scope: 'case',
1043
+ case: target.case
1044
+ }) : node.execute({
1045
+ ...common,
1046
+ scope: 'document',
1047
+ document: target.document
1048
+ });
1049
+ }, phase, stepIndex, {
1050
+ parentSignal: execution.signal,
1051
+ defaultTimeoutMs: execution.defaultTimeoutMs
1052
+ });
1053
+ } finally{
1054
+ const traces = reportCollector.close();
1055
+ if (traces.length > 0 && result) result = {
1056
+ ...result,
1057
+ report: {
1058
+ traces
1008
1059
  }
1009
1060
  };
1010
- return 'case' === target.scope ? node.execute({
1011
- ...common,
1012
- scope: 'case',
1013
- case: target.case
1014
- }) : node.execute({
1015
- ...common,
1016
- scope: 'document',
1017
- document: target.document
1018
- });
1019
- }, phase, stepIndex, {
1020
- parentSignal: execution.signal,
1021
- defaultTimeoutMs: execution.defaultTimeoutMs
1022
- });
1061
+ }
1062
+ if (!result) throw new NodeExecutionError(step.node, new Error('Step execution completed without a result.'));
1063
+ return result;
1023
1064
  }
1024
1065
  const reportPathsFromTeardown = (result)=>{
1025
1066
  if (void 0 === result) return [];
@@ -1654,6 +1695,419 @@ function collectWorkflowDocument(source, options) {
1654
1695
  cases
1655
1696
  };
1656
1697
  }
1698
+ const MAX_DEPTH = 6;
1699
+ const MAX_STRING_LENGTH = 2000;
1700
+ const MAX_ARRAY_ITEMS = 50;
1701
+ const MAX_OBJECT_KEYS = 50;
1702
+ const MAX_SERIALIZED_BYTES = 65536;
1703
+ const TRUNCATED_VALUE = '[Truncated: display value exceeded 64 KiB]';
1704
+ const normalizedSecretKeys = [
1705
+ 'password',
1706
+ 'passwd',
1707
+ 'secret',
1708
+ 'token',
1709
+ 'authorization',
1710
+ 'cookie',
1711
+ 'apikey'
1712
+ ];
1713
+ const normalizedKey = (key)=>key.toLowerCase().replace(/[^a-z0-9]/g, '');
1714
+ const isSecretKey = (key)=>{
1715
+ const normalized = normalizedKey(key);
1716
+ return normalizedSecretKeys.some((secret)=>normalized.includes(secret));
1717
+ };
1718
+ const objectPath = (parent, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${parent}.${key}` : `${parent}[${JSON.stringify(key)}]`;
1719
+ const stringByteLength = (value)=>Buffer.byteLength(value, 'utf8');
1720
+ function sanitizeReportText(value, maximumLength = MAX_STRING_LENGTH) {
1721
+ return value.length <= maximumLength ? value : `${value.slice(0, maximumLength)}… [truncated]`;
1722
+ }
1723
+ function sanitizeReportValue(value) {
1724
+ const truncatedPaths = new Set();
1725
+ const redactedPaths = new Set();
1726
+ const ancestors = new WeakSet();
1727
+ let remainingBytes = MAX_SERIALIZED_BYTES;
1728
+ const consume = (path, text)=>{
1729
+ remainingBytes -= stringByteLength(text);
1730
+ if (remainingBytes >= 0) return true;
1731
+ truncatedPaths.add(path);
1732
+ return false;
1733
+ };
1734
+ const visit = (current, path, depth)=>{
1735
+ if (remainingBytes <= 0) {
1736
+ truncatedPaths.add(path);
1737
+ return '[Truncated: size limit]';
1738
+ }
1739
+ if (null === current || 'boolean' == typeof current) {
1740
+ consume(path, String(current));
1741
+ return current;
1742
+ }
1743
+ if ('string' == typeof current) {
1744
+ if (current.length > MAX_STRING_LENGTH) {
1745
+ truncatedPaths.add(path);
1746
+ const shortened = `${current.slice(0, MAX_STRING_LENGTH)}… [truncated]`;
1747
+ consume(path, shortened);
1748
+ return shortened;
1749
+ }
1750
+ consume(path, current);
1751
+ return current;
1752
+ }
1753
+ if ('number' == typeof current) {
1754
+ if (Number.isFinite(current)) {
1755
+ consume(path, String(current));
1756
+ return current;
1757
+ }
1758
+ truncatedPaths.add(path);
1759
+ return `[Unsupported number: ${String(current)}]`;
1760
+ }
1761
+ if ('bigint' == typeof current) {
1762
+ truncatedPaths.add(path);
1763
+ return `[Unsupported bigint: ${String(current)}]`;
1764
+ }
1765
+ if (void 0 === current || 'function' == typeof current || 'symbol' == typeof current) {
1766
+ truncatedPaths.add(path);
1767
+ return `[Unsupported ${typeof current}]`;
1768
+ }
1769
+ if (depth >= MAX_DEPTH) {
1770
+ truncatedPaths.add(path);
1771
+ return '[Truncated: maximum depth]';
1772
+ }
1773
+ if (current instanceof Date) {
1774
+ const date = Number.isNaN(current.getTime()) ? '[Invalid Date]' : current.toISOString();
1775
+ consume(path, date);
1776
+ return date;
1777
+ }
1778
+ if (ancestors.has(current)) {
1779
+ truncatedPaths.add(path);
1780
+ return '[Unsupported circular value]';
1781
+ }
1782
+ if (Array.isArray(current)) {
1783
+ ancestors.add(current);
1784
+ const output = [];
1785
+ const itemCount = Math.min(current.length, MAX_ARRAY_ITEMS);
1786
+ for(let index = 0; index < itemCount; index += 1){
1787
+ if (remainingBytes <= 0) break;
1788
+ output.push(visit(current[index], `${path}[${index}]`, depth + 1));
1789
+ }
1790
+ if (current.length > output.length) truncatedPaths.add(path);
1791
+ ancestors.delete(current);
1792
+ return output;
1793
+ }
1794
+ const prototype = Object.getPrototypeOf(current);
1795
+ if (prototype !== Object.prototype && null !== prototype) {
1796
+ truncatedPaths.add(path);
1797
+ const constructorName = current.constructor?.name ?? 'object';
1798
+ return `[Unsupported object: ${constructorName}]`;
1799
+ }
1800
+ ancestors.add(current);
1801
+ const output = {};
1802
+ const keys = Object.keys(current);
1803
+ for (const key of keys.slice(0, MAX_OBJECT_KEYS)){
1804
+ const childPath = objectPath(path, key);
1805
+ if (!consume(childPath, key)) break;
1806
+ if (isSecretKey(key)) {
1807
+ output[key] = '[REDACTED]';
1808
+ redactedPaths.add(childPath);
1809
+ } else output[key] = visit(current[key], childPath, depth + 1);
1810
+ if (remainingBytes <= 0) break;
1811
+ }
1812
+ if (keys.length > Object.keys(output).length) truncatedPaths.add(path);
1813
+ ancestors.delete(current);
1814
+ return output;
1815
+ };
1816
+ const sanitized = visit(value, '$', 0);
1817
+ const serialized = JSON.stringify(sanitized);
1818
+ if (void 0 === serialized || stringByteLength(serialized) > MAX_SERIALIZED_BYTES) return {
1819
+ value: TRUNCATED_VALUE,
1820
+ truncatedPaths: [
1821
+ '$'
1822
+ ]
1823
+ };
1824
+ return {
1825
+ value: sanitized,
1826
+ ...truncatedPaths.size > 0 ? {
1827
+ truncatedPaths: [
1828
+ ...truncatedPaths
1829
+ ]
1830
+ } : {},
1831
+ ...redactedPaths.size > 0 ? {
1832
+ redactedPaths: [
1833
+ ...redactedPaths
1834
+ ]
1835
+ } : {}
1836
+ };
1837
+ }
1838
+ function sanitizeReportError(error) {
1839
+ if (error instanceof Error) {
1840
+ const workflowLike = error;
1841
+ return {
1842
+ name: sanitizeReportText(error.name || 'Error', 200),
1843
+ message: sanitizeReportText(error.message || String(error)),
1844
+ ...'string' == typeof workflowLike.code ? {
1845
+ code: sanitizeReportText(workflowLike.code, 200)
1846
+ } : {},
1847
+ ...void 0 === workflowLike.details ? {} : {
1848
+ details: sanitizeReportValue(workflowLike.details)
1849
+ }
1850
+ };
1851
+ }
1852
+ return {
1853
+ name: 'Error',
1854
+ message: sanitizeReportText(String(error))
1855
+ };
1856
+ }
1857
+ const warnReport = getDebug('test-runner:report-assembler', {
1858
+ console: true
1859
+ });
1860
+ const exactMidsceneDetailNodes = new Set([
1861
+ 'aiAct',
1862
+ 'aiAssert',
1863
+ 'recordToReport'
1864
+ ]);
1865
+ const scopedExecutionKey = (scopeId, executionId)=>`${scopeId}\u0000${executionId}`;
1866
+ const reportSourcesForScope = (context, scopeId)=>context.sourcesByScope.get(scopeId) ?? [];
1867
+ const addDiagnostic = (context, diagnostic)=>{
1868
+ context.diagnostics.push(diagnostic);
1869
+ const key = JSON.stringify([
1870
+ diagnostic.code,
1871
+ diagnostic.scopeId,
1872
+ diagnostic.executionId
1873
+ ]);
1874
+ if (!context.warnedDiagnostics.has(key)) {
1875
+ context.warnedDiagnostics.add(key);
1876
+ warnReport(diagnostic.message);
1877
+ }
1878
+ };
1879
+ const buildStep = (step, scopeId, context)=>{
1880
+ const details = [];
1881
+ const unresolvedExecutionIds = [];
1882
+ for (const trace of step.report?.traces ?? []){
1883
+ const previousScope = context.tracedScopeByExecution.get(trace.executionId);
1884
+ if (previousScope && previousScope !== scopeId) throw new Error(`Agent execution ${trace.executionId} was traced by multiple test scopes (${previousScope}, ${scopeId}).`);
1885
+ context.tracedScopeByExecution.set(trace.executionId, scopeId);
1886
+ const detail = context.detailsByScopeAndExecution.get(scopedExecutionKey(scopeId, trace.executionId));
1887
+ if (detail) details.push(detail);
1888
+ else unresolvedExecutionIds.push(trace.executionId);
1889
+ }
1890
+ let agentDetailDiagnostic;
1891
+ if (unresolvedExecutionIds.length > 0) {
1892
+ agentDetailDiagnostic = `Agent detail could not be resolved for ${unresolvedExecutionIds.join(', ')}.`;
1893
+ const sources = reportSourcesForScope(context, scopeId);
1894
+ for (const executionId of unresolvedExecutionIds)addDiagnostic(context, {
1895
+ level: 'warning',
1896
+ code: 'agent-detail-unresolved',
1897
+ message: `Agent detail could not be resolved for execution ${executionId} in test scope ${scopeId}.`,
1898
+ scopeId,
1899
+ executionId,
1900
+ ...sources.length > 0 ? {
1901
+ sourcePaths: sources.map((source)=>source.sourcePath)
1902
+ } : {}
1903
+ });
1904
+ } else if (0 === details.length && exactMidsceneDetailNodes.has(step.node)) {
1905
+ agentDetailDiagnostic = 'This Agent did not expose a stable execution reference for the Step.';
1906
+ addDiagnostic(context, {
1907
+ level: 'warning',
1908
+ code: 'agent-detail-unavailable',
1909
+ message: `No stable Agent execution reference was captured for ${step.node} in test scope ${scopeId}.`,
1910
+ scopeId,
1911
+ ...reportSourcesForScope(context, scopeId).length > 0 ? {
1912
+ sourcePaths: reportSourcesForScope(context, scopeId).map((source)=>source.sourcePath)
1913
+ } : {}
1914
+ });
1915
+ }
1916
+ const prompt = 'string' == typeof step.input?.prompt ? step.input.prompt : void 0;
1917
+ return {
1918
+ id: `${scopeId}:${step.phase}:${step.stepIndex}`,
1919
+ phase: step.phase,
1920
+ stepIndex: step.stepIndex,
1921
+ node: step.node,
1922
+ ...prompt ? {
1923
+ title: sanitizeReportText(prompt, 240)
1924
+ } : {},
1925
+ status: step.status,
1926
+ continuedAfterError: step.continuedAfterError,
1927
+ startedAt: step.startedAt,
1928
+ endedAt: step.endedAt,
1929
+ durationMs: step.durationMs,
1930
+ input: sanitizeReportValue(step.input),
1931
+ ...step.output ? {
1932
+ output: {
1933
+ ...void 0 === step.output.summary ? {} : {
1934
+ summary: sanitizeReportText(step.output.summary)
1935
+ },
1936
+ ...void 0 === step.output.data ? {} : {
1937
+ data: sanitizeReportValue(step.output.data)
1938
+ }
1939
+ }
1940
+ } : {},
1941
+ ...step.error ? {
1942
+ error: sanitizeReportError(step.error)
1943
+ } : {},
1944
+ ...details.length > 0 ? {
1945
+ agentDetails: details
1946
+ } : {},
1947
+ ...agentDetailDiagnostic ? {
1948
+ agentDetailDiagnostic
1949
+ } : {}
1950
+ };
1951
+ };
1952
+ const buildAttempt = (attempt, context)=>({
1953
+ attemptId: attempt.runId,
1954
+ attemptIndex: attempt.attemptIndex,
1955
+ status: attempt.status,
1956
+ startedAt: attempt.startedAt,
1957
+ endedAt: attempt.endedAt,
1958
+ durationMs: attempt.durationMs,
1959
+ beforeEach: attempt.beforeEach.map((step)=>buildStep(step, attempt.runId, context)),
1960
+ steps: attempt.steps.map((step)=>buildStep(step, attempt.runId, context)),
1961
+ afterEach: attempt.afterEach.map((step)=>buildStep(step, attempt.runId, context)),
1962
+ ...attempt.teardownErrors?.length ? {
1963
+ teardownErrors: attempt.teardownErrors.map(sanitizeReportError)
1964
+ } : {},
1965
+ ...reportSourcesForScope(context, attempt.runId).length > 0 ? {
1966
+ scopeReportIds: reportSourcesForScope(context, attempt.runId).map((source)=>source.reportId)
1967
+ } : {}
1968
+ });
1969
+ const buildCase = (outcome, context)=>({
1970
+ caseId: outcome.caseId,
1971
+ name: outcome.name,
1972
+ caseIndex: outcome.caseIndex,
1973
+ status: outcome.status,
1974
+ ...outcome.notRunReason ? {
1975
+ notRunReason: outcome.notRunReason
1976
+ } : {},
1977
+ attempts: (outcome.attempts ?? (outcome.run ? [
1978
+ outcome.run
1979
+ ] : [])).map((attempt)=>buildAttempt(attempt, context))
1980
+ });
1981
+ const buildDocument = (documentId, document, cases, context)=>{
1982
+ const sourcePath = document?.sourcePath ?? cases[0]?.sourcePath ?? '<unknown>';
1983
+ const status = document?.status ?? (cases.some((item)=>'success' !== item.status) ? 'failed' : 'success');
1984
+ return {
1985
+ documentId,
1986
+ sourcePath,
1987
+ status,
1988
+ ...document ? {
1989
+ startedAt: document.startedAt,
1990
+ endedAt: document.endedAt,
1991
+ durationMs: document.durationMs
1992
+ } : {},
1993
+ beforeAll: (document?.beforeAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1994
+ cases: cases.map((outcome)=>buildCase(outcome, context)),
1995
+ afterAll: (document?.afterAll ?? []).map((step)=>buildStep(step, document.documentRunId, context)),
1996
+ ...document?.teardownErrors?.length ? {
1997
+ teardownErrors: document.teardownErrors.map(sanitizeReportError)
1998
+ } : {},
1999
+ ...document && reportSourcesForScope(context, document.documentRunId).length > 0 ? {
2000
+ scopeReportIds: reportSourcesForScope(context, document.documentRunId).map((source)=>source.reportId)
2001
+ } : {}
2002
+ };
2003
+ };
2004
+ const buildProject = (project, context)=>{
2005
+ const documentById = new Map(project.documents.map((document)=>[
2006
+ document.documentId,
2007
+ document
2008
+ ]));
2009
+ const casesByDocumentId = new Map();
2010
+ for (const outcome of project.cases){
2011
+ const current = casesByDocumentId.get(outcome.documentId) ?? [];
2012
+ current.push(outcome);
2013
+ casesByDocumentId.set(outcome.documentId, current);
2014
+ }
2015
+ const documentIds = new Set([
2016
+ ...project.documents.map((document)=>document.documentId),
2017
+ ...project.cases.map((outcome)=>outcome.documentId)
2018
+ ]);
2019
+ return {
2020
+ projectId: project.projectId,
2021
+ name: project.name,
2022
+ platform: project.platform,
2023
+ status: project.status,
2024
+ retry: project.retry,
2025
+ ...project.lifecycle ? {
2026
+ lifecycle: {
2027
+ status: project.lifecycle.status,
2028
+ startedAt: project.lifecycle.startedAt,
2029
+ endedAt: project.lifecycle.endedAt,
2030
+ durationMs: project.lifecycle.durationMs,
2031
+ ...project.lifecycle.setupError ? {
2032
+ setupError: sanitizeReportError(project.lifecycle.setupError)
2033
+ } : {},
2034
+ ...project.lifecycle.teardownErrors?.length ? {
2035
+ teardownErrors: project.lifecycle.teardownErrors.map(sanitizeReportError)
2036
+ } : {}
2037
+ }
2038
+ } : {},
2039
+ documents: [
2040
+ ...documentIds
2041
+ ].map((documentId)=>buildDocument(documentId, documentById.get(documentId), casesByDocumentId.get(documentId) ?? [], context)),
2042
+ collectionErrors: project.collectionErrors.map((collectionError)=>({
2043
+ sourcePath: collectionError.sourcePath,
2044
+ error: sanitizeReportError(collectionError.error)
2045
+ }))
2046
+ };
2047
+ };
2048
+ function collectTestRunReportSources(result) {
2049
+ const sources = [];
2050
+ for (const project of result.projects){
2051
+ for (const document of project.documents)for (const sourcePath of document.reportPaths ?? [])sources.push({
2052
+ scopeId: document.documentRunId,
2053
+ sourcePath
2054
+ });
2055
+ for (const outcome of project.cases)for (const attempt of outcome.attempts ?? (outcome.run ? [
2056
+ outcome.run
2057
+ ] : []))for (const sourcePath of attempt.reportPaths ?? [])sources.push({
2058
+ scopeId: attempt.runId,
2059
+ sourcePath
2060
+ });
2061
+ }
2062
+ return sources;
2063
+ }
2064
+ function buildTestRunReportDump(result, index) {
2065
+ const sourcesByScope = new Map();
2066
+ const detailsByScopeAndExecution = new Map();
2067
+ for (const source of index.sources){
2068
+ const scopeSources = sourcesByScope.get(source.scopeId) ?? [];
2069
+ scopeSources.push(source);
2070
+ sourcesByScope.set(source.scopeId, scopeSources);
2071
+ for (const executionId of source.executionIds)detailsByScopeAndExecution.set(scopedExecutionKey(source.scopeId, executionId), {
2072
+ reportId: source.reportId,
2073
+ executionId
2074
+ });
2075
+ }
2076
+ const context = {
2077
+ diagnostics: [],
2078
+ sourcesByScope,
2079
+ detailsByScopeAndExecution,
2080
+ tracedScopeByExecution: new Map(),
2081
+ warnedDiagnostics: new Set()
2082
+ };
2083
+ for (const source of index.sources)if (0 === source.executionIds.length) addDiagnostic(context, {
2084
+ level: 'warning',
2085
+ code: 'source-without-executions',
2086
+ message: `Agent report contains no executions: ${source.sourcePath}`,
2087
+ scopeId: source.scopeId,
2088
+ sourcePaths: [
2089
+ source.sourcePath
2090
+ ]
2091
+ });
2092
+ const projects = result.projects.map((project)=>buildProject(project, context));
2093
+ return {
2094
+ schemaVersion: 1,
2095
+ kind: 'test-runner',
2096
+ runId: result.runId,
2097
+ status: result.status,
2098
+ startedAt: result.startedAt,
2099
+ endedAt: result.endedAt,
2100
+ durationMs: result.durationMs,
2101
+ summary: {
2102
+ ...result.summary
2103
+ },
2104
+ metrics: index.metrics,
2105
+ projects,
2106
+ ...context.diagnostics.length > 0 ? {
2107
+ diagnostics: context.diagnostics
2108
+ } : {}
2109
+ };
2110
+ }
1657
2111
  const writeJson = (path, value)=>{
1658
2112
  (0, external_node_fs_.mkdirSync)((0, external_node_path_.dirname)(path), {
1659
2113
  recursive: true
@@ -1709,6 +2163,9 @@ const writeTestProjectRunResult = (options)=>{
1709
2163
  } : {},
1710
2164
  factsRoot: '.',
1711
2165
  reportDir: relativeToSummary(result.summaryPath, result.reportDir),
2166
+ ...result.reportPath ? {
2167
+ report: relativeToSummary(result.summaryPath, result.reportPath)
2168
+ } : {},
1712
2169
  summary: result.summary,
1713
2170
  projects: result.projects.map((project)=>({
1714
2171
  projectId: project.projectId,
@@ -2196,7 +2653,24 @@ async function runTestProject(options = {}) {
2196
2653
  } : {},
2197
2654
  result
2198
2655
  });
2199
- return result;
2656
+ const reportPath = new TestRunReportAssembler().assemble({
2657
+ outputDir: reportDir,
2658
+ reportFileName: `test-run-${runId}`,
2659
+ sources: collectTestRunReportSources(result),
2660
+ buildRunnerDump: (index)=>buildTestRunReportDump(result, index)
2661
+ });
2662
+ const completedResult = {
2663
+ ...result,
2664
+ reportPath
2665
+ };
2666
+ writeTestProjectRunResult({
2667
+ projectRoot,
2668
+ ...configPath ? {
2669
+ configPath
2670
+ } : {},
2671
+ result: completedResult
2672
+ });
2673
+ return completedResult;
2200
2674
  }
2201
2675
  const parseTestCliArgs = (args, cwd = process.cwd())=>{
2202
2676
  const command = 'nodes' === args[0] ? args[0] : void 0;
@@ -2288,6 +2762,7 @@ async function runTestCli(args, io = defaultCliIO) {
2288
2762
  io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
2289
2763
  io.log(`Results: ${result.resultDir}`);
2290
2764
  io.log(`Summary: ${result.summaryPath}`);
2765
+ if (result.reportPath) io.log(`Report: ${result.reportPath}`);
2291
2766
  return result.exitCode;
2292
2767
  } catch (error) {
2293
2768
  io.error(error instanceof Error ? error.message : String(error));