--- phase: 06-new-backends plan: "00" type: tdd wave: 0 depends_on: [] files_modified: - src/schemas/registry.test.ts - src/generators/rclone-conf.test.ts - src/components/wizard/RemoteConfigStep.test.tsx autonomous: true requirements: - BACK-01 - BACK-02 - BACK-03 - BACK-04 must_haves: truths: - "registry.test.ts fails (red) with 4 new backend assertions before implementation" - "rclone-conf.test.ts fails (red) with fixtures for onedrive, sftp-password, sftp-key, gcs, b2" - "RemoteConfigStep.test.tsx fails (red) with describe blocks for OneDrive, SFTP, GCS, B2" - "All test stubs reference real field names per rclone docs (token, drive_id, drive_type, host, user, pass, key_pem, project_number, service_account_credentials, account, key)" artifacts: - path: "src/schemas/registry.test.ts" provides: "Failing assertions for all 7 backends including 4 new ones" contains: "EXPECTED_BACKENDS" - path: "src/generators/rclone-conf.test.ts" provides: "Failing fixtures for onedrive, sftp (password), sftp (key), gcs, b2" contains: "type = google cloud storage" - path: "src/components/wizard/RemoteConfigStep.test.tsx" provides: "Failing describe blocks for BACK-01 through BACK-04" contains: "BACK-04: Backblaze B2" key_links: - from: "src/schemas/registry.test.ts" to: "src/schemas/registry.ts" via: "EXPECTED_BACKENDS array import" pattern: "EXPECTED_BACKENDS.*BackendType" - from: "src/generators/rclone-conf.test.ts" to: "src/generators/rclone-conf.ts" via: "buildRcloneConf fixtures with new WizardState shapes" pattern: "backendType.*gcs|b2|onedrive|sftp" --- Write Wave 0 failing test stubs for all four new backends across three test files. Purpose: Establish RED state before any implementation — ensures tests drive and verify the work in Plans 01-03. Output: Three updated test files with failing assertions for OneDrive, SFTP, GCS, and Backblaze B2. @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/STATE.md @.planning/phases/06-new-backends/06-RESEARCH.md From src/schemas/registry.test.ts: ```typescript const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible']; // Pattern: loops over EXPECTED_BACKENDS for generic assertions, // then individual describe blocks for backend-specific field assertions ``` From src/generators/rclone-conf.test.ts: ```typescript // Fixture pattern: const azureState: WizardState = { ...INITIAL_STATE, remote: { name: 'my-azure', backendType: 'azureblob', params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' } }, }; // describe('buildRcloneConf — azureblob', () => { ... }) ``` From src/components/wizard/RemoteConfigStep.test.tsx: ```typescript // renderWithBackend helper: function renderWithBackend(backendType: BackendType) { function Setup() { const { dispatch } = useWizard(); useEffect(() => { dispatch({ type: 'SET_BACKEND_TYPE', payload: backendType }); dispatch({ type: 'SET_STEP', payload: 1 }); }, []); return ; } return render(); } ``` Task 1: Update registry.test.ts with 7-backend assertions (RED) src/schemas/registry.test.ts - EXPECTED_BACKENDS now includes all 7 types: ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'] - Generic loop tests (has fields, has displayName/description, snake_case keys) run over all 7 - New describe 'OneDrive': token field (required), drive_id field (required), drive_type field (required, select) - New describe 'SFTP': host field (required), user field (required), pass field (optional), key_pem field (optional) - New describe 'GCS': project_number field (required), service_account_credentials field (required) - New describe 'Backblaze B2': account field (required), key field (required) - Running `npx vitest run src/schemas/registry.test.ts` MUST fail (RED) because registry.ts still has only 3 BackendType values Replace the content of src/schemas/registry.test.ts with the updated version. 1. Change EXPECTED_BACKENDS to: `const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];` Note: This line will fail TypeScript because BackendType in registry.ts does not yet include the 4 new values. That is the intended RED state. 2. Keep all existing generic loop tests and existing backend-specific describe blocks unchanged. 3. Append four new describe blocks at the bottom: ```typescript describe('OneDrive backend', () => { it('has token field (required)', () => { const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'token'); expect(f).toBeDefined(); expect(f!.required).toBe(true); expect(f!.inputType).toBe('password'); }); it('has drive_id field (required)', () => { const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'drive_id'); expect(f).toBeDefined(); expect(f!.required).toBe(true); }); it('has drive_type field as select', () => { const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'drive_type'); expect(f).toBeDefined(); expect(f!.inputType).toBe('select'); expect(f!.options).toBeDefined(); }); }); describe('SFTP backend', () => { it('has host field (required)', () => { const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'host'); expect(f).toBeDefined(); expect(f!.required).toBe(true); }); it('has user field (required)', () => { const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'user'); expect(f).toBeDefined(); expect(f!.required).toBe(true); }); it('has pass field (optional — SftpAuthToggle handles display)', () => { const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'pass'); expect(f).toBeDefined(); expect(f!.required).toBe(false); }); it('has key_pem field (optional — SftpAuthToggle handles display)', () => { const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'key_pem'); expect(f).toBeDefined(); expect(f!.required).toBe(false); }); }); describe('Google Cloud Storage backend', () => { it('has project_number field (required)', () => { const f = BACKEND_REGISTRY.gcs.fields.find(f => f.key === 'project_number'); expect(f).toBeDefined(); expect(f!.required).toBe(true); }); it('has service_account_credentials field (required, password)', () => { const f = BACKEND_REGISTRY.gcs.fields.find(f => f.key === 'service_account_credentials'); expect(f).toBeDefined(); expect(f!.required).toBe(true); expect(f!.inputType).toBe('password'); }); }); describe('Backblaze B2 backend', () => { it('has account field (required) — applicationKeyId', () => { const f = BACKEND_REGISTRY.b2.fields.find(f => f.key === 'account'); expect(f).toBeDefined(); expect(f!.required).toBe(true); }); it('has key field (required) — application key secret', () => { const f = BACKEND_REGISTRY.b2.fields.find(f => f.key === 'key'); expect(f).toBeDefined(); expect(f!.required).toBe(true); expect(f!.inputType).toBe('password'); }); }); ``` Do NOT suppress TypeScript errors. The RED state is the goal. MISSING — run after Wave 0 to confirm RED: npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20 registry.test.ts references all 7 backends and asserts new field requirements; file committed; test suite fails (RED) because registry.ts has not been updated yet Task 2: Update rclone-conf.test.ts with new backend fixtures (RED) src/generators/rclone-conf.test.ts - Five new WizardState fixtures: onedriveState, sftpPasswordState, sftpKeyState, gcsState, b2State - describe 'buildRcloneConf — onedrive': type = onedrive, contains token, drive_id, drive_type - describe 'buildRcloneConf — sftp (password)': type = sftp, contains host, user, pass; does NOT contain key_pem - describe 'buildRcloneConf — sftp (key)': type = sftp, contains host, user, key_pem; does NOT contain pass - describe 'buildRcloneConf — gcs': type = google cloud storage (WITH spaces), contains project_number, service_account_credentials - describe 'buildRcloneConf — b2': type = b2, contains account (applicationKeyId), key (application key) - Running `npx vitest run src/generators/rclone-conf.test.ts` MUST fail (RED) because RCLONE_TYPE_MAP has no entries for onedrive/sftp/gcs/b2 Append the following fixtures and describe blocks to the END of src/generators/rclone-conf.test.ts (after the existing error cases block). Do not modify existing content. ```typescript // --- New backend fixtures (Phase 6) --- const onedriveState: WizardState = { ...INITIAL_STATE, remote: { name: 'my-onedrive', backendType: 'onedrive', params: { token: '{"access_token":"TOKEN","token_type":"Bearer","refresh_token":"REFRESH","expiry":"2026-01-01T00:00:00Z"}', drive_id: 'b!TESTDRIVEID', drive_type: 'business', }, }, }; const sftpPasswordState: WizardState = { ...INITIAL_STATE, remote: { name: 'my-sftp-pass', backendType: 'sftp', params: { host: 'sftp.example.com', user: 'admin', pass: 'secretpass', key_pem: '' }, }, }; const sftpKeyState: WizardState = { ...INITIAL_STATE, remote: { name: 'my-sftp-key', backendType: 'sftp', params: { host: 'sftp.example.com', user: 'admin', pass: '', key_pem: '-----BEGIN RSA PRIVATE KEY-----\nMIIEo...\n-----END RSA PRIVATE KEY-----' }, }, }; const gcsState: WizardState = { ...INITIAL_STATE, remote: { name: 'my-gcs', backendType: 'gcs', params: { project_number: '123456789', service_account_credentials: '{"type":"service_account","project_id":"my-project"}', }, }, }; const b2State: WizardState = { ...INITIAL_STATE, remote: { name: 'my-b2', backendType: 'b2', params: { account: 'APP_KEY_ID', key: 'APP_KEY_SECRET' }, }, }; describe('buildRcloneConf — onedrive', () => { it('contains type = onedrive', () => { expect(buildRcloneConf(onedriveState)).toContain('type = onedrive'); }); it('contains token value', () => { expect(buildRcloneConf(onedriveState)).toContain('token = '); }); it('contains drive_id', () => { expect(buildRcloneConf(onedriveState)).toContain('drive_id = b!TESTDRIVEID'); }); it('contains drive_type = business', () => { expect(buildRcloneConf(onedriveState)).toContain('drive_type = business'); }); }); describe('buildRcloneConf — sftp (password auth)', () => { it('contains type = sftp', () => { expect(buildRcloneConf(sftpPasswordState)).toContain('type = sftp'); }); it('contains host', () => { expect(buildRcloneConf(sftpPasswordState)).toContain('host = sftp.example.com'); }); it('contains user', () => { expect(buildRcloneConf(sftpPasswordState)).toContain('user = admin'); }); it('contains pass', () => { expect(buildRcloneConf(sftpPasswordState)).toContain('pass = secretpass'); }); it('omits key_pem when empty', () => { expect(buildRcloneConf(sftpPasswordState)).not.toContain('key_pem'); }); }); describe('buildRcloneConf — sftp (key auth)', () => { it('contains type = sftp', () => { expect(buildRcloneConf(sftpKeyState)).toContain('type = sftp'); }); it('contains key_pem', () => { expect(buildRcloneConf(sftpKeyState)).toContain('key_pem = -----BEGIN RSA PRIVATE KEY-----'); }); it('omits pass when empty', () => { expect(buildRcloneConf(sftpKeyState)).not.toContain('pass ='); }); }); describe('buildRcloneConf — gcs', () => { it('contains type = google cloud storage (with spaces)', () => { expect(buildRcloneConf(gcsState)).toContain('type = google cloud storage'); }); it('does NOT contain type = gcs', () => { expect(buildRcloneConf(gcsState)).not.toContain('type = gcs'); }); it('contains project_number', () => { expect(buildRcloneConf(gcsState)).toContain('project_number = 123456789'); }); it('contains service_account_credentials', () => { expect(buildRcloneConf(gcsState)).toContain('service_account_credentials = '); }); }); describe('buildRcloneConf — b2', () => { it('contains type = b2', () => { expect(buildRcloneConf(b2State)).toContain('type = b2'); }); it('contains account (applicationKeyId)', () => { expect(buildRcloneConf(b2State)).toContain('account = APP_KEY_ID'); }); it('contains key (application key secret)', () => { expect(buildRcloneConf(b2State)).toContain('key = APP_KEY_SECRET'); }); }); ``` Note: WizardState type accepts `backendType: 'onedrive' | 'sftp' | 'gcs' | 'b2'` only after Plan 01/02 extend BackendType. In RED state TypeScript will error on these — that is expected. If needed, cast as `backendType: 'onedrive' as BackendType` to allow the file to parse while staying RED on the runtime assertions. MISSING — run after Wave 0 to confirm RED: npx vitest run src/generators/rclone-conf.test.ts 2>&1 | tail -20 rclone-conf.test.ts has fixtures and assertions for all five new backend scenarios; tests fail (RED) because RCLONE_TYPE_MAP does not yet contain the new types Task 3: Update RemoteConfigStep.test.tsx with new backend describe blocks (RED) src/components/wizard/RemoteConfigStep.test.tsx - New describe 'BACK-01 (Phase 6): OneDrive form': renders OAuth Token field (password), renders Drive ID field, renders Drive Type select - New describe 'BACK-02 (Phase 6): SFTP form': renders Host field, renders Username field, renders Password tab button, renders Private Key tab button, Password tab shows pass field, Private Key tab shows key_pem field, switching tabs preserves hidden field value (CSS-hidden pattern, same as Azure toggle test) - New describe 'BACK-03 (Phase 6): GCS form': renders Project Number field, renders Service Account JSON field - New describe 'BACK-04 (Phase 6): Backblaze B2 form': renders Application Key ID field, renders Application Key field - Running `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` MUST fail (RED) because RemoteConfigStep.tsx does not yet handle these backends and BackendType does not include them Append the following describe blocks to the BOTTOM of the existing describe('RemoteConfigStep') block in src/components/wizard/RemoteConfigStep.test.tsx (inside the outermost describe, after the BACK-03 S3-Compatible block): ```typescript describe('BACK-01 (Phase 6): OneDrive form', () => { it('renders OAuth Token field', async () => { renderWithBackend('onedrive' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/oauth token/i)).toBeDefined(); }); }); it('renders Drive ID field', async () => { renderWithBackend('onedrive' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/drive id/i)).toBeDefined(); }); }); it('renders Drive Type select', async () => { renderWithBackend('onedrive' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/drive type/i)).toBeDefined(); }); }); }); describe('BACK-02 (Phase 6): SFTP form', () => { it('renders Host field', async () => { renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/host/i)).toBeDefined(); }); }); it('renders Username field', async () => { renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/username/i)).toBeDefined(); }); }); it('shows Password tab button by default', async () => { renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByText(/^password$/i, { selector: 'button' })).toBeDefined(); }); }); it('shows Private Key tab button', async () => { renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByText(/private key/i, { selector: 'button' })).toBeDefined(); }); }); it('switching to Private Key tab CSS-hides password field and shows key_pem field', async () => { renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByText(/private key/i, { selector: 'button' })).toBeDefined(); }); fireEvent.click(screen.getByText(/private key/i, { selector: 'button' })); await waitFor(() => { const passInput = screen.getByLabelText(/^password$/i); const passWrapper = passInput.closest('div.hidden'); expect(passWrapper).toBeDefined(); const keyInput = screen.getByLabelText(/private key.*pem/i); const keyWrapper = keyInput.closest('div.block'); expect(keyWrapper).toBeDefined(); }); }); it('switching SFTP auth tabs preserves hidden field value', async () => { const user = userEvent.setup(); renderWithBackend('sftp' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/^password$/i)).toBeDefined(); }); await user.type(screen.getByLabelText(/^password$/i), 'mysftppass'); fireEvent.click(screen.getByText(/private key/i, { selector: 'button' })); fireEvent.click(screen.getByText(/^password$/i, { selector: 'button' })); await waitFor(() => { const input = screen.getByLabelText(/^password$/i) as HTMLInputElement; expect(input.value).toBe('mysftppass'); }); }); }); describe('BACK-03 (Phase 6): Google Cloud Storage form', () => { it('renders Project Number field', async () => { renderWithBackend('gcs' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/project number/i)).toBeDefined(); }); }); it('renders Service Account JSON field', async () => { renderWithBackend('gcs' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/service account json/i)).toBeDefined(); }); }); }); describe('BACK-04 (Phase 6): Backblaze B2 form', () => { it('renders Application Key ID field', async () => { renderWithBackend('b2' as BackendType); await waitFor(() => { expect(screen.getByLabelText(/application key id/i)).toBeDefined(); }); }); it('renders Application Key field', async () => { renderWithBackend('b2' as BackendType); await waitFor(() => { // Matches "Application Key" but not "Application Key ID" expect(screen.getByLabelText(/application key$/i)).toBeDefined(); }); }); }); ``` The `as BackendType` casts allow the file to parse. Tests will fail at runtime (RED) because RemoteConfigStep does not yet know about these backends and TypeScript errors on the union. MISSING — run after Wave 0 to confirm RED: npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | tail -20 RemoteConfigStep.test.tsx has describe blocks for OneDrive, SFTP (with toggle), GCS, and B2; tests fail (RED) because implementation does not exist yet After all three tasks: run the full suite to confirm RED state. `npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts src/components/wizard/RemoteConfigStep.test.tsx` Expected: failures for all new backend assertions. Existing tests (azureblob, s3, s3-compatible) must still pass. - All three test files updated with new backend assertions - Existing passing tests remain green - New backend assertions are red (implementation not yet done) - No test stubs use vague matchers — all assertions reference exact field labels and rclone keys After completion, create `.planning/phases/06-new-backends/06-00-SUMMARY.md`