from pathlib import Path import pytest from click.testing import CliRunner from markitect_tool.cli import main from markitect_tool.contract import load_contract_file from markitect_tool.generation import ( GenerationHookRequest, GenerationHookResult, generate_stub_from_contract, generate_with_hook, load_generation_plan_file, run_generation_plan, ) from markitect_tool.template import ( MissingTemplateVariable, analyze_template, render_template, ) def test_analyze_template_extracts_nested_and_unicode_variables(): analysis = analyze_template("Hello {{customer.name}}, café {{café.price}}") assert analysis.valid assert analysis.variables == ["customer.name", "café.price"] assert analysis.root_variables == ["customer", "café"] assert analysis.nested_variables == ["customer.name", "café.price"] assert analysis.max_nesting_depth == 2 def test_analyze_template_reports_invalid_syntax(): analysis = analyze_template("Valid {{name}} but invalid {{1bad}} and {{missing") assert not analysis.valid assert "name" in analysis.variables assert len(analysis.syntax_errors) == 2 def test_render_template_strict_and_lenient_modes(): template = "# {{title}}\n\nOwner: {{owner.name}}\n\n{{items}}" data = {"title": "Plan", "owner": {"name": "Ada"}, "items": ["one", "two"]} result = render_template(template, data) assert result.complete assert "# Plan" in result.markdown assert "Owner: Ada" in result.markdown assert "- one\n- two" in result.markdown with pytest.raises(MissingTemplateVariable): render_template("{{missing}}", {}, strict=True) lenient = render_template("{{missing}}", {}, strict=False) assert lenient.markdown == "{{missing}}" assert lenient.missing_variables == ["missing"] def test_generate_stub_from_contract_uses_sections_and_guidance(): contract = load_contract_file("examples/contracts/adr.contract.md") result = generate_stub_from_contract(contract, data={"status": "proposed"}) assert "document_type: adr" in result.markdown assert "status: proposed" in result.markdown assert "# Architecture Decision Record" in result.markdown assert "## Context" in result.markdown assert "TODO: Explain why the decision exists." in result.markdown assert "Deprecated Approach" not in result.markdown def test_generation_plan_renders_and_writes_outputs(tmp_path: Path): template = tmp_path / "letter.md" data = tmp_path / "data.yaml" rules = tmp_path / "rules.md" template.write_text("# Hello {{person.name}}\n\n{{message}}", encoding="utf-8") data.write_text("person:\n name: Ada\nmessage: Welcome.\n", encoding="utf-8") rules.write_text( """# Generation Rules ```yaml generation template: letter.md data_file: data.yaml output: out/letter.md ``` """, encoding="utf-8", ) plan = load_generation_plan_file(rules) result = run_generation_plan(plan, base_dir=tmp_path, output_dir=tmp_path) assert result.documents[0].markdown == "# Hello Ada\n\nWelcome." assert (tmp_path / "out" / "letter.md").read_text(encoding="utf-8") == "# Hello Ada\n\nWelcome." def test_generation_hook_boundary_accepts_external_provider(): class FakeHook: def generate(self, request: GenerationHookRequest) -> GenerationHookResult: return GenerationHookResult( markdown=f"# {request.data['title']}\n\n{request.prompt}", provider="fake", ) result = generate_with_hook( GenerationHookRequest(prompt="Draft this deterministically in the test.", data={"title": "Hook"}), FakeHook(), ) assert result.provider == "fake" assert result.markdown.startswith("# Hook") def test_mkt_template_render_outputs_markdown(tmp_path: Path): template = tmp_path / "template.md" data = tmp_path / "data.json" template.write_text("# {{title}}\n", encoding="utf-8") data.write_text('{"title": "Rendered"}', encoding="utf-8") result = CliRunner().invoke(main, ["template", "render", str(template), "--data", str(data)]) assert result.exit_code == 0 assert result.output == "# Rendered\n" def test_mkt_template_inspect_outputs_text(tmp_path: Path): template = tmp_path / "template.md" template.write_text("# {{title}}\n\n{{owner.name}}", encoding="utf-8") result = CliRunner().invoke(main, ["template", "inspect", str(template)]) assert result.exit_code == 0 assert "variables: 2" in result.output assert "owner.name" in result.output def test_mkt_generate_stub_outputs_contract_stub(): result = CliRunner().invoke( main, ["generate", "stub", "--contract", "examples/contracts/adr.contract.md", "--set", "status=accepted"], ) assert result.exit_code == 0 assert "status: accepted" in result.output assert "## Decision" in result.output def test_mkt_generate_rules_writes_file(tmp_path: Path): template = tmp_path / "template.md" rules = tmp_path / "rules.md" template.write_text("# {{title}}\n", encoding="utf-8") rules.write_text( """```yaml generation template: template.md data: title: From Rules output: generated.md ```""", encoding="utf-8", ) result = CliRunner().invoke(main, ["generate", "rules", str(rules), "--output-dir", str(tmp_path)]) assert result.exit_code == 0 assert '"count": 1' in result.output assert (tmp_path / "generated.md").read_text(encoding="utf-8") == "# From Rules\n"