docs(05-tech-debt): create phase 5 plan — 4 plans across 2 waves

Wave 0 TDD stubs, Wave 1 parallel (registry + ReviewStep), Wave 2 act() fix.
Covers TECH-01 through TECH-05.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 09:26:17 +02:00
co-authored by Claude Sonnet 4.6
parent c7931621a5
commit 0a0a51b45a
6 changed files with 877 additions and 10 deletions
+8 -2
View File
@@ -39,7 +39,13 @@ Full phase details: [.planning/milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADM
3. Adding a new entry to BACKEND_REGISTRY automatically surfaces that backend in the BackendSelectionStep UI with no other code changes
4. BackendSelectionStep test suite runs with zero `act()` warnings in the Vitest output
5. `BackendFormValues<T>` export is absent from `src/schemas/index.ts` and no TypeScript errors arise
**Plans**: TBD
**Plans**: 4 plans
Plans:
- [ ] 05-00-PLAN.md — Wave 0 TDD stubs: ReviewStep filtering + Back button failing tests, registry.test.ts .fields prep
- [ ] 05-01-PLAN.md — Registry enrichment (TECH-03) + consumers update + dead export removal (TECH-04)
- [ ] 05-02-PLAN.md — ReviewStep scriptTargets filtering + Back button (TECH-01, TECH-02)
- [ ] 05-03-PLAN.md — act() warnings fix: userEvent migration + vi.useFakeTimers (TECH-05)
### Phase 6: New Backends
**Goal**: IT pros can configure OneDrive, SFTP, Google Cloud Storage, and Backblaze B2 remotes through the same wizard flow, with appropriate guidance for OAuth-based and key-based auth methods
@@ -72,6 +78,6 @@ Full phase details: [.planning/milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADM
| 2. Generators | v1.0 | 4/4 | Complete | 2026-03-26 |
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | v1.0 | 5/5 | Complete | 2026-03-27 |
| 5. Tech Debt | v1.1 | 0/TBD | Not started | - |
| 5. Tech Debt | v1.1 | 0/4 | Not started | - |
| 6. New Backends | v1.1 | 0/TBD | Not started | - |
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
+8 -8
View File
@@ -1,11 +1,11 @@
---
gsd_state_version: 1.0
milestone: v1.1
milestone_name: "Backlog & Tech Debt"
status: roadmap_ready
stopped_at: Roadmap created for v1.1 — 3 phases (5-7), 11 requirements mapped
last_updated: "2026-03-27T00:00:00.000Z"
last_activity: 2026-03-27 — v1.1 roadmap created, ready to plan Phase 5
milestone_name: Backlog & Tech Debt
status: planning
stopped_at: Phase 5 context gathered
last_updated: "2026-03-30T07:14:08.967Z"
last_activity: 2026-03-27 — v1.1 roadmap created, 11 requirements mapped across 3 phases
progress:
total_phases: 3
completed_phases: 0
@@ -74,6 +74,6 @@ None yet.
## Session Continuity
Last session: 2026-03-27T00:00:00.000Z
Stopped at: v1.1 roadmap created — Phases 5-7 defined, all 11 requirements mapped
Resume file: None
Last session: 2026-03-30T07:14:08.962Z
Stopped at: Phase 5 context gathered
Resume file: .planning/phases/05-tech-debt/05-CONTEXT.md
+182
View File
@@ -0,0 +1,182 @@
---
phase: 05-tech-debt
plan: "00"
type: tdd
wave: 0
depends_on: []
files_modified:
- src/components/wizard/ReviewStep.test.tsx
- src/schemas/registry.test.ts
autonomous: true
requirements:
- TECH-01
- TECH-02
- TECH-03
must_haves:
truths:
- "ReviewStep test suite has failing cases for intune-only rendering"
- "ReviewStep test suite has a failing case for rmm-only rendering"
- "ReviewStep test suite has a failing case for neither-target rendering (only rclone.conf)"
- "ReviewStep test suite has a failing case for Back button dispatching SET_STEP(2)"
- "registry.test.ts accesses BACKEND_REGISTRY entries via .fields — ready for shape change"
artifacts:
- path: "src/components/wizard/ReviewStep.test.tsx"
provides: "Failing test cases for TECH-01 and TECH-02"
contains: "scriptTargets"
- path: "src/schemas/registry.test.ts"
provides: "Updated field access via .fields for TECH-03"
contains: ".fields"
key_links:
- from: "ReviewStep.test.tsx new cases"
to: "ReviewStep.tsx (not yet modified)"
via: "renderWithDeployment helper dispatching SET_DEPLOYMENT"
pattern: "SET_DEPLOYMENT"
---
<objective>
Write Wave 0 test stubs: failing test cases for TECH-01 (scriptTargets filtering) and TECH-02 (Back button) in ReviewStep.test.tsx, and update registry.test.ts field access to use .fields ahead of the TECH-03 registry shape change.
Purpose: Establish RED state before implementation plans run — per project TDD pattern (Wave 0 stubs before implementation).
Output: Modified ReviewStep.test.tsx with 4 new failing describe blocks, modified registry.test.ts with .fields access.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-tech-debt/05-CONTEXT.md
@.planning/phases/05-tech-debt/05-RESEARCH.md
<interfaces>
<!-- Key types for test setup — extracted from codebase -->
From src/store/types.ts:
```typescript
export interface WizardState {
currentStep: number;
remote: { name: string; backendType: BackendType | null; params: Record<string, string> };
deployment: {
includeInstall: boolean;
configPath: 'machine-wide' | 'user-profile';
scriptTargets: ('intune' | 'rmm')[];
};
}
export type WizardAction =
| { type: 'SET_STEP'; payload: number }
| { type: 'SET_DEPLOYMENT'; payload: Partial<WizardState['deployment']> }
// ... other actions
export const INITIAL_STATE: WizardState = {
deployment: { scriptTargets: ['intune', 'rmm'], ... },
};
```
From src/store/context.tsx:
```typescript
// WizardProvider does NOT accept initialState — uses INITIAL_STATE hardcoded
export function WizardProvider({ children }: { children: React.ReactNode })
```
From src/schemas/registry.ts (CURRENT shape — will change in Plan 01):
```typescript
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]> = { ... }
// After TECH-03 it becomes: Record<BackendType, { displayName, description, fields: FieldDef[] }>
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Add TECH-01 and TECH-02 failing test cases to ReviewStep.test.tsx</name>
<files>src/components/wizard/ReviewStep.test.tsx</files>
<behavior>
- TECH-01-a: When scriptTargets is ['intune'] only, Intune Install and Intune Detection OutputBlocks are present, RMM Script OutputBlock is absent
- TECH-01-b: When scriptTargets is ['rmm'] only, RMM Script OutputBlock is present, Intune blocks are absent
- TECH-01-c: When scriptTargets is [], only rclone.conf OutputBlock is shown (no intune or rmm blocks)
- TECH-01-d: When scriptTargets is [], ZIP download button calls downloadZip with only 1 file (rclone.conf)
- TECH-02: Back button is present in rendered output and clicking it dispatches SET_STEP(2) (verify via step change — component navigates away or via mock)
</behavior>
<action>
Add a `renderWithDeployment` helper at the top of the test file (after existing `renderStep`) that wraps in WizardProvider and dispatches SET_DEPLOYMENT before assertions. Pattern: render with WizardProvider, use `act(() => dispatch({ type: 'SET_DEPLOYMENT', payload: { scriptTargets: [...] } }))` via a TestHelper component that receives a dispatch ref, OR use a wrapper component that accepts a prop and dispatches in useEffect.
Preferred approach (no code changes to production): create a `WizardConsumerSetup` React component inside the test file that calls `dispatch(SET_DEPLOYMENT)` in a `useEffect` on mount, then renders children. Wrap ReviewStep with it in renderWithDeployment.
Add these new describe blocks (append to existing file — do NOT remove any existing tests):
- describe('TECH-01: scriptTargets filtering', ...) with 4 it() cases covering intune-only, rmm-only, neither, and ZIP-neither
- describe('TECH-02: Back button navigation', ...) with 1 it() case verifying button labeled 'Back' renders
These tests MUST FAIL (RED state) because ReviewStep not yet modified. Confirm by running the test suite and seeing failures on the new cases.
Note: ZIP-neither test — mock downloadZip is already set up in beforeEach. The test clicks the ZIP button with scriptTargets=[] and asserts files array has length 1.
Note: TECH-02 Back button test — check `screen.getByRole('button', { name: /back/i })` renders. The navigation itself (SET_STEP dispatch) is trivially verifiable by asserting the button exists and is not disabled (dispatch correctness verified by ReviewStep implementation, not mocked here).
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/wizard/ReviewStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>
Existing ReviewStep tests still pass (CONF-02, CONF-03, DOWN-01 through DOWN-06, SECU-01, SECU-02).
New TECH-01 and TECH-02 test cases exist in the file and fail (RED state — ReviewStep not yet modified).
</done>
</task>
<task type="auto">
<name>Task 2: Update registry.test.ts field access to use .fields</name>
<files>src/schemas/registry.test.ts</files>
<action>
Update registry.test.ts to access fields via `.fields` in preparation for the TECH-03 shape change. The current registry shape is `FieldDef[]` directly — these tests will break after Plan 01 enriches the shape. Update them now so they are ready.
Changes needed (per RESEARCH.md pitfall 2):
- Line 15: `BACKEND_REGISTRY[backend].length``BACKEND_REGISTRY[backend].fields.length`
- Line 21: `for (const field of BACKEND_REGISTRY[backend])``for (const field of BACKEND_REGISTRY[backend].fields)`
- Line 38: `BACKEND_REGISTRY.azureblob.find(f => f.key === 'account')``BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account')`
- Line 44: `BACKEND_REGISTRY.s3.map(f => f.key)``BACKEND_REGISTRY.s3.fields.map(f => f.key)`
- Line 51: `BACKEND_REGISTRY['s3-compatible'].find(f => f.key === 'endpoint')``BACKEND_REGISTRY['s3-compatible'].fields.find(f => f.key === 'endpoint')`
These changes will make registry.test.ts FAIL (RED state) because registry.ts still has the old `FieldDef[]` shape. That's expected — Plan 01 fixes the production code to green.
Also add a new test case verifying the enriched shape structure (will also be RED until Plan 01):
```
it('each backend entry has displayName and description metadata', () => {
for (const backend of EXPECTED_BACKENDS) {
expect(BACKEND_REGISTRY[backend].displayName).toBeTruthy();
expect(BACKEND_REGISTRY[backend].description).toBeTruthy();
}
});
```
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/schemas/registry.test.ts 2>&1 | tail -15</automated>
</verify>
<done>
registry.test.ts uses .fields access throughout.
Test suite shows failures on the registry tests (RED — registry shape not yet enriched). TypeScript may show type errors — that is expected and correct until Plan 01 runs.
</done>
</task>
</tasks>
<verification>
Run full suite to confirm scope of RED state:
```
cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run 2>&1 | tail -20
```
Expected: existing passing tests still pass, only new TECH-01/TECH-02/TECH-03 stubs fail.
</verification>
<success_criteria>
- ReviewStep.test.tsx has new failing test cases for TECH-01 (intune-only, rmm-only, neither, ZIP-neither) and TECH-02 (Back button present)
- registry.test.ts uses .fields access throughout and has a new displayName/description test case
- All pre-existing tests (CONF-02, CONF-03, DOWN-01DOWN-06, SECU-01, SECU-02, WIZD-01, WIZD-04, all registry tests that existed) remain green or show the expected RED only on the new .fields lines
</success_criteria>
<output>
After completion, create `.planning/phases/05-tech-debt/05-00-SUMMARY.md`
</output>
+248
View File
@@ -0,0 +1,248 @@
---
phase: 05-tech-debt
plan: "01"
type: execute
wave: 1
depends_on:
- "05-00"
files_modified:
- src/schemas/registry.ts
- src/schemas/index.ts
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/BackendSelectionStep.tsx
autonomous: true
requirements:
- TECH-03
- TECH-04
must_haves:
truths:
- "BACKEND_REGISTRY entries each have displayName, description, and fields properties"
- "BackendSelectionStep renders cards using Object.entries(BACKEND_REGISTRY) — no hardcoded BACKENDS array"
- "All consumers of BACKEND_REGISTRY access field arrays via .fields"
- "BackendFormValues<T> export is absent from src/schemas/index.ts"
- "TypeScript compiles with zero errors after changes"
artifacts:
- path: "src/schemas/registry.ts"
provides: "Enriched BACKEND_REGISTRY with displayName, description, fields"
contains: "displayName"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Registry-driven card list"
contains: "Object.entries(BACKEND_REGISTRY)"
key_links:
- from: "src/schemas/registry.ts"
to: "src/schemas/index.ts"
via: "buildZodSchema accesses BACKEND_REGISTRY[backendType].fields"
pattern: "\\.fields"
- from: "src/schemas/registry.ts"
to: "src/components/wizard/RemoteConfigStep.tsx"
via: "field iteration uses BACKEND_REGISTRY[backendType].fields"
pattern: "BACKEND_REGISTRY\\[backendType\\]\\.fields"
- from: "src/schemas/registry.ts"
to: "src/components/wizard/BackendSelectionStep.tsx"
via: "Object.entries(BACKEND_REGISTRY) replaces BACKENDS const"
pattern: "Object\\.entries\\(BACKEND_REGISTRY\\)"
---
<objective>
Enrich BACKEND_REGISTRY with display metadata, wire BackendSelectionStep to derive its card list from the registry, update all consumers to use .fields access, and remove the dead BackendFormValues<T> export.
Purpose: TECH-03 makes the registry the single source of truth for both field definitions AND display metadata — Phase 6 backend additions auto-surface with zero additional UI code. TECH-04 removes dead code.
Output: registry.ts with new shape, BackendSelectionStep.tsx driven by registry, schemas/index.ts clean.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/05-tech-debt/05-CONTEXT.md
@.planning/phases/05-tech-debt/05-RESEARCH.md
@.planning/phases/05-tech-debt/05-00-SUMMARY.md
<interfaces>
<!-- Extracted from codebase — executor uses these directly -->
From src/schemas/registry.ts (CURRENT shape — executor REPLACES this):
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
export interface FieldDef { key, label, inputType, required, placeholder?, helpText?, options? }
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]> = { ... }
```
New shape (TECH-03 target):
```typescript
export const BACKEND_REGISTRY: Record<BackendType, {
displayName: string;
description: string;
fields: FieldDef[];
}> = {
azureblob: {
displayName: 'Azure Blob Storage',
description: 'Microsoft Azure cloud storage',
fields: [ /* existing FieldDef[] content unchanged */ ],
},
s3: {
displayName: 'Amazon S3',
description: 'AWS Simple Storage Service',
fields: [ /* existing FieldDef[] content unchanged */ ],
},
's3-compatible': {
displayName: 'S3-Compatible',
description: 'Wasabi, MinIO, Cloudflare R2, and others',
fields: [ /* existing FieldDef[] content unchanged */ ],
},
};
```
Display strings MUST match existing BackendSelectionStep card text exactly (test assertions check these strings).
From src/schemas/index.ts line 10 (consumer 1 — must update):
```typescript
const fields = BACKEND_REGISTRY[backendType]; // CHANGE TO: BACKEND_REGISTRY[backendType].fields
```
From src/components/wizard/RemoteConfigStep.tsx (consumers 2 and 3 — must update):
```typescript
// Line 58: BACKEND_REGISTRY.azureblob.find(f => f.key === 'account')
// CHANGE TO: BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account')
// Line 73: BACKEND_REGISTRY[backendType].map(field => ...)
// CHANGE TO: BACKEND_REGISTRY[backendType].fields.map(field => ...)
```
From src/components/wizard/BackendSelectionStep.tsx (TECH-03 — replace hardcoded BACKENDS):
```typescript
// Remove: const BACKENDS: { type: BackendType; name: string; description: string }[] = [...]
// Replace rendering with:
import { BACKEND_REGISTRY } from '../../schemas/registry';
// ...
{Object.entries(BACKEND_REGISTRY).map(([type, entry]) => (
<BackendCard
key={type}
name={entry.displayName}
description={entry.description}
selected={state.remote.backendType === type}
onClick={() => handleCardClick(type as BackendType)}
/>
))}
```
From src/schemas/index.ts lines 27-28 (TECH-04 — remove entirely):
```typescript
// REMOVE these two lines:
export type BackendFormValues<T extends BackendType> =
z.infer<typeof BACKEND_SCHEMAS[T]>;
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Enrich BACKEND_REGISTRY and update all consumers</name>
<files>src/schemas/registry.ts, src/schemas/index.ts, src/components/wizard/RemoteConfigStep.tsx</files>
<behavior>
- registry.test.ts 'each backend entry has displayName and description metadata' passes
- registry.test.ts 'each backend has at least one field definition' passes (via .fields.length)
- registry.test.ts 'Azure Blob has account field' passes (via .fields.find)
- schemas/index.ts buildZodSchema receives FieldDef[] from .fields — all BACKEND_SCHEMAS build correctly
- RemoteConfigStep renders fields correctly (existing RemoteConfigStep.test.tsx passes)
</behavior>
<action>
1. Edit src/schemas/registry.ts:
- Change BACKEND_REGISTRY value type from `FieldDef[]` to `{ displayName: string; description: string; fields: FieldDef[] }`
- Wrap each backend's existing FieldDef array in the new object shape, adding displayName and description
- displayName and description strings MUST match the existing BACKENDS array in BackendSelectionStep.tsx exactly:
- azureblob: displayName='Azure Blob Storage', description='Microsoft Azure cloud storage'
- s3: displayName='Amazon S3', description='AWS Simple Storage Service'
- s3-compatible: displayName='S3-Compatible', description='Wasabi, MinIO, Cloudflare R2, and others'
- All existing FieldDef content is preserved unchanged inside fields
2. Edit src/schemas/index.ts line 10:
- Change `const fields = BACKEND_REGISTRY[backendType];` to `const fields = BACKEND_REGISTRY[backendType].fields;`
3. Edit src/components/wizard/RemoteConfigStep.tsx:
- Line 58: Change `BACKEND_REGISTRY.azureblob.find(f => f.key === 'account')` to `BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account')`
- Line 73: Change `BACKEND_REGISTRY[backendType].map(field =>` to `BACKEND_REGISTRY[backendType].fields.map(field =>`
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/schemas/registry.test.ts src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | tail -15</automated>
</verify>
<done>
registry.test.ts passes with all tests green (including new displayName/description test and .fields access tests).
RemoteConfigStep.test.tsx still passes.
TypeScript sees no errors on the modified files.
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Wire BackendSelectionStep to registry + remove dead export</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/schemas/index.ts</files>
<behavior>
- BackendSelectionStep renders 'Azure Blob Storage', 'Amazon S3', 'S3-Compatible' cards (from registry, not hardcoded)
- Azure Blob card appears before S3 in DOM order (Object.entries insertion order preserved)
- Clicking a card with a valid remote name still dispatches SET_BACKEND_TYPE and SET_STEP (existing WIZD-01 test passes)
- BackendFormValues<T> type is absent from src/schemas/index.ts
- npx tsc --noEmit exits with code 0
</behavior>
<action>
1. Edit src/components/wizard/BackendSelectionStep.tsx:
- Add import: `import { BACKEND_REGISTRY } from '../../schemas/registry';`
- Remove the `BACKENDS` constant entirely (lines 21-37)
- In the JSX rendering section, replace `{BACKENDS.map((backend) => (` block with:
```tsx
{Object.entries(BACKEND_REGISTRY).map(([type, entry]) => (
<BackendCard
key={type}
name={entry.displayName}
description={entry.description}
selected={state.remote.backendType === type}
onClick={() => handleCardClick(type as BackendType)}
/>
))}
```
- Remove the now-unused local type import for BackendType from '../../store/types' IF it is only used by the BACKENDS const (check — it may still be needed for pendingBackend.current type annotation). If BackendType is still needed, keep the import.
2. Edit src/schemas/index.ts:
- Remove lines 27-28: the `export type BackendFormValues<T extends BackendType> = z.infer<typeof BACKEND_SCHEMAS[T]>;` export
- Also remove the blank comment line above it (line 26: `// Utility type: infer TypeScript type from a backend's Zod schema`) to avoid orphan comment
3. Run TypeScript check to confirm no consumers of BackendFormValues exist:
`npx tsc --noEmit`
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/wizard/BackendSelectionStep.test.tsx && npx tsc --noEmit 2>&1 | tail -10</automated>
</verify>
<done>
BackendSelectionStep.test.tsx passes (WIZD-01 card text assertions still green — display strings match).
npx tsc --noEmit exits with zero errors.
BackendFormValues export is absent from src/schemas/index.ts.
</done>
</task>
</tasks>
<verification>
Full suite green except pre-existing RED stubs (TECH-01, TECH-02 ReviewStep stubs remain RED — implemented in Plan 02):
```
cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run && npx tsc --noEmit
```
Expected: All registry tests green, all BackendSelectionStep tests green, RemoteConfigStep tests green. Only TECH-01/TECH-02 ReviewStep stubs still RED.
</verification>
<success_criteria>
- BACKEND_REGISTRY shape is `Record<BackendType, { displayName, description, fields: FieldDef[] }>`
- BackendSelectionStep uses `Object.entries(BACKEND_REGISTRY)` — no hardcoded BACKENDS array
- schemas/index.ts uses `.fields` access and has no BackendFormValues export
- RemoteConfigStep uses `.fields` access
- registry.test.ts fully green
- BackendSelectionStep.test.tsx fully green
- npx tsc --noEmit: zero errors
</success_criteria>
<output>
After completion, create `.planning/phases/05-tech-debt/05-01-SUMMARY.md`
</output>
+221
View File
@@ -0,0 +1,221 @@
---
phase: 05-tech-debt
plan: "02"
type: execute
wave: 1
depends_on:
- "05-00"
files_modified:
- src/components/wizard/ReviewStep.tsx
autonomous: true
requirements:
- TECH-01
- TECH-02
must_haves:
truths:
- "User who deselected RMM sees only the Intune output blocks in ReviewStep (RMM block is unmounted)"
- "User who deselected Intune sees only the RMM output block in ReviewStep (Intune blocks are unmounted)"
- "User with both targets deselected sees only the rclone.conf block"
- "ZIP bundle respects scriptTargets — deselected targets are excluded from the ZIP file list"
- "User can click Back on ReviewStep to dispatch SET_STEP(2)"
artifacts:
- path: "src/components/wizard/ReviewStep.tsx"
provides: "scriptTargets-driven conditional rendering + Back button"
contains: "showIntune"
key_links:
- from: "ReviewStep.tsx"
to: "state.deployment.scriptTargets"
via: "showIntune and showRmm booleans derived from scriptTargets.includes()"
pattern: "scriptTargets\\.includes"
- from: "Back button"
to: "dispatch({ type: 'SET_STEP', payload: 2 })"
via: "onClick handler using dispatch from useWizard()"
pattern: "SET_STEP.*payload.*2"
---
<objective>
Modify ReviewStep to conditionally render output blocks based on state.deployment.scriptTargets, filter the ZIP bundle to match selected targets, and add a Back button that dispatches SET_STEP(2).
Purpose: TECH-01 eliminates visual noise from deselected script targets. TECH-02 provides the expected Back navigation consistent with the DeploymentStep pattern.
Output: Modified ReviewStep.tsx only — no other files touched.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/phases/05-tech-debt/05-CONTEXT.md
@.planning/phases/05-tech-debt/05-RESEARCH.md
@.planning/phases/05-tech-debt/05-00-SUMMARY.md
<interfaces>
<!-- Extracted from codebase — executor uses these directly -->
From src/store/types.ts:
```typescript
export const INITIAL_STATE: WizardState = {
deployment: {
scriptTargets: ['intune', 'rmm'], // default = both selected
},
};
export type WizardAction =
| { type: 'SET_STEP'; payload: number }
| { type: 'SET_DEPLOYMENT'; payload: Partial<WizardState['deployment']> }
// ...
```
From src/store/context.tsx:
```typescript
export function useWizard(): { state: WizardState; dispatch: React.Dispatch<WizardAction> }
```
Current ReviewStep.tsx signature:
```typescript
const { state } = useWizard(); // dispatch is NOT currently destructured — must add it
```
From CONTEXT.md — locked decisions:
- Intune maps to: intuneInstall + intuneDetection blocks
- RMM maps to: rmmScript block
- rclone.conf OutputBlock is ALWAYS shown regardless of scriptTargets
- Edge case: both deselected → only rclone.conf shown + ZIP has only rclone.conf
- Unmount entirely (NOT CSS-hidden) — no state to preserve between show/hide
Back button pattern (from DeploymentStep.tsx):
```tsx
<div className="flex gap-3 mt-6">
<button
type="button"
onClick={() => dispatch({ type: 'SET_STEP', payload: 2 })}
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
>
Back
</button>
</div>
```
Note: payload is 2 because DeploymentStep is step index 2.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Add scriptTargets filtering and Back button to ReviewStep</name>
<files>src/components/wizard/ReviewStep.tsx</files>
<behavior>
- showIntune = state.deployment.scriptTargets.includes('intune') — boolean
- showRmm = state.deployment.scriptTargets.includes('rmm') — boolean
- {showIntune && <OutputBlock label="Intune Install Script" .../>} — unmounted when false
- {showIntune && <OutputBlock label="Intune Detection Script" .../>} — unmounted when false
- {showRmm && <OutputBlock label="RMM Script" .../>} — unmounted when false
- rclone.conf OutputBlock always rendered (no condition)
- handleDownloadZip builds files array dynamically: rclone.conf always, intune files if showIntune, rmm file if showRmm
- Back button renders at bottom (before ZIP button) with label 'Back', dispatches SET_STEP(2) on click
- useMemo calls for intuneInstall, intuneDetection, rmmScript are KEPT (used in ZIP handler even when hidden)
</behavior>
<action>
Edit src/components/wizard/ReviewStep.tsx:
1. Change `const { state } = useWizard();` to `const { state, dispatch } = useWizard();`
2. After the useMemo calls, add two boolean derivations:
```tsx
const showIntune = state.deployment.scriptTargets.includes('intune');
const showRmm = state.deployment.scriptTargets.includes('rmm');
```
3. Replace handleDownloadZip with the dynamic version:
```tsx
async function handleDownloadZip() {
const files: { name: string; content: string }[] = [
{ name: 'rclone.conf', content: rcloneConf },
];
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');
}
```
4. Wrap Intune OutputBlocks with showIntune condition (unmount entirely):
```tsx
{showIntune && (
<OutputBlock label="Intune Install Script" content={intuneInstall} filename="intune-install.ps1" disabled={!acknowledged} />
)}
{showIntune && (
<OutputBlock label="Intune Detection Script" content={intuneDetection} filename="intune-detection.ps1" disabled={!acknowledged} />
)}
```
5. Wrap RMM OutputBlock with showRmm condition:
```tsx
{showRmm && (
<OutputBlock label="RMM Script" content={rmmScript} filename="rmm-script.ps1" disabled={!acknowledged} />
)}
```
6. Add Back button before the ZIP button, following DeploymentStep's established pattern:
```tsx
<div className="flex gap-3 mt-6">
<button
type="button"
onClick={() => dispatch({ type: 'SET_STEP', payload: 2 })}
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
>
Back
</button>
</div>
```
Do NOT remove any existing comment lines (SECU-01, SECU-02, etc.).
Do NOT touch any useMemo calls — they are needed for ZIP handler even when the blocks are hidden.
The existing DOWN-05 test (both targets selected = 4 files in ZIP) still passes because INITIAL_STATE has scriptTargets=['intune','rmm'].
The existing DOWN-02, DOWN-03, DOWN-04 tests still pass because they use default state with both targets.
Anti-patterns to avoid:
- Do NOT use CSS `hidden` class — must unmount (conditional render with &&)
- Do NOT wrap rclone.conf OutputBlock in a condition — it is always shown
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/wizard/ReviewStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>
All ReviewStep tests pass (green), including:
- Pre-existing CONF-02, CONF-03, DOWN-01 through DOWN-06, SECU-01, SECU-02
- New TECH-01 filtering cases (intune-only, rmm-only, neither, ZIP-neither)
- New TECH-02 Back button case
Zero act() warnings in output (existing tests use fireEvent on checkbox/download buttons which do not trigger async state updates outside the component).
</done>
</task>
</tasks>
<verification>
Run full suite to confirm ReviewStep is green and no regressions:
```
cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run
```
Expected: All ReviewStep tests green. TECH-01 and TECH-02 stubs from Plan 00 now pass.
</verification>
<success_criteria>
- ReviewStep conditionally renders Intune blocks based on showIntune boolean (conditional render, not CSS)
- ReviewStep conditionally renders RMM block based on showRmm boolean
- rclone.conf OutputBlock always renders
- handleDownloadZip builds files array filtered by showIntune/showRmm
- Back button renders and dispatches SET_STEP(2) via dispatch
- All pre-existing ReviewStep tests still pass (DOWN-02/03/04 positional index tests unaffected — default state has both targets)
- New TECH-01 and TECH-02 tests from Plan 00 now pass (GREEN state)
</success_criteria>
<output>
After completion, create `.planning/phases/05-tech-debt/05-02-SUMMARY.md`
</output>
+210
View File
@@ -0,0 +1,210 @@
---
phase: 05-tech-debt
plan: "03"
type: execute
wave: 2
depends_on:
- "05-01"
- "05-02"
files_modified:
- src/components/wizard/BackendSelectionStep.test.tsx
- src/components/wizard/ReviewStep.test.tsx
autonomous: true
requirements:
- TECH-05
must_haves:
truths:
- "BackendSelectionStep test suite runs with zero act() warnings in Vitest output"
- "ReviewStep test suite runs with zero act() warnings (bonus — low-effort fix)"
- "All previously passing tests remain green after the fireEvent-to-userEvent migration"
artifacts:
- path: "src/components/wizard/BackendSelectionStep.test.tsx"
provides: "userEvent-based interactions replacing fireEvent"
contains: "userEvent.setup()"
- path: "src/components/wizard/ReviewStep.test.tsx"
provides: "vi.useFakeTimers() for OutputBlock setTimeout warnings"
contains: "vi.useFakeTimers"
key_links:
- from: "BackendSelectionStep.test.tsx"
to: "userEvent v14 API"
via: "const user = userEvent.setup(); await user.click()"
pattern: "userEvent\\.setup\\(\\)"
---
<objective>
Fix act() warnings in BackendSelectionStep.test.tsx by migrating from fireEvent to userEvent v14, and suppress ReviewStep act() warnings with vi.useFakeTimers() for the OutputBlock setTimeout.
Purpose: TECH-05 — clean test output with zero act() warnings makes CI signal trustworthy and prevents false positives.
Output: Both test files use act()-safe patterns; no production code changes.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/phases/05-tech-debt/05-CONTEXT.md
@.planning/phases/05-tech-debt/05-RESEARCH.md
@.planning/phases/05-tech-debt/05-01-SUMMARY.md
@.planning/phases/05-tech-debt/05-02-SUMMARY.md
<interfaces>
<!-- Key patterns — extracted from RESEARCH.md -->
userEvent v14 API (already installed: @testing-library/user-event 14.6.1):
```typescript
import userEvent from '@testing-library/user-event';
// Per test or per describe block
const user = userEvent.setup();
// ...
await user.click(element); // replaces fireEvent.click(element)
await user.type(input, text); // replaces fireEvent.change(input, { target: { value: text } })
```
The user setup() instance must be created INSIDE describe() or it() (not at module level) to avoid state leakage between tests.
BackendSelectionStep.test.tsx — which fireEvent calls to migrate:
- fireEvent.change(nameInput, { target: { value: '...' } }) → await user.type(nameInput, '...')
NOTE: user.type() appends to existing value. If input has a defaultValue, use user.clear() first or set up user.type with full value.
Alternative: fireEvent.change is safe for setting values (no state updates) — only fireEvent.click triggers async state. Can keep fireEvent.change and only replace fireEvent.click.
- fireEvent.click(azureButton) → await user.click(azureButton)
- All it() callbacks that contain await user.click() MUST be async
vi.useFakeTimers pattern for ReviewStep (for OutputBlock's setCopied setTimeout):
```typescript
import { beforeEach, afterEach, vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
// Note: existing beforeEach in ReviewStep.test.tsx runs vi.clearAllMocks() and sets up stubs
// Add vi.useFakeTimers() call to the EXISTING beforeEach block (not a new one)
});
afterEach(() => {
vi.useRealTimers();
});
```
Root cause reference:
- BackendSelectionStep: fireEvent.click on a backend card → handleSubmit → async React Hook Form validation → dispatch → WizardProvider state update — all without act() wrapping
- ReviewStep: fireEvent.click on Copy button → setCopied(true) → setTimeout(() => setCopied(false), 2000) — timer fires after test assertion, producing act() warning from OutputBlock
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Migrate BackendSelectionStep.test.tsx from fireEvent to userEvent</name>
<files>src/components/wizard/BackendSelectionStep.test.tsx</files>
<behavior>
- All WIZD-01 and WIZD-04 tests continue to pass (same assertions, just act()-safe interactions)
- Vitest output for BackendSelectionStep.test.tsx contains zero occurrences of "act(" in warnings
- Tests that previously used await waitFor() still use it (async validation is still async)
- Tests that clicked backend cards use await user.click() and are async
</behavior>
<action>
Edit src/components/wizard/BackendSelectionStep.test.tsx:
1. Remove fireEvent from the @testing-library/react import (keep render, screen, waitFor)
2. Add import: `import userEvent from '@testing-library/user-event';`
3. For each it() that calls fireEvent.click on a backend card button, convert to userEvent pattern:
- Add `const user = userEvent.setup();` at the top of the it() body
- Make the it() callback async
- Replace `fireEvent.click(azureButton)` with `await user.click(azureButton)`
- Replace `fireEvent.change(nameInput, { target: { value: 'my-remote' } })` with `await user.clear(nameInput); await user.type(nameInput, 'my-remote');`
OR keep fireEvent.change for input value setting and only replace fireEvent.click (fireEvent.change does not trigger act()-needing state updates in this component — either approach is valid)
Specific tests to migrate (all 5 tests in BackendSelectionStep.test.tsx that use fireEvent):
- WIZD-01: 'clicking a backend card dispatches SET_BACKEND_TYPE and SET_STEP'
- WIZD-04: 'shows inline error after first Next attempt with invalid name'
- WIZD-04: 'accepts alphanumeric, dashes, and underscores'
- WIZD-04: 'rejects names with spaces or special characters'
- WIZD-04: 'renders remote name input at the top of the step' (if it uses fireEvent — check)
Tests that only use screen queries and no interactions (e.g., 'renders Azure Blob Storage card') do NOT need changes.
Anti-patterns to avoid:
- Do NOT wrap fireEvent.click in act() manually — replace with userEvent instead
- Do NOT use legacy `userEvent.click()` shorthand — always use `userEvent.setup()` + `await user.click()`
- userEvent.setup() must be inside the test body, not at module level
After changes, verify no fireEvent.click calls remain in the file.
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/wizard/BackendSelectionStep.test.tsx 2>&1 | grep -E "act\(|PASS|FAIL|Tests" | head -20</automated>
</verify>
<done>
All BackendSelectionStep tests pass.
Zero lines containing "act(" appear in the warning output.
Grep for "act(" returns 0 matches in test output.
</done>
</task>
<task type="auto">
<name>Task 2: Fix ReviewStep act() warnings with vi.useFakeTimers</name>
<files>src/components/wizard/ReviewStep.test.tsx</files>
<action>
Edit src/components/wizard/ReviewStep.test.tsx:
1. Add `afterEach` to the existing imports from vitest: `import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';`
2. Add `vi.useFakeTimers()` at the start of the existing `beforeEach` block (before vi.clearAllMocks()):
```typescript
beforeEach(() => {
vi.useFakeTimers(); // ADD THIS LINE FIRST
vi.clearAllMocks();
vi.mocked(downloadZip).mockResolvedValue(undefined);
vi.stubGlobal('navigator', { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:mock'), revokeObjectURL: vi.fn() });
});
```
3. Add a new `afterEach` block after beforeEach:
```typescript
afterEach(() => {
vi.useRealTimers();
});
```
This suppresses the act() warning from OutputBlock's `setTimeout(() => setCopied(false), 2000)` — the timer is frozen during tests and never fires unexpectedly.
No other changes needed — all existing assertions remain valid with fake timers active.
The new TECH-01 and TECH-02 tests from Plan 00 also benefit from this change (no warnings from their interactions either).
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/wizard/ReviewStep.test.tsx 2>&1 | grep -E "act\(|Tests" | head -10</automated>
</verify>
<done>
All ReviewStep tests pass (including TECH-01 and TECH-02 new cases from Plan 00).
Zero lines containing "act(" appear in the test output.
</done>
</task>
</tasks>
<verification>
Full suite must be entirely green with zero act() warnings:
```
cd /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run 2>&1 | grep -E "act\(|Test Files|Tests"
```
Expected:
- "Tests X passed (X)" with no failures
- Zero lines containing "act("
- npx tsc --noEmit: zero errors
</verification>
<success_criteria>
- BackendSelectionStep.test.tsx uses userEvent.setup() + await user.click() for all card interactions
- ReviewStep.test.tsx uses vi.useFakeTimers() in beforeEach and vi.useRealTimers() in afterEach
- Full Vitest run produces zero "act(" warning lines
- All 5 requirements (TECH-01 through TECH-05) verified green in the full test suite
- npx tsc --noEmit: zero errors (phase gate)
</success_criteria>
<output>
After completion, create `.planning/phases/05-tech-debt/05-03-SUMMARY.md`
</output>