* test(#4384): failing-first regression rows for the macOS long-base todo-cap failure The 240-char pending-todo bullet cap must be deterministic w.r.t. where the repo is checked out. Deterministic long-base-path fixtures (a single 110-char segment, no real macOS dependency) reproduce next's own macos shard 3/3 failure (run 34038716700) on every OS: with an absolute link the bullet exceeds the cap and the documented needs-first truncation drops the 'Needs <solution>' clause. Rows cover the determinism property (byte-identical bullets under short and long bases), the CLI surface, relative-path stability, legacy no-projectRoot behavior, drop-order preservation, and adversarial edges (outside-root, path===root, non-string path). * fix(#4384): render the pending-todo bullet link repo-relative renderPendingTodosMarkdown gains an optional projectRoot; when given and the todo's path is absolute, the bullet's [todo file](…) target becomes toPosixPath(path.relative(projectRoot, path)) — the idiom already used for project_exists. cmdInitTodos passes cwd. The JSON todos[].path field stays absolute (#2376). Only the rendered display link changes: embedding the machine-variable absolute base let macOS's /private/var/folders/… temp paths consume the 240-char budget and drop the 'Needs' clause on long-path machines only — next's own macos-latest shard 3/3 went red on exactly this (run 34038716700), Linux's short /tmp passed. The 240-char whole-bullet cap and the needs→title→area drop order are unchanged; this matches PR #4384's own canonical example, docs, and unit tests, which all show repo-relative links. Docs updated at all three surfaces that describe the bullet (COMMANDS.md, templates/state.md, reference/state-md.md — the last was still pre-#4384 'count and reference' prose). Fixes the macOS regression introduced by #4384; next is red on its own CI. * test(#4384): fix substring false positive in the outside-root regression row The ../-form relative link legitimately contains the absolute path as a substring, so !line.includes(absolutePath) fired on correct output (caught by the first remote verify run, linux-node24 44018/44019). Assert the property itself instead: extract the link target and require it to be non-absolute and not equal to the absolute path. * chore(#4398): backfill PR number in changeset --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -167,6 +167,102 @@ test('property: rendered body always has one line per todo, each line <= 240 cha
|
||||
);
|
||||
});
|
||||
|
||||
// ─── #4384 regression: the 240-char cap must be deterministic w.r.t. where the
|
||||
// repo is checked out (macOS /private/var/folders/… bases blew the budget and
|
||||
// dropped the "Needs" clause; Linux /tmp passed — next's own macos shard 3/3
|
||||
// went red on exactly this, CI run 34038716700) ──────────────────────────────
|
||||
|
||||
// Long enough that the pre-fix absolute-link bullet exceeds 240 chars on EVERY
|
||||
// OS (Linux /tmp included): base > ~90 chars is over the threshold since the
|
||||
// fixed skeleton + relative tail + needs clause land near 150.
|
||||
const LONG_BASE_SEGMENT = 'd'.repeat(110);
|
||||
const TODO_RELATIVE_TAIL = path
|
||||
.join('.planning', 'todos', 'pending', '2026-09-01-fix-retry-logic.md')
|
||||
.split(path.sep)
|
||||
.join('/');
|
||||
|
||||
test('renderPendingTodosMarkdown: cap is deterministic w.r.t. base-path length (needs clause survives long absolute bases)', () => {
|
||||
const shortBase = path.resolve('/', 'gsd-2618-short-base');
|
||||
const longBase = path.join(shortBase, LONG_BASE_SEGMENT);
|
||||
const todoAt = (base) =>
|
||||
makeTodo({ path: path.join(base, TODO_RELATIVE_TAIL), needs: 'Add a max-attempts cap.' });
|
||||
|
||||
const fromShort = renderPendingTodosMarkdown([todoAt(shortBase)], shortBase);
|
||||
const fromLong = renderPendingTodosMarkdown([todoAt(longBase)], longBase);
|
||||
|
||||
assert.equal(fromLong, fromShort, 'bullet must not depend on where the repo is checked out');
|
||||
assert.match(fromShort, /Needs Add a max-attempts cap\.$/);
|
||||
assert.match(fromLong, /Needs Add a max-attempts cap\.$/);
|
||||
assert.ok(
|
||||
fromLong.includes(`[todo file](${TODO_RELATIVE_TAIL})`),
|
||||
'link must be the repo-relative tail, not the absolute path',
|
||||
);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: already-relative path is byte-stable with and without projectRoot', () => {
|
||||
const todo = makeTodo({ needs: 'define retry behavior' });
|
||||
const withRoot = renderPendingTodosMarkdown([todo], path.resolve('/', 'gsd-2618-rel-root'));
|
||||
assert.equal(withRoot, renderPendingTodosMarkdown([todo]));
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: absolute path without projectRoot keeps the legacy absolute link and drop order', () => {
|
||||
const base = path.join(path.resolve('/', 'gsd-2618-legacy-base'), LONG_BASE_SEGMENT);
|
||||
const todo = makeTodo({ path: path.join(base, TODO_RELATIVE_TAIL), needs: 'a real needs clause' });
|
||||
const line = renderPendingTodosMarkdown([todo]);
|
||||
// Opt-out callers keep today's behavior: over-cap drops needs first, link
|
||||
// verbatim (raw separators — the legacy renderer never posix-normalizes).
|
||||
assert.doesNotMatch(line, /Needs/);
|
||||
assert.ok(line.includes(`[todo file](${todo.path})`));
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: long base + long title still bounds the bullet and keeps the drop order', () => {
|
||||
const base = path.join(path.resolve('/', 'gsd-2618-order-base'), LONG_BASE_SEGMENT);
|
||||
const todo = makeTodo({
|
||||
path: path.join(base, TODO_RELATIVE_TAIL),
|
||||
title: 'A'.repeat(300),
|
||||
needs: 'something',
|
||||
});
|
||||
const line = renderPendingTodosMarkdown([todo], base);
|
||||
assert.ok(line.length <= MAX, `expected <= ${MAX}, got ${line.length}`);
|
||||
assert.doesNotMatch(line, /Needs/, 'needs clause must be dropped before title is touched');
|
||||
assert.match(line, /…/);
|
||||
assert.ok(line.includes(`[todo file](${TODO_RELATIVE_TAIL})`), 'relative link verbatim');
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: absolute path outside projectRoot renders a ../ relative link', () => {
|
||||
const root = path.resolve('/', 'gsd-2618-outside-root');
|
||||
const outsideTodo = path.join(
|
||||
path.resolve('/', 'gsd-2618-outside-sibling'),
|
||||
TODO_RELATIVE_TAIL,
|
||||
);
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ path: outsideTodo })], root);
|
||||
// The ../-form legitimately CONTAINS the absolute path as a substring, so a
|
||||
// plain includes() negative is a false positive — assert the property
|
||||
// itself: the link target is relative, never the absolute path.
|
||||
const linkMatch = line.match(/\[todo file\]\(([^)]*)\)/);
|
||||
assert.ok(linkMatch, 'bullet must contain a todo link');
|
||||
assert.ok(!path.isAbsolute(linkMatch[1]), 'link target must be relative, not absolute');
|
||||
assert.notEqual(linkMatch[1], outsideTodo, 'link target must not be the machine-variable absolute path');
|
||||
assert.ok(
|
||||
line.includes('[todo file](../'),
|
||||
'link must be relative to the project root, not absolute',
|
||||
);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: path equal to projectRoot falls back to the raw link target', () => {
|
||||
const root = path.resolve('/', 'gsd-2618-eq-root');
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ path: root })], root);
|
||||
assert.ok(line.includes(`[todo file](${root})`));
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: non-string path keeps the empty-link fallback', () => {
|
||||
const line = renderPendingTodosMarkdown(
|
||||
[makeTodo({ path: 42 })],
|
||||
path.resolve('/', 'gsd-2618-num-root'),
|
||||
);
|
||||
assert.match(line, /\[todo file\]\(\)$/);
|
||||
});
|
||||
|
||||
// ─── pending_read_ok / pending_todos_markdown via the real CLI surface ─────
|
||||
|
||||
const { spawnSync } = require('node:child_process');
|
||||
@@ -228,6 +324,46 @@ test('cmdInitTodos: real todo file produces a rendered bullet via the CLI', (t)
|
||||
assert.match(json.pending_todos_markdown, /Needs Add a max-attempts cap\.$/m);
|
||||
});
|
||||
|
||||
test('cmdInitTodos: needs clause survives a deterministically long base path (#4384 macOS shape)', (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2618-long-'));
|
||||
t.after(() => cleanup(root));
|
||||
// One long path segment: deterministic on every OS (no real macOS dependency).
|
||||
// Pre-fix, the absolute-link bullet exceeds 240 chars even on Linux /tmp,
|
||||
// reproducing next's macos shard failure (CI run 34038716700).
|
||||
const dir = path.join(root, LONG_BASE_SEGMENT);
|
||||
const pendingDir = path.join(dir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pendingDir, '2026-09-01-fix-retry-logic.md'),
|
||||
[
|
||||
'---',
|
||||
'created: 2026-09-01T00:00:00.000Z',
|
||||
'title: Fix retry logic',
|
||||
'area: api',
|
||||
'---',
|
||||
'',
|
||||
'## Solution',
|
||||
'',
|
||||
'Add a max-attempts cap.',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const json = runQueryInitTodos(dir);
|
||||
assert.equal(json.pending_read_ok, true);
|
||||
assert.equal(json.todo_count, 1);
|
||||
assert.match(json.pending_todos_markdown, /Fix retry logic/);
|
||||
assert.match(json.pending_todos_markdown, /Needs Add a max-attempts cap\.$/m);
|
||||
assert.ok(
|
||||
json.pending_todos_markdown.includes(`[todo file](${TODO_RELATIVE_TAIL})`),
|
||||
'bullet link must be repo-relative',
|
||||
);
|
||||
assert.ok(
|
||||
!json.pending_todos_markdown.includes(dir.split(path.sep).join('/')),
|
||||
'absolute cwd must not leak into the rendered bullet',
|
||||
);
|
||||
});
|
||||
|
||||
test('cmdInitTodos: bullet order is filename-sorted regardless of write/insertion order', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2618-'));
|
||||
t.after(() => cleanup(dir));
|
||||
|
||||
Reference in New Issue
Block a user