diff --git a/.planning/phases/05-tech-debt/05-RESEARCH.md b/.planning/phases/05-tech-debt/05-RESEARCH.md
new file mode 100644
index 0000000..55bc97f
--- /dev/null
+++ b/.planning/phases/05-tech-debt/05-RESEARCH.md
@@ -0,0 +1,431 @@
+# Phase 5: Tech Debt - Research
+
+**Researched:** 2026-03-30
+**Domain:** React wizard refactoring — conditional rendering, registry-driven UI, test quality
+**Confidence:** HIGH
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+**scriptTargets filtering (TECH-01)**
+- ReviewStep conditionally renders output blocks based on `state.deployment.scriptTargets` — blocks are unmounted entirely when their target is deselected (not CSS-hidden)
+- Intune maps to: intuneInstall + intuneDetection blocks
+- RMM maps to: rmmScript block
+- rclone.conf OutputBlock is always shown regardless of scriptTargets
+- ZIP bundle also respects scriptTargets — only files for selected targets are included (not always 4)
+- Edge case: if both targets are deselected, only rclone.conf is shown and ZIP contains only rclone.conf (no blocking or warning needed)
+
+**Back button (TECH-02)**
+- Must preserve all form data (wizard state is in the store — navigation does not clear it)
+- Back dispatches `SET_STEP(2)` to return to DeploymentStep
+
+**Registry structure (TECH-03)**
+- Enrich `BACKEND_REGISTRY` to `Record`
+- All consumers of `BACKEND_REGISTRY[type]` that access field arrays must update to `BACKEND_REGISTRY[type].fields`
+- `BackendSelectionStep` derives the card list from `Object.entries(BACKEND_REGISTRY)` — no hardcoded `BACKENDS` array
+- Display metadata stays in the registry entry so Phase 6 additions auto-surface with zero additional code
+- Existing test assertions on card text ('Azure Blob Storage', etc.) remain valid — source of truth just moves
+
+**Dead export removal (TECH-04)**
+- Remove `BackendFormValues` export from `src/schemas/index.ts` (lines 27-28)
+- Verify no TypeScript errors arise after removal (no consumers expected)
+
+**act() warnings (TECH-05)**
+- Claude's Discretion — fix the act() warnings in `BackendSelectionStep.test.tsx` using appropriate testing-library patterns
+
+### Claude's Discretion
+
+- Back button (TECH-02): placement and styling left to planner
+- act() warnings (TECH-05): fix approach left to planner
+
+### Deferred Ideas (OUT OF SCOPE)
+
+None — discussion stayed within phase scope.
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| TECH-01 | User sees only the script output blocks matching their selected deployment targets (Intune and/or RMM) in ReviewStep | Conditional render pattern; ZIP filter array construction; existing `state.deployment.scriptTargets` already tracks selections |
+| TECH-02 | User can navigate back from ReviewStep using an explicit Back button | `SET_STEP(2)` action exists; DeploymentStep Back button is the established style pattern |
+| TECH-03 | Backend list in BackendSelectionStep is automatically derived from BACKEND_REGISTRY keys (no hardcoded list) | Registry shape change; three consumers need `.fields` access update; BackendSelectionStep replaces `BACKENDS` constant |
+| TECH-04 | Dead `BackendFormValues` export is removed from `src/schemas/index.ts` | Lines 27-28 confirmed; no consumers found via code audit |
+| TECH-05 | BackendSelectionStep test suite runs without `act()` warnings | Warnings confirmed in live test run; root cause is `fireEvent` triggering async state updates without `act()` wrapping |
+
+
+## Summary
+
+This phase is purely internal code quality work — no new user-facing features except the Back button on ReviewStep and the scriptTargets filtering. All changes are surgical: modify four files (`registry.ts`, `schemas/index.ts`, `BackendSelectionStep.tsx`, `ReviewStep.tsx`) and update three test files to keep assertions aligned.
+
+The codebase is in clean, idiomatic React/TypeScript with Vitest + Testing Library. Patterns are already established: conditional render (not CSS-hide) for content that has no preserved state, store dispatch for navigation, registry-driven loops for field rendering. Phase 5 applies these patterns to the remaining places that weren't wired up in v1.0.
+
+The act() warnings are real and confirmed to be in both `BackendSelectionStep.test.tsx` (warnings about `BackendSelectionStep` and `WizardProvider` components) and `ReviewStep.test.tsx` (warnings about `OutputBlock`). The root cause is `fireEvent` calling React Hook Form's `handleSubmit`, which triggers async state updates and `setTimeout` (in `OutputBlock.handleCopy`) without `act()` wrapping. The fix is `userEvent` (which wraps in `act()`) or explicit `act()` calls around async sequences.
+
+**Primary recommendation:** Address each requirement as a focused, independent task. TECH-03 (registry enrichment) has the most ripple effect — do it first so TECH-01 and the BackendSelectionStep work have the final registry shape available.
+
+## Standard Stack
+
+### Core (already in use — no new installs)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| React | 18.3.1 | UI rendering, conditional render, hooks | Project baseline |
+| TypeScript | 5.5.3 | Type safety for registry shape change | Project baseline |
+| Vitest | 4.1.1 | Test runner | Project baseline |
+| @testing-library/react | 16.3.2 | Component testing | Project baseline |
+| @testing-library/user-event | 14.6.1 | User interaction simulation (wraps in `act()`) | Already installed; fixes act() warnings |
+
+**Installation:** No new packages required. `@testing-library/user-event` is already in `devDependencies`.
+
+## Architecture Patterns
+
+### Recommended File Touch Map
+
+```
+src/
+├── schemas/
+│ ├── registry.ts # TECH-03: enrich shape + add displayName/description to each entry
+│ └── index.ts # TECH-03: update .fields access in buildZodSchema; TECH-04: remove BackendFormValues
+├── components/wizard/
+│ ├── BackendSelectionStep.tsx # TECH-03: replace BACKENDS const with Object.entries(BACKEND_REGISTRY)
+│ ├── BackendSelectionStep.test.tsx # TECH-05: replace fireEvent with userEvent; TECH-03: verify assertions still pass
+│ ├── RemoteConfigStep.tsx # TECH-03: update BACKEND_REGISTRY[backendType] → BACKEND_REGISTRY[backendType].fields
+│ └── ReviewStep.tsx # TECH-01: conditional renders; TECH-02: Back button
+└── (no new files needed)
+```
+
+### Pattern 1: Conditional Render by scriptTargets (TECH-01)
+
+**What:** Unmount OutputBlocks whose target is not in `state.deployment.scriptTargets`
+**When to use:** When the content has no local state worth preserving between show/hide
+**Why not CSS-hide:** OutputBlock has no internal state that must survive deselection (unlike AzureAuthToggle's field values)
+
+```typescript
+// In ReviewStep — derive booleans from state
+const showIntune = state.deployment.scriptTargets.includes('intune');
+const showRmm = state.deployment.scriptTargets.includes('rmm');
+
+// Conditional render (NOT CSS hidden)
+{showIntune && (
+
+)}
+{showIntune && (
+
+)}
+{showRmm && (
+
+)}
+```
+
+### Pattern 2: ZIP Filter by scriptTargets (TECH-01)
+
+**What:** Build the files array dynamically before passing to `downloadZip()`
+
+```typescript
+async function handleDownloadZip() {
+ const files: { name: string; content: string }[] = [
+ { name: 'rclone.conf', content: rcloneConf }, // always included
+ ];
+ if (showIntune) {
+ files.push({ name: 'intune-install.ps1', content: intuneInstall });
+ files.push({ name: 'intune-detection.ps1', content: intuneDetection });
+ }
+ if (showRmm) {
+ files.push({ name: 'rmm-script.ps1', content: rmmScript });
+ }
+ await downloadZip(files, 'rclone-deployment.zip');
+}
+```
+
+### Pattern 3: Back Button in ReviewStep (TECH-02)
+
+**What:** Follow DeploymentStep's exact button layout — Back + action button side-by-side at the bottom
+**Established reference:** `DeploymentStep.tsx` lines 90-105 — `flex gap-3 mt-6` container, `px-4 py-2 text-sm border border-gray-300 rounded-md` for Back, blue variant for primary
+
+```typescript
+// Bottom of ReviewStep JSX, before closing
+
+
+
+```
+
+Note: `dispatch` is not currently destructured from `useWizard()` in ReviewStep — must add it alongside `state`.
+
+### Pattern 4: Registry Shape Enrichment (TECH-03)
+
+**What:** Change `BACKEND_REGISTRY` value type from `FieldDef[]` to `{ displayName: string; description: string; fields: FieldDef[] }`
+
+```typescript
+// registry.ts — new shape
+export const BACKEND_REGISTRY: Record = {
+ azureblob: {
+ displayName: 'Azure Blob Storage',
+ description: 'Microsoft Azure cloud storage',
+ fields: [ /* existing FieldDef array */ ],
+ },
+ s3: {
+ displayName: 'Amazon S3',
+ description: 'AWS Simple Storage Service',
+ fields: [ /* existing FieldDef array */ ],
+ },
+ 's3-compatible': {
+ displayName: 'S3-Compatible',
+ description: 'Wasabi, MinIO, Cloudflare R2, and others',
+ fields: [ /* existing FieldDef array */ ],
+ },
+};
+```
+
+**Consumers that break and must be updated:**
+
+1. `src/schemas/index.ts` line 10: `const fields = BACKEND_REGISTRY[backendType];` → `const fields = BACKEND_REGISTRY[backendType].fields;`
+2. `src/components/wizard/RemoteConfigStep.tsx` line 58: `BACKEND_REGISTRY.azureblob.find(...)` → `BACKEND_REGISTRY.azureblob.fields.find(...)`
+3. `src/components/wizard/RemoteConfigStep.tsx` line 73: `BACKEND_REGISTRY[backendType].map(...)` → `BACKEND_REGISTRY[backendType].fields.map(...)`
+
+**BackendSelectionStep replacement:**
+
+```typescript
+// Replace hardcoded BACKENDS const entirely
+// Object.entries preserves insertion order in V8 (azureblob first, consistent with WIZD-01 test)
+{Object.entries(BACKEND_REGISTRY).map(([type, entry]) => (
+ handleCardClick(type as BackendType)}
+ />
+))}
+```
+
+### Pattern 5: Fixing act() Warnings (TECH-05)
+
+**Root cause confirmed in live test run:**
+- `BackendSelectionStep.test.tsx`: `fireEvent.click(azureButton)` triggers `handleSubmit` → async form validation → `dispatch` in store → state update in `WizardProvider` / `BackendSelectionStep` without act() wrapping
+- `ReviewStep.test.tsx`: `fireEvent.click(copyButtons[0])` triggers `handleCopy` → `setCopied(true)` → `setTimeout(() => setCopied(false), 2000)` — the timeout fires after test assertion, causing the `OutputBlock` warning
+
+**Fix strategy for BackendSelectionStep.test.tsx:**
+Replace `fireEvent` with `userEvent` from `@testing-library/user-event`. `userEvent.setup()` + `await user.click()` wraps all state updates in `act()` automatically.
+
+```typescript
+// Before (produces act() warnings)
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+fireEvent.click(azureButton);
+
+// After (no act() warnings)
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+const user = userEvent.setup();
+// ...
+await user.click(azureButton);
+```
+
+**Fix strategy for ReviewStep act() warnings (OutputBlock setTimeout):**
+Two options (both valid, Claude's Discretion):
+1. Use `vi.useFakeTimers()` in `beforeEach` of the clipboard copy tests so `setTimeout` never fires unexpectedly
+2. Use `userEvent` for click interactions (same as BackendSelectionStep fix)
+
+Option 1 (fake timers) is more surgical for the OutputBlock case since the setTimeout is a 2-second "Copied!" display reset — it's not a test concern.
+
+### Anti-Patterns to Avoid
+
+- **CSS-hiding OutputBlocks:** Tempting to add `hidden` class based on scriptTargets, but decision is locked to unmount — conditional render is the correct approach.
+- **Hardcoding the BACKENDS array after registry enrichment:** The whole point of TECH-03 is `Object.entries(BACKEND_REGISTRY)` — do not leave a parallel BACKENDS constant.
+- **Wrapping every fireEvent in act():** The correct fix is userEvent, not wrapping `fireEvent` calls manually — manual `act()` wrapping produces verbose, harder-to-maintain tests.
+- **Removing useMemo calls for conditionally rendered content:** `intuneInstall`, `intuneDetection`, `rmmScript` useMemo calls can stay even if the block is hidden — the computation is cheap and the memoized value is used in the ZIP handler too.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Act()-safe user interactions | Manual `act()` wrappers around fireEvent | `userEvent` from @testing-library/user-event | userEvent simulates real browser events and automatically wraps in act(); already installed |
+| Fake timers for setTimeout | Custom timer management | `vi.useFakeTimers()` / `vi.useRealTimers()` | Vitest built-in; handles all setTimeout/setInterval cleanly |
+| Registry-driven UI ordering | Custom sort/filter logic | `Object.entries()` (V8 insertion order) | Object.entries() on a `Record` preserves declaration order — Azure first matches WIZD-01 requirement |
+
+## Common Pitfalls
+
+### Pitfall 1: Forgetting dispatch in ReviewStep
+**What goes wrong:** `ReviewStep` currently only destructures `state` from `useWizard()`. Adding a Back button requires `dispatch` too. Easy to miss.
+**Why it happens:** ReviewStep was read-only in v1.0 — no navigation actions needed.
+**How to avoid:** Change `const { state } = useWizard();` to `const { state, dispatch } = useWizard();` at the top of ReviewStep.
+
+### Pitfall 2: registry.test.ts breaks after TECH-03
+**What goes wrong:** `registry.test.ts` accesses `BACKEND_REGISTRY[backend]` and calls `.length` and `.find()` directly on the registry entry — these will fail after the shape change to `{ displayName, description, fields }`.
+**Lines affected:** Lines 14 (`BACKEND_REGISTRY[backend].length`), 19 (`for (const field of BACKEND_REGISTRY[backend])`), 37/43/50 (`BACKEND_REGISTRY.azureblob.find(...)`, etc.)
+**How to avoid:** Update registry.test.ts to access `.fields` — e.g., `BACKEND_REGISTRY[backend].fields.length`, `BACKEND_REGISTRY[backend].fields.find(...)`.
+
+### Pitfall 3: ReviewStep.test.tsx asserts exactly 4 OutputBlocks / 4 downloads
+**What goes wrong:** Tests DOWN-02, DOWN-03, DOWN-04, DOWN-05 use positional indexing (`downloadButtons[1]`, `downloadButtons[2]`, `downloadButtons[3]`) and `expect(files).toHaveLength(4)`. After TECH-01, when scriptTargets is both (default), 4 blocks still render — but tests for individual downloads will break if called with scriptTargets that hide blocks.
+**Current status:** Default `INITIAL_STATE` has `scriptTargets: ['intune', 'rmm']` so existing tests still pass with all 4 blocks. Tests specifically for filtered rendering are new (Wave 0 stubs needed).
+**How to avoid:** Existing ReviewStep tests can remain as-is (they use default state = both targets). Add new test cases for filtered scenarios (intune-only, rmm-only, neither) — these are new Wave 0 stubs.
+
+### Pitfall 4: BackendSelectionStep.test.tsx assertions on card text remain valid
+**What goes wrong:** After TECH-03, tests check for 'Azure Blob Storage', 'Amazon S3', 'S3-Compatible' text. These strings move from the hardcoded `BACKENDS` array to `BACKEND_REGISTRY[type].displayName`. As long as displayName values match, tests pass without any change.
+**How to avoid:** Use the exact same display strings when adding `displayName` to registry entries.
+
+### Pitfall 5: TypeScript error after removing BackendFormValues
+**What goes wrong:** If any file imports `BackendFormValues` from `src/schemas/index.ts`, removing it causes a TS compile error.
+**Evidence:** Code audit found no consumers — `BackendFormValues` only appears in `src/schemas/index.ts` lines 27-28. But TypeScript compile check is the definitive verification step.
+**How to avoid:** After removal, run `tsc --noEmit` or `npm run build` to confirm zero errors.
+
+### Pitfall 6: userEvent requires setup() call
+**What goes wrong:** `userEvent.click()` was called directly in older versions of @testing-library/user-event. In v14 (installed: 14.6.1), the correct API is `const user = userEvent.setup()` then `await user.click()`.
+**How to avoid:** Always use `userEvent.setup()` pattern — not the legacy `userEvent.click()` shorthand.
+
+## Code Examples
+
+### Verified pattern: userEvent v14 API
+```typescript
+// Source: @testing-library/user-event v14 official API
+import userEvent from '@testing-library/user-event';
+
+describe('BackendSelectionStep', () => {
+ it('clicking a backend card dispatches SET_BACKEND_TYPE and SET_STEP', async () => {
+ const user = userEvent.setup();
+ renderStep();
+ const nameInput = screen.getByRole('textbox');
+ await user.type(nameInput, 'my-remote');
+ const azureButton = screen.getAllByRole('button').find(b => b.textContent?.includes('Azure Blob Storage'))!;
+ await user.click(azureButton);
+ expect(screen.queryByRole('alert')).toBeNull();
+ });
+});
+```
+
+### Verified pattern: vi.useFakeTimers() for setTimeout-based act() warnings
+```typescript
+// Source: Vitest official docs — timer mocking
+import { beforeEach, afterEach, vi } from 'vitest';
+
+beforeEach(() => {
+ vi.useFakeTimers();
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+it('clicking Copy calls clipboard.writeText', async () => {
+ renderStep();
+ const checkbox = screen.getByRole('checkbox');
+ fireEvent.click(checkbox);
+ const copyButtons = screen.getAllByText('Copy');
+ fireEvent.click(copyButtons[0]);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
+ // setTimeout(() => setCopied(false)) never fires during test — no act() warning
+});
+```
+
+### Verified pattern: Object.entries() on enriched registry
+```typescript
+// Source: MDN Object.entries() — insertion order preserved for string keys
+{Object.entries(BACKEND_REGISTRY).map(([type, entry]) => (
+ handleCardClick(type as BackendType)}
+ />
+))}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| `fireEvent` for all test interactions | `userEvent.setup()` + `await user.click()` | @testing-library/user-event v14 | Eliminates act() warnings from async state updates triggered by simulated events |
+| `BACKEND_REGISTRY: Record` | `Record` | Phase 5 (TECH-03) | BackendSelectionStep no longer needs a parallel BACKENDS array; Phase 6 additions auto-surface |
+
+**Deprecated/outdated:**
+- `fireEvent` for click interactions that trigger state updates: still works but causes act() warnings; replace with userEvent where state updates occur
+- Hardcoded `BACKENDS` array in `BackendSelectionStep.tsx`: dead after TECH-03; replaced by `Object.entries(BACKEND_REGISTRY)`
+- `BackendFormValues` type export: dead code — no consumers exist; removed in TECH-04
+
+## Open Questions
+
+1. **ReviewStep.test.tsx: DOWN-05 asserts `files.toHaveLength(4)` — does this test need updating?**
+ - What we know: Default INITIAL_STATE has scriptTargets = ['intune', 'rmm'], so with default state the ZIP still gets 4 files.
+ - What's unclear: Should the TECH-01 implementation also add new test cases covering filtered scenarios (2 files for intune-only, etc.)?
+ - Recommendation: Keep the existing DOWN-05 test intact (it documents correct behavior when both targets are selected). Add new test cases in Wave 0 for filtered scenarios (intune-only, rmm-only, neither).
+
+2. **act() warnings in ReviewStep.test.tsx — fix in this phase or leave?**
+ - What we know: TECH-05 requirement text says "BackendSelectionStep test suite" specifically. ReviewStep warnings are from OutputBlock's setTimeout.
+ - What's unclear: Whether the phase goal implicitly includes ReviewStep warnings given CONTEXT.md says "eliminate act() warnings from the BackendSelectionStep test suite."
+ - Recommendation: TECH-05 scope is BackendSelectionStep. Fix ReviewStep act() warnings as a bonus in the same task (low effort — add `vi.useFakeTimers()` to the relevant ReviewStep tests). If time-constrained, BackendSelectionStep warnings are the acceptance criterion.
+
+## Validation Architecture
+
+### Test Framework
+
+| Property | Value |
+|----------|-------|
+| Framework | Vitest 4.1.1 |
+| Config file | `vitest.config.ts` (root) |
+| Quick run command | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx src/components/wizard/ReviewStep.test.tsx src/schemas/registry.test.ts` |
+| Full suite command | `npx vitest run` |
+
+### Phase Requirements → Test Map
+
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| TECH-01 | Intune blocks absent when scriptTargets excludes 'intune' | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ❌ Wave 0 (new cases needed) |
+| TECH-01 | RMM block absent when scriptTargets excludes 'rmm' | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ❌ Wave 0 (new cases needed) |
+| TECH-01 | ZIP contains only rclone.conf when both targets deselected | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ❌ Wave 0 (new cases needed) |
+| TECH-02 | Back button dispatches SET_STEP(2) | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ❌ Wave 0 (new case needed) |
+| TECH-03 | BackendSelectionStep renders cards from BACKEND_REGISTRY | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ (existing assertions still valid) |
+| TECH-03 | registry.test.ts field access via .fields | unit | `npx vitest run src/schemas/registry.test.ts` | ❌ Wave 0 (update existing) |
+| TECH-04 | No TypeScript error after BackendFormValues removal | type-check | `npx tsc --noEmit` | ✅ (compile check) |
+| TECH-05 | BackendSelectionStep test suite zero act() warnings | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx 2>&1 \| grep -c "act("` | ✅ (existing tests, modified) |
+
+### Sampling Rate
+- **Per task commit:** `npx vitest run` (full suite — only 3.4s, fast enough for every commit)
+- **Per wave merge:** `npx vitest run` + `npx tsc --noEmit`
+- **Phase gate:** Full suite green + zero TypeScript errors before `/gsd:verify-work`
+
+### Wave 0 Gaps
+
+- [ ] `src/components/wizard/ReviewStep.test.tsx` — add cases for TECH-01: intune-only, rmm-only, neither (requires WizardProvider accepting initial state, or dispatching SET_DEPLOYMENT in test setup)
+- [ ] `src/components/wizard/ReviewStep.test.tsx` — add case for TECH-02: Back button renders and dispatches SET_STEP(2)
+- [ ] `src/schemas/registry.test.ts` — update field access to `.fields` after TECH-03 shape change (existing tests break without this update)
+
+**Note on ReviewStep test state injection:** Current `renderStep()` wraps with bare `WizardProvider` which uses `INITIAL_STATE`. To test filtered rendering, tests need a way to render with custom deployment state. Options: (a) export a `WizardProvider` variant accepting `initialState` prop, or (b) render and dispatch `SET_DEPLOYMENT` in test setup before assertions. Option (b) requires no code changes to the provider — preferred.
+
+## Sources
+
+### Primary (HIGH confidence)
+- Direct code audit of `src/components/wizard/ReviewStep.tsx` — confirmed no dispatch, no Back button, always 4 OutputBlocks
+- Direct code audit of `src/components/wizard/BackendSelectionStep.tsx` — confirmed hardcoded BACKENDS array
+- Direct code audit of `src/schemas/registry.ts` — confirmed current shape is `Record`
+- Direct code audit of `src/schemas/index.ts` — confirmed BackendFormValues at lines 27-28 with no consumers
+- Live test run (`npx vitest run`) — confirmed act() warnings present in both BackendSelectionStep and ReviewStep test files, all 98 tests passing
+- `src/store/types.ts` — confirmed SET_STEP action exists, scriptTargets in WizardState.deployment
+- `package.json` — confirmed @testing-library/user-event 14.6.1 already installed
+
+### Secondary (MEDIUM confidence)
+- @testing-library/user-event v14 API: `userEvent.setup()` pattern verified against installed version and established community usage
+
+### Tertiary (LOW confidence)
+- None
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — all libraries already in use, confirmed from package.json
+- Architecture: HIGH — all patterns derived from direct code audit of existing files
+- Pitfalls: HIGH — most pitfalls discovered by cross-referencing actual test files with planned changes
+- act() fix: HIGH — userEvent.setup() pattern confirmed against installed v14.6.1
+
+**Research date:** 2026-03-30
+**Valid until:** 2026-05-30 (stable libraries — React 18, Vitest 4, userEvent 14)