343 lines
17 KiB
Markdown
343 lines
17 KiB
Markdown
# TalkEdit — Testing & Robustness Plan
|
|
|
|
Tests are critical before launch to prevent regressions and ensure the app is stable. Below is every function in the codebase that needs test coverage, organized by module.
|
|
|
|
---
|
|
|
|
## 1. Rust backend (`src-tauri/src/`)
|
|
|
|
### licensing.rs — already has partial coverage (4 tests)
|
|
- [ ] `verify_license_key` — valid key, invalid format, invalid signature, expired key ✅ (exists)
|
|
- [ ] `load_cached_license` — file exists, file missing, malformed file
|
|
- [ ] `cache_license` — write and read back
|
|
- [ ] `remove_license` — removes file, no-ops if missing
|
|
- [ ] `get_or_start_trial` — creates new trial file, loads existing, handles corrupt file
|
|
- [ ] `get_trial_info` — active trial (29 days), expired trial (0 days), exactly at boundary
|
|
- [ ] `get_trial_days_remaining` — active returns Some, expired returns None,
|
|
- [ ] `clear_trial` — removes file, no-ops if missing
|
|
- [ ] `get_app_status` — licensed (cached license), trial active, expired (no license, no trial)
|
|
- [ ] `has_feature` — feature exists, feature missing, empty features list
|
|
|
|
### models.rs — no existing tests
|
|
- [ ] `list_models` — whisper models found, llm models found, mixed, empty dirs
|
|
- [ ] `delete_model` — deletes file, deletes directory, path doesn't exist
|
|
- [ ] `huggingface_cache_dir` — HF_HOME set, XDG_CACHE_HOME set, defaults to ~/.cache
|
|
|
|
### transcription.rs — no existing tests
|
|
- [ ] `ensure_model_downloaded` — returns success (stub function)
|
|
|
|
### paths.rs — no existing tests
|
|
- [ ] `project_root` — dev layout, packaged (TAURI_RESOURCE_DIR), fallback
|
|
- [ ] `python_exe` — bundled path, venv paths, fallback to .venv312
|
|
- [ ] `backend_script` — joins project_root/backend
|
|
- [ ] `root_script` — joins project_root
|
|
|
|
---
|
|
|
|
## 2. Frontend store (`frontend/src/store/`)
|
|
|
|
### editorStore.ts (Zustand) — partially covered (2 tests)
|
|
- [ ] `reset` ✅ (in beforeEach)
|
|
- [ ] `setGlobalGainDb` — clamps to -24/+24 ✅ (exists)
|
|
- [ ] `addGainRange` — adds with correct start/end/gain ✅ (exists)
|
|
- [ ] `addCutRange` — adds with correct start/end, handles overlapping ranges
|
|
- [ ] `addMuteRange` — adds with correct start/end
|
|
- [ ] `addSpeedRange` — adds with correct start/end/speed
|
|
- [ ] `removeCutRange` — removes existing, no-ops on missing
|
|
- [ ] `removeMuteRange` — removes existing, no-ops on missing
|
|
- [ ] `removeGainRange` — removes existing, no-ops on missing
|
|
- [ ] `removeSpeedRange` — removes existing, no-ops on missing
|
|
- [ ] `updateCutRange` — updates bounds, prevents negative duration
|
|
- [ ] `updateMuteRange` — updates bounds
|
|
- [ ] `updateGainRangeBounds` — updates bounds
|
|
- [ ] `updateSpeedRangeBounds` — updates bounds
|
|
- [ ] `updateGainRange` — updates gain value
|
|
- [ ] `updateSpeedRange` — updates speed value
|
|
- [ ] `setSelectedWordIndices` — single, multiple, empty, out of range handled
|
|
- [ ] `replaceWordRange` — replaces words in middle, replaces at start, handles invalid indices
|
|
- [ ] `updateWordText` — updates word, preserves timing, no-ops on bad index
|
|
- [ ] `getWordAtTime` — exact match, between words, before first word, after last word, no words
|
|
- [ ] `loadVideo` — sets videoUrl, resets state, handles missing file
|
|
- [ ] `setCurrentTime` — sets time, clamps to 0-duration
|
|
- [ ] `setTranscribing` — toggles flag, sets status
|
|
- [ ] `setTranscription` — sets words and segments, handles empty arrays
|
|
- [ ] `setMarkInTime` / `setMarkOutTime` — sets and clears
|
|
- [ ] `clearMarkRange` — clears both marks
|
|
- [ ] `addTimelineMarker` — adds with label/color/time
|
|
- [ ] `removeTimelineMarker` — removes by id
|
|
- [ ] `updateTimelineMarker` — updates label/color
|
|
- [ ] `setZonePreviewPaddingSeconds` — sets and clamps
|
|
- [ ] `setBackgroundMusic` / `updateBackgroundMusic` — sets and updates
|
|
- [ ] `setAdditionalClips` — adds, removes, reorders
|
|
- [ ] `setSilenceTrimGroups` — sets groups
|
|
|
|
### licenseStore.ts — no existing tests
|
|
- [ ] `canEdit` — true for Licensed, true for Trial, false for Expired, false for null
|
|
- [ ] `checkStatus` — calls getAppStatus, sets correct state, handles error (falls to Expired)
|
|
- [ ] `activateLicense` — valid key sets Licensed, invalid key returns false
|
|
- [ ] `deactivateLicense` — reverts to trial if valid, falls to Expired otherwise
|
|
- [ ] `hasFeature` — returns true for matching, false for missing
|
|
- [ ] `setShowDialog` — toggles dialog visibility
|
|
- [ ] Persist middleware — Licensed status persists, Trial/Expired does not
|
|
|
|
### aiStore.ts — no existing tests
|
|
- [ ] `setProviderConfig` — updates provider, encrypts API keys
|
|
- [ ] `setDefaultProvider` — changes default
|
|
- [ ] `setCustomFillerWords` — sets and clears
|
|
- [ ] `setFillerResult` — sets and clears
|
|
- [ ] `setProcessing` — toggles with message
|
|
|
|
---
|
|
|
|
## 3. Frontend hooks (`frontend/src/hooks/`)
|
|
|
|
### useKeyboardShortcuts.ts — no existing tests
|
|
- [ ] Keyboard event dispatch — space plays/pauses, J/K/L speed controls, I/O marks, Delete cuts, Ctrl+Z undo
|
|
- [ ] `toggleCheatsheet` — creates overlay with correct content, toggles off
|
|
- [ ] Skip logic — skips correctly forward and back from current playhead
|
|
|
|
### useVideoSync.ts — no existing tests
|
|
- [ ] Synchronization of store `isPlaying` with video element, audio element
|
|
- [ ] `togglePlay` — starts playing, pauses
|
|
- [ ] `seekTo` — seeks video to correct time, seeks audio to correct time
|
|
- [ ] Handles video element ref being null (doesn't crash)
|
|
|
|
---
|
|
|
|
## 4. Frontend lib (`frontend/src/lib/`)
|
|
|
|
### keybindings.ts — no existing tests
|
|
- [ ] `loadBindings` — loads from localStorage, falls back to standard preset when missing
|
|
- [ ] `saveBindings` — persists and reloads correctly
|
|
- [ ] `applyPreset` — 'standard' and 'left-hand' both apply all required bindings
|
|
- [ ] `detectConflicts` — detects duplicate keys, returns empty when no conflicts
|
|
|
|
---
|
|
|
|
## 5. Backend services (`backend/services/`)
|
|
|
|
### video_editor.py — no existing tests
|
|
- [ ] `apply_cut_segments` — keeps correct segments from transcript word list, FFmpeg concat cmd
|
|
- [ ] `apply_mute_ranges` — cuts audio for muted ranges
|
|
- [ ] `apply_gain_ranges` — adjusts volume (positive and negative) for FFmpeg filter chains
|
|
- [ ] `apply_speed_ranges` — time-stretches or compresses segments
|
|
- [ ] `mix_background_music` — mixes with ducking enabled, mixes without ducking, handles no music
|
|
- [ ] `build_export_filters` — stitches together all zone types into correct filter order
|
|
|
|
### audio_cleaner.py — no existing tests
|
|
- [ ] `detect_silence` — detects pauses above threshold, returns correct time ranges
|
|
- [ ] `remove_silence` — splits by silence, re-concatenates keep segments
|
|
|
|
### ai_provider.py — no existing tests
|
|
- [ ] `complete` — Ollama completion succeeds, OpenAI completion succeeds, Claude completion succeeds
|
|
- [ ] `complete` — handles missing provider, timeout, bad JSON response
|
|
- [ ] `list_ollama_models` — returns models list, handles connection error
|
|
|
|
### transcription.py — no existing tests
|
|
- [ ] `transcribe_file` — returns words with correct format (word, start, end, confidence)
|
|
- [ ] `transcribe_segment` — re-transcribes a range with offset-adjusted timestamps
|
|
- [ ] `_load_model` — caches model, returns existing from cache, handles GPU/CPU
|
|
|
|
### caption_generator.py — no existing tests
|
|
- [ ] `generate_srt` — correct SRT format, sequential numbering, proper timestamps
|
|
- [ ] `generate_vtt` — correct VTT format with header, chroma key tags
|
|
- [ ] `generate_ass` — correct ASS subtitle format
|
|
|
|
### gpu_utils.py — no existing tests
|
|
- [ ] `get_optimal_device` — returns CUDA when available, returns CPU otherwise
|
|
|
|
### audio_processing.py — no existing tests
|
|
- [ ] `extract_audio` — extracts wav from video, handles audio-only input, temp file cleanup
|
|
|
|
---
|
|
|
|
## 6. Frontend components — integration tests
|
|
|
|
### TranscriptEditor.tsx
|
|
- [ ] Word selection: click, shift+click range, drag select
|
|
- [ ] Ctrl+click seeks video to correct time
|
|
- [ ] Double-click enters edit mode, Enter commits, Escape cancels
|
|
- [ ] Zone mode drag creates correct zone type
|
|
- [ ] Search finds matches, navigates with Enter/Shift+Enter
|
|
- [ ] "Restore" button appears on hover over words in a zone, removes the zone
|
|
- [ ] Re-transcribe calls backend and updates words
|
|
- [ ] Selection toolbar buttons create correct zone types
|
|
- [ ] When `canEdit` is false, buttons are disabled and zone creation is blocked
|
|
|
|
### WaveformTimeline.tsx
|
|
- [ ] Canvas renders waveform when audio data loads
|
|
- [ ] Click seeks to correct time
|
|
- [ ] Zone drag creates zone on mouse up
|
|
- [ ] Zone selection and resize with handles
|
|
- [ ] Delete key removes selected zone
|
|
- [ ] Zoom and scroll work correctly
|
|
- [ ] Zone toggle buttons show/hide overlay layers
|
|
- [ ] Loading spinner shows when no waveform data
|
|
- [ ] Error message with retry button on load failure
|
|
|
|
### ExportDialog.tsx
|
|
- [ ] Fast mode card and re-encode card toggle correctly
|
|
- [ ] Resolution selector only visible in re-encode mode
|
|
- [ ] Format selector disables WAV for video files
|
|
- [ ] Export button triggers export with correct parameters
|
|
- [ ] Progress bar updates during export
|
|
- [ ] Loudness normalization checkbox shows LUFS target selector
|
|
|
|
### AIPanel.tsx
|
|
- [ ] Filler words tab: detect button sends request, displays results
|
|
- [ ] Apply All creates cut ranges for all fillers
|
|
- [ ] Clips tab: find clips shows suggestions
|
|
- [ ] Reprocess tab: model selector + reprocess button
|
|
- [ ] Error state with retry on API failure
|
|
|
|
### App.tsx — layout and toolbar
|
|
- [ ] Welcome screen shows when no video loaded
|
|
- [ ] Trial bar shows on welcome screen for Trial/Expired states
|
|
- [ ] Toolbar buttons toggle modes correctly (Cut, Mute, Gain, Speed)
|
|
- [ ] Toolbar buttons open correct panels (Zones, Silence, Markers, Music, Append, AI, Export, Settings, Help)
|
|
- [ ] File menu opens/closes, items work
|
|
- [ ] Split pane dividers are draggable and keyboard-accessible
|
|
- [ ] First-run welcome overlay shows once
|
|
- [ ] Hotkeys work: Escape clears modes, ? opens cheatsheet
|
|
|
|
---
|
|
|
|
## 7. Error handling regression tests
|
|
|
|
- [ ] Backend crash: show reconnect banner, not broken UI
|
|
- [ ] Transcription failure: show error, allow retry with different model
|
|
- [ ] Export failure: show FFmpeg stderr, allow copy
|
|
- [ ] Model download timeout: show error, allow retry
|
|
- [ ] File not found: handled gracefully on open/load
|
|
- [ ] Permission denied: handled gracefully on save/export
|
|
- [ ] Concurrent operations: block export during transcription, block transcription during export
|
|
|
|
---
|
|
|
|
## 8. Licensing & trial flow tests
|
|
|
|
- [ ] Fresh install: shows 30-day trial
|
|
- [ ] Day 29: still allows editing
|
|
- [ ] Day 31: shows expired, editing locked, export still works
|
|
- [ ] Activate valid license: switches to Licensed, clears trial
|
|
- [ ] Activate invalid license: shows error, stays on trial
|
|
- [ ] Deactivate license: returns to trial if valid, expires if trial over
|
|
- [ ] Expired banner shows correct message and activate link
|
|
- [ ] `canEdit` prop correctly gates all editing controls across all components
|
|
|
|
---
|
|
|
|
## Test infrastructure
|
|
|
|
| Layer | Framework | Run command |
|
|
|-------|-----------|-------------|
|
|
| Rust | `cargo test` (built-in) | `cd src-tauri && cargo test` |
|
|
| Frontend (Vitest) | Vitest + jsdom | `cd frontend && npx vitest run` |
|
|
| Frontend (components) | Playwright or Vitest + testing-library | `cd frontend && npx vitest run` |
|
|
| Python backend | pytest | `cd backend && python -m pytest` |
|
|
|
|
### Setup needed
|
|
- [ ] Frontend: `npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom` (vitest likely already installed)
|
|
- [ ] Frontend: add `"test": "vitest run"` to `package.json` if not present
|
|
- [ ] Python: ensure pytest is installed (`pip install pytest pytest-asyncio`)
|
|
- [ ] CI: add GitHub Actions workflow for `cargo test && vitest run && pytest`
|
|
|
|
---
|
|
|
|
## Priority order
|
|
|
|
1. **store tests** (editorStore, licenseStore, aiStore) — core data integrity
|
|
2. **Rust licensing tests** — payment/trial logic must never break
|
|
3. **Rust models tests** — filesystem operations must be safe
|
|
4. **Backend service unit tests** — export pipeline, transcription, AI
|
|
5. **Component integration tests** — user-facing behavior
|
|
6. **Error handling regression tests** — robustness
|
|
|
|
---
|
|
|
|
## Robustness beyond tests
|
|
|
|
### React Error Boundary
|
|
The app has no error boundary — a single JS error in any component crashes the entire UI to a white screen. Wrap the app in a `<ErrorBoundary>` that catches render errors and shows a fallback with "Something went wrong" + a reload button.
|
|
|
|
- [ ] Create `ErrorBoundary.tsx` component (`componentDidCatch` pattern)
|
|
- [ ] Wrap the entire `<App />` in `main.tsx`
|
|
- [ ] Fallback shows: error message, stack trace (collapsed), "Reload" button, "Reset & Clear State" button
|
|
|
|
### Global JS error logging
|
|
Uncaught errors in async code and event handlers silently break the app. Add a `window.onerror` and `window.onunhandledrejection` handler that logs to the Tauri backend and shows a toast notification.
|
|
|
|
- [ ] Add global error handler in `main.tsx` that intercepts all uncaught errors
|
|
- [ ] Log to Rust backend via `invoke('log_error', { message, stack })`
|
|
- [ ] Show a non-blocking toast notification (bottom-right) for non-fatal errors
|
|
- [ ] Fatal errors still go to the ErrorBoundary
|
|
|
|
### Input validation layer
|
|
The app trusts user input too much. Invalid values in number inputs, empty file paths, or negative durations can cause crashes or silent failures. Add validation at the store level.
|
|
|
|
- [ ] `editorStore.ts` — validate all setters: clamp numbers, reject empty strings for paths, enforce min/max on dB and speed values
|
|
- [ ] `licenseStore.ts` — validate license key format before sending to Rust (prefix check, base64 pattern)
|
|
- [ ] `aiStore.ts` — validate API key formatting, model name not empty
|
|
- [ ] Export options — validate resolution, format, loudness target against allowed values before sending to backend
|
|
|
|
### Frontend runtime assertions
|
|
The app makes assumptions about data shapes (e.g. `words[sorted[0]].start` assumes the index exists). Add assertion checks in critical paths that log a clear error instead of silently producing NaN or undefined.
|
|
|
|
- [ ] Add an `assert` utility function: `assert(condition, message)` that throws a descriptive error in dev, warns in prod
|
|
- [ ] Guard all array index access in TranscriptEditor, WaveformTimeline, ExportDialog
|
|
- [ ] Guard null/undefined checks on store actions that expect existing data
|
|
|
|
### Auto-save crash recovery
|
|
If the app crashes or the system loses power during editing, current work is lost. Add periodic auto-save to a temp file that gets restored on next launch.
|
|
|
|
- [ ] Every 60 seconds, save the full editor state to `app_data_dir/autosave.json`
|
|
- [ ] On launch, check if autosave.json exists and is newer than the last manual save
|
|
- [ ] Show a "Recover unsaved work?" prompt with date/time of autosave
|
|
- [ ] Clean up autosave after a manual save or after recovery is accepted/dismissed
|
|
|
|
### Backend health check & self-diagnostics
|
|
When the Python backend dies mid-session, the app doesn't know until a request fails. Add periodic health checks and a diagnostics panel.
|
|
|
|
- [ ] Poll `GET /health` every 15 seconds from the frontend
|
|
- [ ] If backend is unreachable: show a non-blocking banner "Backend disconnected — retrying..."
|
|
- [ ] When backend comes back online, dismiss the banner automatically
|
|
- [ ] Add a `/diagnostics` endpoint that reports: Python version, available FFmpeg, GPU detection, model cache sizes, disk space
|
|
- [ ] Wire to a "System Info" section in Settings or DevPanel
|
|
|
|
### CI pipeline with automated checks
|
|
Currently no CI exists. A GitHub Actions workflow would catch regressions on every push.
|
|
|
|
- [ ] Add `.github/workflows/ci.yml`:
|
|
- `cargo test` — all Rust tests
|
|
- `cargo clippy -- -D warnings` — enforce Rust lint rules
|
|
- `npx vitest run` — all frontend tests
|
|
- `npx tsc --noEmit` — TypeScript type check
|
|
- `python -m pytest backend/tests/` — Python backend tests
|
|
- `cargo build --release` — verify release build succeeds
|
|
- [ ] Run on push to `main` and on PRs
|
|
- [ ] Add Rust `#[deny(clippy::all)]` to catch common mistakes at build time
|
|
|
|
### Fuzz testing for Rust deserialization
|
|
The app deserializes user-provided JSON (project files, API responses) — any malformed input could crash the Rust backend. Add fuzz tests for the critical deserialization paths.
|
|
|
|
- [ ] Fuzz `TranscriptionResult` deserialization — malformed word/segment JSON
|
|
- [ ] Fuzz `.aive` project file loading — corrupted JSON, missing fields, wrong types
|
|
- [ ] Fuzz `LicensePayload` deserialization — tampered license payloads
|
|
- [ ] Use `cargo-fuzz` or `proptest` crate for property-based testing
|
|
|
|
### Performance telemetry (opt-in)
|
|
Without real-world data, you can't know where the app is slow. Add lightweight timing around operations that users complain about.
|
|
|
|
- [ ] Log timing for: transcription time, export time, AI completion time, model download time
|
|
- [ ] Store in localStorage (not sent anywhere — privacy-first)
|
|
- [ ] Show in DevPanel: last N operation timings
|
|
- [ ] Use this data to identify slow paths before users report them
|
|
|
|
### Recovery from bad project state
|
|
A corrupted `.aive` file or a partially-loaded project can leave the app in an unusable state. Add recovery paths.
|
|
|
|
- [ ] On project load failure: show error, offer "Load anyway with partial data" or "Cancel"
|
|
- [ ] Add "Reset Editor State" button in DevPanel (clears all state back to empty)
|
|
- [ ] Store action: validate all zone ranges are within video duration, auto-remove invalid ones on save
|
|
|