todo-app/test/note_template_test.dart
Krzysztof kuhy Rudnicki a874e09b65 Cut the llm-design-spec template from 12 sections to 7
Audited the template against 514 real Claude sessions (1503 top-level
prompts) joined to the 49 notes in BACKLOG.md. Sections were judged on
whether the agent actually read them, not on fill rate — four of them
said "leave blank if none" in their own helper, so a low fill rate was
the section working correctly.

Measured over the 22 note/session pairs:
  estimate  filled 22, referenced  1 (4%)
  done      filled 22, quoted back 4 (18%)
  refs      filled 14, URLs fetched 5 (35%)
  tech      the agent opened pubspec.yaml/CLAUDE.md anyway in 54%
  ask       100% ask rate with it, 92% without — no measurable effect

Kept: title, what, where, must, done. Dropped: tech (into `where`),
ask, nice and never (into `must` as `optional:` / `must not:` lines),
depends, estimate. Renamed `refs` -> `read first`. Added `verify`, the
only addition: "test it yourself, on the phone" was the single most
repeated correction in the corpus, and 3 of the 7 filled `never`
sections were already smuggling it in.

Fixes a latent data-loss bug found while testing this. parse() treats
an unknown `## heading` as content on purpose (values may contain
subheadings), so a legacy `## tech` block was folded into the preceding
section's value AND the note still reported conforming — opening any
old note in the stepper would have swallowed it on the next save.
NoteTemplate.retiredLabels now makes those notes explicitly
non-conforming, so they open raw and untouched.

tool/migrate_backlog.dart converts an old export (export -> migrate ->
import). It is idempotent and gated: it throws if a rewritten note does
not conform or if any source line vanishes. All 49 notes migrated on
the desktop and merged to the phone (0 new, 25 updated, 24 unchanged).

Also formats the few files dart format had never been run over, and
extracts a local in frame_stats_io.dart so the reflowed line stays
under 80 chars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:03:35 +02:00

157 lines
5.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/note_template.dart';
void main() {
const spec = NoteTemplate.llmDesignSpec;
const blank = NoteTemplate.blank;
group('assemble', () {
test(
'builds # title + ## sections with italic guidance, dropping empty',
() {
final text = assemble(spec, {
'title': 'Add dark mode',
'what': 'A theme toggle.',
'must': '- respects system theme',
// everything else blank → dropped
});
expect(text, contains('# Add dark mode'));
expect(text, contains('## what'));
expect(text, contains('_${spec.sections[1].helper}_'));
expect(text, contains('A theme toggle.'));
expect(text, contains('## must'));
// Dropped empty sections leave no heading behind.
expect(text, isNot(contains('## verify')));
expect(text, isNot(contains('## read first')));
},
);
test('the multi-word "read first" label round-trips as a heading', () {
final text = assemble(spec, {
'title': 'T',
'refs': '- https://example.com/doc',
});
expect(text, contains('## read first'));
final parsed = parse(spec, text);
expect(parsed.conforms, isTrue);
expect(parsed.values['refs'], '- https://example.com/doc');
});
test('omits the title line when the title is blank', () {
final text = assemble(spec, {'what': 'something'});
// No `# ` title line; the body starts straight at the first section.
expect(text.startsWith('# '), isFalse);
expect(text.startsWith('## what'), isTrue);
});
test('freeform template returns the trimmed body verbatim', () {
expect(assemble(blank, {'body': 'just text\n\n'}), 'just text');
expect(assemble(blank, {}), '');
});
});
group('parse round-trip', () {
test('assemble(parse(text)) is idempotent for conforming text', () {
final text = assemble(spec, {
'title': 'Title here',
'what': 'The what.',
'done': 'I can X and Y happens',
});
final parsed = parse(spec, text);
expect(parsed.conforms, isTrue);
expect(parsed.values['title'], 'Title here');
expect(parsed.values['what'], 'The what.');
expect(parsed.values['done'], 'I can X and Y happens');
// Re-assembling the parsed values reproduces the exact text.
expect(assemble(spec, parsed.values), text);
});
test('legacy "what —" notes are reported non-conforming', () {
const legacy = 'Shutdown timer\n\nwhat — a timer\nwhere — new app';
final parsed = parse(spec, legacy);
expect(parsed.conforms, isFalse);
});
test('the pre-2026-07 twelve-section spec is non-conforming', () {
// Notes written before `tech`/`ask`/`nice`/`never`/`depends`/`estimate`
// were cut must fall back to the raw editor rather than be silently
// stripped of the sections the template no longer knows about.
const legacy =
'# Old note\n\n## what\na thing\n\n## tech\nFlutter 3.32\n\n'
'## must\n- do it\n\n## estimate\nM';
final parsed = parse(spec, legacy);
expect(parsed.conforms, isFalse);
// The dropped sections are not resurrected as values either.
expect(parsed.values.containsKey('tech'), isFalse);
expect(parsed.values.containsKey('estimate'), isFalse);
});
test('the section set is the audited seven', () {
expect(spec.sections.map((s) => s.key), [
'title',
'what',
'where',
'must',
'done',
'verify',
'refs',
]);
});
test('out-of-order / duplicate headings are non-conforming', () {
const text = '# T\n\n## done\nx\n\n## what\ny';
expect(parse(spec, text).conforms, isFalse);
const dup = '# T\n\n## what\na\n\n## what\nb';
expect(parse(spec, dup).conforms, isFalse);
});
test('stray content before the first heading is non-conforming', () {
const text = 'loose line\n\n## what\nv';
expect(parse(spec, text).conforms, isFalse);
});
test('strips leading blank lines before a section value', () {
// A raw-edited note may put blank lines (and no guidance) under a
// heading; parsing must skip those blanks to recover the value.
const text = '# T\n\n## what\n\n\nthe value';
final parsed = parse(spec, text);
expect(parsed.conforms, isTrue);
expect(parsed.values['what'], 'the value');
});
test('a ## subheading inside a value is kept, not split out', () {
final text = assemble(spec, {
'title': 'T',
'what': 'intro\n## not a real section\nmore',
});
final parsed = parse(spec, text);
expect(parsed.conforms, isTrue);
expect(parsed.values['what'], contains('## not a real section'));
});
test('freeform template always conforms with the whole text as body', () {
final parsed = parse(blank, 'anything goes\nline two');
expect(parsed.conforms, isTrue);
expect(parsed.values['body'], 'anything goes\nline two');
});
});
group('noteTitle', () {
test('strips the leading # from a heading', () {
expect(noteTitle('# My title\n\n## what\nx'), 'My title');
});
test('uses the first non-empty line for freeform notes', () {
expect(noteTitle('\n\nfirst real line\nsecond'), 'first real line');
});
test('returns empty string for blank text', () {
expect(noteTitle(' \n '), '');
});
});
}