Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions docket_md/_logic/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 27 additions & 14 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import tempfile

import pytest

from docket_md.daemon import (
_extract_title,
Expand All @@ -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",
Expand All @@ -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)
Expand All @@ -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",
Expand All @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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}
],
}
],
}
Expand All @@ -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")
Expand Down Expand Up @@ -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"))
Expand Down
64 changes: 49 additions & 15 deletions tests/test_hier_config.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -15,14 +13,14 @@
set_value,
find_setting_origin,
get_config_tree,
DOCKET_CONFIG_ROOT,
)


# ---------------------------------------------------------------------------
# deep_merge
# ---------------------------------------------------------------------------


class TestDeepMerge:
def test_simple_override(self):
assert deep_merge({"a": 1}, {"a": 2}) == {"a": 2}
Expand Down Expand Up @@ -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")) == {}
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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"}
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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):
Expand Down
Loading
Loading