1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
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()
|