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>
21 KiB
21 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-new-backends | 00 | tdd | 0 |
|
true |
|
|
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.
<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>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-new-backends/06-RESEARCH.mdFrom src/schemas/registry.test.ts:
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:
// 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:
// 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>);
}
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.
```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.
```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.
<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>