diff --git a/docket_md/_logic/graph.py b/docket_md/_logic/graph.py index b403400..c181c6c 100644 --- a/docket_md/_logic/graph.py +++ b/docket_md/_logic/graph.py @@ -244,9 +244,7 @@ def _format_edge(source_id: str, edge: dict, indent: str) -> None: if src_id == node_id: lines.append(f"\n**CONNECTED ({len(src_edges)}):**") else: - lines.append( - f"\n**from `{src_id}` ({src_node.get('title', '?')}):**" - ) + lines.append(f"\n**from `{src_id}` ({src_node.get('title', '?')}):**") indent = " " * (src_depth + 1) for edge in src_edges: _format_edge(src_id, edge, indent) diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 51e973f..796fc5d 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -4,7 +4,6 @@ import os import tempfile -import pytest from docket_md.daemon import ( _extract_title, @@ -18,7 +17,10 @@ # ── Fixtures ───────────────────────────────────────────────────────────────── -def _make_approval_entry(plan: str = "# My Plan\n\nDo the thing.", session_id: str = "abc-123"): + +def _make_approval_entry( + plan: str = "# My Plan\n\nDo the thing.", session_id: str = "abc-123" +): """Build a realistic plan-approval JSONL dict.""" return { "type": "user", @@ -43,18 +45,22 @@ def _make_docket_project(tmpdir: str, project_name: str = "test-project") -> str docket_dir = os.path.join(tmpdir, ".docket") os.makedirs(os.path.join(docket_dir, "plans"), exist_ok=True) with open(os.path.join(docket_dir, "docket.json"), "w") as f: - json.dump({ - "id": "test-guid-1234", - "version": "0.1.0", - "project": project_name, - "created": "2026-01-01", - "docket_path": ".docket", - }, f) + json.dump( + { + "id": "test-guid-1234", + "version": "0.1.0", + "project": project_name, + "created": "2026-01-01", + "docket_path": ".docket", + }, + f, + ) return tmpdir # ── Detection tests ────────────────────────────────────────────────────────── + def test_is_plan_approval_match(): entry = _make_approval_entry() result = _is_plan_approval(entry) @@ -81,9 +87,7 @@ def test_is_plan_approval_no_match_no_approved_text(): entry = { "type": "user", "message": { - "content": [ - {"type": "tool_result", "content": "Some other tool result"} - ] + "content": [{"type": "tool_result", "content": "Some other tool result"}] }, "toolUseResult": { "plan": "# A Plan", @@ -96,6 +100,7 @@ def test_is_plan_approval_no_match_no_approved_text(): # ── Title extraction tests ─────────────────────────────────────────────────── + def test_extract_title_heading(): assert _extract_title("# Deploy the Widget\n\nSteps here.") == "Deploy the Widget" @@ -111,6 +116,7 @@ def test_extract_title_later_heading(): # ── Ingestion tests ────────────────────────────────────────────────────────── + def test_ingest_plan(): with tempfile.TemporaryDirectory() as tmpdir: project_root = _make_docket_project(tmpdir) @@ -151,6 +157,7 @@ def test_deduplication(): # ── Install tests ──────────────────────────────────────────────────────────── + def test_install_creates_hook(tmp_path, monkeypatch): """install() creates settings.json with the SessionStart hook.""" settings_file = str(tmp_path / "settings.json") @@ -177,13 +184,17 @@ def test_install_merges_existing(tmp_path, monkeypatch): "SessionStart": [ { "matcher": "", - "hooks": [{"type": "command", "command": "echo hello", "timeout": 5}], + "hooks": [ + {"type": "command", "command": "echo hello", "timeout": 5} + ], } ], "PostToolUse": [ { "matcher": "", - "hooks": [{"type": "command", "command": "echo post", "timeout": 5}], + "hooks": [ + {"type": "command", "command": "echo post", "timeout": 5} + ], } ], } @@ -210,6 +221,7 @@ def test_install_merges_existing(tmp_path, monkeypatch): # ── Queue processing tests ─────────────────────────────────────────────────── + def test_process_queue(tmp_path, monkeypatch): """process_queue() picks up watch_session jobs and adds watches.""" queue_file = str(tmp_path / "daemon-queue.jsonl") @@ -281,6 +293,7 @@ def test_process_queue_skips_non_docket(tmp_path, monkeypatch): # ── Watch tests ────────────────────────────────────────────────────────────── + def test_watch_detects_approval(tmp_path, monkeypatch): """watch_files() detects a plan approval appended after the offset watermark.""" project_root = _make_docket_project(str(tmp_path / "proj")) diff --git a/tests/test_hier_config.py b/tests/test_hier_config.py index a5a92eb..d2eeafb 100644 --- a/tests/test_hier_config.py +++ b/tests/test_hier_config.py @@ -1,8 +1,6 @@ """Tests for hierarchical config resolution.""" -import json import os -import pytest from unittest.mock import patch from docket_md.hier_config import ( @@ -15,7 +13,6 @@ set_value, find_setting_origin, get_config_tree, - DOCKET_CONFIG_ROOT, ) @@ -23,6 +20,7 @@ # deep_merge # --------------------------------------------------------------------------- + class TestDeepMerge: def test_simple_override(self): assert deep_merge({"a": 1}, {"a": 2}) == {"a": 2} @@ -57,27 +55,41 @@ def test_three_levels(self): # parse_git_remote # --------------------------------------------------------------------------- + class TestParseGitRemote: def test_ssh(self): - assert parse_git_remote("git@github.com:aic-holdings/ciso.git") == ("aic-holdings", "ciso") + assert parse_git_remote("git@github.com:aic-holdings/ciso.git") == ( + "aic-holdings", + "ciso", + ) def test_https_with_git(self): - assert parse_git_remote("https://github.com/aic-holdings/ciso.git") == ("aic-holdings", "ciso") + assert parse_git_remote("https://github.com/aic-holdings/ciso.git") == ( + "aic-holdings", + "ciso", + ) def test_https_without_git(self): - assert parse_git_remote("https://github.com/aic-holdings/ciso") == ("aic-holdings", "ciso") + assert parse_git_remote("https://github.com/aic-holdings/ciso") == ( + "aic-holdings", + "ciso", + ) def test_invalid(self): assert parse_git_remote("not-a-url") is None def test_gitlab_ssh(self): - assert parse_git_remote("git@gitlab.com:myorg/myrepo.git") == ("myorg", "myrepo") + assert parse_git_remote("git@gitlab.com:myorg/myrepo.git") == ( + "myorg", + "myrepo", + ) # --------------------------------------------------------------------------- # Settings I/O # --------------------------------------------------------------------------- + class TestSettingsIO: def test_read_missing(self, tmp_path): assert read_settings(str(tmp_path / "nope.json")) == {} @@ -98,6 +110,7 @@ def test_read_malformed(self, tmp_path): # Config chain resolution # --------------------------------------------------------------------------- + class TestResolveConfigChain: def test_full_chain(self, tmp_path, monkeypatch): config_root = str(tmp_path / "config" / "docket") @@ -121,7 +134,10 @@ def test_full_chain(self, tmp_path, monkeypatch): proj_dir = os.path.join(repo_dir, "projects", "uuid-123") write_settings(os.path.join(proj_dir, "settings.json"), proj_s) - with patch("docket_md.hier_config.detect_org_repo", return_value=("testorg", "testrepo")): + with patch( + "docket_md.hier_config.detect_org_repo", + return_value=("testorg", "testrepo"), + ): chain = resolve_config_chain("/fake/path", project_id="uuid-123") assert len(chain) == 4 @@ -131,7 +147,10 @@ def test_full_chain(self, tmp_path, monkeypatch): assert chain[3][0] == "project" # Verify merged settings - with patch("docket_md.hier_config.detect_org_repo", return_value=("testorg", "testrepo")): + with patch( + "docket_md.hier_config.detect_org_repo", + return_value=("testorg", "testrepo"), + ): merged = resolved_settings("/fake/path", project_id="uuid-123") assert merged["theme"] == "dark" @@ -147,7 +166,9 @@ def test_missing_levels(self, tmp_path, monkeypatch): os.makedirs(config_root, exist_ok=True) write_settings(os.path.join(config_root, "settings.json"), {"theme": "light"}) - with patch("docket_md.hier_config.detect_org_repo", return_value=("myorg", "myrepo")): + with patch( + "docket_md.hier_config.detect_org_repo", return_value=("myorg", "myrepo") + ): merged = resolved_settings("/fake/path", project_id="uuid-456") assert merged == {"theme": "light"} @@ -171,12 +192,15 @@ def test_no_git_remote(self, tmp_path, monkeypatch): # set_value # --------------------------------------------------------------------------- + class TestSetValue: def test_set_dotted_key(self, tmp_path, monkeypatch): config_root = str(tmp_path / "config" / "docket") monkeypatch.setattr("docket_md.hier_config.DOCKET_CONFIG_ROOT", config_root) - with patch("docket_md.hier_config.detect_org_repo", return_value=("org1", "repo1")): + with patch( + "docket_md.hier_config.detect_org_repo", return_value=("org1", "repo1") + ): sp = set_value("/fake", "wrike.enabled", True, level="org") data = read_settings(sp) @@ -195,18 +219,25 @@ def test_set_global(self, tmp_path, monkeypatch): # find_setting_origin # --------------------------------------------------------------------------- + class TestFindSettingOrigin: def test_finds_deepest_level(self, tmp_path, monkeypatch): config_root = str(tmp_path / "config" / "docket") monkeypatch.setattr("docket_md.hier_config.DOCKET_CONFIG_ROOT", config_root) os.makedirs(config_root, exist_ok=True) - write_settings(os.path.join(config_root, "settings.json"), {"wrike": {"enabled": True}}) + write_settings( + os.path.join(config_root, "settings.json"), {"wrike": {"enabled": True}} + ) org_dir = os.path.join(config_root, "orgs", "org1") - write_settings(os.path.join(org_dir, "settings.json"), {"wrike": {"enabled": False}}) + write_settings( + os.path.join(org_dir, "settings.json"), {"wrike": {"enabled": False}} + ) - with patch("docket_md.hier_config.detect_org_repo", return_value=("org1", "repo1")): + with patch( + "docket_md.hier_config.detect_org_repo", return_value=("org1", "repo1") + ): result = find_setting_origin("wrike.enabled", "/fake", project_id="uuid-1") assert result == ("org", False) @@ -226,9 +257,12 @@ def test_not_found(self, tmp_path, monkeypatch): # get_config_tree # --------------------------------------------------------------------------- + class TestGetConfigTree: def test_empty(self, tmp_path, monkeypatch): - monkeypatch.setattr("docket_md.hier_config.DOCKET_CONFIG_ROOT", str(tmp_path / "empty")) + monkeypatch.setattr( + "docket_md.hier_config.DOCKET_CONFIG_ROOT", str(tmp_path / "empty") + ) assert get_config_tree() == {} def test_populated(self, tmp_path, monkeypatch): diff --git a/tests/test_plans.py b/tests/test_plans.py index 9e5a4e2..2f1d7eb 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -1,8 +1,5 @@ """Tests for plan entity type (files + tools).""" -import os -import pytest - from docket_md.config import init_project, register_project from docket_md.files import ( list_plans, @@ -25,6 +22,7 @@ # Helpers # --------------------------------------------------------------------------- + def _setup_project(tmp_path) -> tuple[str, str]: """Init + register a temp project, return (project_root, project_id).""" project_root = str(tmp_path) @@ -37,6 +35,7 @@ def _setup_project(tmp_path) -> tuple[str, str]: # TestPlanFiles # --------------------------------------------------------------------------- + class TestPlanFiles: def test_next_plan_id_empty(self, tmp_path): project_root, _ = _setup_project(tmp_path) @@ -46,14 +45,37 @@ def test_next_plan_id_sequential(self, tmp_path): project_root, _ = _setup_project(tmp_path) # Write two plans manually fp1 = plan_path(project_root, "PLAN-0001", "First plan") - write_markdown(fp1, {"id": "PLAN-0001", "title": "First plan", "status": "draft", "created": "2026-03-25"}, "") + write_markdown( + fp1, + { + "id": "PLAN-0001", + "title": "First plan", + "status": "draft", + "created": "2026-03-25", + }, + "", + ) fp2 = plan_path(project_root, "PLAN-0002", "Second plan") - write_markdown(fp2, {"id": "PLAN-0002", "title": "Second plan", "status": "draft", "created": "2026-03-25"}, "") + write_markdown( + fp2, + { + "id": "PLAN-0002", + "title": "Second plan", + "status": "draft", + "created": "2026-03-25", + }, + "", + ) assert next_plan_id(project_root) == "PLAN-0003" def test_plan_roundtrip(self, tmp_path): project_root, _ = _setup_project(tmp_path) - fm = {"id": "PLAN-0001", "title": "Test Plan", "status": "draft", "created": "2026-03-25"} + fm = { + "id": "PLAN-0001", + "title": "Test Plan", + "status": "draft", + "created": "2026-03-25", + } content = "This is the plan body." fp = plan_path(project_root, "PLAN-0001", "Test Plan") write_markdown(fp, fm, content) @@ -65,7 +87,16 @@ def test_plan_roundtrip(self, tmp_path): def test_find_plan_file(self, tmp_path): project_root, _ = _setup_project(tmp_path) fp = plan_path(project_root, "PLAN-0001", "My Plan") - write_markdown(fp, {"id": "PLAN-0001", "title": "My Plan", "status": "draft", "created": "2026-03-25"}, "") + write_markdown( + fp, + { + "id": "PLAN-0001", + "title": "My Plan", + "status": "draft", + "created": "2026-03-25", + }, + "", + ) found = find_plan_file(project_root, "PLAN-0001") assert found is not None assert "PLAN-0001" in found @@ -80,9 +111,27 @@ def test_list_plans(self, tmp_path): assert list_plans(project_root) == [] # Add two plans fp1 = plan_path(project_root, "PLAN-0001", "Alpha") - write_markdown(fp1, {"id": "PLAN-0001", "title": "Alpha", "status": "draft", "created": "2026-03-25"}, "") + write_markdown( + fp1, + { + "id": "PLAN-0001", + "title": "Alpha", + "status": "draft", + "created": "2026-03-25", + }, + "", + ) fp2 = plan_path(project_root, "PLAN-0002", "Beta") - write_markdown(fp2, {"id": "PLAN-0002", "title": "Beta", "status": "approved", "created": "2026-03-25"}, "") + write_markdown( + fp2, + { + "id": "PLAN-0002", + "title": "Beta", + "status": "approved", + "created": "2026-03-25", + }, + "", + ) plans = list_plans(project_root) assert len(plans) == 2 assert plans[0].frontmatter["id"] == "PLAN-0001" @@ -93,10 +142,13 @@ def test_list_plans(self, tmp_path): # TestPlanTools # --------------------------------------------------------------------------- + class TestPlanTools: def test_plan_create(self, tmp_path): _, pid = _setup_project(tmp_path) - result = plan_create(pid, "Launch Strategy", content="We will launch in Q2.", tags=["strategy"]) + result = plan_create( + pid, "Launch Strategy", content="We will launch in Q2.", tags=["strategy"] + ) assert "PLAN-0001" in result assert "Launch Strategy" in result # Verify file exists @@ -120,7 +172,12 @@ def test_plan_list(self, tmp_path): def test_plan_view(self, tmp_path): _, pid = _setup_project(tmp_path) - plan_create(pid, "Detailed Plan", content="Full details here.", verification=["Check A", "Check B"]) + plan_create( + pid, + "Detailed Plan", + content="Full details here.", + verification=["Check A", "Check B"], + ) result = plan_view(pid, "PLAN-0001") assert "Detailed Plan" in result assert "Full details here." in result diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 363127b..d75812e 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -1,9 +1,7 @@ """Tests for session management helpers.""" -import json import os -import pytest from docket_md.files import ( load_sessions, diff --git a/tests/test_wrike_hook.py b/tests/test_wrike_hook.py index 8e42b19..2c1225d 100644 --- a/tests/test_wrike_hook.py +++ b/tests/test_wrike_hook.py @@ -1,7 +1,6 @@ """Tests for the optional Wrdocket hook.""" -import os -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from docket_md.hooks.wrike_hook import WrikeHook, get_hook @@ -84,7 +83,9 @@ def test_updates_status(self, monkeypatch): hook = WrikeHook("test-token") mock_resp = MagicMock() - mock_resp.json.return_value = {"data": [{"id": "TASK123", "status": "Completed"}]} + mock_resp.json.return_value = { + "data": [{"id": "TASK123", "status": "Completed"}] + } mock_resp.raise_for_status = MagicMock() hook._http = MagicMock() hook._http.put.return_value = mock_resp