Skip to content

feat(cards): action card family for scenes, scripts and buttons - #252

Merged
fx merged 6 commits into
mainfrom
feat/0027-action-cards
Jul 28, 2026
Merged

feat(cards): action card family for scenes, scripts and buttons#252
fx merged 6 commits into
mainfrom
feat/0027-action-cards

Conversation

@fx

@fx fx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

First PR of docs/changes/0027-scene-cards.md: one ActionCard family serving scene, script, button and input_button, with a per-domain action map, activation feedback, script running state, the confirm gate, showLastActivated, tier layouts and registry entries. The spec update is PR 2.

This fixes a defect that was live on main

ButtonCard is the registry fallback for every unmapped domain and dispatches toggle. All four of these domains reached it, and Home Assistant refuses turn_on / turn_off / toggle on the stateless ones — so every tap on a scene, button or input-button card was a call the backend rejected. Recorded on #230 as this change's to own.

The mapping was verified against the running Home Assistant's own service registry rather than taken from the change document: scene offers apply / create / delete / reload / turn_on, script offers reload / toggle / turn_off / turn_on, button offers press, and input_button offers press / reload.

Two reviewer claims about Home Assistant's API were confidently wrong earlier in this wave — the fan set_preset_mode gate and the climate temperature_unit attribute — and both would have shipped defects if implemented literally. Reading the live registry is now the habit for anything touching an HA contract.

Dispatch is guarded from birth

These cards go through dispatchGuarded rather than the retrying path, and were never on it. That is not a migration but the point of the guard: a retried script.turn_on runs the script twice, and a retried button.press fires whatever it is wired to twice — the exact harm #230 names.

Registry

Registered through domainToCard with the four domains added to SUPPORTED_DOMAINS and getFriendlyDomain. The card does not import cardRegistry — that closes a cycle which fails with a temporal-dead-zone error depending on bundle entry order, and it broke the Storybook build once already.

Testing

  • npm test — 3419 passed
  • npm run lint, npm run typecheck
  • npm run test:coverage — patch clean across all 9 changed source files, verified against BRDA branch entries
  • npm run build-storybook and npm run build:ha:prod
  • Playwright E2E, including a spec that presses a button against a real Home Assistant and asserts the entity responded

Summary by CodeRabbit

  • New Features
    • Added action cards for scenes, scripts, buttons, and input buttons.
    • Cards now support domain-specific activation, running/stopping states, confirmation prompts, custom icons, and “last activated” information.
    • Added visual feedback for activating, successful, and failed actions.
    • Added loading, unavailable, disconnected, and retry states.
  • Style
    • Added animated activation indicators with reduced-motion support.
  • Documentation
    • Updated the scene-card implementation checklist.

fx added 4 commits July 28, 2026 00:55
Register one card for `scene`, `script`, `button` and `input_button`,
replacing the generic fallback all four resolve to today.

This is a bugfix before it is a feature. The fallback `ButtonCard`
dispatches `<domain>.toggle`, and three of these four domains have no
such service: `scene.toggle`, `button.toggle` and `input_button.toggle`
all answer HTTP 400 against a live Home Assistant, so every tap on one
of those cards is a call the backend rejects. The per-domain action map
is the fix — `scene.turn_on`, `script.turn_on` / `script.turn_off`,
`button.press`, `input_button.press` — each checked against a running
instance's service registry rather than against documentation.

One family card, not three: the domains diverge only in the service
name and in the script-only running state, so splitting them would
split the option surface, the stories and the tests with them.

Behaviour from the option doc:

- Activation feedback is intrinsic, not an option. These entities
  expose no state change to observe, so the icon swaps to a spinner in
  flight and to a check held ~1.5s on success. Reduced motion drops the
  rotation and the swap transition in the stylesheet but keeps the
  check, which is evidence rather than decoration.
