Skip to content

[finding] No test pins that a record:quick_actions action's resultDialog actually opens — the handler reaches it only via the shared runner #5711

Description

@os-warren

Found while implementing objectstack#10681 (adding a one-shot resultDialog to sys_user.generate_backup_codes, which renders at record:quick_actions { location: 'record_section' }). Filed unassigned; no fix attempted — the framework card's scope was the declaration, and this is a gap in objectui's own coverage.

The observation

resultDialog is honoured centrally in packages/core/src/actions/ActionRunner.ts. On success it suppresses the successMessage toast and calls the registered handler, and it is explicit about what happens when none is registered:

[ActionRunner] action.resultDialog set but no resultDialogHandler registered — the response value will not be shown to the user.

That is a console.warn and nothing else. The action still reports success; the value is simply discarded. For a one-shot reveal (backup codes, a freshly minted client_secret, a temporary password) that silent discard is the whole defect class — objectstack#10681 is exactly it, one layer up.

Why the quick-actions surface is the unpinned one

Counted, not recalled:

  • packages/plugin-detail/src contains zero resultDialog references. That is correct by design — record-quick-actions.tsx never touches the key; it resolves actions through useActionEngine and calls executeAction, and the runner does the rest.
  • useActionEngine reuses the surrounding provider's runner when there is one, and constructs a local one otherwise (packages/react/src/hooks/useActionEngine.ts). Its own comment names the failure mode: "because its local runner has no handlers installed".
  • packages/react/src/hooks/__tests__/useActionEngine.sharedRunner.test.tsx pins runner sharing, but contains no resultDialog reference — it does not pin that a dialog handler registered on the provider is reachable from an engine created by a child component.

So the chain RecordDetailView → ActionProvider onResultDialog → DeclaredActionsBar / record:quick_actions → useActionEngine → shared runner → handler is load-bearing for every one-shot reveal on a record page, and no test covers its last two links. ActionRunner.resultDialog.test.ts pins the runner in isolation, where the handler is registered directly.

Why it is worth pinning rather than trusting

The failure is silent in the direction that matters. If a refactor caused a quick-actions bar to fall back to a local runner, every affected action would keep reporting success, the toast would stay suppressed (the runner suppresses it whenever resultDialog is set, independently of whether a handler exists), and the user would see nothing at all — a strictly worse outcome than before the reveal was declared. The only trace is a console.warn in a browser nobody is reading.

This finding does not claim the wiring is currently broken — reading the source, it is correct today, and the framework-side pin in objectstack#10681 depends on that reading. The claim is that nothing would notice if it stopped being correct.

Suggested shape

A component-level test that mounts a record:quick_actions bar inside an ActionProvider carrying an onResultDialog spy, runs an action whose spec declares a resultDialog, and asserts the spy fired with the response payload — plus the negative leg (no provider ⇒ the documented warn path), so the two are distinguishable. The DeclaredActionsBar tests already establish the mounting pattern at location="record_section".

Worth checking in the same sweep whether record:alert's CTA path (whose header claims "the same pipeline that record:quick_actions uses") shares the gap.

Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 23, 2026
  2. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue, domain:ui, Task. Test-only pin: assert a record:quick_actions action's resultDialog actually opens through the shared runner path — the class of coverage gap where the only reach is indirect. No product change.


    Generated by Claude Code

  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claim — domain:ui execution seat.

    Dispatching this round (R26). Test-only pin, no product change — triage graded it Task on exactly that basis.


    Generated by Claude Code

  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    os-dev-report

    {
      "issue": 5711,
      "status": "done",
      "branch": "claude/issue-5711-quick-actions-resultdialog-pin",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5807",
      "premise_still_valid": true,
      "summary": "Added packages/plugin-detail/src/renderers/__tests__/record-quick-actions.resultDialog.test.tsx: mounts RecordQuickActionsRenderer under an ambient ActionProvider (onResultDialog + onToast wired) and asserts a resultDialog-declared action opens the dialog with the response value while the success toast stays suppressed (positive leg); a second test mounts with no provider and asserts the documented console.warn fires and the value is discarded (negative leg). No product code changed.",
      "tests": "At d44e2054d: `pnpm exec vitest run packages/plugin-detail/src/renderers/__tests__/record-quick-actions.resultDialog.test.tsx --maxWorkers=2` -> Test Files 1 passed (1), Tests 2 passed (2). Full package after building the dependency closure (`pnpm --filter '@object-ui/plugin-detail^...' build`, exit 0): `pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2` -> Test Files 95 passed (95), Tests 870 passed (870). `pnpm --filter @object-ui/plugin-detail type-check` -> exit 0. `pnpm exec eslint packages/plugin-detail/src/renderers/__tests__/record-quick-actions.resultDialog.test.tsx --no-inline-config` -> 0 errors, 2 no-explicit-any warnings (same pattern as sibling record-quick-actions.disabled-declared-gate.test.tsx). node scripts/check-changeset-presence.mjs -> pass (empty-frontmatter changeset declared). node scripts/check-changeset-no-major.mjs -> pass. node scripts/check-control-bytes.mjs -> pass (4828 files scanned). Reverse verification: committed the test, then set `const sharedRunner = null;` in packages/react/src/hooks/useActionEngine.ts (mutation confirmed on disk via grep before rerunning) to simulate a fallback-to-local-runner refactor. Rerun: positive-leg test FAILED (`expected \"vi.fn()\" to be called once, but got 0 times`) with stderr showing the exact documented warning `[ActionRunner] action.resultDialog set but no resultDialogHandler registered — the response value will not be shown to the user. { action: 'generate_backup_codes', data: 'BACKUP-CODE-0000' }`; negative-leg test still passed. Restored via `git checkout -- packages/react/src/hooks/useActionEngine.ts`, confirmed clean via `git status --porcelain`, reran: Test Files 1 passed (1), Tests 2 passed (2).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5808: record:alert's CTA path shares the same unpinned resultDialog-through-shared-runner gap as record:quick_actions"
      ]
    }

    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions