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