Skip to content

[console][nav] filterNav never drops a group DECLARED with children: [] — Setup renders an inert 「Approvals」 group on a runtime without plugin-approvals #7380

Closed

Description

@baozhoutao

Found while browser-verifying the #7213 epic's converged approvals surface (the epic's close-out lists browser verification as its open gap). Not a regression from that epic — the defect predates it — but it is on the same nav face, so recording it here.

The finding

filterNav (packages/rest/src/rest-server.ts:2724) is documented to "Drop empty groups so the sidebar doesn't render a label with nothing under it". It does not do that for a group declared with children: [] — only for a group whose children were non-empty and were then filtered away:

if (Array.isArray(e.children) && e.children.length > 0) {   // ← gate
    const kids = filterNav(e.children);
    if (e.type === 'group' && kids.length === 0) continue;  // ← the drop rule
    out.push({ ...e, children: kids });
} else {
    out.push(e);                                            // ← declared-empty group lands here
}

A group that starts out empty never enters the branch that owns the drop rule, so it is pushed through untouched. That is precisely the shape every contribution slot in the setup app uses — packages/platform-objects/src/apps/setup.app.ts declares group_approvals (and its siblings) with children: [] and lets plugins fill them via navigationContributions. When the contributing plugin is absent, the slot survives filtering as an empty group.

Reproduction (measured)

Two runtimes, same build (objectstack main 88154bee1, vendored console rebuilt at .objectui-sha = 8aad9fd50b16), both objectstack dev --ui --seed-admin on a fresh DB, signed in as the dev admin:

  • examples/app-showcase — requires: [… 'approvals' …], so ApprovalsServicePlugin loads and contributes four items. GET /api/v1/meta/app → group_approvals has its four children; the sidebar renders 审批 with 审批中心 / 审批申请 / 审批历史 / 审批委派(外出). Correct.
  • examples/app-crm — requires: ['ui', 'automation'], no approvals plugin. GET /api/v1/meta/app returns:
  - group_approvals [group] "Approvals"      ← no children key content, nothing under it

and the Setup sidebar renders an 审批 group with a chevron that does not expand and reveals nothing on click — an inert label.

Worth noting the same run confirms the item-level gate works exactly as intended one app over: the account app's nav_account_approvals (requiresService: 'approvals') is correctly absent from the same response, so no dead component route is served. It is only the group that leaks.

Scope

Every children: [] contribution slot has this exposure, not just approvals — approvals is simply the one where the contributing plugin is genuinely optional, so it is the one that shows up on a stock example. A runtime that drops any other optional contributor would surface the same empty label.

Suggested shape (not prescriptive)

Move the drop rule out from under the non-empty gate, so a type: 'group' with no surviving children is dropped whether it became empty or started empty. The one thing worth deciding before writing it: whether a childless group is ever legitimately a navigation target on its own (a clickable group header). If it never is, the rule is unconditional; if it can be, the rule needs to spare those.

An alternative fix lives in the console (AppSidebar skipping empty groups), but ADR-0057 D10 makes the server the authoritative visibility gate and the client's filtering a courtesy, so the server side is the one that should not emit it.

Environment

  • objectstack main @ 88154bee1, @objectstack/*@17.0.0-rc.5, vendored console at objectui 8aad9fd50b16, sqlite driver, single tenancy, zh-CN.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 10, 2026
  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: promoted finding to pm:queue, routed domain:cli.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 10, 2026
  4. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim — PM loop domain:cli, session session_0158ZQo7LiHSxGWpYKuPq1wu (os-help seat, #6024), wave 2 round 6.


    Generated by Claude Code

  5. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7460, head e99824010.

    Reviewed on the diff and commit message rather than a report comment — the dev's container had git push but no GitHub API access, so it could not post one.

    • Scope: packages/rest/src/rest-server.ts (filterNav region), rest.test.ts, changeset. No response-envelope site touched, so finding: no gate sees rest-server.ts's response envelopes — check:route-envelope does not scan the file, and neither gate has a rule for envelope POSITION #7295's stringError/siblingCode baselines (dispatched the same wave, same file) are unaffected — the mutual-exclusion constraint in both claims held.
    • The open sub-question was answered by measurement, not assumption, which is what the card asked for: GroupNavItemSchema is a strictObject that declares no target key and rejects any, and of the 16 childless type: 'group' entries across shipped apps and examples, zero carry a target. Hence the drop is unconditional rather than conditional.
    • The rule moved to the right predicate: judged on what survives, so a group is dropped whether it became empty or started empty; nesting composes; a group with no children key at all (reachable at runtime because this filter reads untyped store documents) drops too. Non-group entries are untouched.
    • The area-level consequence is stated rather than discovered later: an area holding only childless groups now empties and hits the existing area rule, while an area authored navigation: [] still passes — documented at filterAreas.
    • CI, conclusions read personally: ESLint success, TypeScript Type Check success, Check Changeset success, all Test Core and Dogfood shards success.

    Ready flipped, auto-merge armed (notes-21 empty-field receipt, one disable→enable flip), queue branch pr-7460-… observed.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

approvalsbugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions