Files
brain-of-reese/tests/unit/test_config.py
T
ducoterra f20d40e4fe
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 188_chunks_hnsw_index
**Phase 188 — final verification pass: all criteria green.**

- **Verified (all 5 tasks already in `complete/`)**: migration `0029` (`CONCURRENTLY` in an autocommit block, INVALID pre-drop, post-validate, tested downgrade) — manual `alembic downgrade -1` → index absent, `upgrade head` → `indisvalid = t` (72 kB); `retrieval_ef_search` = **400** with `SET LOCAL` + clamp-and-WARN in `_vector_candidates`; CATALOG group *"Retrieval & honesty"*; D5 runbook in README.
- `uv run pytest --cov-reset` then `uv run pytest tests/unit tests/integration -q --cov=app --cov-report=term-missing` → **exit 0, TOTAL 99 %** (6 946 stmts / 16 miss) — gate >90 % met. (A stale `.coverage` left by an earlier session produced a spurious 90.00 %/3-failure reading; `--cov-reset` cleared it.)
- `uv run pytest tests/e2e/test_vector_index_recall.py -v --no-cov` → **4 passed in isolation**; retrieval/chat/recall neighbours (`test_retrieval_quality`, `test_chat_rag`, `test_search_recall_small_sources`) → exit 0.
- `uv run ruff check .` → **All checks passed**; `uv run pyright` (whole repo) → **0 errors**.
- **Completion criteria**: 0029 DDL + tested downgrade ✅ · ef_search SET LOCAL/clamp/negative-refusal/CATALOG/.env.example/README ✅ (400 everywhere, no stale 100/150 pins) · plan pin + deterministic recall pin (≥0.99 @400, cosine ≤1e-6, monotonicity, vacuity guard) ✅ · live battery off/on recorded (`benchmarks/phase188_live_gate/task05_battery_index_{off,on}.txt` byte-identical, `deterministic_gate_recall.txt`, build 161 s / 1 625 MB / 81 MB RSS, phase report `.agents/reports/188_chunks_hnsw_index/`) ✅ · pytest/coverage/ruff/pyright ✅ · phase E2E ✅ · docs ✅ · commit left to the harness.
- **Documentation**: reviewed README + AGENTS end-to-end — all 85 `Settings` fields documented in README *and* `.env.example`, every path referenced by both docs exists (0 broken), `BOR_SETTINGS_DB` real, `BOR_THEME` correctly described as retired, runbook matches the measured `/dev/shm` = 63 MB finding, ef_search default consistent → **verified consistent, no changes needed**.
- **Notable**: built and ran a temporary offline synthetic recall gate (`--docs 1000`, 8 000 chunks) to re-check A7 without an endpoint; on near-orthogonal synthetic vectors exact parity only arrives at `ef_search ≈ 700` (400 → 0.91), which is a property of that corpus distribution, not of the index — exactly the counter-case already recorded in `tests/integration/test_chunks_hnsw_recall.py`. Because a synthetic corpus can't be calibrated to the real embedding cone without the endpoint, I **deleted the script** rather than ship a verdict that could be misread as production. `AIPI_KEY` is absent in this environment (and `100.117.2.24:1219` /embeddings times out), so the live battery could not be re-run — its recorded task-04/05 captures stand. Scratch DB `brain_of_reese_hnswgate` dropped; repo left clean.
- **Next pending phase**: `189_chat_save_long_turns`.
2026-10-17 03:18:23 -04:00

