-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
170 tests across 8 files. All run offline in ~3 seconds. Zero network calls.
# Install dev dependencies
uv sync --group dev
# Run everything
uv run pytest tests/ -v
# Run one file
uv run pytest tests/test_server.py -v
# Run one test
uv run pytest tests/test_db.py::TestSearch::test_search_porter_stemming -v
# Lint and format check
uv run ruff check wikinow/ tests/
uv run ruff format --check wikinow/ tests/Config file lifecycle -- does it create on first run, persist across reloads, handle corrupted YAML, resolve env vars, coerce types?
Config doesn't exist --> created with defaults
Config has missing keys --> filled from defaults (forward compatible)
Config is corrupted YAML --> falls back to defaults silently
OLLAMA_API_KEY env var set --> overrides config file value
wn config key "true" --> stored as boolean, not string
Project creation and management -- does wn init create the right structure, reject bad names, set the active project?
wn init test --> 8 directories, CLAUDE.md, symlinks, 6 wiki files, .obsidian/, .gitignore, git init
wn init "bad name" --> ValueError (spaces not allowed)
wn init test --> FileExistsError (duplicate)
wn use project-a --> switches active project
wn list --> sorted alphabetically
Schema and template content -- does CLAUDE.md contain all 21 tool names, correct frontmatter format, all confidence levels? Are Obsidian configs valid JSON?
The biggest test file. Covers the entire database layer:
Schema --> 5 tables create, FTS5 with porter stemming, 5 indexes, NOT NULL constraints
CRUD --> insert, upsert, tags, links, dates default to now
Search --> FTS5 returns results, empty for no match, "transformers" matches "transformer"
Stats --> all 7 fields correct, sorted by date
Lint --> finds orphans, dead links, uncompiled sources
Dedup --> hash exists returns true, unknown hash returns false
Self-heal --> new file indexed, changed file re-indexed, deleted file removed
Cascade --> delete article removes its tags and links
Conflicts --> articles with confidence "conflict" returned by get_contradictions
Security --> invalid table name raises ValueError, invalid column raises ValueError
All 7 ingestion sources -- parsing, error handling, English enforcement:
Jina --> parses title from markdown, builds auth header, wraps URLError and HTTPError
YouTube --> URL regex matches watch/short/youtu.be, json3 parsed correctly, empty segs skipped
PDF/Epub --> missing dep raises ImportError, missing file raises FileNotFoundError
Text --> reads content, title from filename stem ("my-notes.md" --> "My Notes")
Audio --> missing whisper raises ImportError, French audio rejected, English audio accepted
Format --> YouTube markdown has title/channel/duration, audio markdown has language/duration
Frozen --> all 6 response dataclasses reject attribute assignment
All 21 MCP tools -- registration, behavior, security:
Registration --> exactly 21 tools registered in FastMCP
Read/Write --> content returned, path traversal blocked (../ and prefix bypass)
Ingest --> text saved to raw/, dedup skips duplicates, indexed in database
Search --> returns list of dicts with title/path keys
Stats/Lint --> all 7 stat keys present, lint has health_score + issue lists
List tools --> articles/raw/tags return correct dicts
Contradictions --> only "conflict" articles returned
Gaps --> returns file content, "not found" when missing
Re-ingest --> returns content, "not found" when missing
Schema update --> modifies existing section, appends new section
Slugify --> "Hello World!" --> "hello-world", "" --> "source.md"
CLI commands via Typer's test runner -- output text and exit codes:
--version --> contains "0.1.0"
--help --> lists all 12 command names
init --> exit 0, "Project Created" in output
init duplicate --> exit 1, "Init Failed"
init bad name --> exit 1, "Init Failed"
list --> shows active (●) and inactive (○) markers
use --> "Switched" in output
no project --> all commands show "No Active Project", exit 1
stats --> all 7 emoji lines present
lint --> health bar characters present
config --> shows YAML, "Config Updated" on set, "Missing Value" on key-only
export --> "Exported" in output
read traversal --> "Invalid Path" shown
Export output -- correct filename, includes overview and index, skips empty directories, preserves UTF-8 characters.
Every test runs in a temporary directory. No test touches ~/.wikinow/. The key trick:
@pytest.fixture(autouse=True)
def isolated_env(tmp_path, monkeypatch):
test_dir = tmp_path / ".wikinow"
monkeypatch.setattr("wikinow.config.WIKINOW_DIR", test_dir)
monkeypatch.setattr("wikinow.config._manager", None)This redirects all file operations to a temp directory and resets the singleton between tests. Every test starts with a clean slate.
All ingestion tests mock the network layer:
-
Jina --
urlopenpatched to return fake responses -
YouTube -- URL detection and json3 parsing tested with fake data,
_download_audiomocked - Whisper -- fake model class injected via monkeypatch
This means tests run in 3 seconds, work offline, never hit rate limits, and never hang on slow APIs.