154 lines
5.7 KiB
Python
154 lines
5.7 KiB
Python
"""Tests: task_persist.py surface (dichotomy wave #2479, gap-close).
|
|
|
|
The module has NO delegation seam (pure Python CLI/plugin module — the
|
|
Rust port is a standalone crate driven by tp_parity), so the both-pins
|
|
gate is: the module's behavior must be IDENTICAL with RUNE_TASK_PERSIST_CORE
|
|
unset and set to "python" (the env the loader family would consult —
|
|
task_persist ignores it by design; this pins that the env stays inert).
|
|
|
|
Every test runs against a sandboxed RUNE_MEMORY_DIR — the real ledger
|
|
under /mnt/workdrive/rune/memory is never touched.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = ROOT / "src" / "rune" / "task_persist.py"
|
|
|
|
|
|
@pytest.fixture()
|
|
def sandbox(monkeypatch):
|
|
sd = Path(os.environ.get("TMPDIR", "/tmp")) / "tp_test_sandbox"
|
|
tasks = sd / "tasks"
|
|
tasks.mkdir(parents=True, exist_ok=True)
|
|
monkeypatch.setenv("RUNE_MEMORY_DIR", str(sd))
|
|
ledger = tasks / "persisted.json"
|
|
if ledger.exists():
|
|
ledger.unlink()
|
|
return {"sd": sd, "ledger": ledger}
|
|
|
|
|
|
def cli(*argv, env_extra=None, stdin=None):
|
|
env = dict(os.environ)
|
|
if env_extra:
|
|
env.update(env_extra)
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *argv],
|
|
capture_output=True, text=True, env=env, input=stdin, timeout=60,
|
|
)
|
|
|
|
|
|
def test_import_is_pure_python_surface(sandbox):
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
sys.path.insert(0, str(ROOT / "src" / "rune"))
|
|
for name in list(sys.modules):
|
|
if name == "task_persist":
|
|
del sys.modules[name]
|
|
env = os.environ.copy()
|
|
sys.path.insert(0, str(ROOT / "src/rune"))
|
|
# fresh import under the sandbox; the module reads env at import time
|
|
import importlib
|
|
|
|
task_persist = importlib.import_module("task_persist")
|
|
assert task_persist.LEDGER_DIR.endswith(os.path.join("tasks"))
|
|
assert task_persist.LEDGER_PATH.endswith("persisted.json")
|
|
|
|
|
|
def test_load_default_shape(sandbox):
|
|
ledger = sandbox["ledger"]
|
|
data = json.loads(subprocess.run(
|
|
[sys.executable, "-c",
|
|
"import sys, json; sys.path.insert(0, %r); sys.path.insert(0, %r); "
|
|
"import task_persist; print(json.dumps(task_persist.load()))"
|
|
% (str(ROOT / "src"), str(SCRIPT.parent))],
|
|
capture_output=True, text=True, env=os.environ,
|
|
).stdout)
|
|
assert data == {"tasks": [], "updated": None}
|
|
|
|
|
|
def test_corrupted_file_recovers(sandbox):
|
|
sandbox["ledger"].write_text("{bad json")
|
|
data = json.loads(subprocess.run(
|
|
[sys.executable, "-c",
|
|
"import sys, json; sys.path.insert(0, %r); sys.path.insert(0, %r); "
|
|
"import task_persist; print(json.dumps(task_persist.load()))"
|
|
% (str(ROOT / "src"), str(SCRIPT.parent))],
|
|
capture_output=True, text=True, env=os.environ,
|
|
).stdout)
|
|
assert data == {"tasks": [], "updated": None, "recovered": True}
|
|
|
|
|
|
def test_add_update_restore_cli_roundtrip(sandbox):
|
|
r = cli("add", "ship the port")
|
|
assert r.returncode == 0 and "Added T1: ship the port" in r.stdout
|
|
r = cli("update", "T1", "--status", "done")
|
|
assert "Updated T1" in r.stdout
|
|
r = cli("status")
|
|
assert "[done ] T1: ship the port" in r.stdout
|
|
r = cli("restore")
|
|
assert "# 1 persisted tasks (last updated: " in r.stdout
|
|
assert '[done ] T1: ship the port' in r.stdout
|
|
|
|
|
|
def test_export_replaces_and_warns_on_wipe(sandbox):
|
|
cli("add", "one")
|
|
cli("add", "two")
|
|
r = cli("export", json.dumps([{"id": "T9", "summary": "fresh"}]))
|
|
assert "Exported 1 tasks" in r.stdout
|
|
r = cli("export", "[]")
|
|
assert "Exported 0 tasks" in r.stdout
|
|
assert r.stderr.rstrip() == ("[task_persist] WARNING: export with empty "
|
|
"list will wipe 1 existing tasks")
|
|
|
|
|
|
def test_add_usage_and_invalid_choice(sandbox):
|
|
r = cli("add")
|
|
assert r.returncode == 0
|
|
assert "usage: task_persist.py add '<summary>'" in r.stdout
|
|
r = cli("bogus")
|
|
assert r.returncode == 2
|
|
assert "error: argument cmd: invalid choice: 'bogus'" in r.stderr
|
|
|
|
|
|
def test_parent_id_and_auto_id(sandbox):
|
|
cli("add", "parent task")
|
|
cli("add", "child", "--parent", "T1")
|
|
data = json.loads(sandbox["ledger"].read_text())
|
|
assert [t["id"] for t in data["tasks"]] == ["T1", "T2"]
|
|
assert data["tasks"][1]["parent_id"] == "T1"
|
|
|
|
|
|
def test_env_pin_inert_both_ways(sandbox):
|
|
"""Both pins: the CLI surface is IDENTICAL with the (hypothetical)
|
|
RUNE_TASK_PERSIST_CORE env unset vs pinned to python — task_persist
|
|
has no delegation seam; this proves the env stays inert."""
|
|
seeded = json.dumps({"tasks": [{"id": "T1", "summary": "pinned",
|
|
"status": "open"}]}, indent=2)
|
|
sandbox["ledger"].write_text(seeded)
|
|
out = []
|
|
for extra in ({}, {"RUNE_TASK_PERSIST_CORE": "python"}):
|
|
r1 = cli("status", env_extra=extra)
|
|
r2 = cli("restore", env_extra=extra)
|
|
r3 = cli("add", "two", env_extra=extra)
|
|
r4 = cli("add", "again", env_extra=extra)
|
|
ledger_after = sandbox["ledger"].read_text()
|
|
# wall-clock keys are NOT part of the both-pins contract (the frozen
|
|
# battery pins timestamps against the oracle clock); strip them
|
|
doc = json.loads(ledger_after)
|
|
for t in doc.get("tasks", []):
|
|
t.pop("created", None)
|
|
t.pop("updated", None)
|
|
doc.pop("updated", None)
|
|
out.append((r1.stdout, r2.stdout, r3.stdout, r4.stdout, doc))
|
|
# re-seed from the ORIGINAL constant (dropping whatever pin-1's
|
|
# writes stamped onto the doc)
|
|
sandbox["ledger"].write_text(seeded)
|
|
assert out[0] == out[1]
|
|
assert "[open ] T1: pinned" in out[0][0] |