diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 0722b9a91..1b09347dc 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -597,8 +597,10 @@ def _render_toml_string(value: str) -> str: escaped = value.replace("\\", "\\\\") if '"""' not in escaped: + if escaped.endswith('"'): + return '"""\n' + escaped + '\\\n"""' return '"""\n' + escaped + '"""' - if "'''" not in value: + if "'''" not in value and not value.endswith("'"): return "'''\n" + value + "'''" return '"' + ( diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 2582a9a85..fcded1834 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -204,6 +204,89 @@ def test_toml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch): assert "scripts:" not in parsed["prompt"] assert "---" not in parsed["prompt"] + def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch): + """Multiline body ending with `"` must not produce `""""` (#2113).""" + i = get_integration(self.KEY) + template = tmp_path / "sample.md" + template.write_text( + "---\n" + "description: Test\n" + "scripts:\n" + " sh: echo ok\n" + "---\n" + "Check the following:\n" + '- Correct: "Is X clearly specified?"\n', + encoding="utf-8", + ) + monkeypatch.setattr(i, "list_command_templates", lambda: [template]) + + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + cmd_files = [f for f in created if "scripts" not in f.parts] + assert len(cmd_files) == 1 + + raw = cmd_files[0].read_text(encoding="utf-8") + assert '""""' not in raw, "closing delimiter must not merge with body quote" + assert '"""\n' in raw, "body must use multiline basic string" + parsed = tomllib.loads(raw) + assert parsed["prompt"].endswith('specified?"') + assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline" + + def test_toml_triple_double_and_single_quote_ending(self, tmp_path, monkeypatch): + """Body containing `\"\"\"` and ending with `'` falls back to escaped basic string.""" + i = get_integration(self.KEY) + template = tmp_path / "sample.md" + template.write_text( + "---\n" + "description: Test\n" + "scripts:\n" + " sh: echo ok\n" + "---\n" + 'Use """triple""" quotes\n' + "and end with 'single'\n", + encoding="utf-8", + ) + monkeypatch.setattr(i, "list_command_templates", lambda: [template]) + + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + cmd_files = [f for f in created if "scripts" not in f.parts] + assert len(cmd_files) == 1 + + raw = cmd_files[0].read_text(encoding="utf-8") + assert "''''" not in raw, "literal string must not produce ambiguous closing quotes" + parsed = tomllib.loads(raw) + assert parsed["prompt"].endswith("'single'") + assert '"""triple"""' in parsed["prompt"] + assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline" + + def test_toml_closing_delimiter_inline_when_safe(self, tmp_path, monkeypatch): + """Body NOT ending with `"` keeps closing `\"\"\"` inline (no extra newline).""" + i = get_integration(self.KEY) + template = tmp_path / "sample.md" + template.write_text( + "---\n" + "description: Test\n" + "scripts:\n" + " sh: echo ok\n" + "---\n" + "Line one\n" + "Plain body content\n", + encoding="utf-8", + ) + monkeypatch.setattr(i, "list_command_templates", lambda: [template]) + + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + cmd_files = [f for f in created if "scripts" not in f.parts] + assert len(cmd_files) == 1 + + raw = cmd_files[0].read_text(encoding="utf-8") + parsed = tomllib.loads(raw) + assert parsed["prompt"] == "Line one\nPlain body content" + assert raw.rstrip().endswith('content"""'), \ + "closing delimiter should be inline when body does not end with a quote" + def test_toml_is_valid(self, tmp_path): """Every generated TOML file must parse without errors.""" i = get_integration(self.KEY)