@arjunkhera/atlas 0.4.31 → 0.4.32

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
@@ -186,17 +186,26 @@ function caseTexts(xml) {
186
186
 
187
187
  // The results of one report: Map of case name to pass, assertion, error or skipped.
188
188
  // For pytest, a name with parameters ("test_x[1]") counts under its function name.
189
- export function readReport(xml) {
189
+ // `known` holds the case names that the case reader found in the file. node --test writes a test
190
+ // whose body holds subtests as a <testsuite> with the subtests as its cases, and no case of its own
191
+ // name. So a subtest counts under the outermost suite around it whose name is known (item
192
+ // 30badd46). A suite of describe() has no known name, so its cases count under their own names.
193
+ // Limit: node writes no failure for a parent test that fails outside its subtests, so that failure
194
+ // does not show here.
195
+ export function readReport(xml, known = []) {
190
196
  const cases = readJunit(xml);
191
197
  const texts = caseTexts(xml);
192
198
  const rank = { pass: 0, skipped: 1, error: 2, assertion: 3 };
199
+ const named = new Set(known);
200
+ // The junit reporter of node writes a double quote as "&amp;quot;", so one
201
+ // unescape leaves "&quot;" (f2d07e35). Undo the second escape here.
202
+ const unquote = (text) => text.replaceAll('&quot;', '"');
193
203
  const out = new Map();
194
204
  cases.forEach((c, i) => {
195
205
  let result = c.result;
196
206
  if (result === 'fail') result = /AssertionError|ERR_ASSERTION/.test(`${c.message ?? ''}\n${texts[i] ?? ''}`) ? 'assertion' : 'error';
197
- // The junit reporter of node writes a double quote as "&amp;quot;", so one
198
- // unescape leaves "&quot;" (f2d07e35). Undo the second escape here.
199
- const name = c.name.replaceAll('&quot;', '"').replace(/\[.*\]$/, '');
207
+ const parent = (c.suites ?? []).map(unquote).find((s) => named.has(s));
208
+ const name = parent ?? unquote(c.name).replace(/\[.*\]$/, '');
200
209
  // When one name holds several results, the worst one counts: a fail beats a pass.
201
210
  if (!out.has(name) || rank[result] > rank[out.get(name)]) out.set(name, result);
202
211
  });
@@ -336,7 +345,7 @@ export function runSide({ top, changed, plans, side, work = join(tmpdir(), 'atla
336
345
  if (r.error) why = `the runner did not finish: ${r.error.message}`;
337
346
  else if (!existsSync(report)) why = `the runner wrote no report (exit ${r.status})`;
338
347
  else {
339
- try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'))); } catch (error) { why = `the report does not read: ${error.message}`; }
348
+ try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'), p.known)); } catch (error) { why = `the report does not read: ${error.message}`; }
340
349
  }
341
350
  out.files[p.path] = { command, cases, why };
342
351
  }
@@ -84,7 +84,7 @@ export function plan({ root, base, head = 'HEAD', list = null }) {
84
84
  if (!picked.length && !all) continue;
85
85
  if (!runner) { files.push({ path, runner: null, result: 'not run', why: 'no runner for this file yet' }); plans.push({ path, runner: null, picked, all }); continue; }
86
86
  files.push({ path, runner, result: 'ran', why: null });
87
- plans.push({ path, runner, picked, all, head: r.status === 'removed' ? null : r.path });
87
+ plans.push({ path, runner, picked, all, head: r.status === 'removed' ? null : r.path, known: before ? [...before.cases.keys()] : [] });
88
88
  }
89
89
  files.sort((x, y) => (x.path < y.path ? -1 : 1));
90
90
  return {
@@ -131,7 +131,7 @@ export function runOld({ top, changed, plans, work = join(tmpdir(), 'atlas-after
131
131
  if (r.error) why = `the runner did not finish: ${r.error.message}`;
132
132
  else if (!existsSync(report)) why = `the runner wrote no report (exit ${r.status})`;
133
133
  else {
134
- try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'))); } catch (error) { why = `the report does not read: ${error.message}`; }
134
+ try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'), p.known)); } catch (error) { why = `the report does not read: ${error.message}`; }
135
135
  }
136
136
  out.files[p.path] = { command, cases, why };
137
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/tests/junit.mjs CHANGED
@@ -3,8 +3,10 @@
3
3
  // write this report: node --test (--test-reporter=junit), pytest
4
4
  // (--junitxml), TestNG, JUnit, Vitest and Jest.
5
5
  //
6
- // readJunit(xml) -> [{ name, classname, file, suite, result, message }]
6
+ // readJunit(xml) -> [{ name, classname, file, suite, suites, result, message }]
7
7
  // result: "pass", "fail" or "skipped". An <error> is a fail too.
8
+ // suites: the names of the <testsuite> elements around the case, outermost
9
+ // first. node --test writes a test with subtests as a <testsuite>.
8
10
  //
9
11
  // The reader has no dependency. It reads only the parts it needs: the
10
12
  // <testsuite> nesting, each <testcase> and its <failure>, <error> and
@@ -44,9 +46,10 @@ export function readJunit(xml) {
44
46
  if (tag === 'testcase') {
45
47
  if (closing) { current = null; continue; }
46
48
  const a = attributes(attrs);
47
- const suite = suites.map((s) => s.name).filter(Boolean).join(' > ');
49
+ const names = suites.map((s) => s.name).filter(Boolean);
50
+ const suite = names.join(' > ');
48
51
  const file = a.file ?? suites.map((s) => s.file).filter(Boolean).at(-1) ?? null;
49
- const one = { name: a.name ?? '', classname: a.classname ?? null, file, suite: suite || null, result: 'pass', message: null };
52
+ const one = { name: a.name ?? '', classname: a.classname ?? null, file, suite: suite || null, suites: names, result: 'pass', message: null };
50
53
  cases.push(one);
51
54
  current = selfClosing ? null : one;
52
55
  continue;