Skip to content

Testing

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)
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.


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.

Clone this wiki locally