File size: 4,668 Bytes
c5a913d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""End-to-end tests for the assembled `run_guided_setup` flow (the orchestrator).
Wirers are injected at fake paths; extraction is stubbed (the real-agent extraction is
validated separately by the live `claude -p` trial). conftest forces no-claude-CLI so
the Claude wirer uses the settings.json fallback at the fake path — never real config."""
from pathlib import Path

import pytest

from sibyl_memory_cli import migrate as M
from sibyl_memory_cli.setup import ClaudeCodeWirer, CodexWirer
from sibyl_memory_client import MemoryClient

BLOAT = """# Project Atlas

## Identity
Atlas build agent. Stay in scope.

## Rules
- run tests before commit

## Accumulated memory
- user prefers tabs over spaces
- API base is https://api.atlas.local
- met Jordan about the Q3 roadmap
"""


def _home(tmp_path):
    h = tmp_path / "home"
    (h / "proj").mkdir(parents=True)
    (h / "proj" / "CLAUDE.md").write_text(BLOAT)
    (h / "AGENTS.md").write_text("user likes concise answers\n")
    (h / ".codex").mkdir()
    (h / ".codex" / "config.toml").write_text('model = "o4"\n')
    return h


def _fake_wirers(h):
    return {
        "claude-code": ClaudeCodeWirer(settings_path=h / ".claude" / "settings.json"),
        "codex": CodexWirer(config_path=h / ".codex" / "config.toml"),
    }


def test_orchestrator_full_flow(tmp_path):
    h = _home(tmp_path); proj = h / "proj"
    db = h / ".sibyl-memory" / "memory.db"; db.parent.mkdir(parents=True)
    original = (proj / "CLAUDE.md").read_text()

    def fake_extract(backup_dir, db_path):
        # the agent reads from the BACKUP (never the live file) and writes to Sibyl
        assert (backup_dir / "proj" / "CLAUDE.md").read_text() == original
        c = MemoryClient.local(str(db_path), tenant_id="qa")
        c.set_entity("preferences", "indent", {"value": "tabs"})
        c.set_entity("facts", "api_base", {"value": "https://api.atlas.local"})
        c.set_entity("relationships", "jordan", {"note": "Q3 roadmap"})

    io = M.GuidedIO(scripted=["y"])   # confirm debloat = yes
    rep = M.run_guided_setup(home=h, cwd=proj, db_path=db, backup_parent=tmp_path / "bk",
                             io=io, wirers=_fake_wirers(h), extract_fn=fake_extract)

    assert rep["ok"]
    assert rep["phases"]["backup"]["ok"] and rep["phases"]["backup"]["files"] >= 3
    assert rep["phases"]["wire"]["codex"] in ("wired", "already")
    assert rep["phases"]["wire"]["claude-code"] in ("wired", "already")
    assert rep["phases"]["verify"]["new_total"] == 3
    assert rep["phases"]["debloat"]["written"]
    # live file trimmed, backup holds the full original
    assert (proj / "CLAUDE.md").stat().st_size < rep["phases"]["debloat"]["before"]
    bdir = Path(rep["phases"]["backup"]["dir"])
    assert "API base" in (bdir / "proj" / "CLAUDE.md").read_text()
    # codex config really got the block
    assert "[mcp_servers.sibyl_memory]" in (h / ".codex" / "config.toml").read_text()


def test_orchestrator_no_files_aborts(tmp_path):
    h = tmp_path / "empty"; h.mkdir()
    rep = M.run_guided_setup(home=h, cwd=h, db_path=h / "m.db",
                             backup_parent=tmp_path / "bk", io=M.GuidedIO())
    assert rep["ok"] is False


def test_orchestrator_backup_failure_blocks_everything(tmp_path, monkeypatch):
    h = _home(tmp_path); proj = h / "proj"; orig = (proj / "CLAUDE.md").read_text()
    monkeypatch.setattr(M, "run_backup",
                        lambda files, parent, now=None: M.BackupResult(backup_dir=parent / "x", ok=False, error="disk full"))
    rep = M.run_guided_setup(home=h, cwd=proj, db_path=h / "m.db",
                             backup_parent=tmp_path / "bk", io=M.GuidedIO(["y"]))
    assert rep["ok"] is False
    assert "wire" not in rep["phases"] and "debloat" not in rep["phases"]
    assert (proj / "CLAUDE.md").read_text() == orig   # never touched


def test_orchestrator_declined_debloat_keeps_file(tmp_path):
    h = _home(tmp_path); proj = h / "proj"; orig = (proj / "CLAUDE.md").read_text()
    db = h / ".sibyl-memory" / "memory.db"; db.parent.mkdir(parents=True)

    def fx(bk, dbp):
        MemoryClient.local(str(dbp), tenant_id="qa").set_entity("f", "a", {"v": 1})

    rep = M.run_guided_setup(home=h, cwd=proj, db_path=db, backup_parent=tmp_path / "bk",
                             io=M.GuidedIO(scripted=["n"]),   # decline debloat
                             wirers={"codex": CodexWirer(config_path=h / ".codex" / "config.toml")},
                             extract_fn=fx)
    assert rep["phases"]["verify"]["new_total"] == 1
    assert "debloat" not in rep["phases"]
    assert (proj / "CLAUDE.md").read_text() == orig   # declined -> untouched