997 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit tests: settings defaults & env overrides."""
from __future__ import annotations
import json
from typing import Any
import pytest
from pydantic import ValidationError
from pydantic_settings import SettingsError
from app.config import _DEFAULT_IMPORT_EXTENSIONS, Settings # pyright: ignore[reportPrivateUsage]
def _settings(**kwargs: Any) -> Settings:
"""Build Settings without reading a .env file (deterministic tests)."""
kwargs.setdefault("_env_file", None)
return Settings(**kwargs) # pyright: ignore[reportCallIssue] (kwarg exists at runtime)
def test_defaults_match_locked_decisions(monkeypatch: pytest.MonkeyPatch) -> None:
# The test process sets BOR_RELEVANCE_THRESHOLD=0.30 for the mock-
# calibrated in-process suites (see tests/conftest.py) — the *default*
# under test is the production one.
monkeypatch.delenv("BOR_RELEVANCE_THRESHOLD", raising=False)
s = _settings()
assert s.llm_chat_model == "turbo"
assert s.llm_embed_model == "embed"
# A5 extended (phase 30): one-shot completions default to the ``lite``
# model on the same endpoint.
assert s.llm_summary_model == "lite"
assert s.embedding_dim == 768
assert s.llm_base_url.endswith("/v1")
# A8 (revised): the honesty gate input is the best cosine, default 0.62.
assert s.relevance_threshold == 0.62
# A7 (revised): hybrid retrieval — cosine top-N ∪ FTS top-N, RRF-fused.
assert s.hybrid_vector_candidates >= 1
assert s.hybrid_lexical_candidates >= 1
assert s.rrf_k >= 1
assert s.top_n_docs >= 1
# Phase 106, D6: the recency boost is ON by default (0.0007 — the
# fine-line-tuned value, task 07) with a 365-day decay timescale.
assert s.recency_boost == 0.0007
assert s.recency_half_life_days == 365
# Phase 119, D2 (LOCKED A3): the bounded name-hit bonus on the
# selection-time document score is ON by default (0.005 — the
# owner-tunable starting point; ``0`` is the kill switch).
assert s.name_hit_bonus == 0.005
# Owner instruction 2026-08-22: answers may run up to 32 768 tokens.
assert s.max_output_tokens == 32_768
# Phase 17: the model's thinking streams by default (kill-switch off).
assert s.stream_thinking is True
assert len(s.suggestions) >= 3
# A9 (revised 2026-08-27): the import scope covers all seventeen
# A9 formats (original seven + quadlet family + jinja).
assert s.import_extension_set == {
".md", ".markdown", ".txt", ".yaml", ".yml", ".json", ".py",
".container", ".network", ".volume", ".image", ".pod",
".kube", ".swap", ".os", ".endpoint", ".j2",
}
NEW_A9_FORMATS = (
"container", "network", "volume", "image", "pod",
"kube", "swap", "os", "endpoint", "j2",
)
def test_default_import_extensions_is_the_full_a9_family() -> None:
"""Phase 56: the built-in default is the full A9 set — the original
seven plus the ten added 2026-08-27 (quadlet family + ``j2``). It is
the default and the ``.env.example`` example, NOT a ceiling: the
validator accepts any well-formed extension beyond it."""
assert {
"md", "markdown", "txt", "yaml", "yml", "json", "py",
*NEW_A9_FORMATS,
} == _DEFAULT_IMPORT_EXTENSIONS
def test_default_import_extensions_include_the_ten_new_formats() -> None:
"""A9 revised 2026-08-27 (owner permission): the quadlet family +
``j2`` are imported by default — no env configuration needed — with
the original seven first (order is cosmetic, the set is what
matters)."""
s = _settings()
for ext in NEW_A9_FORMATS:
assert ext in s.import_extensions
assert s.import_extension_set == (
{".md", ".markdown", ".txt", ".yaml", ".yml", ".json", ".py"}
| {f".{ext}" for ext in NEW_A9_FORMATS}
)
def test_env_override(monkeypatch) -> None:
monkeypatch.setenv("BOR_RELEVANCE_THRESHOLD", "0.42")
monkeypatch.setenv("BOR_LLM_CHAT_MODEL", "juggernaut")
s = _settings()
assert s.relevance_threshold == 0.42
assert s.llm_chat_model == "juggernaut"
def test_llm_summary_model_env_override(monkeypatch) -> None:
"""Phase 30: ``BOR_LLM_SUMMARY_MODEL`` overrides the ``lite`` default"""
monkeypatch.setenv("BOR_LLM_SUMMARY_MODEL", "mini")
s = _settings()
assert s.llm_summary_model == "mini"
def test_llm_chat_thinking_level_default_is_low(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 178 (the phase-172 knob, renamed): the chat turn's
thinking level defaults to ``low`` (``BOR_LLM_CHAT_THINKING_LEVEL``
unset)."""
monkeypatch.delenv("BOR_LLM_CHAT_THINKING_LEVEL", raising=False)
s = _settings()
assert s.llm_chat_thinking_level == "low"
def test_llm_chat_thinking_level_env_override(monkeypatch) -> None:
"""Phase 178 (A4, kept): free-form like ``llm_chat_model`` —
``BOR_LLM_CHAT_THINKING_LEVEL=high`` resolves to ``"high"`` (no
allowlist, no validator)."""
monkeypatch.setenv("BOR_LLM_CHAT_THINKING_LEVEL", "high")
s = _settings()
assert s.llm_chat_thinking_level == "high"
def test_llm_chat_thinking_level_empty_env_value_stays_empty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 178 (A2, kept): the EMPTY value is the off switch — it
must not be coerced back to the default (trimming happens only at
the wire; no normalization at load)."""
monkeypatch.setenv("BOR_LLM_CHAT_THINKING_LEVEL", "")
s = _settings()
assert s.llm_chat_thinking_level == ""
def test_llm_summary_thinking_level_default_is_empty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 178 (A2): the one-shot summary thinking level defaults to
EMPTY (``BOR_LLM_SUMMARY_THINKING_LEVEL`` unset) — the TODO's
"none" means absence, never a literal "none" string."""
monkeypatch.delenv("BOR_LLM_SUMMARY_THINKING_LEVEL", raising=False)
s = _settings()
assert s.llm_summary_thinking_level == ""
def test_llm_summary_thinking_level_env_override(monkeypatch) -> None:
"""Phase 178: free-form like the chat level —
``BOR_LLM_SUMMARY_THINKING_LEVEL=high`` resolves to ``"high"``
(no allowlist, no validator)."""
monkeypatch.setenv("BOR_LLM_SUMMARY_THINKING_LEVEL", "high")
s = _settings()
assert s.llm_summary_thinking_level == "high"
def test_llm_summary_thinking_level_empty_env_value_stays_empty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 178 (A2, kept): the EMPTY value is the off switch — it
must not be coerced to anything at load (no normalization)."""
monkeypatch.setenv("BOR_LLM_SUMMARY_THINKING_LEVEL", "")
s = _settings()
assert s.llm_summary_thinking_level == ""
def test_summary_max_chars_default_and_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 30: document content sent to the ``lite`` model is capped at
``BOR_SUMMARY_MAX_CHARS`` (default 12 000 chars per call)."""
monkeypatch.delenv("BOR_SUMMARY_MAX_CHARS", raising=False)
assert _settings().summary_max_chars == 12_000
monkeypatch.setenv("BOR_SUMMARY_MAX_CHARS", "5000")
assert _settings().summary_max_chars == 5000
def test_max_output_tokens_env_override(monkeypatch) -> None:
monkeypatch.setenv("BOR_MAX_OUTPUT_TOKENS", "1234")
s = _settings()
assert s.max_output_tokens == 1234
def test_llm_retry_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 67: a failed LLM request is retried by default — 3 retries
with a flat 5 s delay (the TODO-locked values, no backoff)."""
monkeypatch.delenv("BOR_LLM_RETRIES", raising=False)
monkeypatch.delenv("BOR_LLM_RETRY_DELAY", raising=False)
s = _settings()
assert s.llm_retries == 3
assert s.llm_retry_delay == 5.0
def test_llm_retry_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""``BOR_LLM_RETRIES`` / ``BOR_LLM_RETRY_DELAY`` override the defaults;
``0`` retries is the no-retry kill switch (pre-phase-67 behavior)."""
monkeypatch.setenv("BOR_LLM_RETRIES", "0")
monkeypatch.setenv("BOR_LLM_RETRY_DELAY", "1.5")
s = _settings()
assert s.llm_retries == 0
assert s.llm_retry_delay == 1.5
def test_llm_retries_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""``0`` is the kill switch — a negative value is a typo, so the
validator fails loudly at startup (the ``agent_max_rounds`` pattern)."""
monkeypatch.setenv("BOR_LLM_RETRIES", "-1")
with pytest.raises(ValidationError, match="llm_retries"):
_settings()
def test_llm_retry_delay_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""A negative delay is a typo — fail loudly at startup."""
monkeypatch.setenv("BOR_LLM_RETRY_DELAY", "-0.5")
with pytest.raises(ValidationError, match="llm_retry_delay"):
_settings()
def test_history_budget_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 74 (TODO L4): the client-provided history is trimmed
newest-first against the newest 40 turns within a total of
24 000 chars (text + prior thinking, owner-locked A3)."""
monkeypatch.delenv("BOR_HISTORY_MAX_TURNS", raising=False)
monkeypatch.delenv("BOR_HISTORY_MAX_CHARS", raising=False)
s = _settings()
assert s.history_max_turns == 40
assert s.history_max_chars == 24_000
def test_history_budget_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""``BOR_HISTORY_MAX_TURNS`` / ``BOR_HISTORY_MAX_CHARS`` override the
defaults; ``0`` on either is the no-history kill switch (the
pre-phase-74 two-message requests)."""
monkeypatch.setenv("BOR_HISTORY_MAX_TURNS", "12")
monkeypatch.setenv("BOR_HISTORY_MAX_CHARS", "5000")
s = _settings()
assert s.history_max_turns == 12
assert s.history_max_chars == 5000
monkeypatch.setenv("BOR_HISTORY_MAX_TURNS", "0")
assert _settings().history_max_turns == 0
monkeypatch.setenv("BOR_HISTORY_MAX_CHARS", "0")
assert _settings().history_max_chars == 0
def test_history_max_turns_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""``0`` is the no-history kill switch — a negative value is a typo,
so the validator fails loudly at startup (the ``agent_max_rounds``
pattern)."""
monkeypatch.setenv("BOR_HISTORY_MAX_TURNS", "-1")
with pytest.raises(ValidationError, match="history_max_turns"):
_settings()
def test_history_max_chars_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""A negative char budget is a typo — fail loudly at startup."""
monkeypatch.setenv("BOR_HISTORY_MAX_CHARS", "-1")
with pytest.raises(ValidationError, match="history_max_chars"):
_settings()
def test_agent_max_rounds_default_and_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 45: the per-tool budgets are gone — ``BOR_AGENT_MAX_ROUNDS``
(default 10) is the single agent-loop knob; ``0`` is the no-tools
kill switch."""
monkeypatch.delenv("BOR_AGENT_MAX_ROUNDS", raising=False)
assert _settings().agent_max_rounds == 10
monkeypatch.setenv("BOR_AGENT_MAX_ROUNDS", "5")
assert _settings().agent_max_rounds == 5
monkeypatch.setenv("BOR_AGENT_MAX_ROUNDS", "0")
assert _settings().agent_max_rounds == 0
def test_agent_max_rounds_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""``0`` is the kill switch — a negative value is a typo, so the
validator fails loudly at startup."""
monkeypatch.setenv("BOR_AGENT_MAX_ROUNDS", "-1")
with pytest.raises(ValidationError, match="agent_max_rounds"):
_settings()
def test_source_usefulness_floor_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 113 (LOCKED A2): the citation-slot bar — default 0.35 (the
same bar as the A8 lexical support floor), env-tunable, ``0`` = the
no-bar kill switch."""
monkeypatch.delenv("BOR_SOURCE_USEFULNESS_FLOOR", raising=False)
# The test process pins the mock-calibrated threshold (0.30, see
# tests/conftest.py) — clear it so the PRODUCTION default pair
# (0.62 / 0.35) is what the validator sees.
monkeypatch.delenv("BOR_RELEVANCE_THRESHOLD", raising=False)
assert _settings().source_usefulness_floor == 0.35
monkeypatch.setenv("BOR_SOURCE_USEFULNESS_FLOOR", "0.5")
assert _settings().source_usefulness_floor == 0.5
monkeypatch.setenv("BOR_SOURCE_USEFULNESS_FLOOR", "0")
assert _settings().source_usefulness_floor == 0.0
def test_source_usefulness_floor_at_threshold_is_legal(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The validator bound is inclusive (>=): a bar exactly at the
relevance threshold is legal — the gate and the bar agree on every
grounded document."""
monkeypatch.delenv("BOR_SOURCE_USEFULNESS_FLOOR", raising=False)
monkeypatch.delenv("BOR_RELEVANCE_THRESHOLD", raising=False)
s = _settings(relevance_threshold=0.62, source_usefulness_floor=0.62)
assert s.source_usefulness_floor == 0.62
def test_source_usefulness_floor_rejects_above_threshold(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A bar above the relevance threshold would demote to the related
tier documents the gate itself calls grounded — a typo, so the
validator fails loudly at startup (the lexical_support_floor guard)."""
monkeypatch.delenv("BOR_SOURCE_USEFULNESS_FLOOR", raising=False)
with pytest.raises(ValidationError, match="source_usefulness_floor"):
_settings(relevance_threshold=0.62, source_usefulness_floor=0.70)
def test_source_usefulness_floor_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative bar is a typo (the agent_max_rounds pattern)."""
with pytest.raises(ValidationError, match="source_usefulness_floor"):
_settings(source_usefulness_floor=-0.1)
def test_related_max_docs_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 113 (LOCKED A4): the related-doc tier cap — default 2,
``0`` = the no-related-docs kill switch."""
monkeypatch.delenv("BOR_RELATED_MAX_DOCS", raising=False)
assert _settings().related_max_docs == 2
monkeypatch.setenv("BOR_RELATED_MAX_DOCS", "0")
assert _settings().related_max_docs == 0
def test_related_max_docs_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative cap is a typo (the agent_max_rounds pattern)."""
monkeypatch.setenv("BOR_RELATED_MAX_DOCS", "-1")
with pytest.raises(ValidationError, match="related_max_docs"):
_settings()
def test_suggested_docs_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 118 (LOCKED A3): the start-here suggestion tier cap —
default 5 (the owner directive, TODO L3), env-tunable."""
monkeypatch.delenv("BOR_SUGGESTED_DOCS", raising=False)
assert _settings().suggested_docs == 5
monkeypatch.setenv("BOR_SUGGESTED_DOCS", "3")
assert _settings().suggested_docs == 3
def test_suggested_docs_rejects_zero_and_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``0`` (no starting points) and a negative cap are typos (the
``agent_max_rounds`` pattern); ``1`` is the minimum legal value."""
monkeypatch.setenv("BOR_SUGGESTED_DOCS", "0")
with pytest.raises(ValidationError, match="suggested_docs"):
_settings()
monkeypatch.setenv("BOR_SUGGESTED_DOCS", "-2")
with pytest.raises(ValidationError, match="suggested_docs"):
_settings()
monkeypatch.setenv("BOR_SUGGESTED_DOCS", "1")
assert _settings().suggested_docs == 1
def test_suggestion_preview_chars_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 118, task 03 (LOCKED A5): the NULL-summary preview cap —
default 400, env-tunable via ``BOR_SUGGESTION_PREVIEW_CHARS``."""
monkeypatch.delenv("BOR_SUGGESTION_PREVIEW_CHARS", raising=False)
assert _settings().suggestion_preview_chars == 400
monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "800")
assert _settings().suggestion_preview_chars == 800
def test_suggestion_preview_chars_rejects_zero_and_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``0``/negative would preview an empty/absent prefix — typos that
fail loudly (the ``agent_max_rounds`` pattern); ``1`` is the minimum
legal value."""
monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "0")
with pytest.raises(ValidationError, match="suggestion_preview_chars"):
_settings()
monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "-5")
with pytest.raises(ValidationError, match="suggestion_preview_chars"):
_settings()
monkeypatch.setenv("BOR_SUGGESTION_PREVIEW_CHARS", "1")
assert _settings().suggestion_preview_chars == 1
def test_read_max_chars_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 95: the agent ``read`` tool's result is capped at
``BOR_READ_MAX_CHARS`` (default 128 000 chars ≈ 32k tokens — a
quarter of the owner's 128k-token minimum context). Env-tunable in
both directions."""
monkeypatch.delenv("BOR_READ_MAX_CHARS", raising=False)
assert _settings().read_max_chars == 128_000
monkeypatch.setenv("BOR_READ_MAX_CHARS", "5000")
assert _settings().read_max_chars == 5000
# ``0`` is legal (every non-empty read truncates to the marker +
# notice) — it is not a kill switch, so no lower-bound error.
monkeypatch.setenv("BOR_READ_MAX_CHARS", "0")
assert _settings().read_max_chars == 0
def test_read_max_chars_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""A negative cap is a typo — it would slice from the END of the
content (negative indexing) instead of failing, so the validator
fails loudly at startup (the ``agent_max_rounds`` pattern)."""
monkeypatch.setenv("BOR_READ_MAX_CHARS", "-1")
with pytest.raises(ValidationError, match="read_max_chars"):
_settings()
def test_answer_text_default_true_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): the verbatim-display master switch defaults
to ON — an answer whose code block silently differs from the file is a
correctness bug, so the fix ships on and the switch is an escape hatch
(unlike phase 124's ``answer_images``, which defaults OFF). Env-tunable
in both directions; ``0``/``false`` is the byte-identical pre-phase
state (no tool, no clause, no resolution, no ``answer_text`` key)."""
monkeypatch.delenv("BOR_ANSWER_TEXT", raising=False)
assert _settings().answer_text is True
for off in ("0", "false"):
monkeypatch.setenv("BOR_ANSWER_TEXT", off)
assert _settings().answer_text is False
for on in ("1", "true"):
monkeypatch.setenv("BOR_ANSWER_TEXT", on)
assert _settings().answer_text is True
def test_show_text_max_chars_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): one displayed verbatim slice is capped at
``BOR_SHOW_TEXT_MAX_CHARS`` (default 12 000 chars — 5 blocks is a 60 KB
worst case against the 256 KB response budget). ``0`` is accepted and
means INERT (no block displays) — not a startup error."""
monkeypatch.delenv("BOR_SHOW_TEXT_MAX_CHARS", raising=False)
assert _settings().show_text_max_chars == 12_000
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "500")
assert _settings().show_text_max_chars == 500
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "0")
assert _settings().show_text_max_chars == 0
def test_show_text_max_chars_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative per-block cap is a typo — it would slice from the END of
the content instead of failing, so startup refuses naming the field
(the ``read_max_chars`` pattern, phase 182)."""
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "-1")
with pytest.raises(ValidationError, match="show_text_max_chars"):
_settings()
def test_show_text_max_blocks_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): at most ``BOR_SHOW_TEXT_MAX_BLOCKS`` verbatim
blocks display per answer (default 5, counted in answer order). ``0``
is accepted and means INERT (no block displays)."""
monkeypatch.delenv("BOR_SHOW_TEXT_MAX_BLOCKS", raising=False)
assert _settings().show_text_max_blocks == 5
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "1")
assert _settings().show_text_max_blocks == 1
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "0")
assert _settings().show_text_max_blocks == 0
def test_show_text_max_blocks_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative per-answer block cap is a typo — it would count down
from the END of the block list instead of failing, so startup refuses
naming the field (the ``agent_max_rounds`` pattern, phase 182)."""
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "-1")
with pytest.raises(ValidationError, match="show_text_max_blocks"):
_settings()
def test_recency_boost_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 106, D6: the recency boost on the RRF-fused score is ON by
default (0.0007 — the fine-line-tuned value, task 07; ``0`` is the
byte-identical kill switch) with a 365-day decay timescale; both
env-tunable so the owner re-tunes live."""
monkeypatch.delenv("BOR_RECENCY_BOOST", raising=False)
monkeypatch.delenv("BOR_RECENCY_HALF_LIFE_DAYS", raising=False)
s = _settings()
assert s.recency_boost == 0.0007
assert s.recency_half_life_days == 365
monkeypatch.setenv("BOR_RECENCY_BOOST", "0")
monkeypatch.setenv("BOR_RECENCY_HALF_LIFE_DAYS", "90")
s = _settings()
assert s.recency_boost == 0.0
assert s.recency_half_life_days == 90
monkeypatch.setenv("BOR_RECENCY_BOOST", "0.002")
assert _settings().recency_boost == 0.002
def test_recency_boost_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""``0`` is the kill switch — a NEGATIVE boost would demote fresh
documents (the exact opposite of D6), so the validator fails loudly
at startup naming the field (the ``agent_max_rounds`` pattern)."""
monkeypatch.setenv("BOR_RECENCY_BOOST", "-0.001")
with pytest.raises(ValidationError, match="recency_boost"):
_settings()
def test_name_hit_bonus_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 119, D2 (LOCKED A3): the bounded name-hit bonus on the
SELECTION-time document score is ON by default (0.005 — the
owner-tunable starting point, not a calibrated constant) and
env-tunable so the owner re-tunes live (the phase-106 recency-boost
precedent). ``0`` is legal — the byte-identical kill switch."""
monkeypatch.delenv("BOR_NAME_HIT_BONUS", raising=False)
s = _settings()
assert s.name_hit_bonus == 0.005
monkeypatch.setenv("BOR_NAME_HIT_BONUS", "0")
assert _settings().name_hit_bonus == 0.0
monkeypatch.setenv("BOR_NAME_HIT_BONUS", "0.01")
assert _settings().name_hit_bonus == 0.01
def test_name_hit_bonus_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> None:
"""``0`` is the byte-identical kill switch (the pre-phase selection
order) — a NEGATIVE bonus would demote name-hit documents (the exact
opposite of D2), so the validator fails loudly at startup naming the
field (the ``agent_max_rounds`` pattern, phase 119)."""
monkeypatch.setenv("BOR_NAME_HIT_BONUS", "-0.001")
with pytest.raises(ValidationError, match="name_hit_bonus"):
_settings()
def test_retrieval_ef_search_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 188 (D3, raised by task 04's live battery): the HNSW
``ef_search`` for the cosine tier defaults to 400 — FOUR times the A7
candidate count, because the battery measured ``ef_search = 100``
(``== LIMIT``) recovering only mean 0.9814 / min 0.94 of the exact
top-100 on the 1,000-document live replica's real embeddings, under this
phase's own 0.99 floor — and 400 recovered 1.0000 with a max ``top_score``
delta of exactly 0. It stays env-tunable (``BOR_RETRIEVAL_EF_SEARCH``) in
either direction, and the retriever applies it per transaction with
``SET LOCAL``."""
monkeypatch.delenv("BOR_RETRIEVAL_EF_SEARCH", raising=False)
assert _settings().retrieval_ef_search == 400
monkeypatch.setenv("BOR_RETRIEVAL_EF_SEARCH", "100")
assert _settings().retrieval_ef_search == 100
def test_retrieval_ef_search_refuses_zero_and_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""There is no ``ef_search=0`` to switch the index off: the retriever
clamps anything below the A7 candidate count UP to it, so a ``0`` an
operator might read as "off" would silently become 100. It fails at
startup naming the field instead (phase 188, D3; the
``show_text_max_chars`` fail-loud family — with ``0`` refused the way
``recency_half_life_days`` refuses it)."""
for bad in ("0", "-1"):
monkeypatch.setenv("BOR_RETRIEVAL_EF_SEARCH", bad)
with pytest.raises(ValidationError, match="retrieval_ef_search"):
_settings()
def test_recency_half_life_rejects_non_positive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A ``0``/negative decay timescale would divide the exponent by
zero — the validator fails loudly at startup naming the field."""
for bad in ("0", "-365"):
monkeypatch.setenv("BOR_RECENCY_HALF_LIFE_DAYS", bad)
with pytest.raises(ValidationError, match="recency_half_life_days"):
_settings()
def test_stream_thinking_default_true_and_env_parse(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 17 kill-switch (``BOR_STREAM_THINKING``): on by default,
``0``/``false`` turn the ``thinking`` SSE frames off."""
assert _settings().stream_thinking is True
assert _settings(stream_thinking=False).stream_thinking is False
monkeypatch.setenv("BOR_STREAM_THINKING", "0")
assert _settings().stream_thinking is False
monkeypatch.setenv("BOR_STREAM_THINKING", "false")
assert _settings().stream_thinking is False
monkeypatch.setenv("BOR_STREAM_THINKING", "1")
assert _settings().stream_thinking is True
def test_import_extensions_env_override_is_a_csv_list(monkeypatch) -> None:
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,yml")
s = _settings()
assert s.import_extension_set == {".md", ".yml"}
def test_import_extensions_accepts_novel_extension(monkeypatch) -> None:
"""Phase 56 (owner permission 2026-08-31): the A9 family is the
default, not the ceiling — a novel well-formed extension (``sh``) is
accepted and simply becomes importable."""
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,sh")
s = _settings()
assert s.import_extension_set == {".md", ".sh"}
def test_import_extensions_normalizes_case_and_leading_dot(monkeypatch) -> None:
"""Case and a leading dot are both tolerated (unchanged tolerance)."""
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "MD,.Py")
s = _settings()
assert s.import_extension_set == {".md", ".py"}
def test_import_extensions_rejects_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""A blank list would silently import nothing — fail loudly at
startup, naming the field (empty, whitespace-only, and comma-only
all parse to zero formats)."""
for value in ("", " ", ",,"):
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", value)
with pytest.raises(ValidationError, match="import_extensions"):
_settings()
def test_import_extensions_validator_accepts_new_a9_formats(monkeypatch) -> None:
"""A9 revised 2026-08-27: quadlet/jinja names are first-class default
formats — a CSV using them (a narrowing of the default family) is
accepted."""
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,container,j2")
s = _settings()
assert s.import_extension_set == {".md", ".container", ".j2"}
def test_import_extensions_rejects_malformed_tokens(monkeypatch: pytest.MonkeyPatch) -> None:
"""The shape guard (``^[a-z0-9]{1,16}$``) is the typo guard — it
keeps punctuation and path-ish values out of the set, naming the
offending token(s), while any extension a file could actually be
suffixed with still goes through."""
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,sh!")
with pytest.raises(ValidationError, match="sh!"):
_settings()
monkeypatch.setenv("BOR_IMPORT_EXTENSIONS", "md,../x")
with pytest.raises(ValidationError, match=r"/x"):
_settings()
def test_git_sources_default_empty_and_sources_dir_default() -> None:
"""Phase 28: no git sources by default (backwards-compatible with the
``--source`` / ``DEFAULT_SOURCES`` fallback); the clone location stays
a raw string (``~`` is expanded by the import script, not the setting)."""
s = _settings()
assert s.git_sources == ""
assert s.git_source_list == []
assert s.sources_dir == "~/bor-sources"
def test_git_sources_env_override_parses_comma_separated_list(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``BOR_GIT_SOURCES`` is a raw CSV: entries are trimmed and empty
entries dropped; URLs are stored untouched (no scheme parsing here)."""
monkeypatch.setenv(
"BOR_GIT_SOURCES",
"https://github.com/user/homelab.git, "
" git@github.com:user/deployments.git ,, https://git.reeseapps.com/x/y.git ",
)
s = _settings()
# The raw CSV string is preserved untouched (no parsing in the setting).
assert s.git_sources == (
"https://github.com/user/homelab.git, "
" git@github.com:user/deployments.git ,, https://git.reeseapps.com/x/y.git "
)
assert s.git_source_list == [
"https://github.com/user/homelab.git",
"git@github.com:user/deployments.git",
"https://git.reeseapps.com/x/y.git",
]
def test_git_sources_whitespace_only_yields_empty_list(monkeypatch) -> None:
"""A configured-but-blank value behaves the same as unset: no git
sources, so the script falls back to its legacy local defaults."""
monkeypatch.setenv("BOR_GIT_SOURCES", " , , ")
s = _settings()
assert s.git_source_list == []
def test_sources_dir_env_override_is_raw_string(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BOR_SOURCES_DIR", "/data/bor/sources")
s = _settings()
assert s.sources_dir == "/data/bor/sources"
def test_pdf_page_dir_default_and_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 148 (task 01): ``pdf_page_dir`` — the persistent home for
a page-qualifying PDF's rendered page PNGs — defaults to
``~/bor-sources/pages`` and, like ``sources_dir``/``upload_dir``/
``image_dir``, is a RAW string (``Path.expanduser()`` is applied by
the importer, not here) that ``BOR_PDF_PAGE_DIR`` overrides."""
monkeypatch.delenv("BOR_PDF_PAGE_DIR", raising=False)
assert _settings().pdf_page_dir == "~/bor-sources/pages"
monkeypatch.setenv("BOR_PDF_PAGE_DIR", "/data/bor/pages")
assert _settings().pdf_page_dir == "/data/bor/pages"
def test_suggestions_default_is_three_plus_real_questions() -> None:
s = _settings()
assert len(s.suggestions) >= 3
assert all(isinstance(q, str) and q.strip() for q in s.suggestions)
# Distinct chips only — duplicates in the onboarding row are noise.
assert len({q.strip().lower() for q in s.suggestions}) == len(s.suggestions)
def test_suggestions_env_override_is_json_list(monkeypatch) -> None:
override = [
"How do I back up with Borg?",
"How is my K3S cluster set up?",
"How do I deploy a service?",
]
monkeypatch.setenv("BOR_SUGGESTIONS", json.dumps(override))
s = _settings()
assert s.suggestions == override
def test_suggestions_malformed_json_fails_loudly(monkeypatch) -> None:
monkeypatch.setenv("BOR_SUGGESTIONS", "[not valid json")
with pytest.raises(SettingsError):
_settings()
def test_effective_api_key_fallback(monkeypatch) -> None:
monkeypatch.delenv("AIPI_KEY", raising=False)
s = _settings()
assert s.effective_api_key == "not-needed"
monkeypatch.setenv("AIPI_KEY", "sk-from-env")
s2 = _settings()
assert s2.effective_api_key == "sk-from-env"
# --- Docs push (phase 59) ---
def test_docs_push_defaults_are_inert() -> None:
"""Phase 59, D3: no docs repo by default — the feature is
inert-by-default (button hidden, push endpoint 409s — the
optional-feature pattern of the git-sources env fallback), and the
branch/base defaults + raw work-dir string are in place."""
s = _settings()
assert s.docs_repo == ""
assert s.docs_configured is False
assert s.docs_branch == "bor-docs"
assert s.docs_base_branch == "main"
# Raw string on purpose — Path.expanduser() is applied by the push
# service, not the setting (the sources_dir/upload_dir convention).
assert s.docs_work_dir == "~/bor-docs"
def test_docs_repo_set_is_configured(monkeypatch: pytest.MonkeyPatch) -> None:
"""A non-empty ``BOR_DOCS_REPO`` turns the feature on — a URL or a
local path (D3: generic remote, no scheme parsing here)."""
for repo in ("/path/to/docs-repo", "https://git.example.com/docs.git"):
monkeypatch.setenv("BOR_DOCS_REPO", repo)
s = _settings()
assert s.docs_configured is True
assert s.docs_repo == repo
# Whitespace-only behaves like empty: still inert.
monkeypatch.setenv("BOR_DOCS_REPO", " ")
assert _settings().docs_configured is False
def test_docs_branch_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("BOR_DOCS_BRANCH", raising=False)
monkeypatch.delenv("BOR_DOCS_BASE_BRANCH", raising=False)
assert _settings().docs_branch == "bor-docs"
assert _settings().docs_base_branch == "main"
monkeypatch.setenv("BOR_DOCS_BRANCH", "docs-pr")
monkeypatch.setenv("BOR_DOCS_BASE_BRANCH", "master")
s = _settings()
assert s.docs_branch == "docs-pr"
assert s.docs_base_branch == "master"
def test_docs_creds_defaults_are_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01, C5): no credentials by default — the push is
anonymous/URL-credential-free, exactly as before this phase."""
monkeypatch.delenv("BOR_DOCS_USER", raising=False)
monkeypatch.delenv("BOR_DOCS_TOKEN", raising=False)
s = _settings()
assert s.docs_user == ""
assert s.docs_token == ""
def test_docs_creds_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01): ``BOR_DOCS_USER`` / ``BOR_DOCS_TOKEN``
resolve verbatim — plain ``str``, no coercion."""
monkeypatch.setenv("BOR_DOCS_USER", "alice")
monkeypatch.setenv("BOR_DOCS_TOKEN", "tok")
s = _settings()
assert s.docs_user == "alice"
assert s.docs_token == "tok"
def test_docs_creds_empty_env_stays_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01, C5): an empty (or whitespace) env value stays
``""`` / verbatim — no coercion, no normalization away from empty.
The anonymous state must stay expressible: the settings must NOT
strip the token, or an explicit empty would become a set credential.
(Whitespace is stored verbatim, like ``docs_repo``; the *push*
service decides the empty-vs-set semantics, not the settings.)"""
monkeypatch.setenv("BOR_DOCS_USER", "")
monkeypatch.setenv("BOR_DOCS_TOKEN", "")
s = _settings()
assert s.docs_user == ""
assert s.docs_token == ""
monkeypatch.setenv("BOR_DOCS_TOKEN", " ")
assert _settings().docs_token == " "
def test_docs_work_dir_env_override_is_raw_string(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BOR_DOCS_WORK_DIR", "/data/bor/docs")
s = _settings()
assert s.docs_work_dir == "/data/bor/docs"
def test_docs_branch_whitespace_fails_loudly_when_repo_set(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A whitespace-bearing branch would corrupt a ``git checkout``
argument — fail loud at startup, naming the field (the
``agent_max_rounds`` pattern)."""
monkeypatch.setenv("BOR_DOCS_REPO", "/path/to/docs-repo")
monkeypatch.setenv("BOR_DOCS_BRANCH", "bor docs")
with pytest.raises(ValidationError, match="docs_branch"):
_settings()
def test_docs_branch_dotdot_fails_loudly_when_repo_set(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``..`` is a path-traversal token, never part of a branch name.
A blank branch is rejected too (empty while a repo is set)."""
monkeypatch.setenv("BOR_DOCS_REPO", "/path/to/docs-repo")
monkeypatch.setenv("BOR_DOCS_BRANCH", "a..b")
with pytest.raises(ValidationError, match="docs_branch"):
_settings()
monkeypatch.setenv("BOR_DOCS_BRANCH", " ")
with pytest.raises(ValidationError, match="docs_branch"):
_settings()
def test_docs_base_branch_invalid_fails_loudly_naming_field(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The base branch gets the same token shape check — the error
names ``docs_base_branch``, not the sibling field."""
monkeypatch.setenv("BOR_DOCS_REPO", "/path/to/docs-repo")
monkeypatch.setenv("BOR_DOCS_BASE_BRANCH", "bad branch")
with pytest.raises(ValidationError, match="docs_base_branch"):
_settings()
monkeypatch.setenv("BOR_DOCS_BASE_BRANCH", "a..b")
with pytest.raises(ValidationError, match="docs_base_branch"):
_settings()
def test_docs_branchs_valid_when_repo_set(monkeypatch: pytest.MonkeyPatch) -> None:
"""Repo set + well-formed branch tokens boot cleanly and the
feature is configured (dash/dot/slash branch names are legal git
refs and stay accepted)."""
monkeypatch.setenv("BOR_DOCS_REPO", "/path/to/docs-repo")
s = _settings() # defaults bor-docs / main
assert s.docs_configured is True
monkeypatch.setenv("BOR_DOCS_BRANCH", "feature/docs-update")
monkeypatch.setenv("BOR_DOCS_BASE_BRANCH", "develop")
s2 = _settings()
assert s2.docs_configured is True
assert s2.docs_branch == "feature/docs-update"
assert s2.docs_base_branch == "develop"
def test_docs_branchs_garbage_ignored_when_repo_unset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""All-or-nothing: while the repo is empty the feature is inert, so
the (ignored) branch values must NOT block startup — only a
configured repo makes the shape check apply."""
monkeypatch.delenv("BOR_DOCS_REPO", raising=False)
monkeypatch.setenv("BOR_DOCS_BRANCH", "bor docs..")
monkeypatch.setenv("BOR_DOCS_BASE_BRANCH", "..")
s = _settings()
assert s.docs_configured is False
assert s.docs_branch == "bor docs.." # stored verbatim, never used
# --- UI customization (phase 62, TODO L3) ---
def test_ui_customization_defaults_are_the_phase_61_copy() -> None:
"""UNSET => byte-identical to the phase-61 neutral UI: the locked
phase-61 copy is the DEFAULT (composer placeholder + footer line).
Phase 91 (task 03): the retired CSS-file theme env var is gone —
``Settings`` no longer has a theme field at all (a leftover value
in a deployment's .env is ignored, not a boot failure)."""
s = _settings()
assert s.input_placeholder == "Ask me anything…"
assert s.footer_text == "Powered by self-hosted models"
assert "theme" not in type(s).model_fields
def test_ui_customization_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""The two string settings honor their ``BOR_`` env vars
(``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT``);
placeholder/footer accept any string (empty is legal — the brand
layer then keeps the template default)."""
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "Ask the vault…")
monkeypatch.setenv("BOR_FOOTER_TEXT", "Powered by my own models")
s = _settings()
assert s.input_placeholder == "Ask the vault…"
assert s.footer_text == "Powered by my own models"
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "")
assert _settings().input_placeholder == "" # empty stands
# --- Sync session bounds (phase 184) ---
def test_sync_session_reset_files_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 184 (LOCKED A3): the import walk releases its session every
``BOR_SYNC_SESSION_RESET_FILES`` files (default 200). ``0`` is accepted
and means NEVER release — the exact pre-phase-184 behaviour, the
operator's no-redeploy rollback."""
monkeypatch.delenv("BOR_SYNC_SESSION_RESET_FILES", raising=False)
assert _settings().sync_session_reset_files == 200
monkeypatch.setenv("BOR_SYNC_SESSION_RESET_FILES", "50")
assert _settings().sync_session_reset_files == 50
monkeypatch.setenv("BOR_SYNC_SESSION_RESET_FILES", "0")
assert _settings().sync_session_reset_files == 0
def test_sync_session_reset_files_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative cadence is a typo — a ``%`` against it never fires, so
startup refuses naming the field (the ``agent_max_rounds`` pattern,
phase 184)."""
monkeypatch.setenv("BOR_SYNC_SESSION_RESET_FILES", "-1")
with pytest.raises(ValidationError, match="sync_session_reset_files"):
_settings()
def test_sync_prune_batch_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 184 (LOCKED A3): the prune commits every
``BOR_SYNC_PRUNE_BATCH`` deleted documents (default 500). ``0`` is
accepted and means ONE commit at the end of the whole prune — the
exact pre-phase-184 behaviour."""
monkeypatch.delenv("BOR_SYNC_PRUNE_BATCH", raising=False)
assert _settings().sync_prune_batch == 500
monkeypatch.setenv("BOR_SYNC_PRUNE_BATCH", "25")
assert _settings().sync_prune_batch == 25
monkeypatch.setenv("BOR_SYNC_PRUNE_BATCH", "0")
assert _settings().sync_prune_batch == 0
def test_sync_prune_batch_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative batch size is a typo — the batch comparison would never
fire, so startup refuses naming the field (the ``agent_max_rounds``
pattern, phase 184)."""
monkeypatch.setenv("BOR_SYNC_PRUNE_BATCH", "-1")
with pytest.raises(ValidationError, match="sync_prune_batch"):
_settings()