Skip to content

fix(genbi): harden non-dict apps.yml entries in list/get/register - #2587

Open
Bartok9 wants to merge 5 commits into
Canner:mainfrom
Bartok9:fix/genbi-list-nonduct-entries
Open

fix(genbi): harden non-dict apps.yml entries in list/get/register#2587
Bartok9 wants to merge 5 commits into
Canner:mainfrom
Bartok9:fix/genbi-list-nonduct-entries

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Single PR for hand-edited apps.yml non-dict entries (folds #2583 list/index hardening):

Path Behavior
list prints invalid entry on stderr, continues; non-dict deploy via deploy_state
get_app non-dict → None (CLI "not registered")
register_app replaces truthy non-dict instead of TypeError on item assignment
update_app KeyError when missing/non-dict
deploy link=deploy_state(entry) or None so providers never .get on a scalar

Reproduction (base / before)

apps: {bad: "not-a-map"}
register_app → TypeError: 'str' object does not support item assignment
list with deploy: "https://x" on a valid entry → AttributeError: 'str' object has no attribute 'get'
deploy with deploy: "https://x" → same AttributeError inside provider

Verification

cd core/wren && python -m pytest tests/unit/test_genbi_index.py tests/unit/test_genbi_list_nondict.py -q

Summary by CodeRabbit

  • Bug Fixes

    • Improved app listing reliability when deployment records are incomplete or malformed.
    • Invalid app entries are now reported safely instead of causing failures.
    • Deployment links are handled safely when sharing existing deployment information.
    • Invalid app records can be replaced with valid registrations, while unsupported updates are rejected clearly.
    • Deployment metadata is normalized to ensure deployments complete successfully.
  • Tests

    • Added coverage for malformed app entries, deployment data, and deployment regressions.

@github-actions github-actions Bot added python Pull requests that update Python code core labels Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The GenBI index validates app entries and normalizes deployment state. CLI listing skips malformed apps and safely reads deployment URLs. Provider deployment receives normalized existing deployment data. Tests cover malformed app and deployment entries.

Changes

GenBI malformed data handling

Layer / File(s) Summary
Index entry validation and deployment-state normalization
core/wren/src/wren/genbi/index.py, core/wren/tests/unit/test_genbi_index.py
Index operations handle non-mapping app entries. deploy_state returns only mapping-valued deployment data. Tests cover lookup, registration, updates, and normalization.
CLI handling and provider deployment integration
core/wren/src/wren/genbi/cli.py, core/wren/tests/unit/test_genbi_list_nondict.py, core/wren/tests/unit/test_genbi_deploy.py
genbi list reports and skips malformed app entries. It omits malformed deployment URLs. Provider deployment uses normalized deployment state and persists a dictionary deployment record. Tests cover listing and deployment with malformed values.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GenBI CLI
  participant GenBI index
  participant Deployment provider
  GenBI CLI->>GenBI index: Read app and deploy_state
  GenBI index-->>GenBI CLI: Return normalized deployment mapping
  GenBI CLI->>Deployment provider: Pass existing deployment linkage
  Deployment provider-->>GenBI CLI: Return deployment record
  GenBI CLI->>GenBI index: Persist deploy_record
Loading

Poem

I’m a rabbit with a tidy nest,
Bad mappings now pass the test.
Apps skip cracks, URLs stay clear,
Deploy state hops from far to near.
GenBI runs without a fright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: hardening GenBI handling of non-dictionary apps.yml entries.
Description check ✅ Passed The description covers the summary, observed failures, and test command, but it omits the duplicate-check section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/wren/tests/unit/test_genbi_list_nonduct.py (1)

13-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover malformed deploy values.

This test verifies invalid app entries but not the new non-dictionary deploy handling in core/wren/src/wren/genbi/cli.py lines 210-214. Add a valid app whose deploy value is a scalar and assert listing succeeds without a URL suffix.

Proposed test extension
             "apps": {
                 "good": {"data_mode": "snapshot", "status": "ready"},
                 "bad": "not-a-mapping",
+                "bad-deploy": {
+                    "data_mode": "snapshot",
+                    "status": "ready",
+                    "deploy": "not-a-mapping",
+                },
             },
...
     assert "good" in result.stdout
+    assert "bad-deploy  [snapshot, ready]" in result.stdout
     assert "invalid entry" in result.stdout
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/tests/unit/test_genbi_list_nonduct.py` around lines 13 - 27, Extend
the test around save_index and the genbi_app list invocation with a valid app
whose deploy field is a scalar rather than a mapping. Keep the successful
listing assertions, and verify that this app is listed without a URL suffix
while the existing malformed app still produces the invalid-entry output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/wren/tests/unit/test_genbi_list_nonduct.py`:
- Around line 13-27: Extend the test around save_index and the genbi_app list
invocation with a valid app whose deploy field is a scalar rather than a
mapping. Keep the successful listing assertions, and verify that this app is
listed without a URL suffix while the existing malformed app still produces the
invalid-entry output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c0ae7cda-7e1c-4dd0-8e53-a4f05b649f76

📥 Commits

Reviewing files that changed from the base of the PR and between d472877 and 9921cdd.

📒 Files selected for processing (2)
  • core/wren/src/wren/genbi/cli.py
  • core/wren/tests/unit/test_genbi_list_nonduct.py

- list: report invalid entries; treat non-dict deploy as empty
- get_app: non-dict → None (not registered)
- register_app: replace truthy non-dict instead of TypeError
- update_app: KeyError on missing/non-dict

Addresses goldmedal review on Canner#2583/Canner#2587 (single PR).
@Bartok9 Bartok9 changed the title fix(genbi): tolerate non-dict apps entries in list fix(genbi): harden non-dict apps.yml entries in list/get/register Aug 7, 2026
@Bartok9
Bartok9 force-pushed the fix/genbi-list-nonduct-entries branch from 9921cdd to 0fc97c3 Compare August 7, 2026 04:15
@Bartok9

Bartok9 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@goldmedal folded the #2583 + #2587 work into this single PR and rebased on current main.

  • list covers non-dict entries and non-dict deploy blocks (the crash you reproduced)
  • register_app no longer TypeErrors on truthy non-dicts
  • get_app treats non-dict as unregistered
  • update_app raises KeyError for missing/non-dict (callers still go through _require_registered first)

Closing #2583 as the duplicate against the same files.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/wren/tests/unit/test_genbi_list_nonduct.py`:
- Around line 44-46: Update the test around the genbi_app list invocation to
assert that the invalid non-dictionary deploy value, including “https://x”, is
absent from result.stdout while preserving the existing successful exit-code and
app-name assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 471b8ba2-d888-4d21-b972-8bbec0268150

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdae39 and 0fc97c3.

📒 Files selected for processing (4)
  • core/wren/src/wren/genbi/cli.py
  • core/wren/src/wren/genbi/index.py
  • core/wren/tests/unit/test_genbi_index.py
  • core/wren/tests/unit/test_genbi_list_nonduct.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/wren/src/wren/genbi/cli.py

Comment thread core/wren/tests/unit/test_genbi_list_nondict.py
CodeRabbit: non-dict deploy must not render last_url.

@goldmedal goldmedal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch on the or fall-through in register_app — the truthy-non-dict case is genuinely easy to miss, and folding #2583 in here was the right call. The entry-level hardening looks complete to me. One gap on a nested field, plus some minors.

🔴 Blocking — the deploy command still AttributeErrors on a non-dict deploy block

test_genbi_list_nonduct.py constructs {"deploy": "https://x"} as the realistic malformed shape, and list now tolerates it. But cli.py also consumes that field, unguarded:

# core/wren/src/wren/genbi/cli.py:358
deployment = adapter.deploy(..., link=entry.get("deploy"))

Both providers declare link: dict | None and immediately call .get() on it:

  • providers/vercel.py:86if link and link.get("org_id")
  • providers/cloudflare.py:62(link or {}).get("account_id")

E2E against this branch (register an app → hand-edit deploy to a scalar → run both commands):

LIST   exit: 0     ← fixed by this PR
DEPLOY exit: 1     AttributeError("'str' object has no attribute 'get'")

Same exception class the PR description cites as the bug. It's a worse failure on this path than it was on list: it fires after token resolution, and it surfaces as an uncaught traceback rather than a DeployError (the only exception the deploy command catches). In Vercel's case it's also before _request, so it's a deterministic local crash, not a network edge case.

🟠 Suggestion — the nested-field guard wants to be shared, not inlined

Worth separating the two levels, because they're in different shape:

  • Entry-level (apps.<name> itself is not a mapping) — centralized and complete. All three consumers in cli.py reach entries through _require_registeredget_app, and register_app covers the create path. Nothing to add.
  • Nested-field level (entry["deploy"] is not a mapping) — ad-hoc. Guarded inline in list_apps, missing in deploy. That's the blocking item above.

The exposure here is bounded, which makes this cheap to close properly: deploy is the only entry field consumed as a mapping. data_mode and status are read via .get() with defaults and only string-compared or formatted, so a stray mapping there is ugly output rather than a crash; entry['data_mode']/entry['status'] at line 192 read a dict register_app just built; source isn't read in cli.py at all.

So one small accessor next to get_app in index.py covers it, and both call sites share it:

def deploy_state(entry: dict) -> dict:
    """The entry's deploy block, or {} when hand-edited to a non-mapping."""
    d = entry.get("deploy")
    return d if isinstance(d, dict) else {}
  • list_apps: last_url = deploy_state(entry).get("last_url") — also retires the now-redundant or {} on line 207, which the following isinstance check makes dead.
  • deploy: link=deploy_state(entry) or None — keeps the providers' declared dict | None contract intact.

To be explicit about a road not taken: I don't think this should move into load_index the way _load_views_v1/_load_relationships filter non-mappings. Those loaders return a fresh list that's never written back, so filtering is free. index.py is read-modify-write — register_app/remove_app/update_app all save_index(index) the whole dict — so filtering at load would mean an unrelated wren genbi register otherapp silently deletes a malformed bad entry from disk, and would break remove_app, which today can still delete a non-dict entry and is the only way a user has to clean one up. Per-call-site is the right call for this file. Just worth making the one shared helper explicit so the next nested field doesn't repeat this.

🟡 Minor

  • The list test doesn't lock in the behavior it exists to protect. assert "invalid entry" in (result.stdout + result.stderr) still passes if someone flips err=True to err=False. Streams are cleanly separated under the locked click 8.3.1 — I checked: stdout is 'good [snapshot, ready]\n', stderr is 'bad [invalid entry: str]\n' — so this can assert in result.stderr and not in result.stdout.
  • register silently clobbers where list reports. Same malformed entry, two policies: list writes [invalid entry: str] to stderr, register_app replaces it with no notice. The replacement itself is fine — it's an explicit user command naming that app — but a one-line stderr note ("replacing malformed entry for bad") would make the two consistent and leave a trace.
  • Contradictory diagnosis between commands. list says bad [invalid entry: str]; verify bad says not registered. Run 'wren genbi register bad' first. The suggested remedy does work, so this is cosmetic — but the message is misleading about why, and update_app's new docstring already gestures at this ("callers that need a friendlier message"). Could be a follow-up.
  • Filename typo: test_genbi_list_nonduct.pynondict. It's also in the branch name (fix/genbi-list-nonduct-entries), which stays visible in the PR compare header and merge metadata even after the branch is deleted — so at minimum worth renaming the file.
  • PR body is self-referential: "folds #2583 + prior #2587 list work" — #2587 is this PR.
  • Branch is a couple of commits behind main; neither touches genbi/, so no conflict — just noting it.

Verification I ran: the PR's own suite (36 passed), ruff check src clean, line-level and end-to-end repro of the deploy crash, and the stdout/stderr separation check above. ruff check tests reports 45 pre-existing errors including function-level imports throughout the suite, and CI only lints src — so the import pytest inside test_update_app_rejects_non_dict_entry matches existing convention and isn't a finding.

goldmedal review on Canner#2587: list already tolerated scalar deploy, but
deploy still passed entry.get("deploy") into providers that call .get().

- add index.deploy_state(entry) -> dict
- list_apps and deploy both use it (link=deploy_state(entry) or None)
- rename test_genbi_list_nonduct.py -> nondict; lock stderr-only invalid entry
@Bartok9

Bartok9 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@goldmedal thanks — great catch on the deploy path. Addressed:

  • Added deploy_state(entry) next to get_app in index.py
  • list_apps uses deploy_state(entry).get("last_url") (dropped the redundant or {} + isinstance inline)
  • deploy passes link=deploy_state(entry) or None so providers keep dict | None
  • Renamed test_genbi_list_nonduct.pytest_genbi_list_nondict.py
  • List invalid-entry assertion is now stderr-only / not-in-stdout
  • Unit coverage for deploy_state normalize

Left as follow-up (cosmetic): register stderr notice on replace, and friendlier verify message when entry exists but is non-dict. PR body self-ref cleaned up.

@goldmedal goldmedal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix looks right and I verified it end-to-end — thanks for the quick turnaround. Two new things introduced by this commit, one of which I'd want fixed before merge.

✅ Resolved

  • Deploy path — confirmed against b2eb996a: register an app, hand-edit deploy to a scalar, run wren genbi deploy. No more AttributeError; it now fails cleanly through the normal error path instead of an uncaught traceback.
  • or {} + inline isinstance retired from list_apps; link=deploy_state(entry) or None preserves the providers' dict | None contract (behaviour is identical for the missing-key and valid-dict cases, and an empty {} now normalizes to None, which both providers already treat the same).
  • File renamed to test_genbi_list_nondict.py; the stderr assertion is now precise; PR body self-reference cleaned up. CI 11/11 green.

🟠 The new test doesn't actually guard the fix

test_deploy_state_normalizes_non_dict tests the helper in isolation, but nothing asserts that the deploy command routes through it. I mutation-checked this — reverting just line 361 back to the original bug:

-            link=deploy_state(entry) or None,
+            link=entry.get("deploy"),

...and the whole suite still passes, 37/37. So the regression this PR exists to fix is currently unprotected: any future refactor can reintroduce it silently.

Worth a command-level test in test_genbi_deploy.py, which already has the _make_deployable_project fixture and the _isolate_env_loading autouse fixture to make it hermetic. Roughly: build the project, hand-edit .wren/apps.yml so deploy is a scalar, invoke genbi deploy, and assert not isinstance(result.exception, AttributeError). That fails on the mutated line and passes on the fix.

🟠 deploy_state is shadowed by a local dict inside deploy()

Both bindings live in the same function scope:

def deploy(...):
    from wren.genbi.index import deploy_state, update_app   # line 317 — binds the function
    ...
    link=deploy_state(entry) or None,                       # line 361 — calls it
    ...
    deploy_state = {                                        # line 367 — rebinds to a dict
        "provider": adapter.name, ...
    }
    update_app(project_path, name, status="deployed", deploy=deploy_state)

This works today purely because the call at 361 precedes the rebind at 367. It's a trap for the next person: reorder those blocks, or add a second deploy_state(...) call anywhere below 367, and you get TypeError: 'dict' object is not callable. Ruff doesn't catch it either — I ran ruff check src on this head and it's clean, because the import is used before the rebind, so F811 doesn't fire.

The local dict predates the helper, so renaming the local is the smaller change — deploy_record, or new_state. Either way the two shouldn't share a name in one scope.

🟡 Minor

  • test_deploy_state_normalizes_non_dict lives in test_genbi_list_nondict.py, but it exercises index.deploy_state, not list. test_genbi_index.py is the natural home, next to the other index-level tests.

Verification: ruff check src clean, genbi suite 37 passed, E2E confirmation that the deploy crash is gone, and the mutation check above.

goldmedal review on Canner#2587:
- rename local deploy_state dict to deploy_record in deploy()
- add test_deploy_tolerates_scalar_deploy_block (guards link= path)
- move deploy_state unit test into test_genbi_index.py
@Bartok9

Bartok9 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@goldmedal thanks — both items addressed on this head:

  1. Command-level guardtest_deploy_tolerates_scalar_deploy_block in test_genbi_deploy.py: register → hand-edit deploy to a scalar → genbi deploy with fake Vercel transport. Asserts not isinstance(result.exception, AttributeError) and successful exit; also checks the persisted deploy block is a dict again. Mutation-style intent matches what you described.

  2. Shadowing — local dict renamed to deploy_record before update_app(..., deploy=deploy_record).

  3. Minortest_deploy_state_normalizes_non_dict moved to test_genbi_index.py.

Local: genbi index/list/deploy unit suite 38 passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/wren/tests/unit/test_genbi_deploy.py (1)

119-121: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the normalized provider argument directly.

The test claims that the provider receives link=None, but _FakeTransport records only the HTTP request. assert fake.calls does not verify the link argument. Spy on the provider adapter or patch get_provider, then assert link is None for the scalar deploy entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/tests/unit/test_genbi_deploy.py` around lines 119 - 121, Update the
test around the scalar deploy entry and provider invocation so it spies on or
patches get_provider (or the provider adapter) and captures the link argument.
Assert directly that the provider receives link=None, while retaining the
existing fake.calls assertion only if it still verifies request execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/wren/tests/unit/test_genbi_deploy.py`:
- Around line 119-121: Update the test around the scalar deploy entry and
provider invocation so it spies on or patches get_provider (or the provider
adapter) and captures the link argument. Assert directly that the provider
receives link=None, while retaining the existing fake.calls assertion only if it
still verifies request execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 852cfc07-57e4-4bb6-b6bd-75d6d211e166

📥 Commits

Reviewing files that changed from the base of the PR and between b2eb996 and da27480.

📒 Files selected for processing (4)
  • core/wren/src/wren/genbi/cli.py
  • core/wren/tests/unit/test_genbi_deploy.py
  • core/wren/tests/unit/test_genbi_index.py
  • core/wren/tests/unit/test_genbi_list_nondict.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/wren/tests/unit/test_genbi_list_nondict.py
  • core/wren/tests/unit/test_genbi_index.py
  • core/wren/src/wren/genbi/cli.py

Spy get_provider so the regression test captures the provider link arg
directly (CodeRabbit nit on Canner#2587).
@Bartok9

Bartok9 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai good catch — tightened test_deploy_tolerates_scalar_deploy_block to spy get_provider and assert link is None for a scalar deploy entry (not only HTTP success / no AttributeError).

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Bartok9 Confirmed. The test now verifies the required provider contract: a scalar deploy entry produces link is None.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants