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" class DocForgeCliTests(unittest.TestCase): def copy_fixture(self, destination: Path) -> Path: root = destination / "alpha" shutil.copytree(FIXTURES / "alpha", root) return root 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 if __name__ == "__main__": unittest.main()