Skip to content

Commit 40fb276

Browse files
authored
fix: prevent ambiguous TOML closing quotes when body ends with " (#2113) (#2115)
* 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). * 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. * 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).
1 parent 6536bc4 commit 40fb276

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

src/specify_cli/integrations/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,8 +597,10 @@ def _render_toml_string(value: str) -> str:
597597

598598
escaped = value.replace("\\", "\\\\")
599599
if '"""' not in escaped:
600+
if escaped.endswith('"'):
601+
return '"""\n' + escaped + '\\\n"""'
600602
return '"""\n' + escaped + '"""'
601-
if "'''" not in value:
603+
if "'''" not in value and not value.endswith("'"):
602604
return "'''\n" + value + "'''"
603605

604606
return '"' + (

tests/integrations/test_integration_base_toml.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,89 @@ def test_toml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch):
204204
assert "scripts:" not in parsed["prompt"]
205205
assert "---" not in parsed["prompt"]
206206

207+
def test_toml_no_ambiguous_closing_quotes(self, tmp_path, monkeypatch):
208+
"""Multiline body ending with `"` must not produce `""""` (#2113)."""
209+
i = get_integration(self.KEY)
210+
template = tmp_path / "sample.md"
211+
template.write_text(
212+
"---\n"
213+
"description: Test\n"
214+
"scripts:\n"
215+
" sh: echo ok\n"
216+
"---\n"
217+
"Check the following:\n"
218+
'- Correct: "Is X clearly specified?"\n',
219+
encoding="utf-8",
220+
)
221+
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
222+
223+
m = IntegrationManifest(self.KEY, tmp_path)
224+
created = i.setup(tmp_path, m)
225+
cmd_files = [f for f in created if "scripts" not in f.parts]
226+
assert len(cmd_files) == 1
227+
228+
raw = cmd_files[0].read_text(encoding="utf-8")
229+
assert '""""' not in raw, "closing delimiter must not merge with body quote"
230+
assert '"""\n' in raw, "body must use multiline basic string"
231+
parsed = tomllib.loads(raw)
232+
assert parsed["prompt"].endswith('specified?"')
233+
assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline"
234+
235+
def test_toml_triple_double_and_single_quote_ending(self, tmp_path, monkeypatch):
236+
"""Body containing `\"\"\"` and ending with `'` falls back to escaped basic string."""
237+
i = get_integration(self.KEY)
238+
template = tmp_path / "sample.md"
239+
template.write_text(
240+
"---\n"
241+
"description: Test\n"
242+
"scripts:\n"
243+
" sh: echo ok\n"
244+
"---\n"
245+
'Use """triple""" quotes\n'
246+
"and end with 'single'\n",
247+
encoding="utf-8",
248+
)
249+
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
250+
251+
m = IntegrationManifest(self.KEY, tmp_path)
252+
created = i.setup(tmp_path, m)
253+
cmd_files = [f for f in created if "scripts" not in f.parts]
254+
assert len(cmd_files) == 1
255+
256+
raw = cmd_files[0].read_text(encoding="utf-8")
257+
assert "''''" not in raw, "literal string must not produce ambiguous closing quotes"
258+
parsed = tomllib.loads(raw)
259+
assert parsed["prompt"].endswith("'single'")
260+
assert '"""triple"""' in parsed["prompt"]
261+
assert not parsed["prompt"].endswith("\n"), "parsed value must not gain a trailing newline"
262+
263+
def test_toml_closing_delimiter_inline_when_safe(self, tmp_path, monkeypatch):
264+
"""Body NOT ending with `"` keeps closing `\"\"\"` inline (no extra newline)."""
265+
i = get_integration(self.KEY)
266+
template = tmp_path / "sample.md"
267+
template.write_text(
268+
"---\n"
269+
"description: Test\n"
270+
"scripts:\n"
271+
" sh: echo ok\n"
272+
"---\n"
273+
"Line one\n"
274+
"Plain body content\n",
275+
encoding="utf-8",
276+
)
277+
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
278+
279+
m = IntegrationManifest(self.KEY, tmp_path)
280+
created = i.setup(tmp_path, m)
281+
cmd_files = [f for f in created if "scripts" not in f.parts]
282+
assert len(cmd_files) == 1
283+
284+
raw = cmd_files[0].read_text(encoding="utf-8")
285+
parsed = tomllib.loads(raw)
286+
assert parsed["prompt"] == "Line one\nPlain body content"
287+
assert raw.rstrip().endswith('content"""'), \
288+
"closing delimiter should be inline when body does not end with a quote"
289+
207290
def test_toml_is_valid(self, tmp_path):
208291
"""Every generated TOML file must parse without errors."""
209292
i = get_integration(self.KEY)

0 commit comments

Comments
 (0)