dev-diagnosis-cli 0.1.4 → 0.1.6

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 CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.4 — unreleased
3
+ ## 0.1.6 — unreleased
4
+
5
+ - Fix: an empty JUnit file (0 bytes, whitespace only, or a declaration/comment
6
+ and nothing else) no longer parses as a clean run of zero tests — the XML
7
+ reader now requires a root element, so the file is reported as
8
+ "no root element", listed in the warnings, and the `flakes` check fails
9
+ instead of silently reporting on half-read data. A genuinely empty report
10
+ (`<testsuites></testsuites>`) still parses to zero tests. (62 tests.)
11
+ - Docs: the 0.1.5 entry below is now dated — it went live on npm on
12
+ 2026-11-11 but the entry still said "unreleased".
13
+
14
+ ## 0.1.5 — 2026-11-11
15
+
16
+ - Fix: a JUnit file that cannot be parsed no longer passes silently —
17
+ `flakes` used to collect the parse error into a warnings list nothing
18
+ ever printed, so a run set of only corrupt XML reported "no flaky
19
+ tests" and exited 0. The warning is now printed with the file and the
20
+ parse position, and the check fails with how many files could not be
21
+ parsed, so the verdict is never based on half-read data. (58 tests.)
22
+ - Docs: a Troubleshooting note for running `npx` inside a checkout of
23
+ this repo — npx prefers the checkout's own package.json and fails
24
+ with `dev-diagnosis: not found`; `npm install` does not fix it (no
25
+ dependencies, so no `node_modules` is created). Run
26
+ `node bin/dev-diagnosis.js` directly, `npm link`, or run `npx` from
27
+ another directory.
28
+
29
+ ## 0.1.4 — 2026-11-11
4
30
 
5
31
  - Fix: a large file now carries a suggestion — its exact path from the
6
32
  repo root — so `context --fix` writes it into `.aiignore` and a
package/README.md CHANGED
@@ -107,17 +107,32 @@ Runs the build command twice: cold, with the cache directory moved aside, and wa
107
107
 
108
108
  ### `flakes` — which tests are flaky
109
109
 
110
- Reads JUnit XML across CI runs — each subdirectory of `<junit-dir>` is one run, and each `*.xml` file directly under it is a run of its own — and ranks tests by instability, using a Wilson 95% lower bound so a test that failed twice in two runs outranks one that failed twice in twenty. It distinguishes *passed-on-retry* (failed then passed in the same run, the classic flake signature) from *always-failing* and plain cross-run flakes, and labels each test `new`, `chronic` or `recovered` from its history. CRLF line endings are normalised before parsing, so Windows-produced XML parses the same as Unix-produced XML.
110
+ Reads JUnit XML across CI runs — each subdirectory of `<junit-dir>` is one run, and each `*.xml` file directly under it is a run of its own — and ranks tests by instability, using a Wilson 95% lower bound so a test that failed twice in two runs outranks one that failed twice in twenty. It distinguishes *passed-on-retry* (failed then passed in the same run, the classic flake signature) from *always-failing* and plain cross-run flakes, and labels each test `new`, `chronic` or `recovered` from its history. CRLF line endings are normalised before parsing, so Windows-produced XML parses the same as Unix-produced XML. A file that cannot be parsed — broken XML, or a file with no root element at all (empty, whitespace-only, a lone declaration) — is printed as a warning and fails the check, so the verdict is never based on half-read data; a genuinely empty report (`<testsuites></testsuites>`) still counts as zero tests.
111
111
 
112
112
  ## Development
113
113
 
114
114
  ```sh
115
- npm test # node:test, no extra dependencies — 54 tests
115
+ npm test # node:test, no extra dependencies — 62 tests
116
116
  npm run example # run the CLI against the demo fixture
117
117
  ```
118
118
 
