From d79072c46d59c31f68c456765f88cea4964f951d Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:05:25 -0600 Subject: [PATCH 1/8] feat(agent-integrator): convert tools list to object for opencode target OpenCode expects the `tools` frontmatter field in agent files to be an object (`{Read: true, Glob: true}`) rather than a list (`[Read, Glob]`). This adds a new `_write_opencode_agent` method that converts list- or comma-separated-string `tools` to the object format, and wires it into the `integrate_agents_for_target` dispatch for the `opencode_agent` format_id. Previously, the opencode target fell through to `copy_agent` (verbatim copy), which would pass the list-format `tools` through to opencode, causing a schema validation error: SchemaError: Expected object | undefined, got [...] at ["tools"] --- src/apm_cli/integration/agent_integrator.py | 61 +++++++ .../unit/integration/test_agent_integrator.py | 172 ++++++++++++++++++ 2 files changed, 233 insertions(+) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index 55f1b4649..9ea38d0a5 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -170,6 +170,10 @@ def integrate_agents_for_target( links_resolved = self._write_windsurf_agent_skill( source_file, target_path, diagnostics=diagnostics ) + elif mapping.format_id == "opencode_agent": + links_resolved = self._write_opencode_agent( + source_file, target_path, diagnostics=diagnostics + ) else: links_resolved = self.copy_agent(source_file, target_path) total_links_resolved += links_resolved @@ -365,6 +369,63 @@ def _write_windsurf_agent_skill( target.write_text(result, encoding="utf-8") return links_resolved + # ------------------------------------------------------------------ + # OpenCode agent transformer (list tools -> object tools) + # ------------------------------------------------------------------ + + def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> int: + """Transform an ``.agent.md`` file for OpenCode's schema. + + OpenCode expects the ``tools`` frontmatter field to be an object + (``{Read: true, Glob: true}``) rather than a list + (``["Read", "Glob"]``). This method converts list- or + comma-separated-string ``tools`` to the object format while + preserving all other frontmatter and the markdown body. + """ + if source.is_symlink(): + raise ValueError(f"Refusing to read symlink source: {source}") + content = source.read_text(encoding="utf-8") + + fm_match = AgentIntegrator._FRONTMATTER_RE.match(content) + if fm_match: + body = content[fm_match.end() :] + try: + fm = yaml.safe_load(fm_match.group(1)) or {} + except Exception: + fm = {} + else: + body = content + fm = {} + + if not fm_match and not fm: + result, links_resolved = self.resolve_links(content, source, target) + target.write_text(result, encoding="utf-8") + return links_resolved + + tools = fm.get("tools") + if tools is not None and not isinstance(tools, dict): + if isinstance(tools, list): + fm["tools"] = {t: True for t in tools if isinstance(t, str)} + elif isinstance(tools, str): + fm["tools"] = { + t.strip(): True + for t in tools.split(",") + if t.strip() + } + if diagnostics is not None: + diagnostics.info( + f"Converted tools field from {type(tools).__name__} " + f"to object in {source.name}", + ) + + fm_yaml = yaml.safe_dump( + fm, default_flow_style=False, allow_unicode=True + ).rstrip("\n") + result = f"---\n{fm_yaml}\n---\n" + body + result, links_resolved = self.resolve_links(result, source, target) + target.write_text(result, encoding="utf-8") + return links_resolved + # DEPRECATED: use integrate_agents_for_target(KNOWN_TARGETS["copilot"], ...) instead. def integrate_package_agents( self, diff --git a/tests/unit/integration/test_agent_integrator.py b/tests/unit/integration/test_agent_integrator.py index f3592a3bc..06c0763b1 100644 --- a/tests/unit/integration/test_agent_integrator.py +++ b/tests/unit/integration/test_agent_integrator.py @@ -1088,6 +1088,178 @@ def test_sync_integration_opencode_handles_missing_dir(self): assert result["errors"] == 0 +class TestOpenCodeAgentConversion: + """Tests for _write_opencode_agent tools list→object conversion.""" + + def setup_method(self): + self.temp_dir = tempfile.mkdtemp() + self.root = Path(self.temp_dir) + self.integrator = AgentIntegrator() + + def teardown_method(self): + import shutil + + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_converts_tools_list_to_object(self): + """List-format tools becomes an object with true values.""" + source = self.root / "cc-correctness.agent.md" + source.write_text( + "---\n" + "name: cc-correctness\n" + "description: Finds correctness bugs\n" + "tools:\n" + " - Read\n" + " - Glob\n" + " - Grep\n" + "model: sonnet\n" + "---\n\n" + "# Agent body\n" + ) + target = self.root / "cc-correctness.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "tools:" in content + assert " Read: true" in content + assert " Glob: true" in content + assert " Grep: true" in content + assert "name: cc-correctness" in content + assert "description: Finds correctness bugs" in content + assert "model: sonnet" in content + assert "# Agent body" in content + + def test_converts_comma_separated_tools_string(self): + """Comma-separated string tools becomes an object.""" + source = self.root / "agent.agent.md" + source.write_text('---\nname: my-agent\ntools: "Read, Glob, Grep"\n---\n\nBody text\n') + target = self.root / "agent.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "Read: true" in content + assert "Glob: true" in content + assert "Grep: true" in content + + def test_preserves_no_tools_field(self): + """Absent tools field leaves frontmatter unchanged.""" + source = self.root / "simple.agent.md" + source.write_text("---\nname: simple\ndescription: A simple agent\n---\n\n# Simple\n") + target = self.root / "simple.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "tools" not in content + assert "name: simple" in content + assert "# Simple" in content + + def test_leaves_tools_object_unchanged(self): + """Already-object tools is left as-is.""" + source = self.root / "already.agent.md" + source.write_text("---\nname: already\ntools:\n Read: true\n Bash: true\n---\n\n# Body\n") + target = self.root / "already.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "Read: true" in content + assert "Bash: true" in content + assert "tools:" in content + + def test_converts_empty_tools_list_to_empty_object(self): + """Empty list becomes empty object {}.""" + source = self.root / "empty.agent.md" + source.write_text("---\nname: empty\ntools: []\n---\n\n# Body\n") + target = self.root / "empty.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "tools: {}" in content + + def test_no_frontmatter_copies_verbatim(self): + """File without frontmatter is copied verbatim.""" + source = self.root / "plain.agent.md" + source.write_text("# Plain agent\n\nJust instructions.\n") + target = self.root / "plain.md" + + self.integrator._write_opencode_agent(source, target) + + assert target.read_text() == "# Plain agent\n\nJust instructions.\n" + + def test_frontmatter_body_preserved_verbatim(self): + """Markdown body after frontmatter is preserved.""" + source = self.root / "body.agent.md" + body = "\n## Instructions\n\n1. Read files\n2. Grep for patterns\n3. Report\n" + source.write_text(f"---\nname: checker\ntools:\n - Read\n - Grep\n---\n{body}") + target = self.root / "body.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + assert "## Instructions" in content + assert "1. Read files" in content + assert "2. Grep for patterns" in content + assert "3. Report" in content + + def test_rejects_symlink_source(self): + """Symlink source raises ValueError.""" + import os + + real = self.root / "real.agent.md" + real.write_text("---\ntools:\n - Foo\n---\n\nBody\n") + link = self.root / "link.agent.md" + os.symlink(real, link) + target = self.root / "out.md" + + import pytest + + with pytest.raises(ValueError, match="symlink"): + self.integrator._write_opencode_agent(link, target) + + def test_integrate_via_target_dispatch(self): + """End-to-end: opencode target triggers tools conversion.""" + from apm_cli.integration.targets import KNOWN_TARGETS + + (self.root / ".opencode").mkdir() + pkg = self.root / "package" + agents_dir = pkg / ".apm" / "agents" + agents_dir.mkdir(parents=True) + (agents_dir / "security.agent.md").write_text( + "---\nname: security\ntools:\n - Read\n - Grep\n---\n\n# Security Agent\n" + ) + + package = APMPackage(name="test-pkg", version="1.0.0", package_path=pkg) + resolved_ref = ResolvedReference( + original_ref="main", + ref_type=GitReferenceType.BRANCH, + resolved_commit="abc123", + ref_name="main", + ) + package_info = PackageInfo( + package=package, + install_path=pkg, + resolved_reference=resolved_ref, + installed_at="2024-01-01T00:00:00", + ) + + opencode_target = KNOWN_TARGETS["opencode"] + result = self.integrator.integrate_agents_for_target( + opencode_target, package_info, self.root + ) + + assert result.files_integrated == 1 + deployed = self.root / ".opencode" / "agents" / "security.md" + assert deployed.exists() + content = deployed.read_text() + assert "Read: true" in content + assert "Grep: true" in content + assert "# Security Agent" in content + + class TestCodexAgentIntegration: """Tests for Codex TOML agent transformation.""" From 7a86f645d7bb33faf03ab279c2d542787a9ec9d5 Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:12:00 -0600 Subject: [PATCH 2/8] fix: guard against non-dict yaml frontmatter and ensure parent dir exists in opencode agent writer - Normalize yaml.safe_load result: if frontmatter is not a dict (e.g. bare list), treat as empty and emit a diagnostic warning instead of crashing on fm.get('tools'). - Add target.parent.mkdir(parents=True, exist_ok=True) before writing, aligning with _write_windsurf_agent_skill pattern. - Add test for non-dict frontmatter handling. Addresses Copilot review comments on lines 393, 406, and 426. --- src/apm_cli/integration/agent_integrator.py | 20 +++++++++---------- .../unit/integration/test_agent_integrator.py | 14 +++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index 9ea38d0a5..447cbf3c8 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -393,6 +393,12 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> fm = yaml.safe_load(fm_match.group(1)) or {} except Exception: fm = {} + if not isinstance(fm, dict): + if diagnostics is not None: + diagnostics.warn( + f"Non-dict frontmatter in {source.name}, treating as empty", + ) + fm = {} else: body = content fm = {} @@ -407,22 +413,16 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> if isinstance(tools, list): fm["tools"] = {t: True for t in tools if isinstance(t, str)} elif isinstance(tools, str): - fm["tools"] = { - t.strip(): True - for t in tools.split(",") - if t.strip() - } + fm["tools"] = {t.strip(): True for t in tools.split(",") if t.strip()} if diagnostics is not None: diagnostics.info( - f"Converted tools field from {type(tools).__name__} " - f"to object in {source.name}", + f"Converted tools field from {type(tools).__name__} to object in {source.name}", ) - fm_yaml = yaml.safe_dump( - fm, default_flow_style=False, allow_unicode=True - ).rstrip("\n") + fm_yaml = yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True).rstrip("\n") result = f"---\n{fm_yaml}\n---\n" + body result, links_resolved = self.resolve_links(result, source, target) + target.parent.mkdir(parents=True, exist_ok=True) target.write_text(result, encoding="utf-8") return links_resolved diff --git a/tests/unit/integration/test_agent_integrator.py b/tests/unit/integration/test_agent_integrator.py index 06c0763b1..8953dd69f 100644 --- a/tests/unit/integration/test_agent_integrator.py +++ b/tests/unit/integration/test_agent_integrator.py @@ -1220,6 +1220,20 @@ def test_rejects_symlink_source(self): with pytest.raises(ValueError, match="symlink"): self.integrator._write_opencode_agent(link, target) + def test_handles_non_dict_frontmatter(self): + """Non-dict YAML frontmatter (e.g. bare list) is treated as empty.""" + source = self.root / "badfm.agent.md" + # frontmatter that parses as a YAML list, not a mapping + source.write_text("---\n- one\n- two\n---\n\n# Body\n") + target = self.root / "badfm.md" + + self.integrator._write_opencode_agent(source, target) + + content = target.read_text() + # Should not crash; frontmatter becomes empty + assert "---" in content + assert "# Body" in content + def test_integrate_via_target_dispatch(self): """End-to-end: opencode target triggers tools conversion.""" from apm_cli.integration.targets import KNOWN_TARGETS From 3c5fb975d756abbc045596b3d4f1716808543207 Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:12:23 -0600 Subject: [PATCH 3/8] docs: clarify frontmatter preservation limits in opencode agent writer docstring The docstring claimed 'preserving all other frontmatter' but re-serialising via yaml.safe_dump drops YAML comments and can reorder keys. Updated to accurately describe what is preserved (key values) and what is not (comments, ordering). Addresses Copilot review comments on line 424. --- src/apm_cli/integration/agent_integrator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index 447cbf3c8..c122a43a0 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -380,7 +380,10 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> (``{Read: true, Glob: true}``) rather than a list (``["Read", "Glob"]``). This method converts list- or comma-separated-string ``tools`` to the object format while - preserving all other frontmatter and the markdown body. + keeping the markdown body and non-``tools`` frontmatter keys + intact. YAML comments and key ordering in the frontmatter are + not preserved (the frontmatter is re-serialised after the + ``tools`` conversion). """ if source.is_symlink(): raise ValueError(f"Refusing to read symlink source: {source}") From 1996a9a92f3be728017117fc104a2d0303a053e8 Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:20:38 -0600 Subject: [PATCH 4/8] fix: address copilot review on diagnostic, whitespace, and parent mkdir consistency - Move conversion diagnostic inside list/str branches so it only fires when actual conversion occurs (not for e.g. int/float tools) - Strip whitespace from list-form tools entries, aligning with string-form behavior - Add target.parent.mkdir() to the early-return (no frontmatter) path Addresses Copilot review comments 1, 2, 3. --- src/apm_cli/integration/agent_integrator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index c122a43a0..ed605a073 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -408,19 +408,24 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> if not fm_match and not fm: result, links_resolved = self.resolve_links(content, source, target) + target.parent.mkdir(parents=True, exist_ok=True) target.write_text(result, encoding="utf-8") return links_resolved tools = fm.get("tools") if tools is not None and not isinstance(tools, dict): if isinstance(tools, list): - fm["tools"] = {t: True for t in tools if isinstance(t, str)} + fm["tools"] = {t.strip(): True for t in tools if isinstance(t, str) and t.strip()} + if diagnostics is not None: + diagnostics.info( + f"Converted tools field from list to object in {source.name}", + ) elif isinstance(tools, str): fm["tools"] = {t.strip(): True for t in tools.split(",") if t.strip()} - if diagnostics is not None: - diagnostics.info( - f"Converted tools field from {type(tools).__name__} to object in {source.name}", - ) + if diagnostics is not None: + diagnostics.info( + f"Converted tools field from string to object in {source.name}", + ) fm_yaml = yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True).rstrip("\n") result = f"---\n{fm_yaml}\n---\n" + body From c3b62bc3c40f37f489778c0137d495ef2df9c5a0 Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:20:42 -0600 Subject: [PATCH 5/8] fix: strengthen test assertions per copilot review - Change 'tools' not-in-content assertion to 'tools:' to avoid false negatives from the word appearing in body/frontmatter - Add whitespace-padded list entry to exercise strip() normalization Addresses Copilot review comment 4. --- tests/unit/integration/test_agent_integrator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/integration/test_agent_integrator.py b/tests/unit/integration/test_agent_integrator.py index 8953dd69f..1c997d148 100644 --- a/tests/unit/integration/test_agent_integrator.py +++ b/tests/unit/integration/test_agent_integrator.py @@ -1112,6 +1112,7 @@ def test_converts_tools_list_to_object(self): " - Read\n" " - Glob\n" " - Grep\n" + " - Bash \n" "model: sonnet\n" "---\n\n" "# Agent body\n" @@ -1125,6 +1126,7 @@ def test_converts_tools_list_to_object(self): assert " Read: true" in content assert " Glob: true" in content assert " Grep: true" in content + assert " Bash: true" in content assert "name: cc-correctness" in content assert "description: Finds correctness bugs" in content assert "model: sonnet" in content @@ -1152,7 +1154,7 @@ def test_preserves_no_tools_field(self): self.integrator._write_opencode_agent(source, target) content = target.read_text() - assert "tools" not in content + assert "tools:" not in content assert "name: simple" in content assert "# Simple" in content From 786d2026bb93934480a81663b325c4b62773683e Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:27:56 -0600 Subject: [PATCH 6/8] fix: add YAML parse diagnostic, avoid empty-fm re-serialisation, remove early-return branch - Emit diagnostic warn when YAML parsing fails (comment 1) - Use conversion_occurred flag: when no tools conversion is needed, pass through original content verbatim instead of re-serialising (fixes empty-fm {} output, comment 2) - Drop the early-return branch that duplicated copy_agent; all paths now share a single resolve_links + write tail (comment 3) - Update docstring to reflect pass-through preservation when no conversion occurs --- src/apm_cli/integration/agent_integrator.py | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index ed605a073..a91ad2b7a 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -395,40 +395,45 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> try: fm = yaml.safe_load(fm_match.group(1)) or {} except Exception: + if diagnostics is not None: + diagnostics.warn( + f"Failed to parse YAML frontmatter in {source.name}, preserving original", + ) fm = {} if not isinstance(fm, dict): if diagnostics is not None: diagnostics.warn( - f"Non-dict frontmatter in {source.name}, treating as empty", + f"Non-dict frontmatter in {source.name}, preserving original", ) fm = {} else: body = content fm = {} - if not fm_match and not fm: - result, links_resolved = self.resolve_links(content, source, target) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(result, encoding="utf-8") - return links_resolved - tools = fm.get("tools") + conversion_occurred = False if tools is not None and not isinstance(tools, dict): if isinstance(tools, list): fm["tools"] = {t.strip(): True for t in tools if isinstance(t, str) and t.strip()} + conversion_occurred = True if diagnostics is not None: diagnostics.info( f"Converted tools field from list to object in {source.name}", ) elif isinstance(tools, str): fm["tools"] = {t.strip(): True for t in tools.split(",") if t.strip()} + conversion_occurred = True if diagnostics is not None: diagnostics.info( f"Converted tools field from string to object in {source.name}", ) - fm_yaml = yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True).rstrip("\n") - result = f"---\n{fm_yaml}\n---\n" + body + if conversion_occurred: + fm_yaml = yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True).rstrip("\n") + result = f"---\n{fm_yaml}\n---\n" + body + else: + result = content + result, links_resolved = self.resolve_links(result, source, target) target.parent.mkdir(parents=True, exist_ok=True) target.write_text(result, encoding="utf-8") From 87a93e11535565746eeff47b0d702cf236af0afa Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:28:00 -0600 Subject: [PATCH 7/8] fix: make symlink test portable across platforms - Wrap os.symlink() in try/except (OSError, NotImplementedError) with pytest.skip, matching established project pattern - Move import pytest to top of function so skip guard is reachable Addresses Copilot review comment 4. --- tests/unit/integration/test_agent_integrator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/integration/test_agent_integrator.py b/tests/unit/integration/test_agent_integrator.py index 1c997d148..10be5336d 100644 --- a/tests/unit/integration/test_agent_integrator.py +++ b/tests/unit/integration/test_agent_integrator.py @@ -1211,14 +1211,17 @@ def test_rejects_symlink_source(self): """Symlink source raises ValueError.""" import os + import pytest + real = self.root / "real.agent.md" real.write_text("---\ntools:\n - Foo\n---\n\nBody\n") link = self.root / "link.agent.md" - os.symlink(real, link) + try: + os.symlink(real, link) + except (OSError, NotImplementedError): + pytest.skip("symlink creation not supported on this platform") target = self.root / "out.md" - import pytest - with pytest.raises(ValueError, match="symlink"): self.integrator._write_opencode_agent(link, target) From 858074dbf74ad7ad5d8e3ddf113d180834b2db51 Mon Sep 17 00:00:00 2001 From: Camilo Salazar Date: Mon, 25 May 2026 22:33:47 -0600 Subject: [PATCH 8/8] fix(agent-integrator): narrow YAML exception and align test with non-dict frontmatter preservation - Catch yaml.YAMLError instead of broad Exception - Update test_handles_non_dict_frontmatter docstring from "treated as empty" to "preserved as-is" - Verify original list items survive serialisation --- src/apm_cli/integration/agent_integrator.py | 2 +- tests/unit/integration/test_agent_integrator.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/apm_cli/integration/agent_integrator.py b/src/apm_cli/integration/agent_integrator.py index a91ad2b7a..5b5b67595 100644 --- a/src/apm_cli/integration/agent_integrator.py +++ b/src/apm_cli/integration/agent_integrator.py @@ -394,7 +394,7 @@ def _write_opencode_agent(self, source: Path, target: Path, diagnostics=None) -> body = content[fm_match.end() :] try: fm = yaml.safe_load(fm_match.group(1)) or {} - except Exception: + except yaml.YAMLError: if diagnostics is not None: diagnostics.warn( f"Failed to parse YAML frontmatter in {source.name}, preserving original", diff --git a/tests/unit/integration/test_agent_integrator.py b/tests/unit/integration/test_agent_integrator.py index 10be5336d..2ff47e0ae 100644 --- a/tests/unit/integration/test_agent_integrator.py +++ b/tests/unit/integration/test_agent_integrator.py @@ -1226,7 +1226,7 @@ def test_rejects_symlink_source(self): self.integrator._write_opencode_agent(link, target) def test_handles_non_dict_frontmatter(self): - """Non-dict YAML frontmatter (e.g. bare list) is treated as empty.""" + """Non-dict YAML frontmatter (e.g. bare list) is preserved as-is.""" source = self.root / "badfm.agent.md" # frontmatter that parses as a YAML list, not a mapping source.write_text("---\n- one\n- two\n---\n\n# Body\n") @@ -1235,9 +1235,11 @@ def test_handles_non_dict_frontmatter(self): self.integrator._write_opencode_agent(source, target) content = target.read_text() - # Should not crash; frontmatter becomes empty + # Should not crash; non-dict frontmatter is preserved as-is assert "---" in content assert "# Body" in content + assert "- one" in content + assert "- two" in content def test_integrate_via_target_dispatch(self): """End-to-end: opencode target triggers tools conversion."""