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:
@@ -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-01–DOWN-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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user