"""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