feat(archive): add pkgmgr archive subcommand for task-tracked spec dirs
Walks a directory of numbered NNN-topic.md files, promotes every file with zero unchecked task-list items into the directorys README under the ## Archive section, then deletes the source file. Keeps spec directories (typically docs/requirements) short and focused on open work. The action ships as pkgmgr.actions.archive with four leaf modules (discovery, inspect, readme, workflow) and is wired into the CLI as pkgmgr archive [DIR] [--readme PATH] [--dry-run] [--include-template]. Extracted verbatim from cli/contributing/requirements/archive in infinito-nexus-core so every kpmx-managed repository can rely on the same archival convention without copy-pasting helpers. Twenty unit tests cover discovery, inspection, README merge, and end-to-end workflow paths. Also: realign tests/unit/pkgmgr/cli/commands/test_release.py with the new run_release(retry=False) signature shipped in v1.14.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
0
tests/unit/pkgmgr/actions/archive/__init__.py
Normal file
0
tests/unit/pkgmgr/actions/archive/__init__.py
Normal file
49
tests/unit/pkgmgr/actions/archive/test_discovery.py
Normal file
49
tests/unit/pkgmgr/actions/archive/test_discovery.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Unit tests for `pkgmgr.actions.archive.discovery`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from pkgmgr.actions.archive.discovery import iter_archivable_files
|
||||
|
||||
|
||||
class TestIterArchivableFiles(unittest.TestCase):
|
||||
def test_only_numbered_markdown_files_are_returned_and_sorted(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
(d / "001-alpha.md").write_text("# 001 - A")
|
||||
(d / "002-beta.md").write_text("# 002 - B")
|
||||
(d / "README.md").write_text("# X")
|
||||
(d / "notes.md").write_text("# X")
|
||||
(d / "001-alpha.txt").write_text("x")
|
||||
|
||||
result = [p.name for p in iter_archivable_files(d, include_template=True)]
|
||||
self.assertEqual(result, ["001-alpha.md", "002-beta.md"])
|
||||
|
||||
def test_template_is_skipped_unless_opted_in(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
(d / "000-template.md").write_text("# 000 - Template")
|
||||
(d / "001-alpha.md").write_text("# 001 - A")
|
||||
|
||||
self.assertEqual(
|
||||
[p.name for p in iter_archivable_files(d, include_template=False)],
|
||||
["001-alpha.md"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[p.name for p in iter_archivable_files(d, include_template=True)],
|
||||
["000-template.md", "001-alpha.md"],
|
||||
)
|
||||
|
||||
def test_missing_directory_returns_empty(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
self.assertEqual(
|
||||
iter_archivable_files(Path(tmp) / "missing", include_template=False),
|
||||
[],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
62
tests/unit/pkgmgr/actions/archive/test_inspect.py
Normal file
62
tests/unit/pkgmgr/actions/archive/test_inspect.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Unit tests for `pkgmgr.actions.archive.inspect`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from pkgmgr.actions.archive.inspect import count_unchecked_items, extract_h1
|
||||
|
||||
|
||||
class TestExtractH1(unittest.TestCase):
|
||||
def test_returns_first_h1_title(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "001-x.md"
|
||||
path.write_text("\n# 001 - Title\n\n## Subsection\n# Later H1\n")
|
||||
self.assertEqual(extract_h1(path), "001 - Title")
|
||||
|
||||
def test_returns_none_when_no_h1(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "001-x.md"
|
||||
path.write_text("no heading here\n## h2 only\n")
|
||||
self.assertIsNone(extract_h1(path))
|
||||
|
||||
|
||||
class TestCountUncheckedItems(unittest.TestCase):
|
||||
def test_zero_when_all_checked(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "001-x.md"
|
||||
path.write_text(
|
||||
"# 001 - Done\n\n## Acceptance Criteria\n\n- [x] A\n- [x] B\n"
|
||||
)
|
||||
self.assertEqual(count_unchecked_items(path), 0)
|
||||
|
||||
def test_counts_unchecked_anywhere_in_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "001-x.md"
|
||||
path.write_text(
|
||||
"# 001 - In progress\n\n"
|
||||
"## Acceptance Criteria\n\n"
|
||||
"- [x] A\n"
|
||||
"- [ ] B\n\n"
|
||||
"## Notes\n\n"
|
||||
"- [ ] still tracking this one too\n"
|
||||
" - [ ] nested unchecked\n"
|
||||
)
|
||||
self.assertEqual(count_unchecked_items(path), 3)
|
||||
|
||||
def test_ignores_non_task_dashes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "001-x.md"
|
||||
path.write_text(
|
||||
"# 001 - X\n\n"
|
||||
"- plain list item\n"
|
||||
"- [x] checked\n"
|
||||
"- not [ ] not a task marker\n"
|
||||
)
|
||||
self.assertEqual(count_unchecked_items(path), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
73
tests/unit/pkgmgr/actions/archive/test_readme.py
Normal file
73
tests/unit/pkgmgr/actions/archive/test_readme.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Unit tests for `pkgmgr.actions.archive.readme`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from pkgmgr.actions.archive.readme import (
|
||||
existing_archive_entries,
|
||||
merge_archive_section,
|
||||
)
|
||||
|
||||
|
||||
class TestExistingArchiveEntries(unittest.TestCase):
|
||||
def test_parses_only_archive_section_items(self) -> None:
|
||||
readme = (
|
||||
"# Requirements\n\n"
|
||||
"Intro paragraph.\n\n"
|
||||
"## Other\n\n"
|
||||
"- not an archive entry\n\n"
|
||||
"## Archive\n\n"
|
||||
"- 001 - One\n"
|
||||
"- 002 - Two\n\n"
|
||||
"## Trailing\n\n"
|
||||
"- ignored too\n"
|
||||
)
|
||||
self.assertEqual(
|
||||
existing_archive_entries(readme),
|
||||
{"001 - One", "002 - Two"},
|
||||
)
|
||||
|
||||
def test_returns_empty_when_section_missing(self) -> None:
|
||||
self.assertEqual(existing_archive_entries("# Title\n\nBody.\n"), set())
|
||||
|
||||
|
||||
class TestMergeArchiveSection(unittest.TestCase):
|
||||
def test_creates_section_when_missing(self) -> None:
|
||||
readme = "# Requirements\n\nIntro.\n"
|
||||
merged = merge_archive_section(readme, ["001 - One", "002 - Two"])
|
||||
self.assertEqual(
|
||||
merged,
|
||||
"# Requirements\n\nIntro.\n\n## Archive\n\n- 001 - One\n- 002 - Two\n",
|
||||
)
|
||||
|
||||
def test_appends_to_existing_section_preserving_entries(self) -> None:
|
||||
readme = "# Requirements\n\nIntro.\n\n## Archive\n\n- 001 - One\n"
|
||||
merged = merge_archive_section(readme, ["002 - Two", "003 - Three"])
|
||||
self.assertEqual(
|
||||
merged,
|
||||
(
|
||||
"# Requirements\n\n"
|
||||
"Intro.\n\n"
|
||||
"## Archive\n\n"
|
||||
"- 001 - One\n"
|
||||
"- 002 - Two\n"
|
||||
"- 003 - Three\n"
|
||||
),
|
||||
)
|
||||
|
||||
def test_appends_before_trailing_section(self) -> None:
|
||||
readme = "## Archive\n\n- 001 - One\n\n## Trailing\n\nfooter\n"
|
||||
merged = merge_archive_section(readme, ["002 - Two"])
|
||||
self.assertEqual(
|
||||
merged,
|
||||
"## Archive\n\n- 001 - One\n- 002 - Two\n\n## Trailing\n\nfooter\n",
|
||||
)
|
||||
|
||||
def test_empty_entries_returns_unchanged(self) -> None:
|
||||
readme = "# X\n\nBody\n"
|
||||
self.assertEqual(merge_archive_section(readme, []), readme)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
91
tests/unit/pkgmgr/actions/archive/test_workflow.py
Normal file
91
tests/unit/pkgmgr/actions/archive/test_workflow.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Unit tests for `pkgmgr.actions.archive.workflow.run_archive`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from pkgmgr.actions.archive.workflow import run_archive
|
||||
|
||||
|
||||
class TestRunArchive(unittest.TestCase):
|
||||
def test_dry_run_does_not_touch_files_or_readme(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
done = d / "001-done.md"
|
||||
done.write_text("# 001 - Done\n\n- [x] ok\n")
|
||||
readme = d / "README.md"
|
||||
readme.write_text("# Specs\n\n## Archive\n")
|
||||
|
||||
plan = run_archive(d, readme, dry_run=True)
|
||||
|
||||
self.assertTrue(done.exists())
|
||||
self.assertEqual(readme.read_text(), "# Specs\n\n## Archive\n")
|
||||
self.assertEqual(plan.new_entries, ["001 - Done"])
|
||||
self.assertEqual(plan.archived, [(done, "001 - Done")])
|
||||
|
||||
def test_real_run_archives_and_deletes_completed_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
done = d / "001-done.md"
|
||||
done.write_text("# 001 - Done\n\n- [x] ok\n")
|
||||
readme = d / "README.md"
|
||||
readme.write_text("# Specs\n\n## Archive\n")
|
||||
|
||||
plan = run_archive(d, readme)
|
||||
|
||||
self.assertFalse(done.exists())
|
||||
self.assertIn("- 001 - Done", readme.read_text())
|
||||
self.assertEqual(plan.new_entries, ["001 - Done"])
|
||||
|
||||
def test_incomplete_files_are_skipped(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
wip = d / "002-wip.md"
|
||||
wip.write_text("# 002 - WIP\n\n- [ ] open\n")
|
||||
readme = d / "README.md"
|
||||
readme.write_text("# Specs\n\n## Archive\n")
|
||||
|
||||
plan = run_archive(d, readme)
|
||||
|
||||
self.assertTrue(wip.exists())
|
||||
self.assertEqual(plan.archived, [])
|
||||
self.assertEqual(plan.skipped_incomplete, [(wip, 1)])
|
||||
|
||||
def test_already_archived_title_is_not_duplicated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
done = d / "001-done.md"
|
||||
done.write_text("# 001 - Done\n\n- [x] ok\n")
|
||||
readme = d / "README.md"
|
||||
readme.write_text("# Specs\n\n## Archive\n\n- 001 - Done\n")
|
||||
|
||||
plan = run_archive(d, readme)
|
||||
|
||||
self.assertEqual(plan.new_entries, [])
|
||||
self.assertEqual(plan.archived, [(done, "001 - Done")])
|
||||
# File still deleted; README unchanged.
|
||||
self.assertFalse(done.exists())
|
||||
self.assertEqual(
|
||||
readme.read_text(),
|
||||
"# Specs\n\n## Archive\n\n- 001 - Done\n",
|
||||
)
|
||||
|
||||
def test_missing_directory_raises_filenotfound(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
readme = Path(tmp) / "README.md"
|
||||
readme.write_text("# X\n## Archive\n")
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
run_archive(Path(tmp) / "no-such-dir", readme)
|
||||
|
||||
def test_missing_readme_raises_filenotfound(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
(d / "001-done.md").write_text("# 001 - Done\n- [x] ok\n")
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
run_archive(d, d / "missing.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -112,6 +112,7 @@ class TestReleaseCommand(unittest.TestCase):
|
||||
preview=False,
|
||||
force=True,
|
||||
close=True,
|
||||
retry=False,
|
||||
)
|
||||
|
||||
@patch("pkgmgr.cli.commands.release.os.path.isdir", return_value=True)
|
||||
@@ -160,6 +161,7 @@ class TestReleaseCommand(unittest.TestCase):
|
||||
preview=True,
|
||||
force=False,
|
||||
close=False,
|
||||
retry=False,
|
||||
)
|
||||
|
||||
@patch("pkgmgr.cli.commands.release.run_release")
|
||||
|
||||
Reference in New Issue
Block a user