from __future__ import annotations import os import shutil import tempfile import threading import time import unittest from pathlib import Path from unittest import mock from docforge.changesets import ChangesetStore from docforge.cli import _parser, _run from docforge.project import Project from docforge.viewer_manager import ViewerManager ROOT = Path(__file__).resolve().parents[1] FIXTURES = ROOT / "tests" / "fixtures" GRAPH_CONFIG = """ [graph_render] output_root = ".docforge/portable-graph" [[graph_render.views]] id = "architecture" renderer = "portable_graph_html" output = "architecture.html" title = "Alpha architecture" root = "guide.workflow" initial_mode = "nodes" depth = 2 max_nodes = 20 max_edges = 40 max_work = 1000 include_logic = false """ class DocForgeCliTests(unittest.TestCase): def copy_fixture(self, destination: Path) -> Path: root = destination / "alpha" shutil.copytree(FIXTURES / "alpha", root) return root def test_traversal_commands_accept_explicit_result_limits(self) -> None: parser = _parser() for command in ("backlinks", "dependencies", "impact"): with self.subTest(command=command): arguments = parser.parse_args( [ "--project-root", "/tmp/project", command, "guide.workflow", "--limit", "7", ] ) self.assertEqual(7, arguments.limit) context = parser.parse_args( [ "--project-root", "/tmp/project", "context", "active", "--limit", "7", "--cursor", "opaque", ] ) self.assertEqual(7, context.limit) self.assertEqual("opaque", context.cursor) def test_reindex_apply_and_visualization_commands_are_self_service(self) -> None: with tempfile.TemporaryDirectory() as directory: parent = Path(directory) root = self.copy_fixture(parent) parser = _parser() reindexed = _run(parser.parse_args(["--project-root", str(root), "reindex"])) self.assertTrue(reindexed["reindexed"]) synchronized = _run(parser.parse_args(["--project-root", str(root), "sync"])) self.assertEqual("current", synchronized["synchronization"]["action"]) project = Project.open(root) store = ChangesetStore(project, "alpha-editor") node = next(node for node in project.load().nodes if node.node_id == "guide.workflow") created = store.create("cli-apply") proposed = store.propose_update( changeset_id="cli-apply", expected_changeset_hash=str(created["changeset_hash"]), node_id=node.node_id, expected_content_hash=node.content_hash, metadata={"summary": "Applied through the CLI."}, content=None, relationship_changes=[], rationale="Verify the direct application command.", ) applied = _run( parser.parse_args( [ "--project-root", str(root), "apply", "cli-apply", "--changeset-hash", str(proposed["changeset_hash"]), "--applier", "alpha-editor", ] ) ) self.assertTrue(applied["applied"]) state_path = parent / "viewer-manager.json" manager = ViewerManager(state_path, check_interval_seconds=0.02) thread = threading.Thread(target=manager.serve_forever, daemon=True) previous = os.environ.get("DOCFORGE_VIEWER_MANAGER_STATE") os.environ["DOCFORGE_VIEWER_MANAGER_STATE"] = str(state_path) thread.start() deadline = time.monotonic() + 2 while not state_path.exists() and time.monotonic() < deadline: time.sleep(0.01) try: with mock.patch("docforge.cli.webbrowser.open", return_value=True) as opened: visualized = _run( parser.parse_args( [ "--project-root", str(root), "visualize", "--node", "guide.workflow", ] ) ) self.assertTrue(visualized["opened_browser"]) opened.assert_called_once() status = _run( parser.parse_args(["--project-root", str(root), "visualization-status"]) ) self.assertEqual("running", status["state"]) stopped = _run( parser.parse_args(["--project-root", str(root), "visualization-stop"]) ) self.assertEqual("stopped", stopped["state"]) finally: manager.shutdown() thread.join(timeout=2) if previous is None: os.environ.pop("DOCFORGE_VIEWER_MANAGER_STATE", None) else: os.environ["DOCFORGE_VIEWER_MANAGER_STATE"] = previous def test_portable_graph_plan_render_and_status_are_self_service(self) -> None: with tempfile.TemporaryDirectory() as directory: root = self.copy_fixture(Path(directory)) descriptor = root / ".docforge/project.toml" descriptor.write_text( descriptor.read_text(encoding="utf-8") + GRAPH_CONFIG, encoding="utf-8", ) parser = _parser() planned = _run( parser.parse_args(["--project-root", str(root), "graph-plan", "architecture"]) ) self.assertEqual("architecture", planned["view_id"]) rendered = _run( parser.parse_args(["--project-root", str(root), "graph-render", "architecture"]) ) self.assertEqual("current", rendered["state"]) status = _run( parser.parse_args( ["--project-root", str(root), "graph-render-status", "architecture"] ) ) self.assertEqual("current", status["state"]) if __name__ == "__main__": unittest.main()