- `unknown` is inert only for `script`. For the other three the state
  IS the last-activation timestamp, so a never-activated entity reports
  `unknown` forever and must stay activatable.
- A running script's tap means stop, and the card says so.
- `confirm` supplies the family's own gate through `confirmRoute`,
  replacing the shell's generic on/off rule — so it also covers the
  `homeassistant.*` aliases that rule caught, and fails toward asking.
- `showLastActivated` reads the state for three domains and
  `last_triggered` for `script`, rendering "Never" for anything unset
  or unparseable.

Dispatch goes through the non-retrying guarded path throughout: every
service here is non-idempotent, and a retried press presses twice.

The card renders the icon circle itself rather than through
`GridCard.Icon`, whose `icon` override wins over its children — a
configured icon must not suppress the only evidence a tap did anything.

Adds a `pending` service-call mode to the Storybook mock so the
in-flight state has a story that can observe it.
The grid already publishes an `onConfigure` on the card item context
and owns the configuration modal, which is how the cover and sensor
families reach their options. A second modal inside the card added a
save path that only the grid's own can reach.

Resolving the primary command once in render also removes two guards
that could never fire: the handler's "no command" check and the confirm
route's missing-prompt fallback were both dead behind `isUnavailable`,
which is now derived from the same value.
Two probes survived the mutation run, and both were real gaps.

Nothing asserted the registry mapping, so unmapping `scene` back to the
fallback broke no test — while being exactly the regression this change
exists to prevent. The sequencing cases also read the hold out of the
same constant they were checking, so shortening it moved the code and
the expectation together; the interval is now pinned as a literal.
The defect this change fixes lives exactly at the boundary a unit test
stubs out: the fallback card dispatches `button.toggle`, which is not a
registered service, so Home Assistant answers 400 and the entity never
moves. A mocked dispatch layer agrees with the card either way.

`button.push` carries its last press as its own state, so the proof is
that the state advances.
Copilot AI review requested due to automatic review settings July 28, 2026 01:28
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a unified ActionCard family for scene, script, button, and input_button entities, including domain routing, persisted options, guarded activation feedback, confirmation prompts, Storybook scenarios, unit tests, and an end-to-end button interaction.

Changes

Action card family

