@quenty/nevermore-cli 4.43.4 → 4.44.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 (143) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/build-scripts/{combine-test-places.luau → combine-test-places.lua} +3 -3
  3. package/build-scripts/{transform-inject-deploy-metadata.luau → transform-inject-deploy-metadata.lua} +3 -3
  4. package/dist/commands/batch-command/batch-deploy-command.js +4 -4
  5. package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
  6. package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
  7. package/dist/commands/batch-command/batch-test-command.js +76 -19
  8. package/dist/commands/batch-command/batch-test-command.js.map +1 -1
  9. package/dist/commands/deploy-command/deploy-init-utils.d.ts.map +1 -1
  10. package/dist/commands/deploy-command/deploy-init-utils.js +7 -1
  11. package/dist/commands/deploy-command/deploy-init-utils.js.map +1 -1
  12. package/dist/commands/deploy-command/index.js +1 -1
  13. package/dist/commands/deploy-command/index.js.map +1 -1
  14. package/dist/utils/deploy/deploy-github-columns.d.ts +12 -2
  15. package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
  16. package/dist/utils/deploy/deploy-github-columns.js +11 -4
  17. package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
  18. package/dist/utils/deploy/deploy-metadata.js +1 -1
  19. package/dist/utils/deploy/deploy-metadata.js.map +1 -1
  20. package/dist/utils/job-context/base-job-context.d.ts +2 -2
  21. package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
  22. package/dist/utils/job-context/base-job-context.js +1 -1
  23. package/dist/utils/job-context/base-job-context.js.map +1 -1
  24. package/dist/utils/job-context/batch-script-job-context.d.ts +37 -2
  25. package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
  26. package/dist/utils/job-context/batch-script-job-context.js +59 -22
  27. package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
  28. package/dist/utils/job-context/cloud-job-context.d.ts +2 -2
  29. package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
  30. package/dist/utils/job-context/cloud-job-context.js +10 -5
  31. package/dist/utils/job-context/cloud-job-context.js.map +1 -1
  32. package/dist/utils/job-context/cloud-job-context.test.d.ts +7 -0
  33. package/dist/utils/job-context/cloud-job-context.test.d.ts.map +1 -0
  34. package/dist/utils/job-context/cloud-job-context.test.js +91 -0
  35. package/dist/utils/job-context/cloud-job-context.test.js.map +1 -0
  36. package/dist/utils/job-context/job-context.d.ts +70 -2
  37. package/dist/utils/job-context/job-context.d.ts.map +1 -1
  38. package/dist/utils/job-context/local-job-context.d.ts +2 -2
  39. package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
  40. package/dist/utils/job-context/local-job-context.js +4 -2
  41. package/dist/utils/job-context/local-job-context.js.map +1 -1
  42. package/dist/utils/job-context/local-job-context.test.d.ts +7 -0
  43. package/dist/utils/job-context/local-job-context.test.d.ts.map +1 -0
  44. package/dist/utils/job-context/local-job-context.test.js +68 -0
  45. package/dist/utils/job-context/local-job-context.test.js.map +1 -0
  46. package/dist/utils/open-cloud/open-cloud-client.d.ts +50 -7
  47. package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
  48. package/dist/utils/open-cloud/open-cloud-client.js +49 -13
  49. package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
  50. package/dist/utils/open-cloud/open-cloud-client.test.js +125 -1
  51. package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
  52. package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts +22 -0
  53. package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts.map +1 -0
  54. package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js +202 -0
  55. package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js.map +1 -0
  56. package/dist/utils/package-manager-utils.d.ts.map +1 -1
  57. package/dist/utils/package-manager-utils.js +32 -1
  58. package/dist/utils/package-manager-utils.js.map +1 -1
  59. package/dist/utils/package-manager-utils.test.js +25 -0
  60. package/dist/utils/package-manager-utils.test.js.map +1 -1
  61. package/dist/utils/testing/batch-run.e2e.test.d.ts +17 -0
  62. package/dist/utils/testing/batch-run.e2e.test.d.ts.map +1 -0
  63. package/dist/utils/testing/batch-run.e2e.test.js +343 -0
  64. package/dist/utils/testing/batch-run.e2e.test.js.map +1 -0
  65. package/dist/utils/testing/batch-test-runner-template.test.d.ts +11 -0
  66. package/dist/utils/testing/batch-test-runner-template.test.d.ts.map +1 -0
  67. package/dist/utils/testing/batch-test-runner-template.test.js +38 -0
  68. package/dist/utils/testing/batch-test-runner-template.test.js.map +1 -0
  69. package/dist/utils/testing/log-fetch-stats.d.ts +40 -0
  70. package/dist/utils/testing/log-fetch-stats.d.ts.map +1 -0
  71. package/dist/utils/testing/log-fetch-stats.js +18 -0
  72. package/dist/utils/testing/log-fetch-stats.js.map +1 -0
  73. package/dist/utils/testing/log-fetch-stats.test.d.ts +2 -0
  74. package/dist/utils/testing/log-fetch-stats.test.d.ts.map +1 -0
  75. package/dist/utils/testing/log-fetch-stats.test.js +41 -0
  76. package/dist/utils/testing/log-fetch-stats.test.js.map +1 -0
  77. package/dist/utils/testing/parsers/batch-log-parser.d.ts +126 -1
  78. package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
  79. package/dist/utils/testing/parsers/batch-log-parser.js +440 -103
  80. package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
  81. package/dist/utils/testing/parsers/batch-log-parser.test.js +494 -1
  82. package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
  83. package/dist/utils/testing/parsers/batch-log-renderer.d.ts +79 -0
  84. package/dist/utils/testing/parsers/batch-log-renderer.d.ts.map +1 -0
  85. package/dist/utils/testing/parsers/batch-log-renderer.js +241 -0
  86. package/dist/utils/testing/parsers/batch-log-renderer.js.map +1 -0
  87. package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts +2 -0
  88. package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts.map +1 -0
  89. package/dist/utils/testing/parsers/batch-log-renderer.test.js +202 -0
  90. package/dist/utils/testing/parsers/batch-log-renderer.test.js.map +1 -0
  91. package/dist/utils/testing/runner/combined-project-generator.js +1 -1
  92. package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
  93. package/dist/utils/testing/runner/test-runner.d.ts +24 -0
  94. package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
  95. package/dist/utils/testing/runner/test-runner.js +166 -6
  96. package/dist/utils/testing/runner/test-runner.js.map +1 -1
  97. package/dist/utils/testing/runner/test-runner.test.js +228 -2
  98. package/dist/utils/testing/runner/test-runner.test.js.map +1 -1
  99. package/dist/utils/testing/structured-test-results.d.ts +77 -0
  100. package/dist/utils/testing/structured-test-results.d.ts.map +1 -0
  101. package/dist/utils/testing/structured-test-results.js +140 -0
  102. package/dist/utils/testing/structured-test-results.js.map +1 -0
  103. package/dist/utils/testing/structured-test-results.test.d.ts +7 -0
  104. package/dist/utils/testing/structured-test-results.test.d.ts.map +1 -0
  105. package/dist/utils/testing/structured-test-results.test.js +179 -0
  106. package/dist/utils/testing/structured-test-results.test.js.map +1 -0
  107. package/package.json +6 -6
  108. package/src/commands/batch-command/batch-deploy-command.ts +4 -4
  109. package/src/commands/batch-command/batch-test-command.ts +94 -19
  110. package/src/commands/deploy-command/deploy-init-utils.ts +7 -1
  111. package/src/commands/deploy-command/index.ts +1 -1
  112. package/src/utils/deploy/deploy-github-columns.ts +14 -4
  113. package/src/utils/deploy/deploy-metadata.ts +1 -1
  114. package/src/utils/job-context/base-job-context.ts +3 -2
  115. package/src/utils/job-context/batch-script-job-context.ts +98 -17
  116. package/src/utils/job-context/cloud-job-context.test.ts +112 -0
  117. package/src/utils/job-context/cloud-job-context.ts +12 -6
  118. package/src/utils/job-context/job-context.ts +71 -2
  119. package/src/utils/job-context/local-job-context.test.ts +89 -0
  120. package/src/utils/job-context/local-job-context.ts +7 -3
  121. package/src/utils/open-cloud/open-cloud-client.test.ts +155 -1
  122. package/src/utils/open-cloud/open-cloud-client.ts +94 -18
  123. package/src/utils/open-cloud/open-cloud-logs.characterization.test.ts +277 -0
  124. package/src/utils/package-manager-utils.test.ts +27 -0
  125. package/src/utils/package-manager-utils.ts +34 -1
  126. package/src/utils/testing/batch-run.e2e.test.ts +425 -0
  127. package/src/utils/testing/batch-test-runner-template.test.ts +62 -0
  128. package/src/utils/testing/log-fetch-stats.test.ts +48 -0
  129. package/src/utils/testing/log-fetch-stats.ts +55 -0
  130. package/src/utils/testing/parsers/batch-log-parser.test.ts +660 -1
  131. package/src/utils/testing/parsers/batch-log-parser.ts +721 -116
  132. package/src/utils/testing/parsers/batch-log-renderer.test.ts +330 -0
  133. package/src/utils/testing/parsers/batch-log-renderer.ts +329 -0
  134. package/src/utils/testing/runner/combined-project-generator.ts +1 -1
  135. package/src/utils/testing/runner/test-runner.test.ts +332 -2
  136. package/src/utils/testing/runner/test-runner.ts +231 -9
  137. package/src/utils/testing/structured-test-results.test.ts +267 -0
  138. package/src/utils/testing/structured-test-results.ts +199 -0
  139. package/templates/batch-test-runner.lua +242 -0
  140. package/tsconfig.tsbuildinfo +1 -1
  141. package/templates/batch-test-runner.luau +0 -137
  142. /package/build-scripts/{smoke-test-server.luau → smoke-test-server.lua} +0 -0
  143. /package/build-scripts/{transform-rojo-merge-place.luau → transform-rojo-merge-place.lua} +0 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tag NevermoreTestRunnerUtils stamps on the results table a test script
