diff --git a/core/wren/src/wren/context.py b/core/wren/src/wren/context.py index 7ad8a2917c..d57dc28032 100644 --- a/core/wren/src/wren/context.py +++ b/core/wren/src/wren/context.py @@ -696,7 +696,14 @@ def load_relationships(project_path: Path) -> list[dict]: if not rel_file.exists(): return [] data = yaml.safe_load(rel_file.read_text(encoding="utf-8")) or {} - return data.get("relationships", []) if isinstance(data, dict) else [] + if not isinstance(data, dict): + return [] + raw = data.get("relationships", []) + if not isinstance(raw, list): + return [] + # Annotation is list[dict]; drop non-objects so every consumer trusts the contract + # (validate_project / build_manifest / MCP) without re-filtering at each callsite. + return [item for item in raw if isinstance(item, dict)] def load_instructions(project_path: Path) -> str | None: diff --git a/core/wren/tests/unit/test_context.py b/core/wren/tests/unit/test_context.py index 3bef80df56..2bf24873c7 100644 --- a/core/wren/tests/unit/test_context.py +++ b/core/wren/tests/unit/test_context.py @@ -285,6 +285,30 @@ def test_load_relationships(tmp_path): # ── load_instructions ───────────────────────────────────────────────────── +def test_load_relationships_skips_non_dict_items(tmp_path): + """YAML list may contain scalars; loader must honor list[dict].""" + _make_v2_project(tmp_path) + (tmp_path / "relationships.yml").write_text( + "relationships:\n" + " - not-a-mapping\n" + " - name: orders_customers\n" + " models: [orders, customers]\n" + " join_type: MANY_TO_ONE\n" + " condition: orders.customer_id = customers.customer_id\n" + " - 42\n" + ) + rels = load_relationships(tmp_path) + assert len(rels) == 1 + assert rels[0]["name"] == "orders_customers" + + +def test_load_relationships_non_list_relationships_key(tmp_path): + _make_v2_project(tmp_path) + (tmp_path / "relationships.yml").write_text("relationships: not-a-list\n") + assert load_relationships(tmp_path) == [] + + + def test_load_instructions(tmp_path): _make_v2_project(tmp_path) (tmp_path / "instructions.md").write_text("## Rule 1\nAlways use snake_case.\n")