lllin000_PaperForge/tests/test_config.py
Research Assistant ed95e0f565 feat: runtime contract hardening + skill/command truth alignment (Package A+B)
Atomic snapshots, canonical index mutation serialization, sync post-clean truth,
plugin path config-awareness, embed stop signal honesty, full snapshot bootstrap,
pf_ prefix unification, workflow command/lifecycle/path corrections,
mechanical/cognitive route separation with unknown-command guard.
2026-05-16 22:38:43 +08:00

641 lines
21 KiB
Python

"""Tests for paperforge.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
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_System():
"""Built-in default for system_dir must be 'System'."""
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["system_dir"] == "System"
def test_default_resources_dir_is_Resources():
"""Built-in default for resources_dir must be 'Resources'."""
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["resources_dir"] == "Resources"
def test_default_literature_dir():
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["literature_dir"] == "Literature"
def test_default_control_dir():
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["control_dir"] == "LiteratureControl"
def test_default_base_dir():
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["base_dir"] == "Bases"
def test_default_skill_dir():
from paperforge.config import DEFAULT_CONFIG
assert DEFAULT_CONFIG["skill_dir"] == ".opencode/skills"
def test_default_command_dir():
from paperforge.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.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.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.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.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.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.config import load_vault_config
vault = tmp_path / "vault_empty"
vault.mkdir()
cfg = load_vault_config(vault)
assert cfg["system_dir"] == "System"
assert cfg["resources_dir"] == "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.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",
"zotero_dir",
"resources",
"literature",
"control",
"library_records",
"bases",
"worker_script",
"skill_dir",
"pf_deep_script",
# ── v2.2: canonical locations below paperforge/ ──
"config",
"index",
}
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.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.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 == "__init__.py"
def test_paperforge_paths_includes_pf_deep_script(tmp_path: Path):
"""pf_deep_script key must point to pf_deep.py."""
from paperforge.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 "pf_deep_script" in paths
assert paths["pf_deep_script"].name == "pf_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 pathlib import Path
from paperforge.config import paths_as_strings
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"),
"pf_deep_script": Path("/some/vault/.opencode/skills/paperforge/scripts/pf_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.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.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.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.config import resolve_vault
result = resolve_vault(cli_vault=None, env={}, cwd=tmp_path)
assert result == tmp_path
# ---------------------------------------------------------------------------
# schema_version — CONF-01
# ---------------------------------------------------------------------------
def test_default_config_has_schema_version():
"""DEFAULT_CONFIG must contain schema_version key with value '2'."""
from paperforge.config import DEFAULT_CONFIG
assert "schema_version" in DEFAULT_CONFIG
assert DEFAULT_CONFIG["schema_version"] == "2"
def test_config_keys_includes_schema_version():
"""CONFIG_KEYS must include schema_version (auto-derived from DEFAULT_CONFIG)."""
from paperforge.config import CONFIG_KEYS
assert "schema_version" in CONFIG_KEYS
def test_get_paperforge_schema_version_defaults_to_1(tmp_path: Path):
"""get_paperforge_schema_version returns 1 when no schema_version in paperforge.json."""
from paperforge.config import get_paperforge_schema_version
vault = tmp_path / "no_schema"
vault.mkdir()
(vault / "paperforge.json").write_text("{}", encoding="utf-8")
assert get_paperforge_schema_version(vault) == 1
def test_get_paperforge_schema_version_reads_2(tmp_path: Path):
"""get_paperforge_schema_version returns 2 when schema_version is '2'."""
import json
from paperforge.config import get_paperforge_schema_version
vault = tmp_path / "schema_v2"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({"schema_version": "2"}), encoding="utf-8"
)
assert get_paperforge_schema_version(vault) == 2
def test_load_vault_config_excludes_schema_version(tmp_path: Path):
"""load_vault_config output must NOT contain schema_version key."""
from paperforge.config import load_vault_config
vault = tmp_path / "exclude_schema"
vault.mkdir()
(vault / "paperforge.json").write_text(
'{"schema_version": "2"}', encoding="utf-8"
)
cfg = load_vault_config(vault)
assert "schema_version" not in cfg
# ---------------------------------------------------------------------------
# migrate_paperforge_json — CONF-02 legacy migration
# ---------------------------------------------------------------------------
def test_migrate_legacy_top_level_keys(tmp_path: Path):
"""Legacy top-level path keys are migrated to vault_config block."""
import json
from paperforge.config import migrate_paperforge_json
vault = tmp_path / "legacy_migrate"
vault.mkdir()
data = {
"system_dir": "OldSystem",
"literature_dir": "OldLit",
"version": "1.4",
"vault_config": {
"system_dir": "99_System",
},
}
(vault / "paperforge.json").write_text(json.dumps(data), encoding="utf-8")
assert migrate_paperforge_json(vault) is True
result = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
assert "system_dir" not in result, "top-level key removed"
assert "literature_dir" not in result, "top-level key removed"
# system_dir already exists in vault_config — existing value preserved
assert result["vault_config"]["system_dir"] == "99_System", "existing vault_config preserved"
# literature_dir is only at top-level (gap in vault_config) — filled from top-level
assert result["vault_config"]["literature_dir"] == "OldLit", "top-level fills vault_config gap"
assert result["schema_version"] == "2"
assert (vault / "paperforge.json.bak").exists(), "backup created"
def test_migrate_idempotent(tmp_path: Path):
"""Already-migrated files return False (no-op)."""
import json
from paperforge.config import migrate_paperforge_json
vault = tmp_path / "already_migrated"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({"vault_config": {"system_dir": "99_System"}, "schema_version": "2"}),
encoding="utf-8",
)
assert migrate_paperforge_json(vault) is False
def test_migrate_no_file(tmp_path: Path):
"""No paperforge.json returns False."""
from paperforge.config import migrate_paperforge_json
vault = tmp_path / "no_file"
vault.mkdir()
assert migrate_paperforge_json(vault) is False
def test_migrate_non_path_keys_survive(tmp_path: Path):
"""Non-path top-level keys survive migration in output root."""
import json
from paperforge.config import migrate_paperforge_json
vault = tmp_path / "non_path_survive"
vault.mkdir()
data = {
"system_dir": "OldSystem",
"version": "1.4",
"agent_platform": "opencode",
"vault_config": {},
}
(vault / "paperforge.json").write_text(json.dumps(data), encoding="utf-8")
migrate_paperforge_json(vault)
result = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
assert "version" in result
assert result["version"] == "1.4"
assert "agent_platform" in result
assert result["agent_platform"] == "opencode"
assert result["vault_config"]["system_dir"] == "OldSystem"
def test_migrate_no_vault_config_block_creates_it(tmp_path: Path):
"""Top-level keys without vault_config block create a new vault_config."""
import json
from paperforge.config import migrate_paperforge_json
vault = tmp_path / "creates_vc"
vault.mkdir()
data = {
"system_dir": "OldSystem",
"resources_dir": "OldRes",
"literature_dir": "OldLit",
"version": "1.4",
}
(vault / "paperforge.json").write_text(json.dumps(data), encoding="utf-8")
migrate_paperforge_json(vault)
result = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
assert "vault_config" in result
assert result["vault_config"]["system_dir"] == "OldSystem"
assert result["vault_config"]["resources_dir"] == "OldRes"
assert result["vault_config"]["literature_dir"] == "OldLit"
assert result["schema_version"] == "2"