3
+ * returns. A test place also runs probe scripts (`--script-text`) that return
4
+ * whatever they like, so results are recognized by this rather than by shape.
5
+ */
6
+ export const TEST_RESULTS_FORMAT = 'nevermore-test-results@1';
7
+ function readNumber(source, key) {
8
+ const value = source[key];
9
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
10
+ }
11
+ function readFailures(value) {
12
+ if (!Array.isArray(value)) {
13
+ return [];
14
+ }
15
+ const failures = [];
16
+ for (const entry of value) {
17
+ if (typeof entry !== 'object' || entry === null) {
18
+ continue;
19
+ }
20
+ const record = entry;
21
+ if (typeof record.name !== 'string') {
22
+ continue;
23
+ }
24
+ failures.push({
25
+ name: record.name,
26
+ message: typeof record.message === 'string' ? record.message : undefined,
27
+ });
28
+ }
29
+ return failures;
30
+ }
31
+ /**
32
+ * Decode a single returned value into results, or undefined if it is not one.
33
+ *
34
+ * Fields are read defensively rather than trusted: the value crossed a
35
+ * transport, and the two transports spell exotic Luau types differently, so a
36
+ * malformed field must not become a verdict.
37
+ */
38
+ export function decodeStructuredTestResults(value) {
39
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
40
+ return undefined;
41
+ }
42
+ const record = value;
43
+ if (record.format !== TEST_RESULTS_FORMAT) {
44
+ return undefined;
45
+ }
46
+ return {
47
+ // Only an explicit `true` is a pass. A results table that lost its verdict
48
+ // in transit must not read as one.
49
+ success: record.success === true,
50
+ ranJest: record.ranJest === true,
51
+ passed: readNumber(record, 'passed'),
52
+ failed: readNumber(record, 'failed'),
53
+ skipped: readNumber(record, 'skipped'),
54
+ total: readNumber(record, 'total'),
55
+ suitesPassed: readNumber(record, 'suitesPassed'),
56
+ suitesFailed: readNumber(record, 'suitesFailed'),
57
+ suitesTotal: readNumber(record, 'suitesTotal'),
58
+ failures: readFailures(record.failures),
59
+ omittedFailures: readNumber(record, 'omittedFailures'),
60
+ error: typeof record.error === 'string' ? record.error : undefined,
61
+ };
62
+ }
63
+ /**
64
+ * Find the results a run returned, if it returned any.
65
+ *
66
+ * `undefined` covers both "the transport delivered no return channel" and "the
67
+ * script returned nothing recognizable" — a caller falls back to the logs
68
+ * either way, which is what a test script written before this convention gets.
69
+ */
70
+ export function findStructuredTestResults(returnValues) {
71
+ if (!returnValues) {
72
+ return undefined;
73
+ }
74
+ for (const value of returnValues) {
75
+ const results = decodeStructuredTestResults(value);
76
+ if (results) {
77
+ return results;
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Describe a verdict the run's own counts do not support, for a warning.
84
+ *
85
+ * A run reporting failure while nothing in it failed is self-contradicting, and
86
+ * it is precisely what a runner reading the wrong field produces: the counts
87
+ * stay plausible, so nothing looks wrong except that every package fails at
88
+ * once. That shipped once — a runner consulted jest-lua's inverted
89
+ * `AggregatedResult.success` and failed every passing suite.
90
+ *
91
+ * Reported, never corrected. A runner that says it failed may know something its
92
+ * counts cannot express (an interrupted run, a snapshot check, a reporter
93
+ * error), so the verdict stands and the contradiction is made loud instead.
94
+ */
95
+ export function describeUnexplainedVerdict(results) {
96
+ if (results.success || results.failed > 0 || results.suitesFailed > 0) {
97
+ return undefined;
98
+ }
99
+ return (`the run reports failure but its own counts show nothing failed ` +
100
+ `(${results.passed} passed, 0 failed, ${results.total} total, ` +
101
+ `0 of ${results.suitesTotal} suite(s) failed)` +
102
+ (results.error
103
+ ? `. It says: ${results.error}`
104
+ : ' and gives no reason at all'));
105
+ }
106
+ /** Counts in the shape the reporters already render. */
107
+ export function toParsedTestCounts(results) {
108
+ return {
109
+ passed: results.passed,
110
+ failed: results.failed,
111
+ total: results.total,
112
+ };
113
+ }
114
+ /**
115
+ * Why the runner says the run failed. Empty when it says it passed.
116
+ *
117
+ * Phrased to match the log parser's wording so that merging the two sources
118
+ * collapses the overlap instead of reporting every failure twice.
119
+ */
120
+ export function structuredFailureReasons(results) {
121
+ if (results.success) {
122
+ return [];
123
+ }
124
+ const reasons = [];
125
+ if (results.suitesFailed > 0) {
126
+ reasons.push(`${results.suitesFailed} test suite(s) failed`);
127
+ }
128
+ if (results.failed > 0) {
129
+ reasons.push(`${results.failed} test(s) failed`);
130
+ }
131
+ // The runner's own message restates those two lines when a count explains the
132
+ // failure, so it is only worth repeating when nothing else can say why —
133
+ // an interrupted run, a failed snapshot check, a result shape it could not
134
+ // read. That case is also the one where a reason is most needed.
135
+ if (reasons.length === 0) {
136
+ reasons.push(results.error ?? 'the test runner reported the run as failed');
137
+ }
138
+ return reasons;
139
+ }
140
+ //# sourceMappingURL=structured-test-results.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-test-results.js","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Unit tests for decoding the results a test run returns. The decoder stands
3
+ * between a transport and a pass/fail verdict, so the cases that matter are the
4
+ * ones where a value is not what it claims to be.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=structured-test-results.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-test-results.test.d.ts","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Unit tests for decoding the results a test run returns. The decoder stands
3
+ * between a transport and a pass/fail verdict, so the cases that matter are the
4
+ * ones where a value is not what it claims to be.
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import { TEST_RESULTS_FORMAT, decodeStructuredTestResults, describeUnexplainedVerdict, findStructuredTestResults, structuredFailureReasons, toParsedTestCounts, } from './structured-test-results.js';
8
+ function results(overrides = {}) {
9
+ return {
10
+ format: TEST_RESULTS_FORMAT,
11
+ success: true,
12
+ ranJest: true,
13
+ passed: 25,
14
+ failed: 0,
15
+ skipped: 1,
16
+ total: 26,
17
+ suitesPassed: 3,
18
+ suitesFailed: 0,
19
+ suitesTotal: 3,
20
+ failures: [],
21
+ omittedFailures: 0,
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe('decodeStructuredTestResults', () => {
26
+ it('decodes a results table', () => {
27
+ const decoded = decodeStructuredTestResults(results());
28
+ expect(decoded).toEqual({
29
+ success: true,
30
+ ranJest: true,
31
+ passed: 25,
32
+ failed: 0,
33
+ skipped: 1,
34
+ total: 26,
35
+ suitesPassed: 3,
36
+ suitesFailed: 0,
37
+ suitesTotal: 3,
38
+ failures: [],
39
+ omittedFailures: 0,
40
+ error: undefined,
41
+ });
42
+ });
43
+ it('ignores a value that is not tagged as results', () => {
44
+ // A probe run (--script-text) returns whatever it likes, and none of it is
45
+ // a verdict on anything.
46
+ expect(decodeStructuredTestResults({ success: true })).toBeUndefined();
47
+ expect(decodeStructuredTestResults({ format: 'something-else', success: true })).toBeUndefined();
48
+ expect(decodeStructuredTestResults('nevermore-test-results@1')).toBeUndefined();
49
+ expect(decodeStructuredTestResults(undefined)).toBeUndefined();
50
+ expect(decodeStructuredTestResults([results()])).toBeUndefined();
51
+ });
52
+ it('reads only an explicit true as a pass', () => {
53
+ // Anything else means the verdict did not survive the trip, and a lost
54
+ // verdict must not read as a passing one.
55
+ expect(decodeStructuredTestResults(results({ success: 'yes' }))?.success).toBe(false);
56
+ expect(decodeStructuredTestResults(results({ success: undefined }))?.success).toBe(false);
57
+ });
58
+ it('treats a malformed count as zero rather than as a number', () => {
59
+ const decoded = decodeStructuredTestResults(results({ passed: 'lots', total: null, failed: Number.NaN }));
60
+ expect(decoded?.passed).toBe(0);
61
+ expect(decoded?.total).toBe(0);
62
+ expect(decoded?.failed).toBe(0);
63
+ });
64
+ it('keeps only well-formed failures', () => {
65
+ const decoded = decodeStructuredTestResults(results({
66
+ failures: [
67
+ { name: 'Maid does a thing', message: 'expected true' },
68
+ { name: 'Maid does another thing' },
69
+ { message: 'no name' },
70
+ 'not a failure',
71
+ ],
72
+ }));
73
+ expect(decoded?.failures).toEqual([
74
+ { name: 'Maid does a thing', message: 'expected true' },
75
+ { name: 'Maid does another thing', message: undefined },
76
+ ]);
77
+ });
78
+ it('accepts an empty failure list however the transport spelled it', () => {
79
+ // An empty Lua table marshals as an array on the bridge and can arrive as an
80
+ // object from the cloud.
81
+ expect(decodeStructuredTestResults(results({ failures: {} }))?.failures).toEqual([]);
82
+ });
83
+ });
84
+ describe('findStructuredTestResults', () => {
85
+ it('finds results among other returned values', () => {
86
+ const found = findStructuredTestResults([
87
+ 42,
88
+ 'log',
89
+ results({ passed: 9 }),
90
+ ]);
91
+ expect(found?.passed).toBe(9);
92
+ });
93
+ it('is undefined when the script returned nothing', () => {
94
+ // A test script written before this convention returns nil, which is the
95
+ // case that has to keep falling back to the logs.
96
+ expect(findStructuredTestResults([])).toBeUndefined();
97
+ expect(findStructuredTestResults(undefined)).toBeUndefined();
98
+ });
99
+ });
100
+ describe('structuredFailureReasons', () => {
101
+ it('says nothing about a passing run', () => {
102
+ expect(structuredFailureReasons(decodeStructuredTestResults(results()))).toEqual([]);
103
+ });
104
+ it('words counts the way the log parser does, so the overlap merges', () => {
105
+ const reasons = structuredFailureReasons(decodeStructuredTestResults(results({ success: false, failed: 2, suitesFailed: 1 })));
106
+ expect(reasons).toContain('2 test(s) failed');
107
+ expect(reasons).toContain('1 test suite(s) failed');
108
+ });
109
+ it('explains a failure no count can account for', () => {
110
+ const reasons = structuredFailureReasons(decodeStructuredTestResults(results({ success: false })));
111
+ expect(reasons).toEqual(['the test runner reported the run as failed']);
112
+ });
113
+ it('carries the message the runner supplied', () => {
114
+ const reasons = structuredFailureReasons(decodeStructuredTestResults(results({
115
+ success: false,
116
+ error: '[NevermoreTestRunner] Jest run failed',
117
+ })));
118
+ expect(reasons).toContain('[NevermoreTestRunner] Jest run failed');
119
+ });
120
+ });
121
+ describe('structuredFailureReasons wording', () => {
122
+ it('never builds a reason out of zeros', () => {
123
+ // "0 test(s) and 0 test suite(s) failed" was a real failure reason once. A
124
+ // reason saying nothing failed is unreadable as either verdict, and it is
125
+ // what made the bug behind it hard to see.
126
+ for (const overrides of [
127
+ { success: false },
128
+ { success: false, failed: 0, suitesFailed: 0 },
129
+ {
130
+ success: false,
131
+ error: '[NevermoreTestRunner] the run was interrupted',
132
+ },
133
+ ]) {
134
+ const reasons = structuredFailureReasons(decodeStructuredTestResults(results(overrides)));
135
+ expect(reasons.length).toBeGreaterThan(0);
136
+ for (const reason of reasons) {
137
+ expect(reason).not.toMatch(/\b0 test\(s\)/);
138
+ expect(reason).not.toMatch(/\b0 test suite\(s\)/);
139
+ }
140
+ }
141
+ });
142
+ it('does not restate the counts the reasons already carry', () => {
143
+ const reasons = structuredFailureReasons(decodeStructuredTestResults(results({
144
+ success: false,
145
+ failed: 2,
146
+ suitesFailed: 1,
147
+ error: '[NevermoreTestRunner] 2 test(s) failed, 1 test suite(s) failed',
148
+ })));
149
+ expect(reasons).toEqual(['1 test suite(s) failed', '2 test(s) failed']);
150
+ });
151
+ });
152
+ describe('describeUnexplainedVerdict', () => {
153
+ it('is silent about a passing run', () => {
154
+ // The direction that broke: a fully passing result must read as a pass and
155
+ // raise nothing at all.
156
+ expect(describeUnexplainedVerdict(decodeStructuredTestResults(results()))).toBeUndefined();
157
+ });
158
+ it('is silent about a failure its counts explain', () => {
159
+ expect(describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, failed: 2 })))).toBeUndefined();
160
+ expect(describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, suitesFailed: 1 })))).toBeUndefined();
161
+ });
162
+ it('describes a failure no count supports', () => {
163
+ // The exact signature of a runner reading the wrong field: plausible counts,
164
+ // and every package failed anyway.
165
+ const description = describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, passed: 311, failed: 0, total: 311 })));
166
+ expect(description).toContain('nothing failed');
167
+ expect(description).toContain('311 passed');
168
+ });
169
+ });
170
+ describe('toParsedTestCounts', () => {
171
+ it('narrows to the counts the reporters render', () => {
172
+ expect(toParsedTestCounts(decodeStructuredTestResults(results()))).toEqual({
173
+ passed: 25,
174
+ failed: 0,
175
+ total: 26,
176
+ });
177
+ });
178
+ });
179
+ //# sourceMappingURL=structured-test-results.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-test-results.test.js","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/nevermore-cli",
3
- "version": "4.43.4",
3
+ "version": "4.44.0",
4
4
  "description": "CLI interface for Nevermore",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -25,11 +25,11 @@
