Files
kawaandClaude Sonnet 4.6 b314e65536 docs(06-new-backends): create phase plan
4 plans across 3 waves: Wave 0 TDD stubs, Wave 1 data layer (parallel: OneDrive/GCS/B2 + SFTP/SftpAuthToggle), Wave 2 RemoteConfigStep wiring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 13:38:46 +02:00

507 lines
21 KiB
Markdown

---
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"
---
<objective>
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.
</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/06-new-backends/06-RESEARCH.md
<interfaces>
<!-- Existing test file shapes the executor must follow. -->
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 <RemoteConfigStep key={backendType} />;
}
return render(<WizardProvider><Setup /></WizardProvider>);
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Update registry.test.ts with 7-backend assertions (RED)</name>
<files>src/schemas/registry.test.ts</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Update rclone-conf.test.ts with new backend fixtures (RED)</name>
<files>src/generators/rclone-conf.test.ts</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/generators/rclone-conf.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Update RemoteConfigStep.test.tsx with new backend describe blocks (RED)</name>
<files>src/components/wizard/RemoteConfigStep.test.tsx</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>RemoteConfigStep.test.tsx has describe blocks for OneDrive, SFTP (with toggle), GCS, and B2; tests fail (RED) because implementation does not exist yet</done>
</task>
</tasks>
<verification>
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.
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/06-new-backends/06-00-SUMMARY.md`
</output>