* test(#4439): failing-first regression coverage for pending-todo date rendering renderPendingTodoBullet currently echoes the todo's `created` frontmatter verbatim into the rendered bullet's [date] bracket, so a full ISO-8601 timestamp (the actual stored shape, per add-todo.md's create_file step) leaks through instead of the date-only format documented in docs/reference/state-md.md and docs/COMMANDS.md. This commit adds the regression coverage only; the renderer fix lands in the next commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#4439): render pending-todo 'created' as date-only, matching [date] renderPendingTodoBullet echoed the todo's created frontmatter verbatim into the rendered STATE.md bullet. That value is always a full ISO-8601 timestamp by design (add-todo.md's create_file step writes init.todos' timestamp field), but docs/reference/state-md.md and docs/COMMANDS.md document the bullet as `- [date] ...` — a short calendar date. Added pendingTodoDateOnly(), a pure display-only formatter: a value starting with a well-formed YYYY-MM-DD is shortened to just that; anything else ('unknown', a malformed string) passes through unchanged. The stored frontmatter and the JSON todos[].created field are untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(#4439): cover a non-4-digit-year near-miss for the date-only formatter Standards review flagged a gap: the malformed-date coverage exercised a non-padded month/day but not a non-4-digit year, the other way the input can look almost-but-not-quite like YYYY-MM-DD. Same pass-through code path as the existing malformed-date test; no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#4439): changeset for the pending-todo date-only bullet fix Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(#4439): backfill changeset PR number Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/steady-sloths-parade.md
Normal file
5
.changeset/steady-sloths-parade.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4494
|
||||
---
|
||||
**Pending-todo bullets in STATE.md now show a date, not a full timestamp** — `renderPendingTodosMarkdown` was echoing the todo's `created` frontmatter verbatim (a full ISO-8601 instant) into the rendered `[…]` bracket, instead of the date-only `[date]` format documented in `docs/reference/state-md.md` and `docs/COMMANDS.md`. (#4439)
|
||||
18
src/init.cts
18
src/init.cts
@@ -2308,8 +2308,24 @@ function pendingTodoLinkTarget(todo: Record<string, unknown>, projectRoot: strin
|
||||
return rel;
|
||||
}
|
||||
|
||||
/**
|
||||
* #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
|
||||
* field it round-trips through) is always a full ISO-8601 timestamp, by
|
||||
* design — this is the display-only seam that reformats it to the
|
||||
* date-only `[date]` bullet documented in docs/reference/state-md.md and
|
||||
* docs/COMMANDS.md. A value that doesn't start with a well-formed
|
||||
* `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
|
||||
* string) passes through unchanged rather than being mangled.
|
||||
*/
|
||||
function pendingTodoDateOnly(value: string): string {
|
||||
const match = value.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
return match ? match[0] : value;
|
||||
}
|
||||
|
||||
function renderPendingTodoBullet(todo: Record<string, unknown>, projectRoot?: string): string {
|
||||
const date = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown'));
|
||||
const date = pendingTodoDateOnly(
|
||||
sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')),
|
||||
);
|
||||
let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
|
||||
let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
|
||||
// Strip trailing "." so the fixed "Needs ....` template below never
|
||||
|
||||
@@ -103,6 +103,27 @@ test('renderPendingTodosMarkdown: boundary — 240 chars (limit) is not truncate
|
||||
assert.equal(line, `- [2026-09-01] [api] ${title} — [todo file](${todo.path})`);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: full ISO-8601 \'created\' renders a date-only bracket (#4439)', () => {
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ created: '2026-09-01T00:00:00.000Z', needs: undefined })]);
|
||||
assert.match(line, /^- \[2026-09-01\] \[api\]/);
|
||||
assert.doesNotMatch(line, /2026-09-01T/, 'full ISO timestamp must not leak into the rendered bullet');
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: \'created\' fallback (\'unknown\') is not sliced by the date-only formatter', () => {
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ created: undefined, needs: undefined })]);
|
||||
assert.match(line, /^- \[unknown\] \[api\]/);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: a malformed non-padded date is passed through unchanged, not mis-sliced', () => {
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ created: '2026-9-1', needs: undefined })]);
|
||||
assert.match(line, /^- \[2026-9-1\] \[api\]/);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: a non-4-digit year is not mis-sliced (near-miss YYYY-MM-DD shape)', () => {
|
||||
const line = renderPendingTodosMarkdown([makeTodo({ created: '202-09-01T00:00:00.000Z', needs: undefined })]);
|
||||
assert.match(line, /^- \[202-09-01T00:00:00\.000Z\] \[api\]/);
|
||||
});
|
||||
|
||||
test('renderPendingTodosMarkdown: boundary — 241 chars (limit+1) truncates, needs dropped first', () => {
|
||||
const base = makeTodo({ needs: 'x', title: 'X' });
|
||||
const probe = renderPendingTodosMarkdown([base]);
|
||||
@@ -142,7 +163,7 @@ test('property: rendered body always has one line per todo, each line <= 240 cha
|
||||
// than a vacuous one; the pathological "cap not achievable" case is
|
||||
// covered separately by the fixed "pathological" unit test above.
|
||||
const todoArb = fc.record({
|
||||
created: fc.constantFrom('2026-01-01', '2025-12-31', 'unknown'),
|
||||
created: fc.constantFrom('2026-01-01', '2025-12-31', 'unknown', '2026-01-01T00:00:00.000Z', '2026-9-1'),
|
||||
area: fc.string({ minLength: 0, maxLength: 40 }),
|
||||
title: fc.string({ minLength: 0, maxLength: 500 }),
|
||||
path: fc
|
||||
@@ -320,6 +341,7 @@ test('cmdInitTodos: real todo file produces a rendered bullet via the CLI', (t)
|
||||
const json = runQueryInitTodos(dir);
|
||||
assert.equal(json.pending_read_ok, true);
|
||||
assert.match(json.pending_todos_markdown, /Fix retry logic/);
|
||||
assert.match(json.pending_todos_markdown, /^- \[2026-09-01\] \[api\]/m);
|
||||
// The Needs clause is asserted on the STRUCTURED field, not the rendered
|
||||
// bullet: the bullet embeds the todo file's ABSOLUTE path, so its total
|
||||
// length varies by runner tmpdir (macOS CI's /private/var/folders/… plus
|
||||
|
||||
Reference in New Issue
Block a user