mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
396 lines
14 KiB
Python
396 lines
14 KiB
Python
"""Tests for `smriti quickstart` and the shipped demo-space fixture.
|
|
|
|
Covers:
|
|
- Parser wiring (flags, mutually exclusive --remove/--reset)
|
|
- Fixture integrity — counts, valid intent types / note kinds, the demo
|
|
marker, branch divergence, structured tasks. These are content-integrity
|
|
guards: the demo is shipped data, and these tests fail loudly if an edit
|
|
silently breaks the narrative it is meant to teach.
|
|
- seed_demo_space drives the expected sequence of API calls, and rolls back
|
|
a half-built space on failure
|
|
- cmd_quickstart: seeds when absent, is idempotent when present, fails
|
|
cleanly on an unreachable backend
|
|
- remove_demo_space deletes only a marked demo space; refuses a look-alike
|
|
|
|
Like the rest of cli/tests, these use MagicMock(spec=SmritiClient) — no
|
|
running backend required.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from smriti_cli import main as cli_main
|
|
from smriti_cli.client import SmritiClient, SmritiError
|
|
from smriti_cli.quickstart import (
|
|
CLAIMS,
|
|
DEMO_MARKER,
|
|
DEMO_SPACE_DESCRIPTION,
|
|
DEMO_SPACE_NAME,
|
|
FORK_CHECKPOINT,
|
|
MAIN_CHECKPOINTS,
|
|
NOTES,
|
|
build_guide,
|
|
find_demo_space,
|
|
is_demo_space,
|
|
remove_demo_space,
|
|
seed_demo_space,
|
|
)
|
|
|
|
# These mirror the backend's validation sets (claims.py VALID_INTENT_TYPES,
|
|
# checkpoint.py VALID_NOTE_KINDS). If the demo fixture ever uses a value
|
|
# outside them, seeding would 400 against a real backend — catch it here.
|
|
BACKEND_INTENT_TYPES = {"implement", "review", "investigate", "docs", "test"}
|
|
BACKEND_NOTE_KINDS = {"note", "milestone", "noise"}
|
|
|
|
|
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_seed_client() -> MagicMock:
|
|
"""A MagicMock(spec=SmritiClient) with every seed-path method configured."""
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.base_url = "http://localhost:8000"
|
|
client.list_spaces.return_value = []
|
|
client.create_space.return_value = {"id": "demo-space-id", "name": DEMO_SPACE_NAME}
|
|
client.create_session.return_value = {"id": "session-main"}
|
|
|
|
commit_counter = iter(range(1, 9999))
|
|
|
|
def _commit(payload):
|
|
n = next(commit_counter)
|
|
return {"id": f"commit-{n}", "commit_hash": f"hash{n:04d}", "branch_name": "main"}
|
|
|
|
client.create_chat_commit.side_effect = _commit
|
|
client.fork_session.return_value = {
|
|
"session_id": "session-fork",
|
|
"branch_name": "explore/redis-limiter",
|
|
}
|
|
client.close_branch.return_value = {"branch_name": "explore/redis-limiter"}
|
|
client.add_checkpoint_note.return_value = {"checkpoint_id": "cp", "kind": "note"}
|
|
|
|
claim_counter = iter(range(1, 9999))
|
|
|
|
def _claim(**kwargs):
|
|
return {"id": f"claim-{next(claim_counter)}", **kwargs}
|
|
|
|
client.create_claim.side_effect = _claim
|
|
client.update_claim.return_value = {"id": "claim-1", "status": "done"}
|
|
return client
|
|
|
|
|
|
def _args(*argv: str):
|
|
"""Parse a quickstart argv and stub api_url, the way main() would."""
|
|
parsed = cli_main._build_parser().parse_args(["quickstart", *argv])
|
|
parsed.api_url = None
|
|
return parsed
|
|
|
|
|
|
# ── parser wiring ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_quickstart_parser_wiring():
|
|
args = _args()
|
|
assert args.command == "quickstart"
|
|
assert args.func is cli_main.cmd_quickstart
|
|
assert args.remove is False and args.reset is False
|
|
assert args.yes is False and args.json is False
|
|
|
|
assert _args("--remove").remove is True
|
|
reset = _args("--reset", "-y")
|
|
assert reset.reset is True and reset.yes is True
|
|
|
|
|
|
def test_quickstart_remove_and_reset_are_mutually_exclusive():
|
|
with pytest.raises(SystemExit):
|
|
cli_main._build_parser().parse_args(["quickstart", "--remove", "--reset"])
|
|
|
|
|
|
# ── fixture integrity ────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_fixture_has_four_main_checkpoints_and_one_fork():
|
|
assert len(MAIN_CHECKPOINTS) == 4
|
|
assert FORK_CHECKPOINT["key"] == "redis-spike"
|
|
assert FORK_CHECKPOINT["disposition"] == "abandoned"
|
|
|
|
|
|
def test_fixture_uses_two_distinct_agents():
|
|
authors = {cp["author_agent"] for cp in MAIN_CHECKPOINTS}
|
|
authors.add(FORK_CHECKPOINT["author_agent"])
|
|
assert authors == {"claude-code", "codex-local"}
|
|
|
|
|
|
def test_fixture_has_a_cross_agent_continuation():
|
|
"""At least one adjacent main checkpoint pair changes author — the
|
|
hand-off that makes the coordination metric non-zero."""
|
|
pairs = zip(MAIN_CHECKPOINTS, MAIN_CHECKPOINTS[1:])
|
|
assert any(a["author_agent"] != b["author_agent"] for a, b in pairs)
|
|
|
|
|
|
def test_fixture_note_kinds_cover_all_three():
|
|
kinds = {note["kind"] for note in NOTES}
|
|
assert kinds == BACKEND_NOTE_KINDS
|
|
|
|
|
|
def test_fixture_note_kinds_are_backend_valid():
|
|
for note in NOTES:
|
|
assert note["kind"] in BACKEND_NOTE_KINDS
|
|
|
|
|
|
def test_fixture_notes_reference_real_checkpoints():
|
|
valid_keys = {cp["key"] for cp in MAIN_CHECKPOINTS} | {FORK_CHECKPOINT["key"]}
|
|
for note in NOTES:
|
|
assert note["checkpoint_key"] in valid_keys
|
|
|
|
|
|
def test_fixture_claim_intent_types_are_backend_valid():
|
|
for claim in CLAIMS:
|
|
assert claim["intent_type"] in BACKEND_INTENT_TYPES
|
|
|
|
|
|
def test_fixture_task_intent_hints_are_backend_valid():
|
|
for cp in [*MAIN_CHECKPOINTS, FORK_CHECKPOINT]:
|
|
for task in cp["tasks"]:
|
|
assert task["intent_hint"] in BACKEND_INTENT_TYPES
|
|
|
|
|
|
def test_fixture_has_one_done_and_one_active_claim():
|
|
statuses = {claim["final_status"] for claim in CLAIMS}
|
|
assert statuses == {"done", "active"}
|
|
|
|
|
|
def test_fixture_demo_space_is_obviously_demo_data():
|
|
assert DEMO_SPACE_NAME == "smriti-demo"
|
|
assert DEMO_SPACE_DESCRIPTION.startswith("[DEMO]")
|
|
assert DEMO_MARKER in DEMO_SPACE_DESCRIPTION
|
|
|
|
|
|
def test_fixture_fork_source_is_a_real_main_checkpoint():
|
|
main_keys = {cp["key"] for cp in MAIN_CHECKPOINTS}
|
|
assert FORK_CHECKPOINT["fork_from"] in main_keys
|
|
|
|
|
|
def test_fixture_branch_diverges_from_its_fork_source():
|
|
"""The explored branch must disagree with its source on at least one
|
|
decision or assumption, so `smriti compare` shows a real divergence."""
|
|
source = next(
|
|
cp for cp in MAIN_CHECKPOINTS if cp["key"] == FORK_CHECKPOINT["fork_from"]
|
|
)
|
|
new_decisions = set(FORK_CHECKPOINT["decisions"]) - set(source["decisions"])
|
|
new_assumptions = set(FORK_CHECKPOINT["assumptions"]) - set(source["assumptions"])
|
|
assert new_decisions or new_assumptions
|
|
|
|
|
|
def test_fixture_tasks_are_well_formed():
|
|
for cp in [*MAIN_CHECKPOINTS, FORK_CHECKPOINT]:
|
|
for task in cp["tasks"]:
|
|
assert {"id", "text", "intent_hint", "status"} <= set(task)
|
|
assert task["status"] in {"open", "done"}
|
|
|
|
|
|
def test_fixture_decisions_accumulate_down_the_main_chain():
|
|
"""Each main checkpoint carries the standing decisions forward — so the
|
|
HEAD checkpoint (and `current`/`state`) shows the full picture."""
|
|
seen = [len(cp["decisions"]) for cp in MAIN_CHECKPOINTS]
|
|
assert seen == sorted(seen)
|
|
assert seen[-1] >= 4
|
|
|
|
|
|
# ── seed_demo_space ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_seed_demo_space_makes_expected_api_calls():
|
|
client = _make_seed_client()
|
|
seed_demo_space(client)
|
|
|
|
client.create_space.assert_called_once()
|
|
client.create_session.assert_called_once()
|
|
# 4 main checkpoints + 1 on the explored branch
|
|
assert client.create_chat_commit.call_count == 5
|
|
client.fork_session.assert_called_once()
|
|
client.close_branch.assert_called_once()
|
|
assert client.add_checkpoint_note.call_count == 3
|
|
assert client.create_claim.call_count == 2
|
|
# exactly one claim is marked done; the other stays active
|
|
client.update_claim.assert_called_once()
|
|
|
|
|
|
def test_seed_closes_the_explored_branch_as_abandoned():
|
|
client = _make_seed_client()
|
|
seed_demo_space(client)
|
|
space_id, branch_name, disposition = client.close_branch.call_args[0]
|
|
assert disposition == "abandoned"
|
|
|
|
|
|
def test_seed_marks_the_implement_claim_done():
|
|
client = _make_seed_client()
|
|
seed_demo_space(client)
|
|
assert client.update_claim.call_args[0][1] == "done"
|
|
|
|
|
|
def test_seed_uses_a_long_ttl_so_active_work_stays_visible():
|
|
client = _make_seed_client()
|
|
seed_demo_space(client)
|
|
for call in client.create_claim.call_args_list:
|
|
assert call.kwargs["ttl_hours"] >= 168 # at least a week
|
|
|
|
|
|
def test_seed_returns_handles_for_every_checkpoint():
|
|
client = _make_seed_client()
|
|
result = seed_demo_space(client)
|
|
assert result["space_id"] == "demo-space-id"
|
|
expected = {cp["key"] for cp in MAIN_CHECKPOINTS} | {FORK_CHECKPOINT["key"]}
|
|
assert set(result["checkpoints"]) == expected
|
|
assert len(result["claims"]) == 2
|
|
|
|
|
|
def test_seed_rolls_back_a_half_built_space_on_failure():
|
|
client = _make_seed_client()
|
|
client.create_chat_commit.side_effect = SmritiError("backend exploded")
|
|
with pytest.raises(SmritiError):
|
|
seed_demo_space(client)
|
|
# the partially-created space is removed so a retry starts clean
|
|
client.delete_space.assert_called_once_with("demo-space-id")
|
|
|
|
|
|
# ── build_guide ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_build_guide_fills_in_real_checkpoint_ids():
|
|
seed = {
|
|
"space_id": "s",
|
|
"checkpoints": {
|
|
"frame": {"id": "c-frame"},
|
|
"decide": {"id": "c-decide"},
|
|
"ship": {"id": "c-ship"},
|
|
"loadtest": {"id": "c-loadtest"},
|
|
"redis-spike": {"id": "c-redis"},
|
|
},
|
|
"branch_name": "explore/redis-limiter",
|
|
"claims": [],
|
|
}
|
|
guide = build_guide(seed)
|
|
assert len(guide) == 5
|
|
compare = next(s for s in guide if s["command"].startswith("smriti compare"))
|
|
assert "c-decide" in compare["command"]
|
|
assert "c-redis" in compare["command"]
|
|
|
|
|
|
# ── remove_demo_space ────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_remove_deletes_a_marked_demo_space():
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.resolve_space.return_value = {
|
|
"id": "demo-space-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": DEMO_SPACE_DESCRIPTION,
|
|
}
|
|
result = remove_demo_space(client)
|
|
assert result["removed"] is True
|
|
client.delete_space.assert_called_once_with("demo-space-id")
|
|
|
|
|
|
def test_remove_refuses_a_space_without_the_demo_marker():
|
|
"""A real space that merely shares the smriti-demo name must survive."""
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.resolve_space.return_value = {
|
|
"id": "real-space-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": "My actual project. Do not delete.",
|
|
}
|
|
result = remove_demo_space(client)
|
|
assert result["removed"] is False
|
|
assert result["reason"] == "not-a-demo-space"
|
|
client.delete_space.assert_not_called()
|
|
|
|
|
|
def test_remove_when_no_demo_space_exists():
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.resolve_space.side_effect = SmritiError("no space found")
|
|
result = remove_demo_space(client)
|
|
assert result["removed"] is False
|
|
assert result["reason"] == "no-demo-space"
|
|
client.delete_space.assert_not_called()
|
|
|
|
|
|
def test_is_demo_space_only_true_for_marked_spaces():
|
|
assert is_demo_space({"description": DEMO_SPACE_DESCRIPTION}) is True
|
|
assert is_demo_space({"description": "something else"}) is False
|
|
assert is_demo_space({}) is False
|
|
|
|
|
|
def test_find_demo_space_returns_none_when_absent():
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.resolve_space.side_effect = SmritiError("not found")
|
|
assert find_demo_space(client) is None
|
|
|
|
|
|
# ── cmd_quickstart ───────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_cmd_quickstart_seeds_when_space_absent():
|
|
client = _make_seed_client()
|
|
client.resolve_space.side_effect = SmritiError("not found")
|
|
cli_main.cmd_quickstart(client, _args())
|
|
client.create_space.assert_called_once()
|
|
|
|
|
|
def test_cmd_quickstart_is_idempotent_when_space_present(capsys):
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.base_url = "http://localhost:8000"
|
|
client.list_spaces.return_value = []
|
|
client.resolve_space.return_value = {
|
|
"id": "demo-space-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": DEMO_SPACE_DESCRIPTION,
|
|
}
|
|
cli_main.cmd_quickstart(client, _args())
|
|
client.create_space.assert_not_called()
|
|
assert "already seeded" in capsys.readouterr().out
|
|
|
|
|
|
def test_cmd_quickstart_fails_cleanly_on_unreachable_backend(capsys):
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.base_url = "http://localhost:8000"
|
|
client.list_spaces.side_effect = SmritiError("connection refused")
|
|
with pytest.raises(SystemExit):
|
|
cli_main.cmd_quickstart(client, _args())
|
|
err = capsys.readouterr().err
|
|
assert "make dev-local" in err
|
|
|
|
|
|
def test_cmd_quickstart_remove_deletes_the_demo_space():
|
|
client = MagicMock(spec=SmritiClient)
|
|
client.base_url = "http://localhost:8000"
|
|
client.list_spaces.return_value = []
|
|
client.resolve_space.return_value = {
|
|
"id": "demo-space-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": DEMO_SPACE_DESCRIPTION,
|
|
}
|
|
cli_main.cmd_quickstart(client, _args("--remove", "-y"))
|
|
client.delete_space.assert_called_once_with("demo-space-id")
|
|
|
|
|
|
def test_cmd_quickstart_reset_removes_then_reseeds():
|
|
client = _make_seed_client()
|
|
# present on the first lookup (removal), absent on the second (seed)
|
|
client.resolve_space.side_effect = [
|
|
{
|
|
"id": "old-demo-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": DEMO_SPACE_DESCRIPTION,
|
|
},
|
|
{
|
|
"id": "old-demo-id",
|
|
"name": DEMO_SPACE_NAME,
|
|
"description": DEMO_SPACE_DESCRIPTION,
|
|
},
|
|
SmritiError("not found"),
|
|
]
|
|
cli_main.cmd_quickstart(client, _args("--reset", "-y"))
|
|
client.delete_space.assert_called_once_with("old-demo-id")
|
|
client.create_space.assert_called_once()
|