diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 07:27:17 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 07:27:17 +0200 |
| commit | 81d4e80fd5aabe4e80f58e960affa795cf7d34ec (patch) | |
| tree | e98eeac2af6af63aa4287bba1f6d4a3af26b5727 /tests/test_tasks.py | |
| download | workflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.tar.gz workflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.zip | |
workflow: initial public history
Diffstat (limited to 'tests/test_tasks.py')
| -rw-r--r-- | tests/test_tasks.py | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 0000000..64f219c --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,523 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from wflib import tasks as T +from wflib import lanes as L + +SAMPLE = """\ +# Tasks — demo + +Commands: `make test`. + +## Awaiting your decision + +- **a-smoke**: Smoke screenshots. Need you present. + +## Pending + +Intro prose. + +- **t-first** [P1] (<1h): First thing. Do it well. + - Steps: one + - After: [[t-zero]], [[t-other]] + Ref: DESIGN.md#terrain, docs/x.md (skill rows) + +- **t-second** [P2] (5h, interactive) (in progress: master): Second: with colon. Goal here. + +- **t-third** [P3] (1h) (blocked: [[a-smoke]]): Third. + +Trailing prose after items. + +## Needs human + +- **t-play** [P1] (<1h): Play-test feel. + - [ ] keyboard works + - [ ] motion smooth + +## Deferred + +## Known caveats + +- plain bullet, mentions [[t-first]] +""" + + +class HeaderTest(unittest.TestCase): + def setUp(self): + self.doc = T.parse(SAMPLE) + + def item(self, id): + section, i = self.doc.find(id) + return section.items[i] + + def test_task_with_priority_and_effort(self): + it = self.item("t-first") + self.assertEqual((it.id, it.prio, it.effort, it.interactive, it.status, it.text), + ("t-first", 1, "<1h", False, None, "First thing. Do it well.")) + + def test_interactive_and_in_progress(self): + it = self.item("t-second") + self.assertEqual((it.prio, it.effort, it.interactive, it.status), + (2, "5h", True, "in progress: master")) + + def test_blocked(self): + it = self.item("t-third") + self.assertEqual(it.status, "blocked: [[a-smoke]]") + self.assertEqual(it.blocked_on, "a-smoke") + self.assertIsNone(self.item("t-first").blocked_on) + + def test_awaiting_item_has_no_priority(self): + it = self.item("a-smoke") + self.assertEqual((it.prio, it.effort, it.text), (None, None, "Smoke screenshots. Need you present.")) + + def test_header_rebuilds_the_line(self): + self.assertEqual(self.item("t-first").header(), "- **t-first** [P1] (<1h): First thing. Do it well.") + self.assertEqual(self.item("t-second").header(), + "- **t-second** [P2] (5h, interactive) (in progress: master): Second: with colon. Goal here.") + self.assertEqual(self.item("t-third").header(), "- **t-third** [P3] (1h) (blocked: [[a-smoke]]): Third.") + self.assertEqual(self.item("a-smoke").header(), "- **a-smoke**: Smoke screenshots. Need you present.") + + def test_title_and_goal(self): + it = self.item("t-second") + self.assertEqual(it.title, "Second: with colon") + self.assertEqual(it.goal, "Goal here.") + self.assertEqual(self.item("t-third").title, "Third") + self.assertEqual(self.item("t-third").goal, "") + + def test_after(self): + self.assertEqual(self.item("t-first").after, ["t-zero", "t-other"]) + self.assertEqual(self.item("t-second").after, []) + + def test_refs(self): + self.assertEqual(self.item("t-first").refs, [("DESIGN.md", "terrain"), ("docs/x.md", None)]) + self.assertEqual(self.item("t-second").refs, []) + + def test_body_lines(self): + self.assertEqual(self.item("t-play").body, [" - [ ] keyboard works", " - [ ] motion smooth"]) + + +class StructureTest(unittest.TestCase): + def test_round_trip(self): + self.assertEqual(T.render(T.parse(SAMPLE)), SAMPLE) + + def test_sections_and_keys(self): + doc = T.parse(SAMPLE) + self.assertEqual([(s.heading, s.key) for s in doc.sections], + [("Awaiting your decision", "awaiting"), ("Pending", "pending"), + ("Needs human", "human"), ("Deferred", "deferred"), ("Known caveats", None)]) + self.assertEqual([i.id for i in doc.section("pending").items], ["t-first", "t-second", "t-third"]) + self.assertEqual(doc.section("pending").prefix, ["", "Intro prose.", ""]) + self.assertEqual(doc.section("pending").suffix, ["Trailing prose after items.", ""]) + self.assertEqual(doc.ids(), {"a-smoke", "t-first", "t-second", "t-third", "t-play"}) + + def test_prose_section_bullets_are_not_items(self): + doc = T.parse(SAMPLE) + self.assertEqual(doc.sections[-1].items, []) + self.assertEqual(doc.sections[-1].prefix, ["", "- plain bullet, mentions [[t-first]]"]) + + def test_missing_section_raises(self): + doc = T.parse("# x\n\n## Pending\n") + with self.assertRaisesRegex(T.TaskError, "no '## Deferred' section"): + doc.section("deferred") + + def test_find_unknown_id_names_nearest(self): + with self.assertRaisesRegex(T.TaskError, r"unknown id 't-frist' \(nearest: t-first"): + T.parse(SAMPLE).find("t-frist") + + def test_items_are_separated_by_one_blank_line(self): + text = "## Pending\n- **t-a** [P1] (1h): A.\n\n\n\n- **t-b** [P1] (1h): B.\n body\n- **t-c** [P1] (1h): C.\n" + self.assertEqual(T.render(T.parse(text)), + "## Pending\n- **t-a** [P1] (1h): A.\n\n- **t-b** [P1] (1h): B.\n body\n\n- **t-c** [P1] (1h): C.\n") + + def test_malformed_header_is_kept_and_flagged(self): + text = "## Pending\n\n- **t-x** [P9] (1h) no colon\n body\n\n- **t-ok** [P1] (1h): Fine.\n" + doc = T.parse(text) + bad = doc.section("pending").items[0] + self.assertEqual(bad.raw, "- **t-x** [P9] (1h) no colon") + self.assertIn("header", bad.error) + self.assertEqual(bad.id, "t-x") + self.assertEqual(T.render(doc), text) + + def test_flush_left_line_ends_the_items_and_survives(self): + text = ("## Pending\n\n- **t-a** [P1] (1h): A.\n body\nstray continuation\n" + "- **t-b** [P1] (1h): B.\n\n## Deferred\n") + doc = T.parse(text) + pending = doc.section("pending") + self.assertEqual([i.id for i in pending.items], ["t-a"]) + self.assertEqual(pending.suffix, ["stray continuation", "- **t-b** [P1] (1h): B.", ""]) + self.assertEqual(T.render(doc), + "## Pending\n\n- **t-a** [P1] (1h): A.\n body\n\nstray continuation\n" + "- **t-b** [P1] (1h): B.\n\n## Deferred\n") + + def test_crlf_is_kept(self): + text = "## Pending\r\n\r\n- **t-a** [P1] (1h): A.\r\n body\r\n" + doc = T.parse(text) + self.assertEqual(doc.newline, "\r\n") + self.assertEqual(doc.section("pending").items[0].body, [" body"]) + self.assertEqual(T.render(doc), text) + + def test_missing_final_newline_is_added(self): + self.assertEqual(T.render(T.parse("## Pending\n\n- **t-a** [P1] (1h): A.")), + "## Pending\n\n- **t-a** [P1] (1h): A.\n") + + +if __name__ == "__main__": + unittest.main() + + +EDIT = """\ +## Awaiting your decision + +- **a-key**: Key needed. Which one? + +## Pending + +- **t-p0** [P0] (1h): Zero. + +- **t-p1** [P1] (1h): One. + - Steps: a + - After: [[t-p0]] + Ref: docs/a.md + +- **t-p2** [P2] (1h) (blocked: [[a-key]]): Two. + +- **t-p3** [P3] (1h): Three. + - After: [[t-gone]] + +## Needs human + +- **t-play** [P1] (<1h): Play. + - [ ] keyboard works + - [x] motion smooth + - [ ] gait feels right + +## Deferred +""" + + +def ids(doc, key): + return [i.id for i in doc.section(key).items] + + +class MakeIdTest(unittest.TestCase): + def test_slug_of_title(self): + self.assertEqual(T.make_id("Guards notice lock picking and busting", set()), + "t-guards-notice-lock-picking-and-busting") + + def test_long_title_is_cut_at_a_word(self): + self.assertEqual(T.make_id("Exe function map plus port re-verification of everything", set()), + "t-exe-function-map-plus-port-re") + self.assertEqual(T.make_id("Thrown weapons and grenades for everyone here", set()), + "t-thrown-weapons-and-grenades-for-everyone") + + def test_taken_id_gets_a_number(self): + self.assertEqual(T.make_id("Cave seams", {"t-cave-seams"}), "t-cave-seams-2") + self.assertEqual(T.make_id("Cave seams", {"t-cave-seams", "t-cave-seams-2"}), "t-cave-seams-3") + + def test_empty_slug_is_refused(self): + with self.assertRaisesRegex(T.TaskError, "--id"): + T.make_id("???", set()) + + def test_non_ascii_is_dropped(self): + self.assertEqual(T.make_id("Åäö test", set()), "t-test") + + def test_awaiting_prefix(self): + self.assertEqual(T.make_id("Smoke screenshots", set(), "a-"), "a-smoke-screenshots") + + def test_slice_id(self): + self.assertEqual(T.slice_id("t-map", {"t-map"}), "t-map-1") + self.assertEqual(T.slice_id("t-map", {"t-map", "t-map-1", "t-map-2"}), "t-map-3") + + +class EditTest(unittest.TestCase): + def setUp(self): + self.doc = T.parse(EDIT) + + def new(self, id, prio, body=()): + return T.Item(id=id, prio=prio, effort="1h", text="New.", body=list(body)) + + def test_insert_by_priority(self): + T.insert(self.doc, self.new("t-n", 1), "pending") + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p1", "t-n", "t-p2", "t-p3"]) + + def test_insert_into_empty_section(self): + T.insert(self.doc, self.new("t-n", 0), "deferred") + self.assertEqual(ids(self.doc, "deferred"), ["t-n"]) + + def test_insert_goes_after_its_dependency(self): + T.insert(self.doc, self.new("t-n", 1, [" - After: [[t-p3]]"]), "pending") + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p1", "t-p2", "t-p3", "t-n"]) + + def test_insert_refuses_taken_id(self): + with self.assertRaisesRegex(T.TaskError, "'t-p1' already exists"): + T.insert(self.doc, self.new("t-p1", 1), "pending") + + def test_insert_refuses_wrong_kind_for_section(self): + with self.assertRaisesRegex(T.TaskError, "a- items belong in Awaiting"): + T.insert(self.doc, T.Item(id="a-x", text="X."), "pending") + with self.assertRaisesRegex(T.TaskError, "needs a priority"): + T.insert(self.doc, T.Item(id="t-x", effort="1h", text="X."), "pending") + + def test_remove(self): + item = T.remove(self.doc, "t-p1") + self.assertEqual(item.body, [" - Steps: a", " - After: [[t-p0]]", " Ref: docs/a.md"]) + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p2", "t-p3"]) + + def test_remove_unknown_names_nearest(self): + with self.assertRaisesRegex(T.TaskError, r"unknown id 't-p9' \(nearest: t-p"): + T.remove(self.doc, "t-p9") + + def test_set_prio_moves_the_item_and_keeps_its_body(self): + T.set_prio(self.doc, "t-p1", 3) + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p2", "t-p3", "t-p1"]) + item = self.doc.item("t-p1") + self.assertEqual((item.prio, item.body[0]), (3, " - Steps: a")) + + def test_set_prio_range(self): + with self.assertRaisesRegex(T.TaskError, "priority 0-3"): + T.set_prio(self.doc, "t-p1", 4) + + def test_move_to_section(self): + T.move_to(self.doc, "t-p3", "deferred") + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p1", "t-p2"]) + self.assertEqual(ids(self.doc, "deferred"), ["t-p3"]) + + def test_move_to_refuses_kind_change(self): + with self.assertRaisesRegex(T.TaskError, "a- items belong in Awaiting"): + T.move_to(self.doc, "a-key", "pending") + + def test_move_rel_refuses_priority_break(self): + with self.assertRaisesRegex(T.TaskError, "breaks priority order.*--force"): + T.move_rel(self.doc, "t-p3", "t-p0", before=True) + self.assertEqual(ids(self.doc, "pending"), ["t-p0", "t-p1", "t-p2", "t-p3"]) + + def test_move_rel_with_force(self): + T.move_rel(self.doc, "t-p3", "t-p0", before=True, force=True) + self.assertEqual(ids(self.doc, "pending"), ["t-p3", "t-p0", "t-p1", "t-p2"]) + + def test_move_rel_never_puts_an_item_before_its_dependency(self): + with self.assertRaisesRegex(T.TaskError, "'t-p1' is After: \\[\\[t-p0\\]\\]"): + T.move_rel(self.doc, "t-p1", "t-p0", before=True, force=True) + + def test_move_rel_after_into_other_section(self): + T.move_rel(self.doc, "t-p1", "t-play", before=False) + self.assertEqual(ids(self.doc, "human"), ["t-play", "t-p1"]) + + def test_status(self): + T.set_status(self.doc, "t-p1", "in progress: feature/x") + self.assertEqual(self.doc.item("t-p1").header(), "- **t-p1** [P1] (1h) (in progress: feature/x): One.") + T.set_status(self.doc, "t-p1", "blocked: [[a-key]]") + self.assertEqual(self.doc.item("t-p1").blocked_on, "a-key") + T.set_status(self.doc, "t-p1", None) + self.assertEqual(self.doc.item("t-p1").header(), "- **t-p1** [P1] (1h): One.") + + def test_status_blocked_on_unknown_awaiting_item(self): + with self.assertRaisesRegex(T.TaskError, "'a-none' is not an open Awaiting item"): + T.set_status(self.doc, "t-p1", "blocked: [[a-none]]") + + def test_set_fields_header(self): + T.set_fields(self.doc, "t-p1", title="Uno", effort="5h", interactive=True) + self.assertEqual(self.doc.item("t-p1").header(), "- **t-p1** [P1] (5h, interactive): Uno.") + T.set_fields(self.doc, "t-p0", title="Nil") + self.assertEqual(self.doc.item("t-p0").text, "Nil.") + + def test_set_fields_title_keeps_goal(self): + doc = T.parse("## Pending\n\n- **t-a** [P1] (1h): Old name. The goal. More.\n") + T.set_fields(doc, "t-a", title="New name") + self.assertEqual(doc.item("t-a").text, "New name. The goal. More.") + + def test_set_fields_full_text_replaces_goal(self): + doc = T.parse("## Awaiting your decision\n\n- **a-q** : Colour. OK?\n\n## Pending\n\n" + "- **t-a** [P1] (1h): Old name. Old goal.\n".replace(" :", ":")) + T.set_fields(doc, "a-q", title="Colour red. OK?") + self.assertEqual(doc.item("a-q").text, "Colour red. OK?") + T.set_fields(doc, "t-a", title="New name. New goal") + self.assertEqual(doc.item("t-a").text, "New name. New goal.") + T.set_fields(doc, "t-a", title="Why not?") + self.assertEqual(doc.item("t-a").text, "Why not?") + + def test_set_fields_bad_effort(self): + with self.assertRaisesRegex(T.TaskError, "effort '2h'"): + T.set_fields(self.doc, "t-p1", effort="2h") + + def test_set_after(self): + T.set_fields(self.doc, "t-p1", after=[]) + self.assertEqual(self.doc.item("t-p1").body, [" - Steps: a", " Ref: docs/a.md"]) + T.set_fields(self.doc, "t-p1", after=["t-p0", "t-p2"]) + self.assertEqual(self.doc.item("t-p1").body, + [" - Steps: a", " - After: [[t-p0]], [[t-p2]]", " Ref: docs/a.md"]) + + def test_set_done(self): + doc = T.parse("## Pending\n\n- **t-a** [P1] (1h): A.\n - Steps: a\n - note\n Model: sonnet\n Ref: x.md\n") + T.set_fields(doc, "t-a", done="a works") + self.assertEqual(doc.item("t-a").body, + [" - Steps: a", " - note", " Done: a works", " Model: sonnet", " Ref: x.md"]) + T.set_fields(doc, "t-a", done=" b works ") + self.assertEqual(doc.item("t-a").body[2], " Done: b works") + T.set_fields(doc, "t-a", done="") + self.assertEqual(doc.item("t-a").body, [" - Steps: a", " - note", " Model: sonnet", " Ref: x.md"]) + + def test_set_refs(self): + T.set_fields(self.doc, "t-p1", refs=["docs/b.md#x (rows)", "DESIGN.md"]) + self.assertEqual(self.doc.item("t-p1").body[-1], " Ref: docs/b.md#x (rows), DESIGN.md") + T.set_fields(self.doc, "t-p0", refs=["docs/a.md"]) + self.assertEqual(self.doc.item("t-p0").body, [" Ref: docs/a.md"]) + T.set_fields(self.doc, "t-p0", refs=[]) + self.assertEqual(self.doc.item("t-p0").body, []) + + def test_add_note_goes_before_after_and_ref(self): + T.add_note(self.doc, "t-p1", "found the cause") + self.assertEqual(self.doc.item("t-p1").body, + [" - Steps: a", " - found the cause", " - After: [[t-p0]]", " Ref: docs/a.md"]) + + def test_set_body_keeps_after_and_ref(self): + T.set_body(self.doc, "t-p1", ["- Steps: b", " - sub", "", "- Done: c"]) + self.assertEqual(self.doc.item("t-p1").body, + [" - Steps: b", " - sub", "", " - Done: c", " - After: [[t-p0]]", " Ref: docs/a.md"]) + + def test_set_body_bare_after_ids_become_links(self): + T.set_body(self.doc, "t-p1", ["- Done: c", "- After: t-a, [[t-b]]"]) + self.assertEqual(self.doc.item("t-p1").body[:2], [" - Done: c", " - After: [[t-a]], [[t-b]]"]) + + def test_tick_by_number_and_text(self): + self.assertEqual(T.tick(self.doc, "t-play", "1"), "keyboard works") + self.assertEqual(T.tick(self.doc, "t-play", "gait"), "gait feels right") + self.assertEqual(self.doc.item("t-play").body, + [" - [x] keyboard works", " - [x] motion smooth", " - [x] gait feels right"]) + + def test_tick_errors(self): + with self.assertRaisesRegex(T.TaskError, "already ticked"): + T.tick(self.doc, "t-play", "2") + with self.assertRaisesRegex(T.TaskError, "no box matches 'jump'"): + T.tick(self.doc, "t-play", "jump") + with self.assertRaisesRegex(T.TaskError, "no box 7"): + T.tick(self.doc, "t-play", "7") + + def test_unblock(self): + self.assertEqual(T.unblock(self.doc, "a-key"), ["t-p2"]) + self.assertIsNone(self.doc.item("t-p2").status) + + +class PickTest(unittest.TestCase): + def test_skips_blocked_and_open_dependencies(self): + doc = T.parse(EDIT) + T.remove(doc, "t-p0") + T.remove(doc, "t-p1") + item, skipped = L.pick(doc, set(), L.DEFAULT_LANES, "1h") + self.assertIsNone(item) + self.assertEqual([(i.id, why) for i, why in skipped], + [("t-p2", "blocked: a-key"), ("t-p3", "after: t-gone")]) + + def test_archived_dependency_counts_as_done(self): + doc = T.parse(EDIT) + T.remove(doc, "t-p0") + T.remove(doc, "t-p1") + item, skipped = L.pick(doc, {"t-gone"}, L.DEFAULT_LANES, "1h") + self.assertEqual(item.id, "t-p3") + self.assertEqual([i.id for i, _ in skipped], ["t-p2"]) + + def test_first_item_when_free(self): + item, skipped = L.pick(T.parse(EDIT), set(), L.DEFAULT_LANES, "1h") + self.assertEqual((item.id, skipped), ("t-p0", [])) + + def test_open_dependency_in_file(self): + doc = T.parse(EDIT) + T.move_to(doc, "t-p0", "deferred") + item, skipped = L.pick(doc, set(), L.DEFAULT_LANES, "1h") + self.assertIsNone(item) + self.assertEqual(skipped[0][1], "after: t-p0") + + +class SliceTest(unittest.TestCase): + def test_open_slices(self): + doc = T.parse("## Pending\n\n- **t-p** [P1] (10h): Parent.\n\n- **t-p-2** [P1] (1h): Two.\n\n" + "- **t-px-1** [P1] (1h): Other.\n") + self.assertEqual(T.open_slices(doc, "t-p"), ["t-p-2"]) + + def test_open_slices_by_name(self): + doc = T.parse(SLICED) + self.assertEqual(T.open_slices(doc, "t-p"), ["t-map", "t-p-3"]) + self.assertEqual(T.parent_of(doc, "t-map"), "t-p") + self.assertEqual(T.parent_of(doc, "t-p-3"), "t-p") + self.assertIsNone(T.parent_of(doc, "t-q")) + + def test_pick_skips_parent_with_open_slices(self): + item, skipped = L.pick(T.parse(SLICED), {"t-done"}, L.DEFAULT_LANES, "1h") + self.assertEqual(item.id, "t-q") + self.assertEqual([(i.id, why) for i, why in skipped], + [("t-map", "after: t-x"), ("t-p-3", "after: t-map"), ("t-p", "open slices: t-map, t-p-3")]) + + +class RenameTest(unittest.TestCase): + def test_rename_changes_id_and_links(self): + doc = T.parse(SLICED) + n = T.rename(doc, "t-map", "t-map-scaffold", taken={"t-old"}) + self.assertEqual(n, 2) + self.assertEqual(T.render(doc), SLICED.replace("t-map", "t-map-scaffold")) + + def test_rename_awaiting_updates_blocked_status(self): + doc = T.parse("## Awaiting your decision\n\n- **a-a-foo**: Foo?\n\n## Pending\n\n" + "- **t-a** [P1] (1h) (blocked: [[a-a-foo]]): A.\n") + self.assertEqual(T.rename(doc, "a-a-foo", "a-foo", taken=set()), 1) + self.assertEqual(doc.item("t-a").blocked_on, "a-foo") + + def test_rename_refusals(self): + doc = T.parse(SLICED) + for new, err in [("t-q", "id 't-q' already exists"), ("t-old", "id 't-old' already used in the archive"), + ("a-map", "a rename keeps the kind"), ("t-Bad", "bad id 't-Bad'")]: + with self.assertRaisesRegex(T.TaskError, err): + T.rename(doc, "t-map", new, taken={"t-old"}) + with self.assertRaisesRegex(T.TaskError, "unknown id 't-none'"): + T.rename(doc, "t-none", "t-x", taken=set()) + self.assertEqual(T.render(doc), SLICED) + + +SLICED = """\ +## Pending + +- **t-p** [P1] (10h): Parent. + - Slices: [[t-done]], [[t-map]], [[t-p-3]] + +- **t-map** [P1] (1h): Map. + - After: [[t-x]] + +- **t-p-3** [P1] (1h): Three. + - After: [[t-map]] + +- **t-q** [P2] (1h): Free. +""" + + +class ArchiveTest(unittest.TestCase): + def test_line(self): + item = T.Item(id="t-x", prio=1, effort="1h", text="Cave seams. Close the slit.") + self.assertEqual(T.archive_line("2026-09-29", item, "closed with side walls"), + "- 2026-09-29 **t-x** Cave seams — closed with side walls") + + def test_prepend(self): + self.assertEqual(T.archive_prepend("# Archive (newest first)\n\n- old one\n- older\n", "- new"), + "# Archive (newest first)\n\n- new\n- old one\n- older\n") + + def test_prepend_to_empty_list(self): + self.assertEqual(T.archive_prepend("# Archive (newest first)\n", "- new"), + "# Archive (newest first)\n\n- new\n") + + def test_ids(self): + text = "# A\n\n- 2026-09-29 **t-x** X — done\n- old line without id\n- 2026-09-01 **t-y-2** Y — z\n" + self.assertEqual(T.archive_ids(text), {"t-x", "t-y-2"}) + + +class ParseBlockTest(unittest.TestCase): + def test_block_is_reindented(self): + item = T.parse_block("- **t-n** [P2] (1h): New. Goal.\n - Steps: a\n - sub\n Ref: docs/a.md\n") + self.assertEqual(item.lines(), ["- **t-n** [P2] (1h): New. Goal.", " - Steps: a", " - sub", " Ref: docs/a.md"]) + + def test_old_numbered_format_is_refused(self): + with self.assertRaisesRegex(T.TaskError, "old numbered format"): + T.parse_block("3. **[P2] Old** (Effort: 1h) — goal.") + + def test_bad_header_is_refused(self): + with self.assertRaisesRegex(T.TaskError, "bad header"): + T.parse_block("- **t-n** no colon") |
