Files
TalkEdit/polish_plan.md

343 lines
17 KiB
Markdown
Raw Permalink Normal View History

2026-05-06 13:18:53 -06:00
# 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
2026-05-06 10:53:27 -06:00
---
2026-05-06 13:18:53 -06:00
## 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
2026-05-06 10:53:27 -06:00
---
2026-05-06 13:18:53 -06:00
## 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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)
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
---
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
## 4. Frontend lib (`frontend/src/lib/`)
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
---
2026-05-06 13:18:53 -06:00
## 5. Backend services (`backend/services/`)
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
### 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
2026-05-06 10:53:27 -06:00
---
2026-05-06 13:18:53 -06:00
## 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
---
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
## 7. Error handling regression tests
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
- [ ] 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
---
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
## 8. Licensing & trial flow tests
2026-05-06 10:53:27 -06:00
2026-05-06 13:18:53 -06:00
- [ ] 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
2026-05-06 10:53:27 -06:00
---
2026-05-06 13:18:53 -06:00
## 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