25
25
  "Quenty"
26
26
  ],
27
27
  "dependencies": {
28
- "@quenty/cli-output-helpers": "1.17.2",
29
- "@quenty/nevermore-cli-helpers": "1.17.1",
28
+ "@quenty/cli-output-helpers": "1.18.0",
29
+ "@quenty/nevermore-cli-helpers": "1.18.0",
30
30
  "@quenty/nevermore-deploy": "1.1.1",
31
- "@quenty/nevermore-template-helpers": "1.18.2",
32
- "@quenty/studio-bridge": "0.18.1",
31
+ "@quenty/nevermore-template-helpers": "1.19.0",
32
+ "@quenty/studio-bridge": "0.19.0",
33
33
  "execa": "^9.6.1",
34
34
  "find-git-root": "^1.0.4",
35
35
  "inquirer": "^13.2.0",
@@ -60,5 +60,5 @@
60
60
  "engines": {
61
61
  "node": ">=16"
62
62
  },
63
- "gitHead": "7dac4ebe32676fcfaa3748bfc1d57b6d7e442ad4"
63
+ "gitHead": "f75f93d5b2ac51cf7da3b7599788dcce31fd8a29"
64
64
  }
@@ -57,7 +57,7 @@ import {
57
57
  const SMOKE_TEST_SCRIPT_PATH = resolvePackagePath(
58
58
  import.meta.url,
59
59
  'build-scripts',
60
- 'smoke-test-server.luau'
60
+ 'smoke-test-server.lua'
61
61
  );
62
62
 
63
63
  interface BatchDeployArgs extends NevermoreGlobalArgs {
@@ -285,7 +285,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
285
285
  reporters.push(
286
286
  new GithubCommentTableReporter(
287
287
  state,
288
- createDeployCommentConfig(),
288
+ createDeployCommentConfig(deployLabels),
289
289
  concurrency
290
290
  )
291
291
  );
@@ -524,7 +524,7 @@ function _annotateSmokeTestFailure(logs: string): string {
524
524
  const header =
525
525
  'Post-deploy smoke test failed. The deploy itself succeeded, but a server ' +
526
526
  "script errored on boot. ('TaskScript' in any stack trace below refers to " +
527
- "Nevermore's smoke-test-server.luau, which loadstring()s each Script under " +
527
+ "Nevermore's smoke-test-server.lua, which loadstring()s each Script under " +
528
528
  'ServerScriptService — if you see "loadstring() is not available", set ' +
529
529
  '$properties.LoadStringEnabled = true on ServerScriptService in your ' +
530
530
  'rojo project.)';
@@ -564,7 +564,7 @@ async function _runSmokeTestAsync(
564
564
  );
565
565
 
566
566
  const rawLogs = await client.getRawTaskLogsAsync(task.path);
567
- const parsed = parseTestLogs(rawLogs);
567
+ const parsed = parseTestLogs(rawLogs.text);
568
568
 
569
569
  const infraSuccess = completedTask.state === 'COMPLETE';
570
570
  return {
@@ -1,6 +1,7 @@
1
1
  import { CommandModule } from 'yargs';
2
2
  import { OutputHelper } from '@quenty/cli-output-helpers';
3
3
  import {
4
+ AggregatedBatchReporter,
4
5
  type Reporter,
5
6
  type JobPhase,
6
7
  type ProgressSummary,
@@ -8,6 +9,8 @@ import {
8
9
  import { NevermoreGlobalArgs } from '../../args/global-args.js';
9
10
  import { getApiKeyAsync } from '@quenty/nevermore-cli-helpers';
10
11
  import { runBatchAsync } from '../../utils/batch/batch-runner.js';
12
+ import { renderBatchOutcome } from '../../utils/testing/parsers/batch-log-renderer.js';
13
+ import { emitDiagnostic } from '../../utils/testing/parsers/batch-log-parser.js';
11
14
  import { createBasePlaceResolver } from '../../utils/build/base-place-resolver-factory.js';
12
15
  import {
13
16
  type JobContext,
@@ -182,23 +185,41 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
182
185
  : args.concurrency;
183
186
  const isGrouped = !process.stdout.isTTY || args.verbose || isCI();
184
187
  const packageNames = packages.map((p) => p.name);
188
+ const aggregated = args.aggregated ?? false;
189
+ // Decided once, here, and passed down. Three separate isCI() lookups — the
190
+ // renderer, OutputHelper's group markers and GroupedReporter — could each
191
+ // answer differently, and a half-grouped log is worse than an ungrouped one.
192
+ const presentation = { useGroups: isCI(), color: !process.env.NO_COLOR };
193
+ let aggregatedReporter: AggregatedBatchReporter | undefined;
185
194
 
186
195
  const reporter = new CompositeReporter(
187
196
  packageNames,
188
197
  (state: LiveStateTracker) => {
198
+ // An aggregated run is one execution, so there is no per-package progress
199
+ // to report and no per-package moment to report it at.
200
+ if (aggregated) {
201
+ aggregatedReporter = new AggregatedBatchReporter(packages.length, {
202
+ actionVerb: 'Testing',
203
+ });
204
+ }
205
+
189
206
  const reporters: Reporter[] = [
190
- isGrouped
191
- ? new GroupedReporter(state, {
192
- showLogs: args.logs ?? false,
193
- verbose: args.verbose,
194
- actionVerb: 'Testing',
195
- expectsTestCounts: true,
196
- })
197
- : new SpinnerReporter(state, {
198
- showLogs: args.logs ?? false,
199
- actionVerb: 'Testing',
200
- }),
201
- new SummaryTableReporter(state),
207
+ aggregatedReporter ??
208
+ (isGrouped
209
+ ? new GroupedReporter(state, {
210
+ showLogs: args.logs ?? false,
211
+ verbose: args.verbose,
212
+ actionVerb: 'Testing',
213
+ expectsTestCounts: true,
214
+ })
215
+ : new SpinnerReporter(state, {
216
+ showLogs: args.logs ?? false,
217
+ actionVerb: 'Testing',
218
+ })),
219
+ // Told to expect counts for the same reason the grouped reporter and
220
+ // the PR comment are: a green row with nothing behind it reads exactly
221
+ // like a package that tested something.
222
+ new SummaryTableReporter(state, { expectsTestCounts: true }),
202
223
  ];
203
224
  if (args.output) {
204
225
  reporters.push(new JsonFileReporter(state, args.output));
@@ -244,19 +265,42 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
244
265
  ? new CloudJobContext(innerReporter, client, basePlaceResolver)
245
266
  : new LocalJobContext(innerReporter, client, basePlaceResolver);
246
267
 
247
- const context: JobContext = args.aggregated
268
+ const batchContext = aggregated
248
269
  ? new BatchScriptJobContext(innerContext, packages, {
249
270
  batchPlaceId: args.batchPlaceId,
250
271
  batchUniverseId: args.batchUniverseId,
251
272
  batchTimeoutMs: timeoutMs,
252
273
  reporter,
253
274
  })
254
- : innerContext;
275
+ : undefined;
276
+ const context: JobContext = batchContext ?? innerContext;
255
277
 
256
278
  await reporter.startAsync();
257
279
 
258
280
  let exitCode = 0;
259
281
  try {
282
+ // Run the batch before the per-package loop asks for anything, for two
283
+ // reasons. The run is shown once, in order, before any per-package result
284
+ // could interleave with it; and the shared build, upload and execution
285
+ // happen outside every package's async context, so the verbose output they
286
+ // produce stops being captured into whichever package first reached the
287
+ // shared promise — which is how the upload URL and a 78-line build list
288
+ // came to be filed under two arbitrary packages.
289
+ if (batchContext && aggregatedReporter) {
290
+ // Retried the same way a package's run is. Running the batch up here took
291
+ // it out from under the per-package retry wrapper that used to cover it,
292
+ // and a cloud task that times out once is exactly what that retry is for.
293
+ const outcome = await _retryOnTransientAsync('batch execution', () =>
294
+ batchContext.getExecutionOutcomeAsync()
295
+ );
296
+
297
+ aggregatedReporter.printRun(renderBatchOutcome(outcome, presentation));
298
+
299
+ for (const { level, message } of outcome.diagnostics) {
300
+ emitDiagnostic(level, message, presentation.useGroups);
301
+ }
302
+ }
303
+
260
304
  const results = await runBatchAsync<BatchTarget, BatchTestResult>({
261
305
  items: packages,
262
306
  concurrency,
@@ -264,7 +308,12 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
264
308
  bufferOutput: isGrouped,
265
309
  stateTracker: reporter.state,
266
310
  executeAsync: async (pkg) => {
267
- const result = await _runWithRetryAsync(pkg, context, timeoutMs);
311
+ const result = await _runWithRetryAsync(
312
+ pkg,
313
+ context,
314
+ timeoutMs,
315
+ args.aggregated ?? false
316
+ );
268
317
 
269
318
  return {
270
319
  packageName: pkg.name,
@@ -278,6 +327,12 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
278
327
  progressSummary: result.testCounts
279
328
  ? { kind: 'test-counts' as const, ...result.testCounts }
280
329
  : undefined,
330
+ // Every reporter downstream reads state through resolveResultStatus,
331
+ // so a run judged without its logs is marked once here rather than
332
+ // being re-derived by each of them.
333
+ caveats: result.logsLost
334
+ ? (['logs-lost'] as const).slice()
335
+ : undefined,
281
336
  error: result.error,
282
337
  };
283
338
  },
@@ -300,23 +355,43 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
300
355
  async function _runWithRetryAsync(
301
356
  pkg: BatchTarget,
302
357
  context: JobContext,
303
- timeoutMs: number
358
+ timeoutMs: number,
359
+ aggregated: boolean
304
360
  ): Promise<SingleTestResult> {
305
361
  const opts = {
306
362
  packagePath: pkg.path,
307
363
  packageName: pkg.name,
308
364
  timeoutMs,
309
365
  target: pkg.target,
366
+ // One fetch serves every package here, so its provenance is reported once
367
+ // for the whole batch rather than restated per package.
368
+ suppressCountsProvenance: aggregated,
310
369
  };
311
370
 
371
+ return _retryOnTransientAsync(pkg.name, () =>
372
+ runSingleTestAsync(context, opts)
373
+ );
374
+ }
375
+
376
+ /**
377
+ * Run something once more if it failed for a reason that tends not to repeat.
378
+ *
379
+ * Shared so the batch execution and a single package's run agree on what counts
380
+ * as transient — the batch used to inherit this by being triggered from inside
381
+ * a package's run, and stopped when it moved out to the command.
382
+ */
383
+ async function _retryOnTransientAsync<T>(
384
+ label: string,
385
+ runAsync: () => Promise<T>
386
+ ): Promise<T> {
312
387
  try {
313
- return await runSingleTestAsync(context, opts);
388
+ return await runAsync();
314
389
  } catch (err) {
315
390
  const message = err instanceof Error ? err.message : String(err);
316
391
 
317
392
  if (message.includes('timed out') || message.includes('fetch failed')) {
318
- OutputHelper.warn(`${pkg.name}: transient failure, retrying...`);
319
- return await runSingleTestAsync(context, opts);
393
+ OutputHelper.warn(`${label}: transient failure, retrying...`);
394
+ return await runAsync();
320
395
  }
321
396
 
322
397
  throw err;
@@ -52,11 +52,17 @@ export async function detectProjectFileAsync(
52
52
  export async function detectScriptFileAsync(
53
53
  packagePath: string
54
54
  ): Promise<string | undefined> {
55
+ // Both extensions for each, since a package may use either. The client
56
+ // entry read `ClientMain.server.luau` and so matched nothing: every client
57
+ // script in this repo is `ClientMain.client.lua`, and a `.server` client
58
+ // script does not exist.
55
59
  const candidates = [
56
60
  'test/scripts/Server/ServerMain.server.lua',
57
61
  'test/scripts/Server/ServerMain.server.luau',
58
62
  'scripts/Server/ServerMain.server.lua',
59
- 'scripts/Client/ClientMain.server.luau',
63
+ 'scripts/Server/ServerMain.server.luau',
64
+ 'scripts/Client/ClientMain.client.lua',
65
+ 'scripts/Client/ClientMain.client.luau',
60
66
  ];
61
67
  for (const candidate of candidates) {
62
68
  if (await fileExistsAsync(path.join(packagePath, candidate))) {
@@ -559,7 +559,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
559
559
  reporters.push(
560
560
  new GithubCommentTableReporter(
561
561
  state,
562
- createDeployCommentConfig(),
562
+ createDeployCommentConfig(deployLabels),
563
563
  isMultiPlace ? MULTI_PLACE_CONCURRENCY : 1
564
564
  )
565
565
  );