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>
507 lines
21 KiB
Markdown
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>
|