mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
test(01-01): add failing test for config resolver contract
- Tests DEFAULT_CONFIG defaults (99_System, 03_Resources, Literature, etc.) - Tests ENV_KEYS has all required PAPERFORGE_* overrides - Tests load_vault_config precedence: overrides > env > JSON > defaults - Tests nested vault_config honored per CONF-04 - Tests top-level keys override nested for backward compat per CONF-04 - Tests paperforge_paths returns exactly required user-facing keys - Tests paths are absolute Path objects - Tests worker_script and ld_deep_script keys present - Tests paths_as_strings returns JSON-serializable dict[str,str] - Tests resolve_vault precedence: cli > env > json search > cwd RED phase: module not yet implemented, all tests fail as expected.
This commit is contained in:
parent
8097ec6ff3
commit
6b0b16b1dd
1 changed files with 448 additions and 0 deletions
448
tests/test_config.py
Normal file
448
tests/test_config.py
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
"""Tests for paperforge_lite.config resolver contract.
|
||||
|
||||
These tests prove:
|
||||
- CONF-01: Env vars override JSON values
|
||||
- CONF-02: paperforge_paths returns required key inventory
|
||||
- CONF-03: All consumers use the same resolver
|
||||
- CONF-04: Top-level and nested paperforge.json keys are both honored
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_vault(tmp_path: Path) -> Path:
|
||||
"""A vault with no paperforge.json."""
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_with_nested_config(tmp_path: Path) -> Path:
|
||||
"""A vault with nested vault_config block."""
|
||||
vault = tmp_path / "vault_nested"
|
||||
vault.mkdir()
|
||||
pf = vault / "paperforge.json"
|
||||
pf.write_text(
|
||||
json.dumps({
|
||||
"vault_config": {
|
||||
"system_dir": "CustomSystem",
|
||||
"resources_dir": "CustomResources",
|
||||
"literature_dir": "CustomLiterature",
|
||||
"control_dir": "CustomControl",
|
||||
"base_dir": "CustomBases",
|
||||
}
|
||||
}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return vault
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vault_with_top_level_config(tmp_path: Path) -> Path:
|
||||
"""A vault with legacy top-level keys that should override nested for backward compat."""
|
||||
vault = tmp_path / "vault_legacy"
|
||||
vault.mkdir()
|
||||
pf = vault / "paperforge.json"
|
||||
pf.write_text(
|
||||
json.dumps({
|
||||
"vault_config": {
|
||||
"system_dir": "NestedSystem",
|
||||
"resources_dir": "NestedResources",
|
||||
},
|
||||
# Legacy top-level keys — these take precedence per CONF-04
|
||||
"system_dir": "LegacySystem",
|
||||
"resources_dir": "LegacyResources",
|
||||
"literature_dir": "LegacyLiterature",
|
||||
"control_dir": "LegacyControl",
|
||||
"base_dir": "LegacyBases",
|
||||
}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return vault
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def populated_vault(tmp_path: Path) -> Path:
|
||||
"""A vault with realistic directory structure."""
|
||||
vault = tmp_path / "vault_populated"
|
||||
vault.mkdir()
|
||||
system = vault / "99_System"
|
||||
system.mkdir()
|
||||
pf_dir = system / "PaperForge"
|
||||
pf_dir.mkdir(parents=True)
|
||||
(pf_dir / "exports").mkdir()
|
||||
(pf_dir / "ocr").mkdir()
|
||||
resources = vault / "03_Resources"
|
||||
resources.mkdir()
|
||||
literature = resources / "Literature"
|
||||
literature.mkdir(parents=True)
|
||||
control = resources / "LiteratureControl"
|
||||
control.mkdir(parents=True)
|
||||
control_records = control / "library-records"
|
||||
control_records.mkdir(parents=True)
|
||||
bases = vault / "05_Bases"
|
||||
bases.mkdir()
|
||||
skills = vault / ".opencode" / "skills"
|
||||
skills.mkdir(parents=True)
|
||||
command = vault / ".opencode" / "command"
|
||||
command.mkdir(parents=True)
|
||||
return vault
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env_dict() -> dict[str, str]:
|
||||
"""Empty env dict for tests that control env injection."""
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DEFAULT_CONFIG tests — truths from must_haves
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_default_system_dir_is_99_System():
|
||||
"""Built-in default for system_dir must be '99_System'."""
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["system_dir"] == "99_System"
|
||||
|
||||
|
||||
def test_default_resources_dir_is_03_Resources():
|
||||
"""Built-in default for resources_dir must be '03_Resources'."""
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["resources_dir"] == "03_Resources"
|
||||
|
||||
|
||||
def test_default_literature_dir():
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["literature_dir"] == "Literature"
|
||||
|
||||
|
||||
def test_default_control_dir():
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["control_dir"] == "LiteratureControl"
|
||||
|
||||
|
||||
def test_default_base_dir():
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["base_dir"] == "05_Bases"
|
||||
|
||||
|
||||
def test_default_skill_dir():
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["skill_dir"] == ".opencode/skills"
|
||||
|
||||
|
||||
def test_default_command_dir():
|
||||
from paperforge_lite.config import DEFAULT_CONFIG
|
||||
assert DEFAULT_CONFIG["command_dir"] == ".opencode/command"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ENV_KEYS coverage — CONF-01
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_env_keys_has_all_required_overrides():
|
||||
"""All PAPERFORGE_* env vars must be registered in ENV_KEYS."""
|
||||
from paperforge_lite.config import ENV_KEYS
|
||||
required = {
|
||||
"PAPERFORGE_VAULT",
|
||||
"PAPERFORGE_SYSTEM_DIR",
|
||||
"PAPERFORGE_RESOURCES_DIR",
|
||||
"PAPERFORGE_LITERATURE_DIR",
|
||||
"PAPERFORGE_CONTROL_DIR",
|
||||
"PAPERFORGE_BASE_DIR",
|
||||
"PAPERFORGE_SKILL_DIR",
|
||||
"PAPERFORGE_COMMAND_DIR",
|
||||
}
|
||||
env_values = set(ENV_KEYS.values())
|
||||
assert required.issubset(env_values), f"Missing env keys: {required - env_values}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# load_vault_config precedence — CONF-01, CONF-04
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_env_overrides_nested_json(env_dict):
|
||||
"""PAPERFORGE_SYSTEM_DIR overrides nested vault_config.system_dir (CONF-01)."""
|
||||
from paperforge_lite.config import load_vault_config
|
||||
|
||||
# Create a vault with nested config
|
||||
vault = Path("test_vault_env_override")
|
||||
vault.mkdir(exist_ok=True)
|
||||
(vault / "paperforge.json").write_text(
|
||||
json.dumps({"vault_config": {"system_dir": "NestedSystem"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Env overrides JSON
|
||||
env_dict["PAPERFORGE_SYSTEM_DIR"] = "EnvSystem"
|
||||
cfg = load_vault_config(vault, env=env_dict)
|
||||
|
||||
assert cfg["system_dir"] == "EnvSystem", (
|
||||
f"Expected 'EnvSystem' from PAPERFORGE_SYSTEM_DIR, got '{cfg['system_dir']}'"
|
||||
)
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
shutil.rmtree(vault, ignore_errors=True)
|
||||
|
||||
|
||||
def test_explicit_overrides_win_over_env(env_dict):
|
||||
"""Explicit overrides passed to load_vault_config win over env vars (CONF-01)."""
|
||||
from paperforge_lite.config import load_vault_config
|
||||
|
||||
vault = Path("test_vault_explicit")
|
||||
vault.mkdir(exist_ok=True)
|
||||
(vault / "paperforge.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
env_dict["PAPERFORGE_SYSTEM_DIR"] = "EnvSystem"
|
||||
overrides = {"system_dir": "OverrideSystem"}
|
||||
cfg = load_vault_config(vault, env=env_dict, overrides=overrides)
|
||||
|
||||
assert cfg["system_dir"] == "OverrideSystem", (
|
||||
f"Expected 'OverrideSystem' from explicit override, got '{cfg['system_dir']}'"
|
||||
)
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(vault, ignore_errors=True)
|
||||
|
||||
|
||||
def test_nested_vault_config_is_honored(tmp_path: Path):
|
||||
"""Nested vault_config keys are honored when present (CONF-04)."""
|
||||
from paperforge_lite.config import load_vault_config
|
||||
|
||||
vault = tmp_path / "vault_nested"
|
||||
vault.mkdir()
|
||||
(vault / "paperforge.json").write_text(
|
||||
json.dumps({"vault_config": {"system_dir": "NestedSystem"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
cfg = load_vault_config(vault)
|
||||
assert cfg["system_dir"] == "NestedSystem"
|
||||
|
||||
|
||||
def test_top_level_keys_override_nested_for_backward_compat(tmp_path: Path):
|
||||
"""Top-level paperforge.json keys override nested vault_config (CONF-04 backward compat)."""
|
||||
from paperforge_lite.config import load_vault_config
|
||||
|
||||
vault = tmp_path / "vault_legacy"
|
||||
vault.mkdir()
|
||||
(vault / "paperforge.json").write_text(
|
||||
json.dumps({
|
||||
"vault_config": {"system_dir": "NestedSystem"},
|
||||
"system_dir": "LegacySystem",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
cfg = load_vault_config(vault)
|
||||
assert cfg["system_dir"] == "LegacySystem", (
|
||||
f"Expected 'LegacySystem' from top-level key, got '{cfg['system_dir']}'"
|
||||
)
|
||||
|
||||
|
||||
def test_defaults_used_when_no_json(tmp_path: Path):
|
||||
"""When no paperforge.json exists, defaults are returned."""
|
||||
from paperforge_lite.config import load_vault_config
|
||||
|
||||
vault = tmp_path / "vault_empty"
|
||||
vault.mkdir()
|
||||
|
||||
cfg = load_vault_config(vault)
|
||||
assert cfg["system_dir"] == "99_System"
|
||||
assert cfg["resources_dir"] == "03_Resources"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# paperforge_paths key inventory — CONF-02, CONF-03
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_paperforge_paths_returns_exact_keys(tmp_path: Path):
|
||||
"""paperforge_paths() must return exactly the required user-facing keys."""
|
||||
from paperforge_lite.config import paperforge_paths
|
||||
|
||||
vault = tmp_path / "vault_paths"
|
||||
vault.mkdir()
|
||||
|
||||
# Create required directory structure
|
||||
(vault / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
|
||||
(vault / "99_System" / "PaperForge" / "ocr").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "Literature").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "LiteratureControl" / "library-records").mkdir(parents=True)
|
||||
(vault / "05_Bases").mkdir(parents=True)
|
||||
(vault / ".opencode" / "skills").mkdir(parents=True)
|
||||
(vault / ".opencode" / "command").mkdir(parents=True)
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
|
||||
required_keys = {
|
||||
"vault",
|
||||
"system",
|
||||
"paperforge",
|
||||
"exports",
|
||||
"ocr",
|
||||
"resources",
|
||||
"literature",
|
||||
"control",
|
||||
"library_records",
|
||||
"bases",
|
||||
"worker_script",
|
||||
"skill_dir",
|
||||
"ld_deep_script",
|
||||
}
|
||||
|
||||
actual_keys = set(paths.keys())
|
||||
missing = required_keys - actual_keys
|
||||
extra = actual_keys - required_keys
|
||||
|
||||
assert not missing, f"Missing required path keys: {sorted(missing)}"
|
||||
assert not extra, f"Extra path keys not in spec: {sorted(extra)}"
|
||||
|
||||
|
||||
def test_paperforge_paths_values_are_absolute(tmp_path: Path):
|
||||
"""All path values returned must be absolute Path objects."""
|
||||
from paperforge_lite.config import paperforge_paths
|
||||
|
||||
vault = tmp_path / "vault_absolute"
|
||||
vault.mkdir()
|
||||
(vault / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
|
||||
(vault / "99_System" / "PaperForge" / "ocr").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "Literature").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "LiteratureControl" / "library-records").mkdir(parents=True)
|
||||
(vault / "05_Bases").mkdir(parents=True)
|
||||
(vault / ".opencode" / "skills").mkdir(parents=True)
|
||||
(vault / ".opencode" / "command").mkdir(parents=True)
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
for name, path in paths.items():
|
||||
assert path.is_absolute(), f"Path '{name}' is not absolute: {path}"
|
||||
|
||||
|
||||
def test_paperforge_paths_includes_worker_script(tmp_path: Path):
|
||||
"""worker_script key must point to literature_pipeline.py."""
|
||||
from paperforge_lite.config import paperforge_paths
|
||||
|
||||
vault = tmp_path / "vault_ws"
|
||||
vault.mkdir()
|
||||
(vault / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
|
||||
(vault / "99_System" / "PaperForge" / "ocr").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "Literature").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "LiteratureControl" / "library-records").mkdir(parents=True)
|
||||
(vault / "05_Bases").mkdir(parents=True)
|
||||
(vault / ".opencode" / "skills").mkdir(parents=True)
|
||||
(vault / ".opencode" / "command").mkdir(parents=True)
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
assert "worker_script" in paths
|
||||
assert paths["worker_script"].name == "literature_pipeline.py"
|
||||
|
||||
|
||||
def test_paperforge_paths_includes_ld_deep_script(tmp_path: Path):
|
||||
"""ld_deep_script key must point to ld_deep.py."""
|
||||
from paperforge_lite.config import paperforge_paths
|
||||
|
||||
vault = tmp_path / "vault_ld"
|
||||
vault.mkdir()
|
||||
(vault / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
|
||||
(vault / "99_System" / "PaperForge" / "ocr").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "Literature").mkdir(parents=True)
|
||||
(vault / "03_Resources" / "LiteratureControl" / "library-records").mkdir(parents=True)
|
||||
(vault / "05_Bases").mkdir(parents=True)
|
||||
(vault / ".opencode" / "skills").mkdir(parents=True)
|
||||
(vault / ".opencode" / "command").mkdir(parents=True)
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
assert "ld_deep_script" in paths
|
||||
assert paths["ld_deep_script"].name == "ld_deep.py"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# paths_as_strings — JSON serializable output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_paths_as_strings_returns_string_values():
|
||||
"""paths_as_strings must return dict[str, str] with all values as strings."""
|
||||
from paperforge_lite.config import paths_as_strings
|
||||
from pathlib import Path
|
||||
|
||||
paths = {
|
||||
"vault": Path("/some/vault"),
|
||||
"system": Path("/some/vault/99_System"),
|
||||
"paperforge": Path("/some/vault/99_System/PaperForge"),
|
||||
"exports": Path("/some/vault/99_System/PaperForge/exports"),
|
||||
"ocr": Path("/some/vault/99_System/PaperForge/ocr"),
|
||||
"resources": Path("/some/vault/03_Resources"),
|
||||
"literature": Path("/some/vault/03_Resources/Literature"),
|
||||
"control": Path("/some/vault/03_Resources/LiteratureControl"),
|
||||
"library_records": Path("/some/vault/03_Resources/LiteratureControl/library-records"),
|
||||
"bases": Path("/some/vault/05_Bases"),
|
||||
"worker_script": Path("/some/vault/99_System/PaperForge/worker/scripts/literature_pipeline.py"),
|
||||
"skill_dir": Path("/some/vault/.opencode/skills"),
|
||||
"ld_deep_script": Path("/some/vault/.opencode/skills/literature-qa/scripts/ld_deep.py"),
|
||||
}
|
||||
|
||||
result = paths_as_strings(paths)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
for key, value in result.items():
|
||||
assert isinstance(value, str), f"Expected str for '{key}', got {type(value).__name__}"
|
||||
|
||||
# Verify JSON serializable
|
||||
json.dumps(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_vault precedence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_resolve_vault_precedence_explicit_first(tmp_path: Path):
|
||||
"""resolve_vault returns explicit cli_vault first."""
|
||||
from paperforge_lite.config import resolve_vault
|
||||
|
||||
vault_a = tmp_path / "vault_a"
|
||||
vault_b = tmp_path / "vault_b"
|
||||
vault_a.mkdir()
|
||||
vault_b.mkdir()
|
||||
|
||||
result = resolve_vault(cli_vault=vault_a, env={}, cwd=vault_b)
|
||||
assert result == vault_a
|
||||
|
||||
|
||||
def test_resolve_vault_precedence_env_second(tmp_path: Path):
|
||||
"""resolve_vault returns PAPERFORGE_VAULT when no explicit cli_vault."""
|
||||
from paperforge_lite.config import resolve_vault
|
||||
|
||||
vault = tmp_path / "vault_env"
|
||||
vault.mkdir()
|
||||
|
||||
result = resolve_vault(cli_vault=None, env={"PAPERFORGE_VAULT": str(vault)}, cwd=tmp_path)
|
||||
assert result == vault
|
||||
|
||||
|
||||
def test_resolve_vault_precedence_json_search_third(tmp_path: Path):
|
||||
"""resolve_vault falls back to scanning cwd for paperforge.json."""
|
||||
from paperforge_lite.config import resolve_vault
|
||||
|
||||
vault = tmp_path / "vault_json"
|
||||
vault.mkdir()
|
||||
(vault / "paperforge.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
result = resolve_vault(cli_vault=None, env={}, cwd=vault)
|
||||
assert result == vault
|
||||
|
||||
|
||||
def test_resolve_vault_precedence_cwd_last(tmp_path: Path):
|
||||
"""resolve_vault falls back to cwd when no explicit, env, or json."""
|
||||
from paperforge_lite.config import resolve_vault
|
||||
|
||||
result = resolve_vault(cli_vault=None, env={}, cwd=tmp_path)
|
||||
assert result == tmp_path
|
||||
Loading…
Reference in a new issue