237 lines
8.0 KiB
Python
237 lines
8.0 KiB
Python
"""Tests for project_cli.py — deterministic file operations for the /project skill."""
|
|
|
|
import io
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
import project_cli # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def projects(tmp_path, monkeypatch):
|
|
root = tmp_path / "projects"
|
|
root.mkdir()
|
|
monkeypatch.setenv("PROJECTS_DIR", str(root))
|
|
return root
|
|
|
|
|
|
def make_project(projects, slug, prompt="", memory="", state=""):
|
|
directory = projects / slug
|
|
directory.mkdir()
|
|
(directory / "prompt.md").write_text(prompt, encoding="utf-8")
|
|
(directory / "memory.md").write_text(memory, encoding="utf-8")
|
|
(directory / "state.md").write_text(state, encoding="utf-8")
|
|
return directory
|
|
|
|
|
|
def today():
|
|
return datetime.now(ZoneInfo("Europe/Prague")).date().isoformat()
|
|
|
|
|
|
@pytest.fixture
|
|
def entry_file(tmp_path):
|
|
"""Write entry text to a file and return its path, the way the skill does."""
|
|
|
|
def write(text):
|
|
path = tmp_path / "entry.md"
|
|
path.write_text(text, encoding="utf-8")
|
|
return str(path)
|
|
|
|
return write
|
|
|
|
|
|
# -- log ---------------------------------------------------------------------
|
|
|
|
|
|
def test_log_appends_with_today_date(projects, entry_file, capsys):
|
|
directory = make_project(projects, "chata")
|
|
|
|
assert project_cli.cmd_log("chata", entry_file("Dřevo objednáno")) == 0
|
|
|
|
memory = (directory / "memory.md").read_text(encoding="utf-8")
|
|
assert memory == f"- {today()}: Dřevo objednáno\n"
|
|
assert json.loads(capsys.readouterr().out)["appended"].startswith(f"- {today()}:")
|
|
|
|
|
|
def test_log_does_not_join_when_file_lacks_trailing_newline(projects, entry_file):
|
|
directory = make_project(projects, "chata", memory="- 2026-09-01: první")
|
|
|
|
project_cli.cmd_log("chata", entry_file("druhý"))
|
|
|
|
lines = (directory / "memory.md").read_text(encoding="utf-8").splitlines()
|
|
assert lines == ["- 2026-09-01: první", f"- {today()}: druhý"]
|
|
|
|
|
|
def test_log_creates_missing_memory_file(projects, entry_file):
|
|
directory = projects / "chata"
|
|
directory.mkdir()
|
|
|
|
assert project_cli.cmd_log("chata", entry_file("první")) == 0
|
|
assert (directory / "memory.md").read_text(encoding="utf-8").endswith("první\n")
|
|
|
|
|
|
def test_log_reads_file_verbatim(projects, entry_file):
|
|
directory = make_project(projects, "chata")
|
|
text = "Uvozovky „takhle\" a apostrof ' a \"tohle\"\ndruhý řádek"
|
|
|
|
assert project_cli.cmd_log("chata", entry_file(text)) == 0
|
|
|
|
memory = (directory / "memory.md").read_text(encoding="utf-8")
|
|
assert memory == f"- {today()}: {text}\n"
|
|
|
|
|
|
def test_log_accepts_text_the_exec_guard_would_reject_on_a_command_line(projects, entry_file):
|
|
"""Regression: `Cíl:` / `Závěr:` parse as Windows drive paths in the exec guard.
|
|
|
|
The guard scans the raw command string, so such text must reach the script
|
|
through a file, never as an argument, a heredoc or a pipe.
|
|
"""
|
|
directory = make_project(projects, "chata")
|
|
text = "Cíl: srovnat pánev. Diagnóza/směr: mobilita kyčle. Závěr: pokračovat."
|
|
|
|
assert project_cli.cmd_log("chata", entry_file(text)) == 0
|
|
assert text in (directory / "memory.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_log_reads_stdin_when_no_file_given(projects, monkeypatch):
|
|
directory = make_project(projects, "chata")
|
|
monkeypatch.setattr(sys, "stdin", io.StringIO("ze stdinu"))
|
|
|
|
assert project_cli.cmd_log("chata", None) == 0
|
|
|
|
assert (directory / "memory.md").read_text(encoding="utf-8") == f"- {today()}: ze stdinu\n"
|
|
|
|
|
|
def test_log_rejects_missing_file_without_touching_memory(projects, tmp_path, capsys):
|
|
directory = make_project(projects, "chata", memory="- 2026-09-01: první\n")
|
|
|
|
assert project_cli.cmd_log("chata", str(tmp_path / "chybi.md")) == 1
|
|
|
|
assert "No such file" in capsys.readouterr().err
|
|
assert (directory / "memory.md").read_text(encoding="utf-8") == "- 2026-09-01: první\n"
|
|
|
|
|
|
def test_log_does_not_shorten_long_entry(projects, entry_file):
|
|
directory = make_project(projects, "chata")
|
|
text = "x" * 3000
|
|
|
|
project_cli.cmd_log("chata", entry_file(text))
|
|
|
|
assert text in (directory / "memory.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_log_rejects_empty_input(projects, entry_file):
|
|
directory = make_project(projects, "chata", memory="- 2026-09-01: první\n")
|
|
|
|
assert project_cli.cmd_log("chata", entry_file(" ")) == 1
|
|
assert (directory / "memory.md").read_text(encoding="utf-8") == "- 2026-09-01: první\n"
|
|
|
|
|
|
def test_log_rejects_unknown_project(projects, entry_file):
|
|
assert project_cli.cmd_log("neznamy", entry_file("text")) == 1
|
|
|
|
|
|
def test_log_no_longer_accepts_text_on_the_command_line(monkeypatch):
|
|
"""`--text` is gone for good: prose in argv is what the exec guard blocks."""
|
|
monkeypatch.setattr(sys, "argv", ["project_cli.py", "log", "chata", "--text", "něco"])
|
|
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
project_cli.main()
|
|
|
|
assert excinfo.value.code == 2
|
|
|
|
|
|
# -- activate ----------------------------------------------------------------
|
|
|
|
|
|
def test_activate_prints_all_three_sections(projects, capsys):
|
|
make_project(projects, "chata", prompt="# Chata", memory="- 2026-09-01: a\n", state="stav")
|
|
|
|
assert project_cli.cmd_activate("chata") == 0
|
|
|
|
out = capsys.readouterr().out
|
|
assert "### prompt.md\n# Chata" in out
|
|
assert "### memory.md\n- 2026-09-01: a" in out
|
|
assert "### state.md\nstav" in out
|
|
assert "[!] state.md is empty" not in out
|
|
|
|
|
|
def test_activate_creates_missing_files(projects, capsys):
|
|
directory = projects / "chata"
|
|
directory.mkdir()
|
|
|
|
assert project_cli.cmd_activate("chata") == 0
|
|
|
|
assert all((directory / name).is_file() for name in project_cli.PROJECT_FILES)
|
|
|
|
|
|
def test_activate_flags_empty_state(projects, capsys):
|
|
make_project(projects, "chata", memory="- 2026-09-01: a\n")
|
|
|
|
project_cli.cmd_activate("chata")
|
|
|
|
assert "[!] state.md is empty" in capsys.readouterr().out
|
|
|
|
|
|
def test_activate_omits_oldest_entries_without_touching_disk(projects, capsys):
|
|
memory = "".join(f"- 2026-09-01: entry {i} {'x' * 200}\n" for i in range(200))
|
|
directory = make_project(projects, "big", prompt="P", memory=memory, state="S")
|
|
size_before = (directory / "memory.md").stat().st_size
|
|
|
|
assert project_cli.cmd_activate("big") == 0
|
|
|
|
out = capsys.readouterr().out
|
|
assert len(out) <= project_cli.MAX_OUTPUT_CHARS
|
|
assert "older entries not shown" in out
|
|
assert "entry 199" in out and "entry 0 " not in out
|
|
assert "### prompt.md\nP" in out and "### state.md\nS" in out
|
|
assert (directory / "memory.md").stat().st_size == size_before
|
|
|
|
|
|
def test_activate_rejects_unknown_project(projects, capsys):
|
|
make_project(projects, "chata")
|
|
|
|
assert project_cli.cmd_activate("neznamy") == 1
|
|
assert "chata" in capsys.readouterr().err
|
|
|
|
|
|
# -- list / new --------------------------------------------------------------
|
|
|
|
|
|
def test_list_reports_sizes_and_flags_empty_state(projects, capsys):
|
|
make_project(projects, "chata", prompt="x" * 595, memory="y" * 2048)
|
|
make_project(projects, "life", prompt="a", memory="b", state="c")
|
|
|
|
assert project_cli.cmd_list() == 0
|
|
|
|
lines = capsys.readouterr().out.splitlines()
|
|
assert "prompt 595B" in lines[0] and "memory 2.0K" in lines[0]
|
|
assert "state 0B (!)" in lines[0]
|
|
assert "(!)" not in lines[1]
|
|
|
|
|
|
def test_list_without_projects(projects, capsys):
|
|
assert project_cli.cmd_list() == 0
|
|
assert capsys.readouterr().out.strip() == "(no projects yet)"
|
|
|
|
|
|
def test_new_creates_empty_files_without_artifacts(projects, capsys):
|
|
assert project_cli.cmd_new("novy") == 0
|
|
|
|
directory = projects / "novy"
|
|
assert all((directory / name).read_text(encoding="utf-8") == "" for name in project_cli.PROJECT_FILES)
|
|
assert not (directory / "artifacts").exists()
|
|
|
|
|
|
def test_new_refuses_existing_project(projects):
|
|
make_project(projects, "chata", memory="- 2026-09-01: a\n")
|
|
|
|
assert project_cli.cmd_new("chata") == 1
|
|
assert (projects / "chata" / "memory.md").read_text(encoding="utf-8") == "- 2026-09-01: a\n"
|