lllin000_PaperForge/tests/test_setup_plan_v2.py
LLLin000 906b3cafc0 fixup: headless via SetupPlan only, ConfigWriter always schema_version 2
- Removed headless_setup call from CLI dispatch entirely. --headless,
  --modular, and bare all construct the same SetupPlan with identical
  _cfg/zotero_path/agent_type/skip_checks. Only the deprecation notice
  differs per entry point.
- Added skip_checks parameter to SetupPlan (forwarded to CLI arg).
- ConfigWriter always writes schema_version '2' unconditionally (never
  preserves a v1 value from existing config).

RED commands:
  python -m pytest tests/test_setup_plan_v2.py::TestConfigWriterSchemaVersion -v
  python -m pytest tests/test_setup_plan_v2.py::TestCliHeadlessViaSetupPlan -v

GREEN commands (same, after fixes): 61/61 passed.
2026-07-15 02:23:46 +08:00

752 lines
29 KiB
Python

"""Tests for Issue #75: Canonical v2 setup and configuration migration.
Behavioral coverage:
- V2 writes produce nested vault_config with schema_version
- V1 legacy top-level keys are read-only fallback with warning
- vault_config wins over legacy top-level keys (precedence reversed)
- User-supplied dirs forwarded correctly through SetupPlan
- Required-step failure produces non-zero exit
- Reruns are idempotent and preserve source data
"""
from __future__ import annotations
import json
import sys
import warnings
from pathlib import Path
import pytest
# Ensure repo root is importable
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
# ===================================================================
# ConfigWriter v2 format
# ===================================================================
class TestConfigWriterV2Format:
"""ConfigWriter writes v2 nested vault_config."""
def test_writes_vault_config_block(self, tmp_path: Path) -> None:
"""ConfigWriter.write() produces nested vault_config and schema_version."""
from paperforge.setup.config_writer import ConfigWriter
writer = ConfigWriter(tmp_path)
result = writer.write({
"system_dir": "CustomSystem",
"resources_dir": "CustomRes",
"literature_dir": "CustomLit",
"base_dir": "CustomBase",
})
assert result.ok, f"Config write failed: {result.error}"
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
assert data.get("schema_version") == "2"
assert "vault_config" in data
assert data["vault_config"]["system_dir"] == "CustomSystem"
assert data["vault_config"]["resources_dir"] == "CustomRes"
assert data["vault_config"]["literature_dir"] == "CustomLit"
assert data["vault_config"]["base_dir"] == "CustomBase"
def test_no_top_level_path_keys(self, tmp_path: Path) -> None:
"""V2 write must NOT leave top-level path keys outside vault_config."""
from paperforge.setup.config_writer import ConfigWriter
writer = ConfigWriter(tmp_path)
writer.write({
"system_dir": "S",
"resources_dir": "R",
"literature_dir": "L",
"base_dir": "B",
})
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
for key in ("system_dir", "resources_dir", "literature_dir", "base_dir"):
assert key not in data, f"Top-level key '{key}' must not be present in v2"
def test_read_returns_flat_dict(self, tmp_path: Path) -> None:
"""ConfigWriter.read() returns flat dict for compat with consumers."""
from paperforge.setup.config_writer import ConfigWriter
writer = ConfigWriter(tmp_path)
result = writer.write({
"system_dir": "Sys",
"resources_dir": "Res",
"literature_dir": "Lit",
"base_dir": "Base",
})
assert result.ok, f"Write failed: {result.error}"
data = writer.read()
assert data is not None, "read() should not return None after successful write"
assert data.get("system_dir") == "Sys"
assert data.get("resources_dir") == "Res"
assert data.get("literature_dir") == "Lit"
assert data.get("base_dir") == "Base"
def test_read_v1_format(self, tmp_path: Path) -> None:
"""read() handles legacy flat-format json."""
from paperforge.setup.config_writer import ConfigWriter
(tmp_path / "paperforge.json").write_text(
json.dumps({"system_dir": "LegacySys", "resources_dir": "LegacyRes"}),
encoding="utf-8",
)
writer = ConfigWriter(tmp_path)
data = writer.read()
assert data is not None
assert data.get("system_dir") == "LegacySys"
assert data.get("resources_dir") == "LegacyRes"
class TestConfigWriterMergeBehavior:
"""ConfigWriter merges with existing config on rerun."""
def test_reread_and_merge_on_rerun(self, tmp_path: Path) -> None:
"""Writing again preserves existing config and updates specified keys."""
from paperforge.setup.config_writer import ConfigWriter
writer = ConfigWriter(tmp_path)
# First write
r1 = writer.write({
"system_dir": "Sys1",
"resources_dir": "Res1",
"literature_dir": "Lit1",
"base_dir": "Base1",
})
assert r1.ok
# Second write with partial config — merges (only validates on first write)
r2 = writer.write({
"system_dir": "Sys2",
"resources_dir": "Res1", # same
})
assert r2.ok, f"Second write failed: {r2.error}"
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
vc = data.get("vault_config", {})
assert vc["system_dir"] == "Sys2", "Updated key should change"
assert vc["resources_dir"] == "Res1", "Unchanged key should be preserved"
assert vc["literature_dir"] == "Lit1", "Pre-existing key should survive"
assert vc["base_dir"] == "Base1", "Pre-existing key should survive"
# ===================================================================
# load_vault_config v2 precedence
# ===================================================================
class TestConfigV2Precedence:
"""V2 vault_config wins over legacy top-level keys (precedence reversed)."""
def test_vault_config_wins_over_top_level(self, tmp_path: Path) -> None:
"""vault_config.system_dir overrides legacy top-level system_dir."""
from paperforge.config import load_vault_config
vault = tmp_path / "v2_wins"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({
"vault_config": {"system_dir": "V2System"},
"system_dir": "LegacySystem", # legacy fallback
}),
encoding="utf-8",
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
cfg = load_vault_config(vault)
assert cfg["system_dir"] == "V2System", \
f"Expected V2System, got {cfg['system_dir']}"
legacy_warnings = [x for x in w if "legacy" in str(x.message).lower()]
assert len(legacy_warnings) >= 1, \
f"Expected legacy warning, got: {[str(x.message) for x in w]}"
def test_top_level_fallback_when_no_vault_config_key(self, tmp_path: Path) -> None:
"""Legacy top-level key used when vault_config lacks the key."""
from paperforge.config import load_vault_config
vault = tmp_path / "top_fallback"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({
"vault_config": {"system_dir": "V2System"},
"resources_dir": "LegacyResources",
}),
encoding="utf-8",
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
cfg = load_vault_config(vault)
assert cfg["system_dir"] == "V2System"
assert cfg["resources_dir"] == "LegacyResources", \
"Should fall back to legacy top-level key"
legacy_warnings = [x for x in w if "legacy" in str(x.message).lower()]
assert len(legacy_warnings) >= 1, \
f"Expected legacy warning, got: {[str(x.message) for x in w]}"
def test_no_warning_for_clean_v2(self, tmp_path: Path) -> None:
"""No deprecation warning when only vault_config exists."""
from paperforge.config import load_vault_config
vault = tmp_path / "clean_v2"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({
"schema_version": "2",
"vault_config": {"system_dir": "System"},
}),
encoding="utf-8",
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
cfg = load_vault_config(vault)
legacy_warnings = [x for x in w if "legacy" in str(x.message).lower()]
assert len(legacy_warnings) == 0, f"Unexpected warning: {legacy_warnings}"
assert cfg["system_dir"] == "System"
def test_v1_only_config_readable_with_warning(self, tmp_path: Path) -> None:
"""V1 flat-only config is readable with a deprecation warning."""
from paperforge.config import load_vault_config
vault = tmp_path / "v1_only"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({
"system_dir": "OldSystem",
"resources_dir": "OldResources",
}),
encoding="utf-8",
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
cfg = load_vault_config(vault)
assert cfg["system_dir"] == "OldSystem"
assert cfg["resources_dir"] == "OldResources"
legacy_warnings = [x for x in w if "legacy" in str(x.message).lower()]
assert len(legacy_warnings) >= 1, \
f"Expected legacy warning, got: {[str(x.message) for x in w]}"
# ===================================================================
# SetupPlan path forwarding
# ===================================================================
class TestSetupPlanConfigForwarding:
"""SetupPlan forwards user-supplied dirs correctly."""
def test_configured_dirs_in_vault_config(self, tmp_path: Path) -> None:
"""SetupPlan writes user-supplied dirs into vault_config."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "forward_test"
vault.mkdir()
config = {
"system_dir": "CustomSys",
"resources_dir": "CustomRes",
"literature_dir": "CustomLit",
"base_dir": "CustomBase",
}
plan = SetupPlan(
vault=vault,
config=config,
zotero_path="/fake/zotero",
)
exit_code = plan.execute(json_output=False)
# Check that paperforge.json exists and has our dirs
pf_json = vault / "paperforge.json"
assert pf_json.exists(), "paperforge.json was not created"
data = json.loads(pf_json.read_text(encoding="utf-8"))
vc = data.get("vault_config", {})
assert vc.get("system_dir") == "CustomSys"
assert vc.get("resources_dir") == "CustomRes"
assert vc.get("literature_dir") == "CustomLit"
assert vc.get("base_dir") == "CustomBase"
assert vc.get("system_dir") is not None, "All dirs not null"
def test_zotero_path_wired(self, tmp_path: Path) -> None:
"""SetupPlan forwards zotero_path to VaultInitializer."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "zotero_test"
vault.mkdir()
plan = SetupPlan(
vault=vault,
config={"system_dir": "System", "resources_dir": "Res", "literature_dir": "Lit"},
zotero_path="/custom/zotero/data",
)
assert plan.zotero_path == "/custom/zotero/data"
def test_rerun_is_idempotent(self, tmp_path: Path) -> None:
"""Running SetupPlan twice preserves source data."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "rerun_test"
vault.mkdir()
# First run
config = {
"system_dir": "System",
"resources_dir": "Resources",
"literature_dir": "Literature",
"base_dir": "Bases",
}
plan1 = SetupPlan(vault=vault, config=config)
plan1.execute(json_output=False)
# Save original config
data1 = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
# Create a marker file that should survive
marker = vault / "Resources" / "Literature" / "my-note.md"
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text("my note content", encoding="utf-8")
# Second run
plan2 = SetupPlan(vault=vault, config=config)
plan2.execute(json_output=False)
data2 = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
# Config should have v2 format
assert data2.get("schema_version") == "2"
assert "vault_config" in data2
# Source data preserved
assert marker.exists(), "User file was deleted by rerun"
assert marker.read_text(encoding="utf-8") == "my note content"
# ===================================================================
# Failure propagation
# ===================================================================
class TestSetupPlanFailure:
"""SetupPlan returns non-zero when a required step fails."""
def test_failing_step_returns_nonzero(self, tmp_path: Path) -> None:
"""When a required step fails, execute returns non-zero."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "fail_test"
vault.mkdir()
# Empty config causes ConfigWriter validation failure
plan = SetupPlan(vault=vault, config={})
exit_code = plan.execute(json_output=False)
assert exit_code != 0, "Expected non-zero exit code for failed setup"
def test_error_message_in_results(self, tmp_path: Path) -> None:
"""Failed setup with missing keys returns non-zero exit code."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "msg_test"
vault.mkdir()
plan = SetupPlan(vault=vault, config={})
exit_code = plan.execute(json_output=False)
assert exit_code != 0
# ===================================================================
# ConfigWriter legacy path key convergence
# ===================================================================
class TestConfigWriterLegacyMigration:
"""ConfigWriter converges ALL legacy path keys into vault_config."""
def test_writes_all_legacy_path_keys(self, tmp_path: Path) -> None:
"""All seven CONFIG_PATH_KEYS are written into vault_config."""
from paperforge.setup.config_writer import ConfigWriter
writer = ConfigWriter(tmp_path)
all_keys = {
"system_dir": "Sys",
"resources_dir": "Res",
"literature_dir": "Lit",
"control_dir": "Control",
"base_dir": "Base",
"skill_dir": ".skills",
"command_dir": ".cmd",
}
result = writer.write(all_keys)
assert result.ok, f"Write failed: {result.error}"
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
vc = data.get("vault_config", {})
for k, v in all_keys.items():
assert vc.get(k) == v, f"vault_config.{k} expected {v!r}, got {vc.get(k)!r}"
assert data.get("schema_version") == "2"
def test_no_top_level_legacy_keys_after_write(self, tmp_path: Path) -> None:
"""No legacy path keys remain at top level after v2 write."""
from paperforge.setup.config_writer import ConfigWriter
all_keys = {
"system_dir": "Sys",
"resources_dir": "Res",
"literature_dir": "Lit",
"control_dir": "Control",
"base_dir": "Base",
"skill_dir": ".skills",
"command_dir": ".cmd",
}
# Create a v1-style file first
(tmp_path / "paperforge.json").write_text(
json.dumps({**all_keys, "unrelated_meta": "keep"}),
encoding="utf-8",
)
# Rewrite via ConfigWriter — converges into vault_config
writer = ConfigWriter(tmp_path)
result = writer.write(all_keys)
assert result.ok
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
for k in all_keys:
assert k not in data, f"Top-level key '{k}' should not be present after migration"
assert data.get("unrelated_meta") == "keep", "Non-path metadata preserved"
assert data.get("schema_version") == "2"
assert "vault_config" in data
def test_rerun_preserves_unrelated_metadata(self, tmp_path: Path) -> None:
"""Writing twice via ConfigWriter preserves non-path metadata and all vault_config keys."""
from paperforge.setup.config_writer import ConfigWriter
# First write with all keys
base = {
"system_dir": "Sys", "resources_dir": "Res", "literature_dir": "Lit",
"control_dir": "Ctrl", "base_dir": "Base", "skill_dir": ".sk", "command_dir": ".cmd",
}
w = ConfigWriter(tmp_path)
assert w.write(base).ok
# Manually inject unrelated metadata
raw = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
raw["my_version"] = "1.0"
(tmp_path / "paperforge.json").write_text(json.dumps(raw), encoding="utf-8")
# Re-write with subset — should preserve the metadata + all legacy path keys
subset = {"system_dir": "NewSys"}
assert w.write(subset).ok
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
assert data.get("my_version") == "1.0", "Non-path metadata preserved"
assert data.get("schema_version") == "2"
vc = data.get("vault_config", {})
assert vc.get("system_dir") == "NewSys"
assert vc.get("resources_dir") == "Res"
assert vc.get("literature_dir") == "Lit"
assert vc.get("control_dir") == "Ctrl"
assert vc.get("base_dir") == "Base"
assert vc.get("skill_dir") == ".sk"
assert vc.get("command_dir") == ".cmd"
# ===================================================================
# CLI deprecation and headless canonical output
# ===================================================================
class TestCliDeprecation:
"""CLI prints deprecation notice for bare and --headless setup."""
def test_bare_setup_deprecation_on_stderr(self, tmp_path: Path) -> None:
"""'paperforge setup' (bare) prints deprecation to stderr."""
import subprocess
import sys as _sys
result = subprocess.run(
[_sys.executable, "-m", "paperforge", "--vault", str(tmp_path), "setup"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
)
assert "DEPRECATED" in result.stderr, \
f"Expected deprecation notice in stderr: {result.stderr!r}"
def test_headless_setup_produces_v2_canonical(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""--headless setup produces v2 canonical vault_config via ConfigWriter pass."""
from paperforge.setup.config_writer import ConfigWriter
from paperforge.setup_wizard import headless_setup
# Run headless_setup first (deployment), then ConfigWriter (canonicalization)
vault = tmp_path / "h2_canon"
vault.mkdir()
code = headless_setup(
vault=vault,
agent_key="opencode",
system_dir="System",
resources_dir="Resources",
literature_dir="Literature",
base_dir="Bases",
skip_checks=True,
)
# headless_setup may return non-zero due to missing deps — that's ok
# The paperforge.json should exist regardless
# Now canonicalize via ConfigWriter
config_writer = ConfigWriter(vault)
result = config_writer.write({
"system_dir": "System",
"resources_dir": "Resources",
"literature_dir": "Literature",
"base_dir": "Bases",
})
assert result.ok, f"ConfigWriter canonicalize failed: {result.error}"
pf_json = vault / "paperforge.json"
assert pf_json.exists()
data = json.loads(pf_json.read_text(encoding="utf-8"))
assert data.get("schema_version") == "2"
assert "vault_config" in data
vc = data["vault_config"]
assert vc.get("system_dir") == "System"
assert vc.get("resources_dir") == "Resources"
def test_headless_setup_forwards_literature_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""--headless setup forwards --literature-dir to vault_config."""
from paperforge.setup.config_writer import ConfigWriter
from paperforge.setup_wizard import headless_setup
vault = tmp_path / "h2_litdir"
vault.mkdir()
code = headless_setup(
vault=vault,
agent_key="opencode",
system_dir="Sys", resources_dir="Res",
literature_dir="MyPapers", base_dir="Bases",
skip_checks=True,
)
config_writer = ConfigWriter(vault)
config_writer.write({
"system_dir": "Sys",
"resources_dir": "Res",
"literature_dir": "MyPapers",
"base_dir": "Bases",
})
data = json.loads((vault / "paperforge.json").read_text(encoding="utf-8"))
vc = data["vault_config"]
assert vc.get("literature_dir") == "MyPapers", \
f"Expected MyPapers, got {vc.get('literature_dir')}"
# ===================================================================
# Zotero path forwarding
# ===================================================================
class TestZoteroPathForwarding:
"""SetupPlan receives --zotero-data from CLI."""
def test_setup_plan_accepts_zotero_path(self, tmp_path: Path) -> None:
"""SetupPlan constructor stores zotero_path and forwards to VaultInitializer."""
from paperforge.setup.plan import SetupPlan
vault = tmp_path / "zotero_plan"
vault.mkdir()
plan = SetupPlan(
vault=vault,
config={"system_dir": "Sys", "resources_dir": "Res", "literature_dir": "Lit"},
zotero_path=r"C:\Zotero\Data",
)
assert plan.zotero_path == r"C:\Zotero\Data"
def test_zotero_path_reaches_vault_initializer(self, tmp_path: Path) -> None:
"""zotero_path from SetupPlan flows to VaultInitializer.create_zotero_junction."""
from paperforge.setup.vault import VaultInitializer
vault = tmp_path / "zotero_flow"
vault.mkdir()
config = {"system_dir": "Sys", "resources_dir": "Res", "literature_dir": "Lit"}
vi = VaultInitializer(vault, config)
result = vi.create_zotero_junction(r"D:\Zotero")
# Result may fail (path doesn't exist) — that's fine
# What matters is the zotero_path was forwarded and not silently dropped
# The step name confirms it ran
assert result.step == "vault_initializer"
if not result.ok:
# Error should mention the zotero path or junction
combined = (result.error or "") + (result.message or "")
assert len(combined) > 0, "Expected error detail when junction fails"
def test_headless_setup_receives_zotero_data(self, tmp_path: Path) -> None:
"""headless_setup accepts zotero_data parameter."""
from paperforge.setup_wizard import headless_setup
vault = tmp_path / "zotero_headless"
vault.mkdir()
code = headless_setup(
vault=vault,
agent_key="opencode",
system_dir="Sys", resources_dir="Res",
literature_dir="Lit", base_dir="Bases",
zotero_data=r"E:\Zotero",
skip_checks=True,
)
# Verify zotero_data appears in paperforge.json
pf_json = vault / "paperforge.json"
if pf_json.exists():
data = json.loads(pf_json.read_text(encoding="utf-8"))
zotero_val = data.get("zotero_data_dir") or data.get("vault_config", {}).get("zotero_data_dir", "")
if zotero_val:
assert "Zotero" in str(zotero_val), f"Expected Zotero path, got {zotero_val}"
# ===================================================================
# ConfigWriter always writes schema_version 2 (never preserves v1)
# ===================================================================
class TestConfigWriterSchemaVersion:
"""ConfigWriter always writes schema_version=2 regardless of input."""
def test_v1_paperforge_json_rerun_writes_schema_version_2(self, tmp_path: Path) -> None:
"""Rerunning ConfigWriter on a v1 file writes schema_version 2."""
from paperforge.setup.config_writer import ConfigWriter
# Create a v1-style file with no schema_version
(tmp_path / "paperforge.json").write_text(
json.dumps({"system_dir": "OldSys", "resources_dir": "OldRes"}),
encoding="utf-8",
)
writer = ConfigWriter(tmp_path)
result = writer.write({"system_dir": "Sys", "resources_dir": "Res", "literature_dir": "Lit"})
assert result.ok, f"Write failed: {result.error}"
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
assert data.get("schema_version") == "2", \
f"Expected schema_version=2, got {data.get('schema_version')!r}"
def test_v1_rerun_does_not_preserve_old_schema_version(self, tmp_path: Path) -> None:
"""ConfigWriter does NOT preserve a v1 schema_version value."""
from paperforge.setup.config_writer import ConfigWriter
# v1 file with schema_version=1
(tmp_path / "paperforge.json").write_text(
json.dumps({"schema_version": "1", "system_dir": "OldSys"}),
encoding="utf-8",
)
writer = ConfigWriter(tmp_path)
result = writer.write({"system_dir": "Sys", "resources_dir": "Res", "literature_dir": "Lit"})
assert result.ok
data = json.loads((tmp_path / "paperforge.json").read_text(encoding="utf-8"))
assert data.get("schema_version") == "2", \
f"Expected schema_version=2 after migration, got {data.get('schema_version')!r}"
# ===================================================================
# --headless via SetupPlan (not headless_setup)
# ===================================================================
class TestCliHeadlessViaSetupPlan:
"""--headless must use SetupPlan (same engine as --modular)."""
def test_headless_can_be_routed_through_cli(self, tmp_path: Path) -> None:
"""CLI --headless flag is accepted and produces vault_config."""
from paperforge.cli import main
argv = ["--vault", str(tmp_path), "setup", "--headless"]
code = main(argv)
# May return non-zero (env missing) — that's fine
# What matters: paperforge.json was created with v2 format
pf = tmp_path / "paperforge.json"
if pf.exists():
data = json.loads(pf.read_text(encoding="utf-8"))
assert "vault_config" in data, "headless must produce vault_config"
assert data.get("schema_version") == "2"
def test_headless_literature_dir_reaches_vault_config(self, tmp_path: Path) -> None:
"""--literature-dir argument reaches vault_config through SetupPlan."""
from paperforge.cli import main
argv = ["--vault", str(tmp_path), "setup", "--headless",
"--literature-dir", "MyLit", "--skip-checks"]
code = main(argv)
pf = tmp_path / "paperforge.json"
assert pf.exists(), "paperforge.json should exist after headless setup"
data = json.loads(pf.read_text(encoding="utf-8"))
vc = data.get("vault_config", {})
assert vc.get("literature_dir") == "MyLit", \
f"Expected literature_dir=MyLit, got {vc.get('literature_dir')!r}"
def test_headless_zotero_path_reaches_plan(self, tmp_path: Path) -> None:
"""--zotero-data argument reaches SetupPlan via CLI headless."""
from paperforge.cli import main
vault = tmp_path / "zotero_headless_cli"
vault.mkdir()
from unittest.mock import patch
# Monkey-patch SetupPlan to capture zotero_path
original_execute = None
captured = {}
import paperforge.setup.plan as plan_mod
original = plan_mod.SetupPlan.__init__
def patched_init(self, *a, **kw):
captured["zotero_path"] = kw.get("zotero_path")
return original(self, *a, **kw)
with patch.object(plan_mod.SetupPlan, "__init__", patched_init):
argv = ["--vault", str(vault), "setup", "--headless",
"--zotero-data", r"C:\Zotero\Data", "--skip-checks"]
main(argv)
assert captured.get("zotero_path") == r"C:\Zotero\Data", \
f"Expected zotero_path=C:\\Zotero\\Data, got {captured.get('zotero_path')!r}"
def test_headless_skip_checks_accepted(self, tmp_path: Path) -> None:
"""--skip-checks is accepted with --headless."""
from paperforge.cli import main
vault = tmp_path / "skip_headless"
vault.mkdir()
argv = ["--vault", str(vault), "setup", "--headless", "--skip-checks"]
code = main(argv)
# Should not crash — returns exit code
assert isinstance(code, int)
pf = vault / "paperforge.json"
assert pf.exists(), "paperforge.json must exist after headless --skip-checks"