@quenty/nevermore-cli 4.40.0 → 4.41.1
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/CHANGELOG.md +12 -0
- package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-deploy-command.js +2 -1
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +2 -0
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +3 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.d.ts +2 -0
- package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-metadata.js +2 -0
- package/dist/utils/deploy/deploy-metadata.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.test.js +8 -0
- package/dist/utils/deploy/deploy-metadata.test.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +5 -1
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/nevermore-cli-utils.d.ts +1 -0
- package/dist/utils/nevermore-cli-utils.d.ts.map +1 -1
- package/dist/utils/nevermore-cli-utils.js +10 -0
- package/dist/utils/nevermore-cli-utils.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +20 -2
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +16 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +80 -7
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.js +65 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -0
- package/dist/utils/testing/reporting/test-github-columns.d.ts.map +1 -1
- package/dist/utils/testing/reporting/test-github-columns.js +1 -0
- package/dist/utils/testing/reporting/test-github-columns.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +2 -0
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +3 -0
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +5 -1
- package/src/commands/batch-command/batch-test-command.ts +2 -0
- package/src/commands/deploy-command/index.ts +7 -1
- package/src/utils/deploy/deploy-metadata.test.ts +13 -0
- package/src/utils/deploy/deploy-metadata.ts +3 -0
- package/src/utils/job-context/batch-script-job-context.ts +5 -1
- package/src/utils/nevermore-cli-utils.ts +15 -0
- package/src/utils/open-cloud/open-cloud-client.ts +25 -2
- package/src/utils/testing/parsers/batch-log-parser.test.ts +85 -0
- package/src/utils/testing/parsers/batch-log-parser.ts +98 -7
- package/src/utils/testing/reporting/test-github-columns.ts +1 -0
- package/src/utils/testing/runner/test-runner.ts +5 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { countTracebacks, parseBatchTestLogs } from './batch-log-parser.js';
|
|
4
|
+
|
|
5
|
+
const SLUG_MAP = new Map([['egghunt2026', 'egghunt2026']]);
|
|
6
|
+
|
|
7
|
+
function buildLogs(section: string): string {
|
|
8
|
+
return [
|
|
9
|
+
'===BATCH_TEST_BEGIN egghunt2026===',
|
|
10
|
+
section,
|
|
11
|
+
'===BATCH_TEST_END egghunt2026 PASS 1000===',
|
|
12
|
+
'===BATCH_TEST_SUMMARY===',
|
|
13
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('parseBatchTestLogs', () => {
|
|
18
|
+
it('reads test counts out of a package section', () => {
|
|
19
|
+
const logs = buildLogs('Tests: 275 passed, 275 total');
|
|
20
|
+
|
|
21
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
22
|
+
|
|
23
|
+
expect(result?.testCounts).toEqual({ passed: 275, failed: 0, total: 275 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('fails a pass it cannot read, rather than trusting the pcall', () => {
|
|
27
|
+
// The summary prints last, so it survives a log window that dropped the
|
|
28
|
+
// head. The pcall said success, but nothing here saw a test run.
|
|
29
|
+
const logs = [
|
|
30
|
+
'Tests: 275 passed, 275 total',
|
|
31
|
+
'===BATCH_TEST_END egghunt2026 PASS 1000===',
|
|
32
|
+
'===BATCH_TEST_SUMMARY===',
|
|
33
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
37
|
+
|
|
38
|
+
expect(result?.success).toBe(false);
|
|
39
|
+
expect(result?.error).toContain('No test output could be attributed');
|
|
40
|
+
expect(result?.testCounts).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('still shows output it could not attribute', () => {
|
|
44
|
+
// 330KB of real test output was being reported as "(no output)" because no
|
|
45
|
+
// BEGIN marker survived to delimit it.
|
|
46
|
+
const logs = [
|
|
47
|
+
'PlayerBadgeHelper: attempt to index nil',
|
|
48
|
+
'===BATCH_TEST_SUMMARY===',
|
|
49
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
53
|
+
|
|
54
|
+
expect(result?.logs).toContain('PlayerBadgeHelper');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('counts tracebacks that jest reports as a clean pass', () => {
|
|
58
|
+
const logs = buildLogs(
|
|
59
|
+
[
|
|
60
|
+
'Tests: 155 passed, 155 total',
|
|
61
|
+
'PlayerBadgeHelper: attempt to index nil',
|
|
62
|
+
'Stack Begin',
|
|
63
|
+
"Script 'PlayerBadgeHelper', Line 365",
|
|
64
|
+
'Stack End',
|
|
65
|
+
].join('\n')
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
69
|
+
|
|
70
|
+
expect(result?.testCounts?.failed).toBe(0);
|
|
71
|
+
expect(result?.tracebackCount).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('countTracebacks', () => {
|
|
76
|
+
it('counts each traceback separately', () => {
|
|
77
|
+
expect(countTracebacks('Stack Begin\nfoo\nStack End\nStack Begin\n')).toBe(
|
|
78
|
+
2
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('is zero for empty output', () => {
|
|
83
|
+
expect(countTracebacks('')).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -8,6 +8,14 @@ export interface BatchPackageResult {
|
|
|
8
8
|
/** Inner pcall execution time reported by the Luau batch runner. */
|
|
9
9
|
durationMs?: number;
|
|
10
10
|
testCounts?: ParsedTestCounts;
|
|
11
|
+
/** Why this package failed, when the failure is the parser's own verdict. */
|
|
12
|
+
error?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Luau tracebacks found in this package's section. Reported, not gated on:
|
|
15
|
+
* jest cannot count these, since a deferred-callback crash fires outside any
|
|
16
|
+
* test — often after the test that scheduled it already passed.
|
|
17
|
+
*/
|
|
18
|
+
tracebackCount: number;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
const BEGIN_MARKER = '===BATCH_TEST_BEGIN ';
|
|
@@ -55,6 +63,8 @@ export function parseBatchTestLogs(
|
|
|
55
63
|
let currentSlug: string | null = null;
|
|
56
64
|
let currentLines: string[] = [];
|
|
57
65
|
let summaryLineIndex = -1;
|
|
66
|
+
let beginMarkersSeen = 0;
|
|
67
|
+
let endMarkersSeen = 0;
|
|
58
68
|
|
|
59
69
|
// Matches "<slug> PASS|FAIL [<durationMs>]" — slugs have no whitespace.
|
|
60
70
|
const endInnerPattern = /^(\S+)\s+(?:PASS|FAIL)(?:\s+(\d+))?$/;
|
|
@@ -65,11 +75,13 @@ export function parseBatchTestLogs(
|
|
|
65
75
|
if (trimmed.startsWith(BEGIN_MARKER) && trimmed.endsWith('===')) {
|
|
66
76
|
currentSlug = trimmed.slice(BEGIN_MARKER.length, -3);
|
|
67
77
|
currentLines = [];
|
|
78
|
+
beginMarkersSeen++;
|
|
68
79
|
continue;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
if (trimmed.startsWith(END_MARKER) && trimmed.endsWith('===')) {
|
|
72
83
|
const inner = trimmed.slice(END_MARKER.length, -3);
|
|
84
|
+
endMarkersSeen++;
|
|
73
85
|
const match = endInnerPattern.exec(inner);
|
|
74
86
|
const endSlug = match ? match[1] : inner;
|
|
75
87
|
const durationStr = match?.[2];
|
|
@@ -144,26 +156,76 @@ export function parseBatchTestLogs(
|
|
|
144
156
|
);
|
|
145
157
|
}
|
|
146
158
|
|
|
159
|
+
// The summary is printed last, so it survives a log fetch that dropped the
|
|
160
|
+
// output above it. Summary present with no sections is exactly that case, and
|
|
161
|
+
// it is invisible to the check above — the results below would then be built
|
|
162
|
+
// from pcall exit status alone, knowing nothing about what actually ran.
|
|
163
|
+
const lostSectionOutput = logSections.size === 0 && summaryResults.size > 0;
|
|
164
|
+
if (lostSectionOutput) {
|
|
165
|
+
OutputHelper.warn(
|
|
166
|
+
`[batch-log-parser] Received ${rawLogs.length} chars of output but could not ` +
|
|
167
|
+
`attribute any of it to a package section ` +
|
|
168
|
+
`(${beginMarkersSeen} BEGIN, ${endMarkersSeen} END markers; summary at line ` +
|
|
169
|
+
`${summaryLineIndex} of ${lines.length}). ` +
|
|
170
|
+
'Test counts are not derived from unattributed output.'
|
|
171
|
+
);
|
|
172
|
+
// BEGIN is printed first and the summary last, so their positions separate
|
|
173
|
+
// the two ways this happens: a log window that dropped the head keeps the
|
|
174
|
+
// summary at the end, while reordered messages put it early.
|
|
175
|
+
OutputHelper.warn(
|
|
176
|
+
summaryLineIndex >= lines.length - 2
|
|
177
|
+
? '[batch-log-parser] Summary sits at the end of the log — consistent with the head being dropped by a log size/retention limit.'
|
|
178
|
+
: '[batch-log-parser] Summary sits early in the log — consistent with out-of-order messages ending the scan prematurely.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
// ── Pass 3: Combine log sections with summary results ──
|
|
148
183
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
184
|
+
// Output that arrived but could not be split into sections is still output.
|
|
185
|
+
// Reporting "(no output)" over it hides the very thing needed to diagnose the
|
|
186
|
+
// parse failure, so it is shown verbatim instead. Only safe to hand to a
|
|
187
|
+
// single package: with several, there is no way to say whose output this is.
|
|
188
|
+
const fallbackLogs = noOutputAtAll || lostSectionOutput ? rawLogs.trim() : '';
|
|
189
|
+
|
|
190
|
+
// The unattributed stream is the same text for every package, so it is
|
|
191
|
+
// attached once rather than repeated per package across a large log.
|
|
192
|
+
let unattributedClaimed = false;
|
|
152
193
|
|
|
153
194
|
for (const [packageName, slug] of slugMap) {
|
|
154
|
-
const
|
|
195
|
+
const attributedLogs = logSections.get(slug);
|
|
196
|
+
let sectionLogs = attributedLogs ?? '';
|
|
197
|
+
if (!attributedLogs && fallbackLogs && !unattributedClaimed) {
|
|
198
|
+
sectionLogs = fallbackLogs;
|
|
199
|
+
unattributedClaimed = true;
|
|
200
|
+
}
|
|
155
201
|
const summarySuccess = summaryResults.get(slug);
|
|
156
202
|
|
|
157
203
|
// The JSON summary (pcall result) is the primary success signal.
|
|
158
204
|
// Jest failure detection provides a secondary override.
|
|
205
|
+
//
|
|
206
|
+
// Gate on attributed output only. Unattributed output is shown but not
|
|
207
|
+
// judged: whatever broke attribution is reason enough not to trust which
|
|
208
|
+
// package a failure line belongs to.
|
|
159
209
|
let success = summarySuccess ?? false;
|
|
160
|
-
|
|
161
|
-
|
|
210
|
+
let error: string | undefined;
|
|
211
|
+
if (success && attributedLogs) {
|
|
212
|
+
const hasJestFailures = _hasJestFailuresInLogs(attributedLogs);
|
|
162
213
|
if (hasJestFailures) {
|
|
163
214
|
success = false;
|
|
215
|
+
error = 'Jest reported failing tests';
|
|
164
216
|
}
|
|
165
217
|
}
|
|
166
218
|
|
|
219
|
+
// A pass we cannot read is not a pass. The pcall only proves the script did
|
|
220
|
+
// not throw, which says nothing about whether any test ran or passed.
|
|
221
|
+
if (success && !attributedLogs) {
|
|
222
|
+
success = false;
|
|
223
|
+
error =
|
|
224
|
+
`No test output could be attributed to this package ` +
|
|
225
|
+
`(${rawLogs.length} chars received, ${beginMarkersSeen} BEGIN markers found). ` +
|
|
226
|
+
'Unattributed output follows.';
|
|
227
|
+
}
|
|
228
|
+
|
|
167
229
|
if (!success && !noOutputAtAll) {
|
|
168
230
|
console.error(
|
|
169
231
|
`[batch-log-parser] ${slug}: summarySuccess=${summarySuccess} hasLogs=${
|
|
@@ -172,7 +234,19 @@ export function parseBatchTestLogs(
|
|
|
172
234
|
);
|
|
173
235
|
}
|
|
174
236
|
|
|
175
|
-
|
|
237
|
+
// Deliberately not parsed from unattributed output: attribution is exactly
|
|
238
|
+
// what failed, so a count read from it could belong to anything. The logs
|
|
239
|
+
// are still shown, so the jest summary remains readable by eye.
|
|
240
|
+
const testCounts = attributedLogs
|
|
241
|
+
? parseTestCounts(attributedLogs)
|
|
242
|
+
: undefined;
|
|
243
|
+
const tracebackCount = countTracebacks(sectionLogs);
|
|
244
|
+
if (tracebackCount > 0) {
|
|
245
|
+
OutputHelper.warn(
|
|
246
|
+
`[batch-log-parser] ${slug}: ${tracebackCount} Luau traceback(s) in output. ` +
|
|
247
|
+
'Jest does not count these — a deferred-callback crash fires outside any test.'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
176
250
|
// Prefer the JSON summary (authoritative, immune to log reordering);
|
|
177
251
|
// fall back to the END-marker value if the summary was truncated.
|
|
178
252
|
const durationMs = summaryDurations.get(slug) ?? markerDurations.get(slug);
|
|
@@ -182,12 +256,29 @@ export function parseBatchTestLogs(
|
|
|
182
256
|
logs: sectionLogs,
|
|
183
257
|
durationMs,
|
|
184
258
|
testCounts,
|
|
259
|
+
tracebackCount,
|
|
260
|
+
error,
|
|
185
261
|
});
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
return results;
|
|
189
265
|
}
|
|
190
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Count Luau tracebacks in log output.
|
|
269
|
+
*
|
|
270
|
+
* Reported separately from test counts because jest never sees these: a crash in
|
|
271
|
+
* a deferred callback fires outside any test, so a run can print
|
|
272
|
+
* "Tests: 155 passed, 0 failed" while something is genuinely broken.
|
|
273
|
+
*/
|
|
274
|
+
export function countTracebacks(rawOutput: string): number {
|
|
275
|
+
if (!rawOutput) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const cleanLogs = OutputHelper.stripAnsi(rawOutput);
|
|
279
|
+
return cleanLogs.match(/Stack Begin\s/g)?.length ?? 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
191
282
|
/**
|
|
192
283
|
* Check specifically for Jest test suite/test failures in logs.
|
|
193
284
|
* Unlike parseTestLogs, this ignores "Stack Begin" runtime errors since
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
injectDeployMetadataInPlaceAsync,
|
|
15
15
|
packageUsesManifestAsync,
|
|
16
16
|
} from '../../deploy/deploy-metadata.js';
|
|
17
|
+
import { readPackageVersionAsync } from '../../nevermore-cli-utils.js';
|
|
17
18
|
|
|
18
19
|
export interface SingleTestResult {
|
|
19
20
|
success: boolean;
|
|
@@ -25,6 +26,8 @@ export interface SingleTestResult {
|
|
|
25
26
|
* non-aggregated cloud/local runs — callers should fall back to wall-clock.
|
|
26
27
|
*/
|
|
27
28
|
durationMs?: number;
|
|
29
|
+
/** Why the run failed, when the runner can say more than "it failed". */
|
|
30
|
+
error?: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface SingleTestOptions {
|
|
@@ -78,6 +81,7 @@ export async function runSingleTestAsync(
|
|
|
78
81
|
timestamp: new Date().toISOString(),
|
|
79
82
|
placeId: builtPlace.target.placeId,
|
|
80
83
|
universeId: builtPlace.target.universeId,
|
|
84
|
+
packageVersion: await readPackageVersionAsync(packagePath),
|
|
81
85
|
})
|
|
82
86
|
);
|
|
83
87
|
}
|
|
@@ -107,6 +111,7 @@ export async function runSingleTestAsync(
|
|
|
107
111
|
logs: parsed.logs,
|
|
108
112
|
testCounts: parseTestCounts(parsed.logs),
|
|
109
113
|
durationMs: result.durationMs,
|
|
114
|
+
error: result.errorMessage,
|
|
110
115
|
};
|
|
111
116
|
} finally {
|
|
112
117
|
await context.releaseAsync(deployment);
|