Skip to content
ramacharanreddy-k edited this page Apr 27, 2026 · 1 revision

170 tests across 8 files. All run offline in ~3 seconds. Zero network calls.


Running Tests

# 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/

What Each File Tests

test_config.py (18 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

test_project.py (18 tests)

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

test_templates.py (12 tests)

Schema and template content -- does CLAUDE.md contain all 21 tool names, correct frontmatter format, all confidence levels? Are Obsidian configs valid JSON?

test_db.py (38 tests)

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

test_ingestion.py (31 tests)

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

test_server.py (29 tests)

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"

test_cli.py (18 tests)

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

test_export.py (6 tests)

Export output -- correct filename, includes overview and index, skips empty directories, preserves UTF-8 characters.


How Tests Stay Isolated

Every test runs in a temporary directory. No test touches ~/.wikinow/. The key trick:

@pytest.fixture(autouse=True)defisolated_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.


Why No Network Tests

All ingestion tests mock the network layer:

  • Jina -- urlopen patched to return fake responses
  • YouTube -- URL detection and json3 parsing tested with fake data, _download_audio mocked
  • 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.