"""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() # -- log --------------------------------------------------------------------- def test_log_appends_with_today_date(projects, capsys): directory = make_project(projects, "chata") assert project_cli.cmd_log("chata", "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): directory = make_project(projects, "chata", memory="- 2026-09-01: první") project_cli.cmd_log("chata", "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): directory = projects / "chata" directory.mkdir() assert project_cli.cmd_log("chata", "první") == 0 assert (directory / "memory.md").read_text(encoding="utf-8").endswith("první\n") def test_log_reads_stdin_verbatim(projects, monkeypatch): directory = make_project(projects, "chata") text = "Uvozovky „takhle\" a apostrof ' a \"tohle\"\ndruhý řádek" monkeypatch.setattr(sys, "stdin", io.StringIO(text)) assert project_cli.cmd_log("chata", None) == 0 memory = (directory / "memory.md").read_text(encoding="utf-8") assert memory == f"- {today()}: {text}\n" def test_log_does_not_shorten_long_entry(projects): directory = make_project(projects, "chata") text = "x" * 3000 project_cli.cmd_log("chata", text) assert text in (directory / "memory.md").read_text(encoding="utf-8") def test_log_rejects_empty_input(projects): directory = make_project(projects, "chata", memory="- 2026-09-01: první\n") assert project_cli.cmd_log("chata", " ") == 1 assert (directory / "memory.md").read_text(encoding="utf-8") == "- 2026-09-01: první\n" def test_log_rejects_unknown_project(projects): assert project_cli.cmd_log("neznamy", "text") == 1 # -- 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"