From df2c0e97c19bb5787271587b49732d57bed7b6d7 Mon Sep 17 00:00:00 2001 From: Hyun Oh <317391116+hyun-oho@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:03:23 +0800 Subject: [PATCH] fix(frontmatter): handle quote characters inside unquoted inline-array values --- .../core/src/agent_memory/core/frontmatter.py | 13 +++- tests/unit/test_frontmatter.py | 75 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_frontmatter.py diff --git a/packages/core/src/agent_memory/core/frontmatter.py b/packages/core/src/agent_memory/core/frontmatter.py index 3a3f6809..a07b7403 100644 --- a/packages/core/src/agent_memory/core/frontmatter.py +++ b/packages/core/src/agent_memory/core/frontmatter.py @@ -95,19 +95,30 @@ def _split_items(inner: str) -> list[str]: items: list[str] = [] current: list[str] = [] quote: str | None = None + at_value_start = True for char in inner: if quote: if char == quote: quote = None current.append(char) - elif char in ("'", '"'): + at_value_start = False + elif char in ("'", '"') and at_value_start: quote = char current.append(char) + at_value_start = False elif char == ",": items.append("".join(current)) current = [] + at_value_start = True + elif char in (" ", "\t"): + if not current: + at_value_start = True + else: + current.append(char) + at_value_start = False else: current.append(char) + at_value_start = False items.append("".join(current)) return [item for item in (item.strip() for item in items) if item] diff --git a/tests/unit/test_frontmatter.py b/tests/unit/test_frontmatter.py new file mode 100644 index 00000000..b502a6f0 --- /dev/null +++ b/tests/unit/test_frontmatter.py @@ -0,0 +1,75 @@ +"""Frontmatter dialect: parse and render round-trip, including inline arrays.""" + +from agent_memory.core import frontmatter + + +def _round_trip(fields, body): + text = frontmatter.render(fields, body) + parsed_fields, parsed_body = frontmatter.parse(text) + return parsed_fields, parsed_body + + +def test_parse_returns_empty_dict_when_no_frontmatter(): + fields, body = frontmatter.parse("just body text\n") + assert fields == {} + assert body == "just body text\n" + + +def test_parse_round_trip_preserves_scalar_fields(): + fields = {"name": "test-entry", "status": "active", "weight": 1.5} + parsed, _ = _round_trip(fields, "body content") + assert parsed["name"] == "test-entry" + assert parsed["status"] == "active" + assert parsed["weight"] == 1.5 + + +def test_inline_array_of_quoted_strings_round_trips(): + fields = {"links": ["slug-one", "slug-two"]} + parsed, _ = _round_trip(fields, "body") + assert parsed["links"] == ["slug-one", "slug-two"] + + +def test_inline_array_with_unquoted_value_containing_apostrophe(): + """An unquoted value with a quote character mid-value must not enter quote mode. + + Before the fix, the apostrophe in 'it's' toggled quote tracking on, + which prevented the comma from splitting the next item correctly. + """ + text = "---\nlinks: [it's, done]\n---\nbody\n" + fields, body = frontmatter.parse(text) + assert fields["links"] == ["it's", "done"] + + +def test_inline_array_with_quoted_value_containing_other_quote_type(): + """A double-quoted value containing a single quote must round-trip.""" + text = '---\nlinks: ["it\'s done", "hello"]\n---\nbody\n' + fields, body = frontmatter.parse(text) + assert fields["links"] == ["it's done", "hello"] + + +def test_inline_array_single_unquoted_value_with_apostrophe(): + text = "---\ntags: [it's]\n---\nbody\n" + fields, body = frontmatter.parse(text) + assert fields["tags"] == ["it's"] + + +def test_empty_inline_array(): + fields = {"links": []} + parsed, _ = _round_trip(fields, "body") + assert parsed["links"] == [] + + +def test_render_wraps_strings_needing_quoting(): + rendered = frontmatter._render_scalar("hello: world") + assert rendered == '"hello: world"' + + +def test_render_scalar_boolean_and_null(): + assert frontmatter._render_scalar(True) == "true" + assert frontmatter._render_scalar(False) == "false" + assert frontmatter._render_scalar(None) == "null" + + +def test_split_document_with_no_closing_delimiter_returns_none_header(): + header, body = frontmatter.split_document("---\nname: test\nno closing marker\n") + assert header is None