119
+ There is nothing to install: the CLI has no runtime dependencies, so
120
+ `npm test` works straight after cloning. Run the checkout as
121
+ `node bin/dev-diagnosis.js …` — see Troubleshooting if you reach for
122
+ `npx` inside the checkout.
123
+
119
124
  Requires Node.js 18 or later. CI runs the tests and the demo on Node 18, 20, 22 and 24 across Ubuntu, macOS and Windows, and smoke-tests the packed tarball by installing it in a clean directory. Windows is covered by CI only — the Windows-only code paths (the `cmd.exe` retry for `.cmd`/`.bat` shims, `PATHEXT` lookup) have not been run on a real Windows machine.
120
125
 
126
+ ## Troubleshooting
127
+
128
+ **`npx dev-diagnosis-cli` inside a checkout of this repo fails with
129
+ `dev-diagnosis: not found`.** npx sees the checkout's own `package.json`
130
+ — same name, same version — and tries to run the local bin, which only
131
+ exists once the bin is linked. `npm install` does not link it: with no
132
+ dependencies to install, npm creates no `node_modules` at all. Run the
133
+ checkout directly instead — `node bin/dev-diagnosis.js …` — or `npm link`
134
+ to get a `dev-diagnosis` command, or run `npx` from any other directory.
135
+
121
136
  ## Credits
122
137
 
123
138
  dev-diagnosis merges four tools by the same author, each of which keeps working on its own:
