import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from wflib import migrate as M from wflib import tasks as T OLD = """\ # Tasks — demo Conventions: CLAUDE.md. ## Awaiting your decision - **Smoke screenshots (need you present)**: windows open hidden, so screenshots are black. - Online mod updates (spec step 4) not built. ## Pending 1. **[P1] Terrain: cave seams** (Effort: <1h) — close the slit beside the lintel. - Tests (red first): no edge left open. - After: Terrain 6/6: cave rendering + sound Reference: [DESIGN.md#terrain](DESIGN.md#terrain) 2. N. **[P2] Reputation factions** (Effort: 5h, interactive) (in progress: master; slices 1–2/3 done (see plan)) — make factions friendly. - Steps: use the table - nested - After: Terrain: cave seams Reference: docs/notes/exe-map.md (ai.c rows), [spec](docs/spec.md#rep). 3. **[P2] Spirit Powers 3/5: technique keys** (Effort: 1h) — keys 1/2/3. 4. **[P3] Spirit Powers 4/5: passives. And more** (Effort: 1h) - After: Spirit Powers 3/5: technique keys ## Needs human Only what a script can't judge. 1. **[P1] Play-test feel** (Effort: <1h) — last pass 2026-09-13. - [ ] keyboard works - [ ] motion smooth ## Deferred (explicitly out of scope, tracked for later) **Final art**: later. """ NEW = """\ # Tasks — demo Conventions: CLAUDE.md. ## Awaiting your decision - **a-smoke-screenshots**: Smoke screenshots (need you present). windows open hidden, so screenshots are black. - **a-online-mod-updates-not-built**: Online mod updates (spec step 4) not built. ## Pending - **t-terrain-cave-seams** [P1] (<1h): Terrain: cave seams. close the slit beside the lintel. - Tests (red first): no edge left open. Ref: DESIGN.md#terrain - **t-reputation-factions** [P2] (5h, interactive) (in progress: master; slices 1–2/3 done [see plan]): Reputation factions. make factions friendly. - Steps: use the table - nested - After: [[t-terrain-cave-seams]] Ref: docs/notes/exe-map.md (ai.c rows), docs/spec.md#rep - **t-spirit-powers-3** [P2] (1h): Spirit Powers 3/5: technique keys. keys 1/2/3. - **t-spirit-powers-4** [P3] (1h): Spirit Powers 4/5: passives, And more. - After: [[t-spirit-powers-3]] ## Needs human Only what a script can't judge. - **t-play-test-feel** [P1] (<1h): Play-test feel. last pass 2026-09-13. - [ ] keyboard works - [ ] motion smooth ## Deferred (explicitly out of scope, tracked for later) **Final art**: later. """ class MigrateTest(unittest.TestCase): def test_converts_all_oddities(self): new, report = M.migrate(OLD) self.assertEqual(new, NEW) self.assertEqual(report.ids, { "Smoke screenshots (need you present)": "a-smoke-screenshots", "Online mod updates (spec step 4) not built": "a-online-mod-updates-not-built", "Terrain: cave seams": "t-terrain-cave-seams", "Reputation factions": "t-reputation-factions", "Spirit Powers 3/5: technique keys": "t-spirit-powers-3", "Spirit Powers 4/5: passives. And more": "t-spirit-powers-4", "Play-test feel": "t-play-test-feel", }) self.assertEqual(report.notes, [ "t-terrain-cave-seams: dropped 'After: Terrain 6/6: cave rendering + sound' (no open task with that title: done)"]) def test_result_parses_clean(self): doc = T.parse(M.migrate(OLD)[0]) self.assertEqual([i.error for i in doc.all_items()], [None] * 7) self.assertEqual(T.render(doc), NEW) def test_idempotent(self): new, report = M.migrate(NEW) self.assertEqual(new, NEW) self.assertEqual((report.ids, report.notes), ({}, [])) def test_missing_sections_are_added(self): new, _ = M.migrate("# T\n\n## Pending\n\n1. **[P1] A** (Effort: 1h) — a.\n\n## Notes\n\nprose\n") self.assertEqual(new, "# T\n\n## Pending\n\n- **t-a** [P1] (1h): A. a.\n\n## Notes\n\nprose\n\n" "## Awaiting your decision\n\n## Needs human\n\n## Deferred\n") def test_same_title_twice_gets_distinct_ids(self): new, _ = M.migrate("## Pending\n\n1. **[P1] A** (Effort: 1h) — x.\n\n2. **[P1] A** (Effort: 1h) — y.\n") self.assertIn("- **t-a** [P1] (1h): A. x.", new) self.assertIn("- **t-a-2** [P1] (1h): A. y.", new) def test_ids_avoid_the_archive(self): new, _ = M.migrate("## Pending\n\n1. **[P1] A** (Effort: 1h) — x.\n", taken={"t-a"}) self.assertIn("- **t-a-2** [P1] (1h): A. x.", new) def test_one_column_deeper_under_a_colon_line_is_a_child(self): new, _ = M.migrate("## Pending\n\n1. **[P1] A** (Effort: 1h) — x.\n - Seed Qs:\n - first?\n - second?\n" " - [ ] box\n - [ ] sibling typo\n") self.assertIn("- **t-a** [P1] (1h): A. x.\n - Seed Qs:\n - first?\n - second?\n" " - [ ] box\n - [ ] sibling typo\n", new) def test_reference_words_that_are_no_path_become_a_note(self): new, _ = M.migrate("## Pending\n\n1. **[P1] A** (Effort: 1h) — x.\n" " Reference: docs/a.md (fate rows), skill row notes, src/X.cs, DESIGN.md#terrain\n\n" "2. **[P1] B** (Effort: 1h) — y.\n Reference: main spec §5\n") self.assertIn(" Ref: docs/a.md (fate rows; skill row notes), src/X.cs, DESIGN.md#terrain\n", new) self.assertIn("- **t-b** [P1] (1h): B. y.\n - Reference: main spec §5\n", new) def test_reference_note_keeps_the_body_indent(self): new, _ = M.migrate("## Pending\n\n1. **[P1] A** (Effort: 1h) — x.\n - Steps: s\n Reference: main spec §5\n") self.assertIn("- **t-a** [P1] (1h): A. x.\n - Steps: s\n - Reference: main spec §5\n", new) def test_unreadable_numbered_item_is_reported_and_kept(self): new, report = M.migrate("## Pending\n\n1. something without the pattern\n body\n") self.assertIn("1. something without the pattern\n body\n", new) self.assertEqual(report.notes, ["line 3: numbered item not understood, left as it is: 1. something without the pattern"]) if __name__ == "__main__": unittest.main()