utils/md_naming/tests/test_migrate.py
Krzysztof kuhy Rudnicki a10d190b65
Some checks failed
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
design_system tests / test (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
web_ui tests / test (push) Has been cancelled
Add the shared markdown-naming gate and migrate utils' own docs
Markdown across every repo had accumulated a dozen ad-hoc names
(refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION,
HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a
finished record. An audit found docs asserting work was pending that
shipped months earlier, and one asserting completion while the work
was open.

Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task
files carry the literal "REMOVE ME AFTER FINISH" and are deleted when
the work lands.

Enforced bidirectionally -- a TODO without the marker fails, AND any
file carrying the marker that is not named TODO*.md fails. Without the
reverse rule a task can be renamed into the permanent namespace and
quietly live forever, which is the failure being fixed.

md_naming/ mirrors the file_length topology and imports its tables
(EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN)
rather than restating them, so the two gates cannot disagree about the
same file. 71 tests, 100% branch coverage.

stale.py warns on TODOs past 90 days but never fails: no machine can
prove a task is finished, so the honest mechanism is a nudge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:53:12 +02:00

108 lines
3.8 KiB
Python

"""The one-shot migration's decision logic.
The moves have already run, but the rules that produced them are the record of
why 203 files are named what they are -- and two real bugs were found here by
dry-running: basename matching that would have rewritten vendored node_modules
files, and a path-boundary guard that silently skipped qualified references.
Both are pinned below.
"""
from __future__ import annotations
import pytest
from md_naming.migrate import classify
from md_naming.migrate.plan import HOME, auto_name, rewrite, slugify
TODO_250 = HOME / "todo" / "refactor_claude_todo.md"
NEW_250 = HOME / "todo" / "TODO-file-length-250.md"
@pytest.mark.parametrize(
("text", "expected"),
[
("see refactor_claude_todo.md here", "see TODO-file-length-250.md here"),
("(refactor_claude_todo.md)", "(TODO-file-length-250.md)"),
("docs/refactor_claude_todo.md", "docs/TODO-file-length-250.md"),
],
)
def test_distinctive_name_is_rewritten(text: str, expected: str) -> None:
assert rewrite(text, TODO_250, NEW_250) == expected
def test_longer_filename_is_not_rewritten() -> None:
"""The guard must not match the tail of a different filename."""
text = "my_refactor_claude_todo.md.bak"
assert rewrite(text, TODO_250, NEW_250) == text
def test_ambiguous_name_needs_qualifying_directory() -> None:
"""PLAN.md appears in unrelated repos, so a bare mention is left alone.
Pinning the bug that would have rewritten 12 vendored copies of
qs/.github/SECURITY.md during a dry run.
"""
old = HOME / "roadside-assistance" / "PLAN.md"
new = HOME / "roadside-assistance" / "DOCS-plan.md"
assert rewrite("an unrelated PLAN.md", old, new) == "an unrelated PLAN.md"
assert (
rewrite("see roadside-assistance/PLAN.md", old, new)
== "see roadside-assistance/DOCS-plan.md"
)
def test_distinctive_name_matches_regardless_of_directory() -> None:
"""A name absent from AMBIGUOUS_NAMES is rewritten wherever it appears.
SESSION_RESULTS.md is distinctive enough that no unrelated repo owns one,
so it is matched unqualified -- which is what let `vmbox/README.md` be
repaired. The narrowing in AMBIGUOUS_NAMES is reserved for names that
genuinely collide (PLAN.md, README.md, SPEC.md).
"""
old = HOME / "utils" / "vmbox" / "SESSION_RESULTS.md"
new = HOME / "utils" / "vmbox" / "DOCS-session-results.md"
assert (
rewrite("vmbox/SESSION_RESULTS.md", old, new) == "vmbox/DOCS-session-results.md"
)
assert (
rewrite("bare SESSION_RESULTS.md", old, new) == "bare DOCS-session-results.md"
)
@pytest.mark.parametrize(
("stem", "expected"),
[
("Hello World", "hello-world"),
("01-motion_tokens", "01-motion-tokens"),
("---", "untitled"),
],
)
def test_slugify(stem: str, expected: str) -> None:
assert slugify(stem) == expected
def test_auto_name_defaults_to_docs() -> None:
assert auto_name("todo/docs/whatever.md") == ("DOCS-whatever.md", "record")
def test_auto_name_fixes_readme_casing() -> None:
"""A misspelled README stays a README rather than entering DOCS."""
assert auto_name("x/Readme.md") == ("README.md", "record")
def test_dopamine_ux_parts_are_tasks_but_index_is_not() -> None:
"""00-INDEX records the programme; 01-09 are its unstarted parts."""
assert auto_name("utils/dopamine-ux/03-diet-guard.md")[1] == "task"
assert auto_name("utils/dopamine-ux/00-INDEX.md")[1] == "record"
def test_classified_paths_do_not_overlap() -> None:
"""A file cannot be both a task and a record."""
assert not set(classify.TASKS) & set(classify.RECORDS)
def test_deleted_files_are_not_also_renamed() -> None:
deleted = set(classify.PROVEN_DONE) | set(classify.CLAIMED_DONE)
assert not deleted & set(classify.TASKS)
assert not deleted & set(classify.RECORDS)