--- phase: 01-foundation plan: 02 type: execute wave: 2 depends_on: - 01-01 files_modified: - src/schemas/registry.ts - src/schemas/registry.test.ts - src/schemas/index.test.ts - src/store/reducer.test.ts autonomous: true requirements: [] must_haves: truths: - "BACKEND_REGISTRY exports entries for 'azureblob', 's3', and 's3-compatible'" - "Each registry entry is an array of FieldDef objects with the correct rclone config keys" - "Test stubs exist for all three success criteria (SC-2, SC-3, SC-4) and fail with informative messages" - "npx vitest run fails on registry.test.ts (RED — implementation not yet written for index.ts and reducer)" artifacts: - path: "src/schemas/registry.ts" provides: "BackendType union, FieldDef interface, BACKEND_REGISTRY constant" exports: ["BackendType", "FieldDef", "BACKEND_REGISTRY"] - path: "src/schemas/registry.test.ts" provides: "Test stubs for SC-2 (registry structure verification)" contains: "BACKEND_REGISTRY" - path: "src/schemas/index.test.ts" provides: "Test stubs for SC-3 (Zod schema safeParse verification)" contains: "BACKEND_SCHEMAS" - path: "src/store/reducer.test.ts" provides: "Test stubs for SC-4 (WizardState reducer action verification)" contains: "wizardReducer" key_links: - from: "src/schemas/registry.ts" to: "rclone config keys" via: "FieldDef.key values must match rclone's INI key names exactly" pattern: "key:.*'account'|key:.*'access_key_id'|key:.*'secret_access_key'" --- Create the Backend Schema Registry — the single source of truth for all backend field definitions — and write the Wave 0 test stubs that will drive implementation in Plans 03 and 04. Purpose: The registry is architecturally critical: Phase 2 generators use its keys to build rclone.conf, Phase 3 uses it to render dynamic forms. Wrong keys here cause silently broken configs. Writing test stubs first establishes the acceptance criteria before any implementation. Output: src/schemas/registry.ts with typed field definitions for three backends, plus three test stub files (failing, per TDD RED phase). @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-01-SUMMARY.md Task 1: Create the Backend Schema Registry src/schemas/registry.ts - BackendType is a union: 'azureblob' | 's3' | 's3-compatible' - FieldDef has: key (string), label (string), inputType ('text'|'password'|'select'|'toggle'), required (boolean), and optional placeholder, helpText, options - BACKEND_REGISTRY['azureblob'] has at least 3 fields: account (required), key (optional), sas_url (optional) - BACKEND_REGISTRY['s3'] has at least 4 fields: provider (required, select, value='AWS'), access_key_id (required), secret_access_key (required, password), region (required) - BACKEND_REGISTRY['s3-compatible'] has at least 5 fields: provider (required, select, value='Other'), access_key_id (required), secret_access_key (required, password), endpoint (required), region (optional) - All FieldDef.key values use snake_case matching rclone's actual config key names (not camelCase) Create src/schemas/registry.ts: ```typescript // src/schemas/registry.ts // Backend Schema Registry — single source of truth for all rclone backend field definitions. // IMPORTANT: FieldDef.key values MUST match rclone config key names exactly. // These keys are used by Phase 2 generators to build rclone.conf INI content. // Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before Phase 2. export type BackendType = 'azureblob' | 's3' | 's3-compatible'; export interface FieldDef { key: string; // MUST match rclone config key exactly (snake_case) label: string; inputType: 'text' | 'password' | 'select' | 'toggle'; required: boolean; placeholder?: string; helpText?: string; options?: { value: string; label: string }[]; // for inputType: 'select' } export const BACKEND_REGISTRY: Record = { azureblob: [ { key: 'account', label: 'Storage Account Name', inputType: 'text', required: true, placeholder: 'mystorageaccount', helpText: 'The storage account name (not the full URL)', }, { key: 'key', label: 'Access Key', inputType: 'password', required: false, helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.', }, { key: 'sas_url', label: 'SAS URL', inputType: 'password', required: false, placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...', helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.', }, ], s3: [ { key: 'provider', label: 'Provider', inputType: 'select', required: true, options: [{ value: 'AWS', label: 'Amazon S3' }], }, { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true, placeholder: 'AKIAIOSFODNN7EXAMPLE', }, { key: 'secret_access_key', label: 'Secret Access Key', inputType: 'password', required: true, }, { key: 'region', label: 'Region', inputType: 'text', required: true, placeholder: 'us-east-1', }, ], 's3-compatible': [ { key: 'provider', label: 'Provider', inputType: 'select', required: true, options: [{ value: 'Other', label: 'S3-Compatible' }], helpText: 'Covers Wasabi, MinIO, Cloudflare R2, and any S3-compatible storage', }, { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true, }, { key: 'secret_access_key', label: 'Secret Access Key', inputType: 'password', required: true, }, { key: 'endpoint', label: 'Endpoint URL', inputType: 'text', required: true, placeholder: 'https://s3.wasabisys.com', helpText: 'The S3-compatible endpoint URL for your storage provider', }, { key: 'region', label: 'Region', inputType: 'text', required: false, placeholder: 'us-east-1', helpText: 'Optional for most S3-compatible providers', }, ], }; ``` npx vitest run src/schemas/registry.test.ts 2>&1; echo "exit: $?" registry.ts exports BackendType, FieldDef, and BACKEND_REGISTRY. All three backend entries have the correct rclone-compatible key names. registry.test.ts passes. Task 2: Write Wave 0 test stubs for SC-2, SC-3, SC-4 src/schemas/registry.test.ts, src/schemas/index.test.ts, src/store/reducer.test.ts Create three test stub files. These are the Wave 0 test scaffolds — they define acceptance criteria now so Plans 03 and 04 implement against them (TDD RED phase). Tests for registry.test.ts should PASS; tests for index.test.ts and reducer.test.ts will FAIL (the implementation files don't exist yet). **src/schemas/registry.test.ts** (SC-2 — verifies registry structure; should PASS after Task 1): ```typescript import { describe, it, expect } from 'vitest'; import { BACKEND_REGISTRY, BackendType } from './registry'; const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible']; describe('Backend Schema Registry', () => { it('exports all three required backend types', () => { for (const backend of EXPECTED_BACKENDS) { expect(BACKEND_REGISTRY[backend]).toBeDefined(); } }); it('each backend has at least one field definition', () => { for (const backend of EXPECTED_BACKENDS) { expect(BACKEND_REGISTRY[backend].length).toBeGreaterThan(0); } }); it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => { for (const backend of EXPECTED_BACKENDS) { for (const field of BACKEND_REGISTRY[backend]) { expect(field.key).toBeTruthy(); // Reject camelCase — rclone keys are snake_case or lowercase expect(field.key).not.toMatch(/[A-Z]/); } } }); it('each FieldDef has a non-empty label', () => { for (const backend of EXPECTED_BACKENDS) { for (const field of BACKEND_REGISTRY[backend]) { expect(field.label).toBeTruthy(); } } }); it('Azure Blob has account field (required)', () => { const accountField = BACKEND_REGISTRY.azureblob.find(f => f.key === 'account'); expect(accountField).toBeDefined(); expect(accountField!.required).toBe(true); }); it('S3 has access_key_id, secret_access_key, and region fields', () => { const keys = BACKEND_REGISTRY.s3.map(f => f.key); expect(keys).toContain('access_key_id'); expect(keys).toContain('secret_access_key'); expect(keys).toContain('region'); }); it('S3-compatible has endpoint field (required)', () => { const endpointField = BACKEND_REGISTRY['s3-compatible'].find(f => f.key === 'endpoint'); expect(endpointField).toBeDefined(); expect(endpointField!.required).toBe(true); }); }); ``` **src/schemas/index.test.ts** (SC-3 — will FAIL until Plan 03 creates src/schemas/index.ts): ```typescript import { describe, it, expect } from 'vitest'; import { BACKEND_SCHEMAS } from './index'; describe('Azure Blob Zod schema', () => { it('accepts valid account + access key', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: 'mystorageaccount', key: 'dGVzdGtleQ==', }); expect(result.success).toBe(true); }); it('accepts valid account + sas_url', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: 'mystorageaccount', sas_url: 'https://mystorageaccount.blob.core.windows.net/?sv=2021-01-01', }); expect(result.success).toBe(true); }); it('rejects empty account (required field)', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: '' }); expect(result.success).toBe(false); }); it('rejects missing account (required field)', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ key: 'dGVzdGtleQ==' }); expect(result.success).toBe(false); }); }); describe('S3 Zod schema', () => { it('accepts valid S3 credentials', () => { const result = BACKEND_SCHEMAS.s3.safeParse({ provider: 'AWS', access_key_id: 'AKIAIOSFODNN7EXAMPLE', secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', region: 'us-east-1', }); expect(result.success).toBe(true); }); it('rejects missing access_key_id (required field)', () => { const result = BACKEND_SCHEMAS.s3.safeParse({ provider: 'AWS', secret_access_key: 'secret', region: 'us-east-1', }); expect(result.success).toBe(false); }); }); describe('S3-compatible Zod schema', () => { it('accepts valid S3-compatible credentials with endpoint', () => { const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ provider: 'Other', access_key_id: 'mykey', secret_access_key: 'mysecret', endpoint: 'https://s3.wasabisys.com', }); expect(result.success).toBe(true); }); it('rejects missing endpoint (required for s3-compatible)', () => { const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ provider: 'Other', access_key_id: 'mykey', secret_access_key: 'mysecret', }); expect(result.success).toBe(false); }); }); ``` **src/store/reducer.test.ts** (SC-4 — will FAIL until Plan 04 creates reducer.ts and types.ts): ```typescript import { describe, it, expect } from 'vitest'; import { wizardReducer } from './reducer'; import { INITIAL_STATE, WizardState } from './types'; describe('wizardReducer', () => { it('returns INITIAL_STATE on first call', () => { // @ts-expect-error intentional undefined action for initialization test const state = wizardReducer(undefined, { type: '@@INIT' }); expect(state.currentStep).toBe(0); expect(state.remote.backendType).toBeNull(); expect(state.remote.name).toBe(''); expect(state.remote.params).toEqual({}); expect(state.deployment.includeInstall).toBe(false); expect(state.deployment.configPath).toBe('machine-wide'); }); it('SET_STEP updates currentStep', () => { const state = wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 2 }); expect(state.currentStep).toBe(2); }); it('SET_BACKEND_TYPE updates remote.backendType', () => { const state = wizardReducer(INITIAL_STATE, { type: 'SET_BACKEND_TYPE', payload: 'azureblob' }); expect(state.remote.backendType).toBe('azureblob'); }); it('SET_REMOTE_NAME updates remote.name', () => { const state = wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_NAME', payload: 'my-blob' }); expect(state.remote.name).toBe('my-blob'); }); it('SET_REMOTE_PARAMS updates remote.params', () => { const state = wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_PARAMS', payload: { account: 'myaccount', key: 'mykey' }, }); expect(state.remote.params).toEqual({ account: 'myaccount', key: 'mykey' }); }); it('SET_DEPLOYMENT partially updates deployment', () => { const state = wizardReducer(INITIAL_STATE, { type: 'SET_DEPLOYMENT', payload: { includeInstall: true }, }); expect(state.deployment.includeInstall).toBe(true); expect(state.deployment.configPath).toBe('machine-wide'); // unchanged }); it('RESET returns to INITIAL_STATE', () => { const modified: WizardState = { ...INITIAL_STATE, currentStep: 3, remote: { name: 'test', backendType: 's3', params: { region: 'us-east-1' } }, }; const state = wizardReducer(modified, { type: 'RESET' }); expect(state).toEqual(INITIAL_STATE); }); it('is a pure function — does not mutate input state', () => { const before = { ...INITIAL_STATE }; wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 5 }); expect(INITIAL_STATE.currentStep).toBe(before.currentStep); }); }); ``` After creating all three files, run the test suite. registry.test.ts should pass; index.test.ts and reducer.test.ts will fail with "Cannot find module" errors — this is correct (RED phase). npx vitest run src/schemas/registry.test.ts 2>&1; echo "registry exit: $?" All three test stub files exist. registry.test.ts passes (GREEN). index.test.ts and reducer.test.ts fail with module-not-found errors (RED — expected). Test stubs provide complete acceptance criteria for Plans 03 and 04. 1. `npx vitest run src/schemas/registry.test.ts` — all tests pass 2. `npx vitest run src/schemas/index.test.ts` — fails with "Cannot find module './index'" (expected at this stage) 3. `npx vitest run src/store/reducer.test.ts` — fails with "Cannot find module './reducer'" (expected at this stage) 4. `cat src/schemas/registry.ts` — confirm no camelCase keys (grep for uppercase in key values) - src/schemas/registry.ts exports BackendType, FieldDef, BACKEND_REGISTRY - All FieldDef.key values are snake_case matching rclone config key names - BACKEND_REGISTRY contains entries for all three backends with correct field definitions - registry.test.ts passes (7 tests green) - index.test.ts and reducer.test.ts exist and fail with module-not-found (RED phase — correct) After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` using the summary template.