@@ -247,13 +247,21 @@ function printFlakes(result, top, json) {
247
247
  function runFlakes(dir) {
248
248
  if (!dir) throw new UsageError('flakes needs a runs directory, for example: dev-diagnosis flakes examples/runs');
249
249
  const result = scanRuns(dir);
250
- const passed = result.totals.flaky === 0 && result.totals.alwaysFailing === 0;
250
+ const unparsable = result.warnings.length;
251
+ // A file that could not be parsed means the verdict is based on
252
+ // incomplete data: "no flaky tests" on a half-read run set is not a
253
+ // pass, so the check fails and the verdict says how many files were
254
+ // unreadable. The warnings themselves are printed with the table.
255
+ const passed = result.totals.flaky === 0 && result.totals.alwaysFailing === 0 && unparsable === 0;
256
+ const base = result.totals.flaky === 0 && result.totals.alwaysFailing === 0
257
+ ? `no flaky tests across ${result.totals.runs} runs`
258
+ : `${result.totals.flaky} flaky, ${result.totals.alwaysFailing} always-failing across ${result.totals.runs} runs`;
251
259
  return {
252
260
  result,
253
261
  passed,
254
- verdict: passed
255
- ? `no flaky tests across ${result.totals.runs} runs`
256
- : `${result.totals.flaky} flaky, ${result.totals.alwaysFailing} always-failing across ${result.totals.runs} runs`,
262
+ verdict: unparsable > 0
263
+ ? `${base}; ${unparsable} file${unparsable === 1 ? '' : 's'} could not be parsed`
264
+ : base,
257
265
  };
258
266
  }
259
267
 
package/lib/flakes.mjs CHANGED
@@ -166,7 +166,10 @@ export function scanRuns(root, options = {}) {
166
166
  try {
167
167
  parsed = parseJunitFile(file);
168
168
  } catch (error) {
169
- warnings.push(`${file}: ${error.message}`);
169
+ // parseJunitFile already puts the file path in an XML error's
170
+ // message; only prepend it for errors that do not carry it, so
171
+ // the warning never reads "file: file:line:col: ...".
172
+ warnings.push(error.message.startsWith(file) ? error.message : `${file}: ${error.message}`);
170
173
  continue;
171
174
  }
172
175
  for (const test of parsed) {
@@ -253,13 +256,20 @@ export function formatFlakesTable(result, top = null) {
253
256
  lines.push(`runs: ${result.totals.runs} tests: ${result.totals.tests} flaky: ${result.totals.flaky} always-failing: ${result.totals.alwaysFailing}`);
254
257
  if (unstable.length === 0) {
255
258
  lines.push('No flaky tests: every test passed in every run it appeared in.');
256
- return lines.join('\n');
259
+ } else {
260
+ const header = `${'test'.padEnd(46)} ${'unstable'.padStart(8)} ${'runs'.padStart(5)} ${'trend'.padStart(8)} classification`;
261
+ lines.push(header);
262
+ for (const test of unstable) {
263
+ const label = test.id.length > 46 ? `${test.id.slice(0, 43)}...` : test.id;
264
+ lines.push(`${label.padEnd(46)} ${`${test.unstableRuns}/${test.eligibleRuns}`.padStart(8)} ${String(test.eligibleRuns).padStart(5)} ${test.trend.padStart(8)} ${test.classification}`);
265
+ }
257
266
  }
258
- const header = `${'test'.padEnd(46)} ${'unstable'.padStart(8)} ${'runs'.padStart(5)} ${'trend'.padStart(8)} classification`;
259
- lines.push(header);
260
- for (const test of unstable) {
261
- const label = test.id.length > 46 ? `${test.id.slice(0, 43)}...` : test.id;
262
- lines.push(`${label.padEnd(46)} ${`${test.unstableRuns}/${test.eligibleRuns}`.padStart(8)} ${String(test.eligibleRuns).padStart(5)} ${test.trend.padStart(8)} ${test.classification}`);
267
+ // A file that could not be parsed is not "no flaky tests": say so on
268
+ // screen, with the file and the reason, instead of passing silently.
269
+ if (result.warnings && result.warnings.length > 0) {
270
+ const noun = result.warnings.length === 1 ? 'file' : 'files';
271
+ lines.push(`Warning: ${result.warnings.length} ${noun} could not be parsed — results are incomplete:`);
272
+ for (const warning of result.warnings) lines.push(` ${warning}`);
263
273
  }
264
274
  return lines.join('\n');
265
275
  }
package/lib/xml.mjs CHANGED
@@ -165,6 +165,11 @@ export function parseXml(source, options = {}) {
165
165
  index = start.end;
166
166
  }
167
167
  if (stack.length > 1) fail(`unclosed tag <${stack[stack.length - 1].name}>`, length);
168
+ // An empty document, a file of only whitespace, or a file of only a
169
+ // declaration/comment is not XML at all: it has no root element, so
170
+ // treating it as "zero testcases" would silently report a run set as
171
+ // clean when one of its files was never read.
172
+ if (!document.children.some((child) => isElement(child))) fail('no root element', length);
168
173
  return document;
169
174
  }
170
175
 
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"WJDdidi-1","bin":{"dev-diagnosis":"bin/dev-diagnosis.js"},"bugs":{"url":"https://github.com/WJDdidi-1/dev-doctor/issues"},"description":"One offline CLI that diagnoses a repo the way four small tools do: context noise for AI agents, missing CLI commands, build-cache value, and flaky tests.","engines":{"node":">=18"},"files":["bin/","lib/","examples/","README.md","LICENSE","CHANGELOG.md"],"homepage":"https://github.com/WJDdidi-1/dev-doctor#readme","keywords":["cli","diagnostics","ai-coding","context","build-cache","flaky-tests","path","developer-tools","offline"],"license":"Apache-2.0","name":"dev-diagnosis-cli","repository":{"type":"git","url":"git+https://github.com/WJDdidi-1/dev-doctor.git"},"scripts":{"example":"node examples/demo.mjs","test":"node --test"},"type":"module","version":"0.1.4"}
1
+ {"author":"WJDdidi-1","bin":{"dev-diagnosis":"bin/dev-diagnosis.js"},"bugs":{"url":"https://github.com/WJDdidi-1/dev-doctor/issues"},"description":"One offline CLI that diagnoses a repo the way four small tools do: context noise for AI agents, missing CLI commands, build-cache value, and flaky tests.","engines":{"node":">=18"},"files":["bin/","lib/","examples/","README.md","LICENSE","CHANGELOG.md"],"homepage":"https://github.com/WJDdidi-1/dev-doctor#readme","keywords":["cli","diagnostics","ai-coding","context","build-cache","flaky-tests","path","developer-tools","offline"],"license":"Apache-2.0","name":"dev-diagnosis-cli","repository":{"type":"git","url":"git+https://github.com/WJDdidi-1/dev-doctor.git"},"scripts":{"example":"node examples/demo.mjs","test":"node --test"},"type":"module","version":"0.1.6"}