diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index d5ebce78e..37d3cdf96 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -1178,12 +1178,18 @@ class YamlIntegration(IntegrationBase): default_flow_style=False, ).strip() - # Indent the body for YAML block scalar + # Indent the body for YAML block scalar. Use an explicit indentation + # indicator ("|2") rather than a bare "|": YAML infers a plain block + # scalar's indentation from its first non-empty line, so a body whose + # first line is itself indented (e.g. a markdown code block or a nested + # list item) would make the parser expect that deeper indent for the + # whole block and reject the later, less-indented lines. Pinning the + # indent to 2 keeps the recipe parseable whatever the body looks like. indented = "\n".join(f" {line}" for line in body.split("\n")) lines = [ header_yaml, - "prompt: |", + "prompt: |2", indented, "", f"# Source: {source_id}", diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 74cdab2d7..56bed09eb 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -184,6 +184,23 @@ class YamlIntegrationTests: assert "scripts:" not in parsed["prompt"] assert "---" not in parsed["prompt"] + def test_yaml_prompt_with_indented_first_line_stays_valid(self): + """A body whose first line is indented must still parse. + + A bare ``|`` block scalar infers its indentation from the first + non-empty line, so a body starting with an indented line (e.g. a + markdown code block or nested list item) made the parser expect that + deeper indent for the whole block and reject the later, shallower + lines. The explicit ``|2`` indicator pins the indent so it parses.""" + body = " indented first line\nback to normal\n indented again" + rendered = YamlIntegration._render_yaml("Title", "Desc", body, "src") + + yaml_lines = [ + ln for ln in rendered.split("\n") if not ln.startswith("# Source:") + ] + parsed = yaml.safe_load("\n".join(yaml_lines)) + assert parsed["prompt"].rstrip("\n") == body + def test_plan_command_has_no_context_placeholder(self, tmp_path): """The generated plan command must not carry a context-file placeholder.