From e288495768df61ab89230b415847e02f2418370c Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:42:44 -0500 Subject: [PATCH 1/3] fix: prevent ambiguous TOML closing quotes when body ends with `"` (#2113) _render_toml_string placed the closing `"""` inline with content, so a body ending with `"` produced `""""` (four consecutive quotes). While technically valid TOML 1.0, this breaks stricter parsers such as Gemini CLI v0.27.2. Insert a newline before the closing delimiter when the body ends with a quote character. Same treatment for the single-quote (`'''`) fallback. Adds both a positive test (body ending with `"` must not produce `""""`) and a negative test (safe bodies keep the inline delimiter). --- src/specify_cli/integrations/base.py | 6 ++- .../test_integration_base_toml.py | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 0722b9a91..0feafc3cb 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -597,9 +597,11 @@ def _render_toml_string(value: str) -> str: escaped = value.replace("\\", "\\\\") if '"""' not in escaped: - return '"""\n' + escaped + '"""' + sep = "\n" if escaped.endswith('"') else "" + return '"""\n' + escaped + sep + '"""' if "'''" not in value: - return "'''\n" + value + "'''" + sep = "\n" if value.endswith("'") else "" + return "'''\n" + value + sep + "'''" return '"' + ( value.replace("\\", "\\\\") diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 2582a9a85..67abdedd1 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -204,6 +204,58 @@ 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): + """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" + '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" + parsed = tomllib.loads(raw) + assert parsed["prompt"].endswith('specified?"') + + 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) From ba7b052d9b87ea14335e3e2d73119d36639b424e Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:59:40 -0500 Subject: [PATCH 2/3] fix: use line-ending backslash instead of newline for TOML closing delimiters Address PR review feedback: - Replace sep=newline with TOML line-ending backslash so the parsed value does not gain a trailing newline when body ends with a quote. - For literal string (''') fallback, skip to escaped basic string when value ends with single quote instead of inserting a newline. - Make test body multiline so it exercises the """ rendering path, and assert no trailing newline in parsed value. --- src/specify_cli/integrations/base.py | 10 +++++----- tests/integrations/test_integration_base_toml.py | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 0feafc3cb..1b09347dc 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -597,11 +597,11 @@ def _render_toml_string(value: str) -> str: escaped = value.replace("\\", "\\\\") if '"""' not in escaped: - sep = "\n" if escaped.endswith('"') else "" - return '"""\n' + escaped + sep + '"""' - if "'''" not in value: - sep = "\n" if value.endswith("'") else "" - return "'''\n" + value + sep + "'''" + if escaped.endswith('"'): + return '"""\n' + escaped + '\\\n"""' + return '"""\n' + escaped + '"""' + if "'''" not in value and not value.endswith("'"): + return "'''\n" + value + "'''" return '"' + ( value.replace("\\", "\\\\") diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 67abdedd1..e45c7dd76 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -205,7 +205,7 @@ def test_toml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch): assert "---" not in parsed["prompt"] def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch): - """Body ending with `"` must not produce `""""` (#2113).""" + """Multiline body ending with `"` must not produce `""""` (#2113).""" i = get_integration(self.KEY) template = tmp_path / "sample.md" template.write_text( @@ -214,7 +214,8 @@ def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch): "scripts:\n" " sh: echo ok\n" "---\n" - 'Is "X clearly specified?"\n', + "Check the following:\n" + '- Correct: "Is X clearly specified?"\n', encoding="utf-8", ) monkeypatch.setattr(i, "list_command_templates", lambda: [template]) @@ -226,8 +227,10 @@ def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch): 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_closing_delimiter_inline_when_safe(self, tmp_path, monkeypatch): """Body NOT ending with `"` keeps closing `\"\"\"` inline (no extra newline).""" From 4c2be675fcdc881193ef772dc8b5b4983f45821f Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:07:50 -0500 Subject: [PATCH 3/3] test: cover escaped basic-string fallback when body has triple-quotes and ends with single-quote Addresses review feedback from PR #2115: adds test for the branch where the body contains '"""' and ends with "'", which forces _render_toml_string() through the escaped basic-string fallback instead of the '''...''' literal-string path (since '''' would produce the same ambiguous-closing-delimiter problem). --- .../test_integration_base_toml.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index e45c7dd76..fcded1834 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -232,6 +232,34 @@ def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch): 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)