# Phase 5: UA Testing — Tests for Phases 2–4

## What Was Built

Ten test modules covering the full data layer (models, database connection, repositories) and static analysis pipeline (scoring, import graphs, clustering, test detection, tree-sitter adapters, language adapters, and security scanning). All tests use in-memory SQLite or mocked external dependencies — zero real IO in unit tests.

---

## Running Automated Tests

### Full suite

```bash
cd /home/erinmeaker/Documents/source/unfamiliar
python3 -m pytest tests/ -v --tb=short
```

### With coverage

```bash
python3 -m pytest tests/ --cov=src/unfamiliar --cov-report=term-missing
```

### Individual test files

```bash
python3 -m pytest tests/test_models.py -v
python3 -m pytest tests/test_db_connection.py -v
python3 -m pytest tests/test_repositories.py -v
python3 -m pytest tests/test_scoring.py -v
python3 -m pytest tests/test_import_graph.py -v
python3 -m pytest tests/test_test_detection.py -v
python3 -m pytest tests/test_treesitter_utils.py -v
python3 -m pytest tests/test_adapters.py -v
python3 -m pytest tests/test_security.py -v
```

### Linting

```bash
ruff check tests/
mypy tests/ --ignore-missing-imports
```

---

## Expected Outcomes

All 255 tests should pass with 0 failures and 0 errors. The linting commands should exit with 0.

If tree-sitter grammar packages are not installed, `test_treesitter_utils.py` and `test_adapters.py` will fail on import. Fix with:

```bash
pip install tree-sitter-javascript tree-sitter-typescript
```

---

## Manual Verification Checklist

Work through these after the automated suite passes.

### Task 1 — Fixtures (`conftest.py`)
- [ ] `python3 -m pytest tests/ --collect-only` runs without collection errors
- [ ] `db_conn` fixture creates tables (check via `TestInitDatabase.test_creates_schema_tables`)
- [ ] `ts_parser` and `js_parser` fixtures yield parsers without import errors
- [ ] `all_repos` and `sample_campaign` fixtures are available to all test files

### Task 2 — Pydantic Models (`test_models.py`)
- [ ] `Campaign()` with no args raises `ValidationError` on `repo_path`
- [ ] `Campaign(repo_path="...", status="invalid")` raises `ValidationError`
- [ ] All 7 `InvestigationCategory` values accepted by `Module`
- [ ] Hypothesis property test for category JSON round-trip runs 100 examples

### Task 3 — DB Connection (`test_db_connection.py`)
- [ ] WAL mode enabled after `open_database`
- [ ] `PRAGMA foreign_keys` returns 1
- [ ] Double `init_database` leaves exactly 1 `schema_version` row
- [ ] `get_schema_version` returns `None` for uninitialized DB
- [ ] `sqlite_sequence` (from AUTOINCREMENT) correctly excluded from table set comparison

### Task 4 — Repositories (`test_repositories.py`)
- [ ] `create()` returns model with `id` set and `created_at`/`updated_at` populated
- [ ] `get(999)` returns `None` (not an exception)
- [ ] `create_many` duplicate path test runs and asserts IDs are set (fragility documented)
- [ ] `get_by_campaign` for modules returns results ordered by path (a, m, z)
- [ ] `add_prerequisites` + `get_dependents` verify bidirectional graph
- [ ] `upsert` on `FileSnapshot` leaves exactly 1 row for the same `(campaign_id, file_path)`

### Task 5 — Scoring (`test_scoring.py`)
- [ ] File with no signals scores 0 with empty `reasons`
- [ ] Entry point file scores exactly 100
- [ ] Hub file (in_degree >= 3) scores exactly 50
- [ ] High complexity file (>=15) scores exactly 30
- [ ] Test file scores exactly 20
- [ ] All 4 signals combined = exactly 200 with exactly 4 reasons
- [ ] Sort order: highest score first; alphabetical tie-break
- [ ] `select_key_files` with budget smaller than first file: returns 1 file (always-at-least-one rule)

### Task 6 — Import Graph & Clustering (`test_import_graph.py`)
- [ ] Empty graph returns empty `edges` and `in_degree`
- [ ] A->B import: edge exists, B's `in_degree == 1`
- [ ] Self-import: no edge to self
- [ ] External import (not in repo_files): no edge created
- [ ] Three disconnected files -> 3 connected components, each size 1
- [ ] A->B->C chain -> 1 component, membership = {A, B, C}
- [ ] Same-directory edge weight == 1.3; different-directory == 1.0
- [ ] Fully internal cluster confidence == 1.0; no-edge cluster == 0.0

### Task 7 — Test Detection (`test_test_detection.py`)
- [ ] `tests/utils.py`, `test_scoring.py`, `utils_test.py` all return `True` from `_is_test_file`
- [ ] `.spec.ts`, `.spec.js`, `.test.ts`, `.test.js` all return `True`
- [ ] `__init__.py`, `setup.py`, `pyproject.toml` return `False`
- [ ] No test files -> `test_files == set()`, all source files in `untested_files`
- [ ] Test file detected by naming -> removed from `untested_files`
- [ ] Coverage XML: 0.75 line-rate parses to `0.75` exactly; `./` prefix stripped
- [ ] Invalid XML raises exception (not silent failure)

