Private
Public Access
feat(aggregate): add Inherits resolution + Non-directive context to preset parser
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""Tests for aggregate_directives preset inheritance and non-directive context."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add scripts/ to sys.path so we can import aggregate_directives
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
from aggregate_directives import parse_preset_sections, resolve_inheritance, aggregate_directives
|
||||
|
||||
|
||||
def _preset_dir(root: Path) -> Path:
|
||||
"""Return the preset directory under root, creating it if needed."""
|
||||
d = root / "conductor" / "directives" / "presets"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
class TestParsePresetSections:
|
||||
"""Test the new section-based parser that extracts Inherits, Directives, Non-directive context."""
|
||||
|
||||
def test_parse_returns_three_sections(self, tmp_path: Path):
|
||||
preset = tmp_path / "test_preset.md"
|
||||
preset.write_text("""# Preset: test
|
||||
|
||||
## Inherits
|
||||
baseline
|
||||
|
||||
## Directives to warm
|
||||
Read each file below before any action.
|
||||
|
||||
- foo: conductor/directives/foo/v1.md
|
||||
- bar: conductor/directives/bar/v1.md
|
||||
|
||||
## Non-directive context
|
||||
Read each file below as full documents.
|
||||
|
||||
- project rules: AGENTS.md
|
||||
|
||||
## Notes
|
||||
test
|
||||
""", encoding="utf-8")
|
||||
sections = parse_preset_sections(preset)
|
||||
assert sections.inherits == "baseline"
|
||||
assert len(sections.directives) == 2
|
||||
assert sections.directives[0] == ("foo", Path("conductor/directives/foo/v1.md"))
|
||||
assert sections.directives[1] == ("bar", Path("conductor/directives/bar/v1.md"))
|
||||
assert len(sections.non_directive_context) == 1
|
||||
assert sections.non_directive_context[0] == ("project rules", Path("AGENTS.md"))
|
||||
|
||||
def test_parse_preset_without_inherits(self, tmp_path: Path):
|
||||
preset = tmp_path / "test_preset.md"
|
||||
preset.write_text("""# Preset: test
|
||||
|
||||
## Directives to warm
|
||||
- foo: conductor/directives/foo/v1.md
|
||||
|
||||
## Non-directive context
|
||||
- doc: AGENTS.md
|
||||
""", encoding="utf-8")
|
||||
sections = parse_preset_sections(preset)
|
||||
assert sections.inherits == ""
|
||||
assert len(sections.directives) == 1
|
||||
assert len(sections.non_directive_context) == 1
|
||||
|
||||
def test_parse_preset_without_non_directive_context(self, tmp_path: Path):
|
||||
preset = tmp_path / "test_preset.md"
|
||||
preset.write_text("""# Preset: test
|
||||
|
||||
## Directives to warm
|
||||
- foo: conductor/directives/foo/v1.md
|
||||
""", encoding="utf-8")
|
||||
sections = parse_preset_sections(preset)
|
||||
assert sections.inherits == ""
|
||||
assert len(sections.directives) == 1
|
||||
assert sections.non_directive_context == []
|
||||
|
||||
|
||||
class TestResolveInheritance:
|
||||
"""Test that resolve_inheritance merges parent directives into child, dedup on name."""
|
||||
|
||||
def test_child_directives_plus_parent_directives(self, tmp_path: Path):
|
||||
pdir = _preset_dir(tmp_path)
|
||||
(pdir / "parent.md").write_text("""# Preset: parent
|
||||
|
||||
## Directives to warm
|
||||
- alpha: conductor/directives/alpha/v1.md
|
||||
- beta: conductor/directives/beta/v1.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "child.md").write_text("""# Preset: child
|
||||
|
||||
## Inherits
|
||||
parent
|
||||
|
||||
## Directives to warm
|
||||
- beta: conductor/directives/beta/v1.md
|
||||
- gamma: conductor/directives/gamma/v1.md
|
||||
""", encoding="utf-8")
|
||||
merged = resolve_inheritance(pdir / "child.md", root=tmp_path)
|
||||
names = [n for n, _ in merged.directives]
|
||||
assert set(names) == {"alpha", "beta", "gamma"}
|
||||
assert names.count("beta") == 1
|
||||
assert len(merged.non_directive_context) == 0
|
||||
|
||||
def test_child_wins_on_directive_conflict(self, tmp_path: Path):
|
||||
pdir = _preset_dir(tmp_path)
|
||||
(pdir / "parent.md").write_text("""# Preset: parent
|
||||
|
||||
## Directives to warm
|
||||
- shared: conductor/directives/shared_parent/v1.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "child.md").write_text("""# Preset: child
|
||||
|
||||
## Inherits
|
||||
parent
|
||||
|
||||
## Directives to warm
|
||||
- shared: conductor/directives/shared_child/v1.md
|
||||
""", encoding="utf-8")
|
||||
merged = resolve_inheritance(pdir / "child.md", root=tmp_path)
|
||||
paths = [p for _, p in merged.directives]
|
||||
assert Path("conductor/directives/shared_child/v1.md") in paths
|
||||
assert Path("conductor/directives/shared_parent/v1.md") not in paths
|
||||
|
||||
def test_non_directive_context_merges_dedup_on_path(self, tmp_path: Path):
|
||||
pdir = _preset_dir(tmp_path)
|
||||
(pdir / "parent.md").write_text("""# Preset: parent
|
||||
|
||||
## Directives to warm
|
||||
- alpha: conductor/directives/alpha/v1.md
|
||||
|
||||
## Non-directive context
|
||||
- rules: AGENTS.md
|
||||
- product: conductor/product.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "child.md").write_text("""# Preset: child
|
||||
|
||||
## Inherits
|
||||
parent
|
||||
|
||||
## Directives to warm
|
||||
- beta: conductor/directives/beta/v1.md
|
||||
|
||||
## Non-directive context
|
||||
- rules: AGENTS.md
|
||||
- tech: conductor/tech-stack.md
|
||||
""", encoding="utf-8")
|
||||
merged = resolve_inheritance(pdir / "child.md", root=tmp_path)
|
||||
paths = [p for _, p in merged.non_directive_context]
|
||||
assert Path("AGENTS.md") in paths
|
||||
assert Path("conductor/product.md") in paths
|
||||
assert Path("conductor/tech-stack.md") in paths
|
||||
assert paths.count(Path("AGENTS.md")) == 1
|
||||
|
||||
def test_three_level_inheritance(self, tmp_path: Path):
|
||||
pdir = _preset_dir(tmp_path)
|
||||
(pdir / "grandparent.md").write_text("""# Preset: grandparent
|
||||
|
||||
## Directives to warm
|
||||
- alpha: conductor/directives/alpha/v1.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "parent.md").write_text("""# Preset: parent
|
||||
|
||||
## Inherits
|
||||
grandparent
|
||||
|
||||
## Directives to warm
|
||||
- beta: conductor/directives/beta/v1.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "child.md").write_text("""# Preset: child
|
||||
|
||||
## Inherits
|
||||
parent
|
||||
|
||||
## Directives to warm
|
||||
- gamma: conductor/directives/gamma/v1.md
|
||||
""", encoding="utf-8")
|
||||
merged = resolve_inheritance(pdir / "child.md", root=tmp_path)
|
||||
names = [n for n, _ in merged.directives]
|
||||
assert "alpha" in names
|
||||
assert "beta" in names
|
||||
assert "gamma" in names
|
||||
|
||||
def test_inheritance_loop_detected(self, tmp_path: Path):
|
||||
pdir = _preset_dir(tmp_path)
|
||||
(pdir / "a.md").write_text("""# Preset: a
|
||||
|
||||
## Inherits
|
||||
b
|
||||
|
||||
## Directives to warm
|
||||
- alpha: conductor/directives/alpha/v1.md
|
||||
""", encoding="utf-8")
|
||||
(pdir / "b.md").write_text("""# Preset: b
|
||||
|
||||
## Inherits
|
||||
a
|
||||
|
||||
## Directives to warm
|
||||
- beta: conductor/directives/beta/v1.md
|
||||
""", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="inheritance loop"):
|
||||
resolve_inheritance(pdir / "a.md", root=tmp_path)
|
||||
Reference in New Issue
Block a user