* test(#3817): audit-open counts must include the truncation remainder * fix(#3817): count the truncation remainder — display truncates, counting must not * chore(#3817): changeset fragment (pr number backfilled after PR creation) * chore(#3817): backfill changeset PR number (4034) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/graceful-elks-wander.md
Normal file
5
.changeset/graceful-elks-wander.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4034
|
||||
---
|
||||
audit-open summary counts now include the display-truncation remainder: a milestone with more than 5 open files reported counts capped at 5 because the _remainder_count display marker was counted as one item instead of the real files it records (#3817)
|
||||
@@ -257,10 +257,11 @@
|
||||
"audit-command-cutover.test.cjs",
|
||||
"audit-fix-command.test.cjs",
|
||||
"audit-milestone-filename-guard.test.cjs",
|
||||
"audit-open-remainder-count.test.cjs",
|
||||
"audit-uat-acknowledged.test.cjs",
|
||||
"audit-workstream-layouts.test.cjs"
|
||||
],
|
||||
"issue": "#3804/#3805 \u2014 audit-layout and acknowledged-marker suites drive the real audit-uat CLI against on-disk fixtures"
|
||||
"issue": "#3804/#3805 \u2014 audit-layout and acknowledged-marker suites drive the real audit-uat CLI against on-disk fixtures | #3817 \u2014 regression suite proving countReal counts the truncation remainder marker instead of treating it as one phantom item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,9 +1337,17 @@ function auditOpenArtifacts(cwd: string): AuditResult {
|
||||
try { return scanDeferredItems(planDir, cwd); } catch { return { items: [{ scan_error: true, phase: '', file: '', text: '' }], acknowledged: 0 }; }
|
||||
})();
|
||||
|
||||
// Count real items (not scan_error sentinels)
|
||||
// Count real items (not scan_error sentinels). #3817: a `_remainder_count`
|
||||
// marker is not a phantom — it RECORDS real items the detail list truncated
|
||||
// away for display, so its value counts toward the total. Truncation limits
|
||||
// display, never counting; only scan_error (a read failure, not an item)
|
||||
// contributes zero.
|
||||
const countReal = (arr: Array<{ scan_error?: boolean; _remainder_count?: number }>) =>
|
||||
arr.filter(i => !i.scan_error && !i._remainder_count).length;
|
||||
arr.reduce((sum, i) => {
|
||||
if (i.scan_error) return sum;
|
||||
if (typeof i._remainder_count === 'number') return sum + i._remainder_count;
|
||||
return sum + 1;
|
||||
}, 0);
|
||||
|
||||
const counts: AuditCounts = {
|
||||
debug_sessions: countReal(debugSessions.items),
|
||||
|
||||
@@ -2259,7 +2259,9 @@ describe('bug #950: quick-task SUMMARY must carry status: complete', () => {
|
||||
}
|
||||
|
||||
const before = audit(tmpDir);
|
||||
assert.equal(before.counts.todos, 5, 'display cap: 5 of 7 shown in one scan');
|
||||
// #3817: the display cap truncates the DETAIL list, not the count —
|
||||
// 7 pending files means counts.todos is 7 (5 shown + remainder 2).
|
||||
assert.equal(before.counts.todos, 7, 'counts include the truncation remainder (#3817)');
|
||||
assert.equal(before.has_open_items, true);
|
||||
|
||||
const shown = before.items.todos.filter((i) => !i.scan_error && !i._remainder_count).map((i) => i.filename);
|
||||
|
||||
120
tests/audit-open-remainder-count.test.cjs
Normal file
120
tests/audit-open-remainder-count.test.cjs
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #3817 — audit-open's counts must include the truncation remainder.
|
||||
//
|
||||
// The todo scan truncates its detail list at 5 and pushes a synthetic
|
||||
// {_remainder_count: N} marker; the counting pass filtered that marker out
|
||||
// alongside scan_error, so counts.todos and counts.total read systematically
|
||||
// low by exactly N — the remainder existed only in the "… and N more" prose
|
||||
// (reporter: 19 pending, reported 5). Truncation limits display, never
|
||||
// counting.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
function runAuditOpen(cwd) {
|
||||
const r = runGsdTools(['audit-open', '--json'], cwd);
|
||||
assert.ok(r.success, r.error);
|
||||
return JSON.parse(r.output);
|
||||
}
|
||||
|
||||
describe('#3817: audit-open counts include the truncation remainder', () => {
|
||||
test('#3817: more than 5 todo files → counts.todos counts every pending file', (t) => {
|
||||
const tmpDir = createTempProject('gsd-3817-many-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
const TOTAL = 8; // 5 shown + remainder 3
|
||||
for (let i = 1; i <= TOTAL; i++) {
|
||||
fs.writeFileSync(path.join(pendingDir, `todo-${String(i).padStart(2, '0')}.md`), [
|
||||
'---',
|
||||
'title: Fix thing ' + i,
|
||||
'area: general',
|
||||
'created: 2026-08-01',
|
||||
'---',
|
||||
'',
|
||||
'Body ' + i,
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
const out = runAuditOpen(tmpDir);
|
||||
assert.equal(
|
||||
out.counts.todos,
|
||||
TOTAL,
|
||||
`#3817: counts.todos must count every pending todo file (${TOTAL}); got ${out.counts.todos}`,
|
||||
);
|
||||
assert.equal(
|
||||
out.counts.total,
|
||||
TOTAL,
|
||||
`#3817: counts.total must include the remainder; got ${out.counts.total}`,
|
||||
);
|
||||
// The display contract is unchanged: at most 5 detail objects, and the
|
||||
// prose still announces the remainder.
|
||||
assert.ok(
|
||||
Array.isArray(out.items && out.items.todos),
|
||||
'todos detail list present',
|
||||
);
|
||||
const detailFiles = out.items.todos.filter((i) => i && i.filename);
|
||||
assert.ok(detailFiles.length <= 5, `display stays truncated; got ${detailFiles.length}`);
|
||||
});
|
||||
|
||||
test('#3817 boundary: exactly 5 files → no marker, counts exact; 6 files → remainder 1', (t) => {
|
||||
const tmpDir = createTempProject('gsd-3817-boundary-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
const seed = (n) => fs.writeFileSync(path.join(pendingDir, `todo-${n}.md`), [
|
||||
'---',
|
||||
'title: T ' + n,
|
||||
'area: general',
|
||||
'created: 2026-08-01',
|
||||
'---',
|
||||
'',
|
||||
'Body',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
for (let i = 1; i <= 5; i++) seed(i);
|
||||
let out = runAuditOpen(tmpDir);
|
||||
assert.equal(out.counts.todos, 5, 'exactly at the cap: no marker, count exact');
|
||||
const markers5 = (out.items.todos || []).filter((i) => i && typeof i._remainder_count === 'number');
|
||||
assert.equal(markers5.length, 0, 'no marker emitted at exactly 5');
|
||||
|
||||
seed(6);
|
||||
out = runAuditOpen(tmpDir);
|
||||
assert.equal(out.counts.todos, 6, 'one past the cap: remainder 1 counted');
|
||||
const marker = (out.items.todos || []).find((i) => i && typeof i._remainder_count === 'number');
|
||||
assert.ok(marker, 'marker present at 6');
|
||||
assert.equal(marker._remainder_count, 1, 'marker records exactly the dropped count');
|
||||
});
|
||||
|
||||
test('#3817 control: 5 or fewer todo files → counts unchanged', (t) => {
|
||||
const tmpDir = createTempProject('gsd-3817-few-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
fs.writeFileSync(path.join(pendingDir, `todo-${i}.md`), [
|
||||
'---',
|
||||
'title: T ' + i,
|
||||
'area: general',
|
||||
'created: 2026-08-01',
|
||||
'---',
|
||||
'',
|
||||
'Body',
|
||||
'',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
const out = runAuditOpen(tmpDir);
|
||||
assert.equal(out.counts.todos, 3, 'no truncation → counts exact');
|
||||
assert.equal(out.counts.total, 3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user