### Task 8 — Tree-sitter Utilities (`test_treesitter_utils.py`)
- [ ] `traverse` yields root node first with no duplicates (verified by position tuple, not id)
- [ ] 100 levels of nesting: no `RecursionError`
- [ ] `import foo from 'bar'` -> 1 import, `source_path="test.js"`, `target="bar"`, `is_relative=False`, `names=["foo"]`
- [ ] `import { a, b } from 'mod'` -> `names == {"a", "b"}`
- [ ] `import * as ns from 'mod'` -> `names == ["*"]`
- [ ] `const x = require('./path')` -> `is_relative=True`, `target="./path"`
- [ ] `require(variable)` -> 0 imports (dynamic require skipped)
- [ ] `require.resolve('./mod')` -> 0 imports
- [ ] Named function -> `kind="function"`, `line_start >= 1`
- [ ] `async function` -> `is_async=True`
- [ ] `if` statement -> complexity == 1; empty source -> complexity == 0

### Task 9 — Language Adapters (`test_adapters.py`)
- [ ] `TypeScriptAdapter.can_handle`: `.ts` -> True, `.tsx` -> True, `.js` -> False
- [ ] `JavaScriptAdapter.can_handle`: `.js` -> True, `.jsx` -> True, `.ts` -> False
- [ ] `PythonAdapter.can_handle`: `.py` -> True, `.ts` -> False
- [ ] Valid TS content: `ParsedFile.language == "typescript"`, imports and definitions populated
- [ ] Valid JS content with `require()`: import captured with `is_relative=True`
- [ ] Syntax error content: returns `ParsedFile` with empty lists, no exception
- [ ] `.tsx` file: JSX content parses without error
- [ ] TS `resolve_import("./utils")` with `src/utils.ts` and `src/utils.js` in repo -> returns `"src/utils.ts"` (TS preferred)
- [ ] JS `resolve_import("./mod")` with `src/mod.js` and `src/mod.ts` -> returns `"src/mod.js"` (JS preferred)
- [ ] `create_default_registry()` -> 3 adapters; `get_adapter("file.rb")` -> `None`
- [ ] Hypothesis property: `get_adapter` returns correct type for all 5 extensions

### Task 10 — Security (`test_security.py`)
- [ ] Empty `file_paths` -> `[]`, no detect-secrets calls
- [ ] `detect_secrets` unavailable (ImportError) -> `[]`, no exception
- [ ] Mock returning 1 finding -> exactly 1 `SecretFinding`, all fields asserted
- [ ] File > 1MB -> skipped (not scanned)
- [ ] `is_secret=False` finding -> not in results
- [ ] Binary file -> `scan_file` not called
- [ ] No manifest files -> `([], [], [])`
- [ ] `requirements.txt` present -> pip-audit called with `-r` and `-f json`; `"requirements.txt"` in `manifests_scanned`
- [ ] `pyproject.toml` present -> pip-audit called with `--locked`
- [ ] `package.json` + lock file -> npm audit called with `--json`; `"package.json"` in `manifests_scanned`
- [ ] `package.json` without lock file -> npm audit NOT called; not in `manifests_scanned`
- [ ] `FileNotFoundError` from subprocess -> error string in `scan_errors`, no exception
- [ ] `TimeoutExpired` -> "timed out" in error message
- [ ] Mock pip-audit CVE -> 1 `VulnerabilityFinding` with `ecosystem="python"`, all 7 fields asserted
- [ ] Mock npm CVE -> 1 `VulnerabilityFinding` with `ecosystem="javascript"`
- [ ] RCE keywords in description -> `severity="critical"`
- [ ] SQL injection keywords -> `severity="high"`

---

## Known Issues and Weaknesses

### Fixed during review

The following issues from the original build were fixed during the Phase 5 review:

1. **`connection.py` chmod bug** — `init_database()` with `:memory:` ran `os.chmod(Path(""), 0o600)` on the cwd, stripping execute permissions. Fixed by guarding the raw string before `Path()` construction and adding `is_file()` check.
2. **`test_scoring.py` type mismatch** — Tests passed `EntryPoint` objects but `score_files()` expected `list[dict]`. Fixed to use dicts matching the implementation signature.
3. **`test_scoring.py` missing arg** — `select_key_files()` requires `token_budget` positionally; test omitted it.
4. **`test_security.py` patch targets** — `SecretsCollection` is imported inside `scan_for_secrets()`, not at module level. Patches changed from `unfamiliar.analysis.security.secrets.SecretsCollection` to `detect_secrets.SecretsCollection`.
5. **`test_treesitter_utils.py` id() reuse** — `id(node)` is unreliable for tree-sitter cursor-based traversal (Python reuses addresses). Changed to `(type, start_byte, end_byte)` tuple comparison.
6. **`test_db_connection.py` sqlite_sequence** — AUTOINCREMENT creates an internal `sqlite_sequence` table. Fixed by filtering it from the comparison set.

### Remaining: `create_many` fragility test is documentation-only

`test_repositories.py:235` confirms both records get ids, but does not assert which id is wrong. The actual failure mode (second module overwrites first in the path-to-id mapping) is not pinned. Sufficient for documentation but does not prevent the bug from silently changing behavior.

---

## Permission Safety Verification

After the `connection.py` chmod incident, verify directory permissions survive a test run:

```bash
# Before running tests
stat -c '%a %n' /home/erinmeaker/Documents/source/unfamiliar

# Run tests
python3 -m pytest tests/ -v --tb=short

# After running tests — should still be 700 or 755
stat -c '%a %n' /home/erinmeaker/Documents/source/unfamiliar
```

---

## Completion Checklist

- [ ] `python3 -m pytest tests/ -v` — all 255 tests pass, 0 failures
- [ ] `ruff check tests/` — 0 lint errors
- [ ] `mypy tests/ --ignore-missing-imports` — 0 type errors
- [ ] Coverage report reviewed (no specific target — the goal is correctness, not percentage)
- [ ] Directory permissions unchanged after test run
- [ ] The known issues above noted and acceptable (or fixed)
