256 lines
7.9 KiB
Python
256 lines
7.9 KiB
Python
"""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)
|
|
|
|
|
|
class TestAggregateWithNonDirectiveContext:
|
|
"""Integration: aggregate_directives returns non-directive files in a separate section."""
|
|
|
|
def test_aggregate_includes_non_directive_section(self, tmp_path: Path):
|
|
directives_dir = tmp_path / "conductor" / "directives" / "alpha"
|
|
directives_dir.mkdir(parents=True)
|
|
(directives_dir / "v1.md").write_text("# Alpha rule\n\nDo the thing.\n", encoding="utf-8")
|
|
context_file = tmp_path / "AGENTS.md"
|
|
context_file.write_text("# Project Rules\n\nRule 1.\n", encoding="utf-8")
|
|
preset = tmp_path / "test_preset.md"
|
|
preset.write_text("""# Preset: test
|
|
|
|
## Directives to warm
|
|
- alpha: conductor/directives/alpha/v1.md
|
|
|
|
## Non-directive context
|
|
- project rules: AGENTS.md
|
|
""", encoding="utf-8")
|
|
rendered = aggregate_directives(str(preset), project_root=str(tmp_path))
|
|
assert "alpha" in rendered
|
|
assert "Alpha rule" in rendered
|
|
assert "NON-DIRECTIVE CONTEXT" in rendered
|
|
assert "Project Rules" in rendered
|
|
assert rendered.index("alpha") < rendered.index("NON-DIRECTIVE CONTEXT")
|
|
|
|
def test_aggregate_non_directive_section_after_directives(self, tmp_path: Path):
|
|
directives_dir = tmp_path / "conductor" / "directives" / "beta"
|
|
directives_dir.mkdir(parents=True)
|
|
(directives_dir / "v1.md").write_text("# Beta rule\n\nDo beta.\n", encoding="utf-8")
|
|
context_file = tmp_path / "conductor" / "product.md"
|
|
context_file.parent.mkdir(parents=True, exist_ok=True)
|
|
context_file.write_text("# Product\n\nVision.\n", encoding="utf-8")
|
|
preset = tmp_path / "test_preset.md"
|
|
preset.write_text("""# Preset: test
|
|
|
|
## Directives to warm
|
|
- beta: conductor/directives/beta/v1.md
|
|
|
|
## Non-directive context
|
|
- product: conductor/product.md
|
|
""", encoding="utf-8")
|
|
rendered = aggregate_directives(str(preset), project_root=str(tmp_path))
|
|
assert "Beta rule" in rendered
|
|
assert "Product" in rendered
|
|
assert rendered.index("Beta rule") < rendered.index("NON-DIRECTIVE CONTEXT")
|
|
assert "product" in rendered.lower() or "Product" in rendered |