Layer / File(s) Summary
Action contracts and persisted options
src/components/ActionCard/actions.ts, src/store/*, src/components/configurations/cardConfigurations.ts, src/test/fixtures/entities.ts
Defines domain commands, script stop behavior, timestamp formatting, confirmation schemas, action-card options, configuration entries, and entity fixtures.
ActionCard behavior and feedback
src/components/ActionCard/index.tsx, src/components/ActionCard/hooks.ts, src/components/ActionCard/ActionCard.css
Implements rendering, guarded service dispatch, activation phases, last-activated text, confirmation routing, animations, and reduced-motion behavior.
Domain registration and discovery
src/components/cardRegistry.ts, src/components/cardDomains.ts, src/components/EntitiesBrowserTab.tsx, src/components/__tests__/cardRegistry.test.ts
Maps the four domains to ActionCard and includes button-related domains in discovery and registry tests.
Stories, integration, and behavioral validation
src/components/ActionCard/ActionCard.stories.tsx, src/components/ActionCard/__tests__/*, .storybook/*, tests/e2e/*, docs/changes/0027-scene-cards.md
Covers resting, pending, success, failure, running, confirmation, layout, accessibility, CSS, dispatch, and end-to-end button behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ActionCard
  participant dispatchGuarded
  participant HomeAssistant
  User->>ActionCard: Tap action card
  ActionCard->>dispatchGuarded: Resolve and dispatch primary command
  dispatchGuarded->>HomeAssistant: callService(domain, service, target)
  HomeAssistant-->>dispatchGuarded: Return service result
  dispatchGuarded-->>ActionCard: Update pending, success, or error feedback
  ActionCard-->>User: Render updated icon and state
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.52% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing an ActionCard family for scene, script, and button entities.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/0027-action-cards

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Introduces a new ActionCard family to correctly support Home Assistant scene, script, button, and input_button entities (previously falling through to the fallback ButtonCard and incorrectly dispatching <domain>.toggle), including guarded non-retrying dispatch, activation feedback UI, confirmation gating, and “last activated” display.

Changes:

  • Add the ActionCard component family (UI, per-domain action map, activation feedback, script running state) plus Storybook stories and unit tests.
  • Wire the family into the app: registry/domain lists, configuration modal definitions, config schema validation + option reader for confirm / showLastActivated.
  • Add boundary coverage: new Playwright E2E spec against a real Home Assistant demo button.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e/helpers.ts Add demo button entity + dedicated seed config for action-card E2E screen.
tests/e2e/action-card.spec.ts New E2E spec asserting button.press works against real Home Assistant by observing state change.
src/test/fixtures/storyParameters.ts Extend Storybook service-call simulation to include a pending state.
src/test/fixtures/entities.ts Add fixture factories for scene, script, button, input_button.
src/store/configSchema.ts Merge action-card option schema into persisted config validation.
src/store/actionOptions.ts New persisted option contract + reader for confirm and showLastActivated.
src/store/tests/actionOptions.test.ts Unit tests for action options reader + schema import gate behavior.
src/components/EntitiesBrowserTab.tsx Add action-family domains to supported domains and friendly labels.
src/components/configurations/cardConfigurations.ts Add per-domain entries for the action family’s config UI (icon + confirm + last activated).
src/components/cardRegistry.ts Register scene/script/button/input_button to render via ActionCard.
src/components/cardDomains.ts Add the four new mapped domains to the domain type list.
src/components/ActionCard/index.tsx New card component implementing per-domain activation + guarded dispatch and tier layouts.
src/components/ActionCard/hooks.ts New hooks for activation feedback sequencing and last-activated ticker.
src/components/ActionCard/actions.ts Per-domain action map + confirm-route classification + activation timestamp parsing/formatting.
src/components/ActionCard/ActionCard.stories.tsx Full Storybook matrix for domains/states/options/tiers, including pending and confirm flows.
src/components/ActionCard/ActionCard.css Layered styles for spinner + motion transitions with prefers-reduced-motion handling.
src/components/ActionCard/tests/activationStyles.test.ts CSS-level tests asserting reduced-motion suppression and layering/animation invariants.
src/components/ActionCard/tests/actions.test.ts Unit tests for the per-domain action map, inertness rules, confirm routing, and timestamps.
src/components/ActionCard/tests/ActionCard.test.tsx Integration-style RTL tests exercising real hooks/guarded dispatch via mocked hass.callService.
src/components/tests/cardRegistry.test.ts Assert the registry routes the four domains to ActionCard.
docs/changes/0027-scene-cards.md Mark PR1 task as completed for change 0027.
.storybook/mockHass.ts Add pending option to simulate never-settling service calls for in-flight UI stories.
.storybook/decorators.tsx Thread new pending story parameter into the mock hass factory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/store/configSchema.ts
fx added 2 commits July 28, 2026 01:38
# Conflicts:
#	src/components/configurations/cardConfigurations.ts
#	src/store/configSchema.ts
`confirm` was declared by both `switchOptionsConfigSchema` and
`actionOptionsConfigSchema`, and `configSchema.ts` merges every family
fragment into one item schema where `zod.merge()` is last-one-wins. The
switch fragment therefore governed `confirm` for action cards too.

Nothing misbehaved, because both declared it identically — which is what
made it worth fixing: tightening the switch card's gate would have
changed what action cards accept, with no diff touching them and no test
failing.

Both fragments now merge one shared object from `store/confirmOption.ts`,
so the merge order cannot change what `confirm` accepts. Not the
universal fragment, deliberately: `confirm` is a per-card option in both
the switch and scene documents, and the universal set is closed at the
five display keys and the three action keys — declaring it universal
would say every family accepts it, which is a spec change rather than a
refactor.

`readCardConfirm` moves there with it. The shell applies the gate and had
been importing a *switch* module to read an option three families define.

Tests assert schema identity rather than equivalence — two structurally
identical declarations would pass an equivalence check and still be free
to drift — and pin `confirm` through the whole merged item schema, which
is the level a fragment-level test cannot see.
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.47%. Comparing base (e56a4c2) to head (49a5420).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #252      +/-   ##
==========================================
+ Coverage   84.13%   84.47%   +0.34%     
==========================================
  Files         192      197       +5     
  Lines        6927     7080     +153     
  Branches     2274     2333      +59     
==========================================
+ Hits         5828     5981     +153     
  Misses        874      874              
  Partials      225      225              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/components/ActionCard/__tests__/activationStyles.test.ts (1)

81-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Animation-name extraction is order-sensitive.

/animation:\s*([a-z-]+)/ captures the first token after the colon, so a legal shorthand reorder (animation: 1s linear infinite action-spin) makes this fail with a misleading message. Matching the name anywhere in the declaration would be more robust.

🤖 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 `@src/components/ActionCard/__tests__/activationStyles.test.ts` around lines 81
- 88, Update the animation-name extraction in the reduced-motion test so it
identifies animation names regardless of their position within a legal animation
shorthand declaration, including declarations where duration, timing, or
iteration values precede the name. Preserve the existing filtering of “none” and
the expected “action-spin” assertion.
src/components/ActionCard/index.tsx (1)

144-172: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

feedback object is a fresh reference every render, defeating useMemo.

useActivationFeedback() returns a new { phase, run } literal each render, so listing feedback whole in the dependency array (Line 172) means handlePrimary is recreated on every render, not just when its actionable inputs change.

♻️ Proposed dependency fix
-  const handlePrimary = useMemo(() => {
+  const handlePrimary = useMemo(() => {
     if (!command) return undefined

     return () => {
-      if (!isRunning && feedback.phase !== 'idle') return
+      if (!isRunning && feedback.phase !== 'idle') return

       if (error) clearError()

       void feedback.run(() =>
         dispatchGuarded({ domain: command.domain, service: command.service, entityId })
       )
     }
-  }, [clearError, command, dispatchGuarded, entityId, error, feedback, isRunning])
+  }, [clearError, command, dispatchGuarded, entityId, error, feedback.phase, feedback.run, isRunning])
🤖 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 `@src/components/ActionCard/index.tsx` around lines 144 - 172, Update the
handlePrimary useMemo dependency list to depend on the stable, actionable
members of feedback rather than the whole feedback object: reference
feedback.phase and feedback.run separately in the callback and dependency array.
Preserve the existing behavior and all other dependencies.
🤖 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 `@src/components/ActionCard/__tests__/ActionCard.test.tsx`:
- Around line 878-900: Update the test around “offers a reload as the way out of
a disconnection” to capture window.location’s original property descriptor
before overriding it, then restore that descriptor after the test completes,
including on assertion or interaction failure. Avoid reconstructing location via
object spread; preserve the original descriptor while replacing only reload for
the test.

In `@src/store/__tests__/actionOptions.test.ts`:
- Around line 82-86: Remove the misleading test case around
actionOptionsConfigSchema.shape.confirm, including its contradictory title and
comments, since confirm is intentionally shared with confirmOptionsConfigSchema
and the toBeDefined assertion does not validate that contract.

---

Nitpick comments:
In `@src/components/ActionCard/__tests__/activationStyles.test.ts`:
- Around line 81-88: Update the animation-name extraction in the reduced-motion
test so it identifies animation names regardless of their position within a
legal animation shorthand declaration, including declarations where duration,
timing, or iteration values precede the name. Preserve the existing filtering of
“none” and the expected “action-spin” assertion.

In `@src/components/ActionCard/index.tsx`:
- Around line 144-172: Update the handlePrimary useMemo dependency list to
depend on the stable, actionable members of feedback rather than the whole
feedback object: reference feedback.phase and feedback.run separately in the
callback and dependency array. Preserve the existing behavior and all other
dependencies.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5fe2143-d395-4804-8e6a-7ca00dd2b0f1

📥 Commits

Reviewing files that changed from the base of the PR and between e56a4c2 and 49a5420.

📒 Files selected for processing (28)
  • .storybook/decorators.tsx
  • .storybook/mockHass.ts
  • docs/changes/0027-scene-cards.md
  • src/components/ActionCard/ActionCard.css
  • src/components/ActionCard/ActionCard.stories.tsx
  • src/components/ActionCard/__tests__/ActionCard.test.tsx
  • src/components/ActionCard/__tests__/actions.test.ts
  • src/components/ActionCard/__tests__/activationStyles.test.ts
  • src/components/ActionCard/actions.ts
  • src/components/ActionCard/hooks.ts
  • src/components/ActionCard/index.tsx
  • src/components/EntitiesBrowserTab.tsx
  • src/components/__tests__/cardRegistry.test.ts
  • src/components/cardDomains.ts
  • src/components/cardRegistry.ts
  • src/components/configurations/cardConfigurations.ts
  • src/hooks/useCardActions.ts
  • src/store/__tests__/actionOptions.test.ts
  • src/store/__tests__/confirmOption.test.ts
  • src/store/__tests__/switchOptions.test.ts
  • src/store/actionOptions.ts
  • src/store/configSchema.ts
  • src/store/confirmOption.ts
  • src/store/switchOptions.ts
  • src/test/fixtures/entities.ts
  • src/test/fixtures/storyParameters.ts
  • tests/e2e/action-card.spec.ts
  • tests/e2e/helpers.ts
💤 Files with no reviewable changes (1)
  • src/store/tests/switchOptions.test.ts

Comment on lines +878 to +900
it('offers a reload as the way out of a disconnection', async () => {
const reload = vi.fn()
// jsdom's own `location.reload` throws "not implemented", so the retry can
// only be exercised against a replaced one.
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...window.location, reload },
})

entityStore.setState((state) => ({
...state,
isConnected: false,
isInitialLoading: false,
entities: {},
}))
renderCard(<ActionCard entityId="scene.movie_night" />)

// The card-variant error tile opens a detail modal; Retry lives inside it.
fireEvent.click(screen.getByLabelText('Disconnected: Disconnected from Home Assistant'))
fireEvent.click(await screen.findByRole('button', { name: 'Retry' }))

expect(reload).toHaveBeenCalledTimes(1)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the original window.location descriptor after this test.

The override is installed but never reverted, so every later test in this file (and any sharing the environment) sees the replaced object. Also, { ...window.location } copies only own enumerable properties; jsdom's Location exposes href/origin/etc. as prototype accessors, so the replacement is likely missing them.

Based on learnings, when simulating window.location you should capture the existing property descriptor and restore it afterward.

🧪 Proposed fix
   it('offers a reload as the way out of a disconnection', async () => {
     const reload = vi.fn()
     // jsdom's own `location.reload` throws "not implemented", so the retry can
     // only be exercised against a replaced one.
+    const originalLocation = Object.getOwnPropertyDescriptor(window, 'location')!
     Object.defineProperty(window, 'location', {
       configurable: true,
-      value: { ...window.location, reload },
+      value: Object.assign(Object.create(window.location), { reload }),
     })
+    try {
     expect(reload).toHaveBeenCalledTimes(1)
+    } finally {
+      Object.defineProperty(window, 'location', originalLocation)
+    }
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('offers a reload as the way out of a disconnection', async () => {
const reload = vi.fn()
// jsdom's own `location.reload` throws "not implemented", so the retry can
// only be exercised against a replaced one.
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...window.location, reload },
})
entityStore.setState((state) => ({
...state,
isConnected: false,
isInitialLoading: false,
entities: {},
}))
renderCard(<ActionCard entityId="scene.movie_night" />)
// The card-variant error tile opens a detail modal; Retry lives inside it.
fireEvent.click(screen.getByLabelText('Disconnected: Disconnected from Home Assistant'))
fireEvent.click(await screen.findByRole('button', { name: 'Retry' }))
expect(reload).toHaveBeenCalledTimes(1)
})
it('offers a reload as the way out of a disconnection', async () => {
const reload = vi.fn()
// jsdom's own `location.reload` throws "not implemented", so the retry can
// only be exercised against a replaced one.
const originalLocation = Object.getOwnPropertyDescriptor(window, 'location')!
Object.defineProperty(window, 'location', {
configurable: true,
value: Object.assign(Object.create(window.location), { reload }),
})
try {
entityStore.setState((state) => ({
...state,
isConnected: false,
isInitialLoading: false,
entities: {},
}))
renderCard(<ActionCard entityId="scene.movie_night" />)
// The card-variant error tile opens a detail modal; Retry lives inside it.
fireEvent.click(screen.getByLabelText('Disconnected: Disconnected from Home Assistant'))
fireEvent.click(await screen.findByRole('button', { name: 'Retry' }))
expect(reload).toHaveBeenCalledTimes(1)
} finally {
Object.defineProperty(window, 'location', originalLocation)
}
})
🤖 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 `@src/components/ActionCard/__tests__/ActionCard.test.tsx` around lines 878 -
900, Update the test around “offers a reload as the way out of a disconnection”
to capture window.location’s original property descriptor before overriding it,
then restore that descriptor after the test completes, including on assertion or
interaction failure. Avoid reconstructing location via object spread; preserve
the original descriptor while replacing only reload for the test.

Source: Learnings

Comment on lines +82 to +86
it('declares confirm itself rather than leaning on the switch card’s schema', () => {
// The two families name the option independently; a later change to the
// switch surface must not take this one's validation with it.
expect(actionOptionsConfigSchema.shape.confirm).toBeDefined()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test title/comment contradicts the actual shared-schema design (and confirmOption.test.ts).

This test's name and comment claim the action family "declares confirm itself rather than leaning on the switch card's schema" and that the two "name the option independently." But actionOptionsConfigSchema merges the shared confirmOptionsConfigSchema (line 55 of actionOptions.ts), and confirmOption.test.ts's "one definition of the gate" suite explicitly asserts actionOptionsConfigSchema.shape.confirm and confirmOptionsConfigSchema.shape.confirm are the same object reference — the opposite of independent declaration. The toBeDefined() assertion here also wouldn't catch a regression either way, since it passes whether confirm is shared or duplicated.

🧹 Suggested fix: remove the stale/misleading test
-  it('declares confirm itself rather than leaning on the switch card's schema', () => {
-    // The two families name the option independently; a later change to the
-    // switch surface must not take this one's validation with it.
-    expect(actionOptionsConfigSchema.shape.confirm).toBeDefined()
-  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('declares confirm itself rather than leaning on the switch card’s schema', () => {
// The two families name the option independently; a later change to the
// switch surface must not take this one's validation with it.
expect(actionOptionsConfigSchema.shape.confirm).toBeDefined()
})
🤖 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 `@src/store/__tests__/actionOptions.test.ts` around lines 82 - 86, Remove the
misleading test case around actionOptionsConfigSchema.shape.confirm, including
its contradictory title and comments, since confirm is intentionally shared with
confirmOptionsConfigSchema and the toBeDefined assertion does not validate that
contract.

@fx
fx merged commit 882062f into main Jul 28, 2026
7 checks passed
@fx
fx deleted the feat/0027-action-cards branch July 28, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants