workflow

git clone https://git.godosa.eu/workflow

master

raw · 6292 bytes

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()