From 35143b545daa712019fdc396cfb0c34e75aa6f47 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Sat, 25 Jul 2026 02:07:03 -0400 Subject: [PATCH 1/3] fix(genbi): treat non-dict apps.yml entries as missing --- core/wren/src/wren/genbi/index.py | 11 +++++++++-- core/wren/tests/unit/test_genbi_get_app_guard.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 core/wren/tests/unit/test_genbi_get_app_guard.py diff --git a/core/wren/src/wren/genbi/index.py b/core/wren/src/wren/genbi/index.py index 01b6a6fba9..93a56ecb00 100644 --- a/core/wren/src/wren/genbi/index.py +++ b/core/wren/src/wren/genbi/index.py @@ -103,13 +103,20 @@ def remove_app(project_path: Path, name: str) -> bool: def get_app(project_path: Path, name: str) -> dict | None: """Return the entry for ``name`` or None if not registered.""" - return load_index(project_path)["apps"].get(name) + entry = load_index(project_path)["apps"].get(name) + # Hand-edited apps.yml may put a scalar/list under an app key; treat as + # unregistered rather than letting callers AttributeError on .get keys. + if entry is not None and not isinstance(entry, dict): + return None + return entry def update_app(project_path: Path, name: str, **fields) -> dict: """Merge ``fields`` into the entry for ``name`` and persist.""" index = load_index(project_path) - entry = index["apps"][name] + entry = index["apps"].get(name) + if not isinstance(entry, dict): + raise KeyError(name) entry.update(fields) save_index(project_path, index) return entry diff --git a/core/wren/tests/unit/test_genbi_get_app_guard.py b/core/wren/tests/unit/test_genbi_get_app_guard.py new file mode 100644 index 0000000000..7b76af62fc --- /dev/null +++ b/core/wren/tests/unit/test_genbi_get_app_guard.py @@ -0,0 +1,13 @@ +from pathlib import Path + +import yaml + +from wren.genbi.index import get_app, load_index, save_index + + +def test_get_app_skips_non_dict_entry(tmp_path: Path): + idx = {"schema_version": 1, "apps": {"bad": "not-a-map", "good": {"source": "apps/good"}}} + save_index(tmp_path, idx) + assert get_app(tmp_path, "bad") is None + assert get_app(tmp_path, "good") == {"source": "apps/good"} + assert get_app(tmp_path, "missing") is None From 721ba0f5b40e53570f79286d73781499a0b64dc4 Mon Sep 17 00:00:00 2001 From: Bartok9 <259807879+Bartok9@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:17:35 -0400 Subject: [PATCH 2/3] fix(genbi): harden register/list against non-dict apps.yml entries Address goldmedal review on #2583: register_app no longer TypeErrors on truthy non-dict entries, list skips malformed entries, fold get_app guard tests into test_genbi_index and cover update_app contract. --- core/wren/src/wren/genbi/cli.py | 4 + core/wren/src/wren/genbi/index.py | 16 ++-- .../tests/unit/test_genbi_get_app_guard.py | 13 --- core/wren/tests/unit/test_genbi_index.py | 80 +++++++++++++++++++ 4 files changed, 95 insertions(+), 18 deletions(-) delete mode 100644 core/wren/tests/unit/test_genbi_get_app_guard.py diff --git a/core/wren/src/wren/genbi/cli.py b/core/wren/src/wren/genbi/cli.py index 49ff47c02a..95bf8c5a97 100644 --- a/core/wren/src/wren/genbi/cli.py +++ b/core/wren/src/wren/genbi/cli.py @@ -204,6 +204,10 @@ def list_apps(path: ProjectPathOpt = None) -> None: return for name, entry in apps.items(): + # Hand-edited non-dict entries: skip rather than AttributeError on .get + # (same lenient stance as get_app treating them as unregistered). + if not isinstance(entry, dict): + continue deploy = entry.get("deploy") or {} suffix = f" → {deploy['last_url']}" if deploy.get("last_url") else "" typer.echo( diff --git a/core/wren/src/wren/genbi/index.py b/core/wren/src/wren/genbi/index.py index 93a56ecb00..8e24b39a58 100644 --- a/core/wren/src/wren/genbi/index.py +++ b/core/wren/src/wren/genbi/index.py @@ -80,11 +80,17 @@ def save_index(project_path: Path, index: dict) -> None: def register_app(project_path: Path, name: str, *, data_mode: str) -> dict: """Create or update the entry for ``name``. Returns the entry.""" index = load_index(project_path) - entry = index["apps"].get(name) or { - "source": f"apps/{name}", - "status": "scaffolded", - "created_at": date.today().isoformat(), - } + existing = index["apps"].get(name) + # Hand-edited truthy non-dicts (e.g. a string) must not fall through the + # ``or`` default — assignment would raise TypeError. Treat like missing. + if not isinstance(existing, dict): + entry = { + "source": f"apps/{name}", + "status": "scaffolded", + "created_at": date.today().isoformat(), + } + else: + entry = existing entry["data_mode"] = data_mode index["apps"][name] = entry save_index(project_path, index) diff --git a/core/wren/tests/unit/test_genbi_get_app_guard.py b/core/wren/tests/unit/test_genbi_get_app_guard.py deleted file mode 100644 index 7b76af62fc..0000000000 --- a/core/wren/tests/unit/test_genbi_get_app_guard.py +++ /dev/null @@ -1,13 +0,0 @@ -from pathlib import Path - -import yaml - -from wren.genbi.index import get_app, load_index, save_index - - -def test_get_app_skips_non_dict_entry(tmp_path: Path): - idx = {"schema_version": 1, "apps": {"bad": "not-a-map", "good": {"source": "apps/good"}}} - save_index(tmp_path, idx) - assert get_app(tmp_path, "bad") is None - assert get_app(tmp_path, "good") == {"source": "apps/good"} - assert get_app(tmp_path, "missing") is None diff --git a/core/wren/tests/unit/test_genbi_index.py b/core/wren/tests/unit/test_genbi_index.py index 2be768ef29..5b9c9a24eb 100644 --- a/core/wren/tests/unit/test_genbi_index.py +++ b/core/wren/tests/unit/test_genbi_index.py @@ -192,3 +192,83 @@ def test_load_index_preserves_falsy_schema_version(tmp_path: Path) -> None: data = load_index(tmp_path) assert data["schema_version"] == 0 + + +def test_get_app_skips_non_dict_entry(tmp_path: Path) -> None: + from wren.genbi.index import get_app, save_index + + idx = { + "schema_version": 1, + "apps": {"bad": "not-a-map", "good": {"source": "apps/good"}}, + } + save_index(tmp_path, idx) + assert get_app(tmp_path, "bad") is None + assert get_app(tmp_path, "good") == {"source": "apps/good"} + assert get_app(tmp_path, "missing") is None + + +def test_register_app_replaces_non_dict_entry(tmp_path: Path) -> None: + from wren.genbi.index import register_app, save_index + + project = _make_project(tmp_path, with_app="bad") + save_index( + project, + {"schema_version": 1, "apps": {"bad": "not-a-map"}}, + ) + entry = register_app(project, "bad", data_mode="snapshot") + assert isinstance(entry, dict) + assert entry["data_mode"] == "snapshot" + assert entry["source"] == "apps/bad" + assert entry["status"] == "scaffolded" + + +def test_update_app_rejects_missing_and_non_dict(tmp_path: Path) -> None: + from wren.genbi.index import save_index, update_app + + save_index( + tmp_path, + { + "schema_version": 1, + "apps": { + "bad": "not-a-map", + "good": {"source": "apps/good", "status": "scaffolded"}, + }, + }, + ) + with pytest.raises(KeyError): + update_app(tmp_path, "missing", status="built") + with pytest.raises(KeyError): + update_app(tmp_path, "bad", status="built") + updated = update_app(tmp_path, "good", status="built") + assert updated["status"] == "built" + assert updated["source"] == "apps/good" + # Persistence: re-load and confirm + data = load_index(tmp_path) + assert data["apps"]["good"]["status"] == "built" + assert data["apps"]["bad"] == "not-a-map" + + +def test_list_skips_non_dict_entries(tmp_path: Path) -> None: + from wren.genbi.index import save_index + + project = _make_project(tmp_path, with_app="good") + save_index( + project, + { + "schema_version": 1, + "apps": { + "bad": "not-a-map", + "good": { + "source": "apps/good", + "data_mode": "snapshot", + "status": "scaffolded", + }, + }, + }, + ) + result = runner.invoke(app, ["genbi", "list", "-p", str(project)]) + assert result.exit_code == 0, result.output + assert "good" in result.output + assert "snapshot" in result.output + # Must not traceback; bad entry is skipped (lenient listing). + assert "Traceback" not in result.output From d3b17185dd07701b5d57f28c9be076d3524f560d Mon Sep 17 00:00:00 2001 From: Bartok9 <259807879+Bartok9@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:20:38 -0400 Subject: [PATCH 3/3] test(genbi): tighten non-dict entry persistence/list assertions CodeRabbit: reload after register_app repair; assert list omits "bad". --- core/wren/tests/unit/test_genbi_index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/wren/tests/unit/test_genbi_index.py b/core/wren/tests/unit/test_genbi_index.py index 5b9c9a24eb..702b199bc9 100644 --- a/core/wren/tests/unit/test_genbi_index.py +++ b/core/wren/tests/unit/test_genbi_index.py @@ -208,7 +208,7 @@ def test_get_app_skips_non_dict_entry(tmp_path: Path) -> None: def test_register_app_replaces_non_dict_entry(tmp_path: Path) -> None: - from wren.genbi.index import register_app, save_index + from wren.genbi.index import load_index, register_app, save_index project = _make_project(tmp_path, with_app="bad") save_index( @@ -220,6 +220,7 @@ def test_register_app_replaces_non_dict_entry(tmp_path: Path) -> None: assert entry["data_mode"] == "snapshot" assert entry["source"] == "apps/bad" assert entry["status"] == "scaffolded" + assert load_index(project)["apps"]["bad"] == entry def test_update_app_rejects_missing_and_non_dict(tmp_path: Path) -> None: @@ -270,5 +271,6 @@ def test_list_skips_non_dict_entries(tmp_path: Path) -> None: assert result.exit_code == 0, result.output assert "good" in result.output assert "snapshot" in result.output + assert "bad" not in result.output # Must not traceback; bad entry is skipped (lenient listing). assert "Traceback" not in result.output