import json from pathlib import Path from click.testing import CliRunner from markitect_tool.cli import main from markitect_tool.document_function import ( DocumentFunctionDescriptor, DocumentFunctionParameter, DocumentFunctionRegistry, default_document_function_registry, parse_document_function_calls, render_document_functions, validate_document_functions, ) from markitect_tool.extension import ProcessingContext def test_parse_inline_and_fenced_function_calls(): text = """# Demo Inline {{mkt:text.upper "draft"}}. ```mkt-function md.heading level=3 Decision ``` """ calls = parse_document_function_calls(text) assert [call.function_id for call in calls] == ["text.upper", "md.heading"] assert calls[0].args == ["draft"] assert calls[1].kwargs == {"level": 3} assert calls[1].body.strip() == "Decision" def test_render_document_functions_expands_inline_and_fenced_calls(): text = """# Demo Inline {{mkt:text.upper "draft"}}. ```mkt-function md.heading level=3 Decision ``` """ result = render_document_functions(text) assert result.valid assert "Inline DRAFT." in result.content assert "### Decision" in result.content assert len(result.calls) == 2 assert result.provenance[0].operation == "document_function.text.upper" def test_pipeline_passes_previous_output_to_next_function(): result = render_document_functions('{{mkt:text.upper "draft" | text.replace DRAFT Final}}') assert result.valid assert result.content == "Final" def test_pipeline_separator_inside_quotes_is_literal(): result = render_document_functions('{{mkt:text.replace "a|b" "|" "/"}}') assert result.valid assert result.content == "a/b" def test_context_variables_can_be_used_in_function_arguments(): context = ProcessingContext(variables={"title": "Architecture Decision"}) result = render_document_functions("{{mkt:md.heading ${title} level=2}}", context=context) assert result.content == "## Architecture Decision" def test_validate_document_functions_reports_forbidden_calls(): result = validate_document_functions("{{mkt:text.upper draft}}", forbidden=["text.upper"]) assert not result.valid assert result.diagnostics[0].code == "function.forbidden" def test_validate_document_functions_reports_argument_errors(): result = validate_document_functions("{{mkt:text.upper draft unexpected=value}}") assert not result.valid assert result.diagnostics[0].code == "function.arguments" def test_registry_can_expose_custom_function_without_core_rewrite(): registry = DocumentFunctionRegistry() registry.register( DocumentFunctionDescriptor( id="demo.wrap", summary="Wrap text.", parameters=[DocumentFunctionParameter("value")], implementation=lambda value: f"[{value}]", ) ) result = render_document_functions("{{mkt:demo.wrap ok}}", registry=registry) assert result.valid assert result.content == "[ok]" def test_unknown_function_is_left_in_place_with_diagnostic(): result = render_document_functions("{{mkt:nope.missing value}}") assert not result.valid assert result.content == "{{mkt:nope.missing value}}" assert result.diagnostics[0].code == "function.unknown" def test_mkt_function_list_outputs_builtin_catalog(): result = CliRunner().invoke(main, ["function", "list", "--format", "json"]) data = json.loads(result.output) assert result.exit_code == 0 ids = {function["id"] for function in data["functions"]} assert {"text.upper", "md.heading", "md.codeblock"} <= ids def test_mkt_function_render_outputs_expanded_markdown(tmp_path: Path): file = tmp_path / "functions.md" file.write_text("# Demo\n\n{{mkt:md.bold Important}}\n", encoding="utf-8") result = CliRunner().invoke(main, ["function", "render", str(file)]) assert result.exit_code == 0 assert "**Important**" in result.output def test_mkt_function_check_can_restrict_allowed_functions(tmp_path: Path): file = tmp_path / "functions.md" file.write_text("{{mkt:text.upper draft}}\n", encoding="utf-8") result = CliRunner().invoke(main, ["function", "check", str(file), "--allow", "md.heading"]) assert result.exit_code == 1 assert "function.not_allowed" in result.output def test_default_registry_serializes_without_implementations(): data = default_document_function_registry().to_dict() assert data["count"] >= 1 assert "implementation" not in data["functions"][0]