feat(ack): shard task records for merge-safe workspaces
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS = REPO_ROOT / "skills" / "ack" / "scripts"
|
||||
VALIDATOR = SCRIPTS / "validate_tasks.py"
|
||||
SELECTOR = SCRIPTS / "select_tasks.py"
|
||||
MIGRATOR = SCRIPTS / "migrate_tasks.py"
|
||||
RECORDER = SCRIPTS / "record_task.py"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from task_store import materialize_board # noqa: E402
|
||||
|
||||
|
||||
def manifest() -> dict:
|
||||
return {
|
||||
"version": 1,
|
||||
"ackVersion": "0.20.0",
|
||||
"taskStore": {"version": 1, "directory": "task-records"},
|
||||
"project": {"name": "demo", "orchestration": {
|
||||
"profileVersion": 1,
|
||||
"mode": "manual",
|
||||
"allowedWorktrees": [],
|
||||
"modelAllowlist": {},
|
||||
"profiles": {},
|
||||
"defaults": {},
|
||||
}},
|
||||
"tasks": [],
|
||||
"workerReceipts": [],
|
||||
}
|
||||
|
||||
|
||||
def record(record_id: str, task_id: str, task: dict, parents: list[str] | None = None) -> dict:
|
||||
return {
|
||||
"recordVersion": 1,
|
||||
"recordId": record_id,
|
||||
"taskId": task_id,
|
||||
"parents": parents or [],
|
||||
"kind": "task.snapshot",
|
||||
"task": task,
|
||||
}
|
||||
|
||||
|
||||
class AckTaskStoreTests(unittest.TestCase):
|
||||
def write_board(self, root: Path, records: list[dict]) -> Path:
|
||||
board = root / "tasks.yaml"
|
||||
board.write_text(json.dumps(manifest()), encoding="utf-8")
|
||||
record_dir = root / "task-records"
|
||||
record_dir.mkdir()
|
||||
for item in records:
|
||||
(record_dir / f"{item['recordId']}.yaml").write_text(
|
||||
json.dumps(item), encoding="utf-8"
|
||||
)
|
||||
return board
|
||||
|
||||
def test_materializes_current_task_from_one_head(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
path = self.write_board(
|
||||
root,
|
||||
[record("EV-1", "T-1", {"id": "T-1", "title": "one", "status": "open"})],
|
||||
)
|
||||
from validate_tasks import load_document
|
||||
|
||||
board = load_document(path)
|
||||
self.assertEqual([task["id"] for task in board["tasks"]], ["T-1"])
|
||||
self.assertEqual(board["summary"]["open"], ["T-1"])
|
||||
self.assertNotIn("taskStoreErrors", board)
|
||||
|
||||
def test_two_heads_are_reported_instead_of_last_write_wins(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
path = self.write_board(
|
||||
root,
|
||||
[
|
||||
record("EV-1", "T-1", {"id": "T-1", "title": "one", "status": "open"}),
|
||||
record("EV-2", "T-1", {"id": "T-1", "title": "two", "status": "verified"}, ["EV-1"]),
|
||||
record("EV-3", "T-1", {"id": "T-1", "title": "three", "status": "blocked"}, ["EV-1"]),
|
||||
],
|
||||
)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(VALIDATOR), str(path)],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("未合并 head", result.stderr)
|
||||
|
||||
def test_selector_reads_records_without_changing_cli_shape(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
path = self.write_board(
|
||||
root,
|
||||
[record("EV-1", "T-1", {"id": "T-1", "title": "one", "status": "open"})],
|
||||
)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SELECTOR), str(path)],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
payload = json.loads(result.stdout)
|
||||
self.assertEqual(payload["selection"]["selectedTaskIds"], ["T-1"])
|
||||
|
||||
def test_migrator_dry_run_does_not_modify_legacy_board(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp) / "tasks.yaml"
|
||||
original = {
|
||||
"version": 1,
|
||||
"project": {"name": "demo"},
|
||||
"tasks": [{"id": "T-1", "title": "one", "status": "open"}],
|
||||
"workerReceipts": [],
|
||||
}
|
||||
path.write_text(json.dumps(original), encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(MIGRATOR), str(path)],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(json.loads(path.read_text()), original)
|
||||
self.assertFalse((path.parent / "task-records").exists())
|
||||
|
||||
def test_migrator_apply_keeps_backup_and_materializes_same_board(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp) / "tasks.yaml"
|
||||
path.write_text(json.dumps({
|
||||
"version": 1,
|
||||
"project": {"name": "demo"},
|
||||
"tasks": [{"id": "T-1", "title": "one", "status": "open"}],
|
||||
"workerReceipts": [],
|
||||
}), encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(MIGRATOR), str(path), "--apply"],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((path.parent / "tasks.legacy.yaml").is_file())
|
||||
self.assertTrue((path.parent / "task-records" / "EV-T-1-initial.yaml").is_file())
|
||||
from validate_tasks import load_document
|
||||
|
||||
board = load_document(path)
|
||||
self.assertEqual(board["tasks"][0]["title"], "one")
|
||||
|
||||
def test_recorder_appends_exclusive_snapshot(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
path = self.write_board(root, [])
|
||||
snapshot = root / "task.json"
|
||||
snapshot.write_text(
|
||||
json.dumps({"id": "T-1", "title": "one", "status": "open"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable, str(RECORDER), str(path), "--task-id", "T-1",
|
||||
"--task-json", str(snapshot), "--record-id", "EV-1",
|
||||
],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((root / "task-records" / "EV-1.yaml").is_file())
|
||||
duplicate = subprocess.run(
|
||||
[
|
||||
sys.executable, str(RECORDER), str(path), "--task-id", "T-1",
|
||||
"--task-json", str(snapshot), "--record-id", "EV-1",
|
||||
],
|
||||
cwd=REPO_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(duplicate.returncode, 1)
|
||||
self.assertIn("拒绝覆盖", duplicate.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
CASES = REPO_ROOT / "skills" / "tester" / "scripts" / "generate_cases_csv.py"
|
||||
RUNS = REPO_ROOT / "skills" / "tester" / "scripts" / "generate_run_csv.py"
|
||||
sys.path.insert(0, str(RUNS.parent))
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class TesterRunRecordTests(unittest.TestCase):
|
||||
def test_cases_csv_contains_definitions_only(self) -> None:
|
||||
cases = load_module("tester_cases_csv", CASES)
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
cases_dir = root / ".pouch" / "tester" / "cases"
|
||||
cases_dir.mkdir(parents=True)
|
||||
(root / ".pouch" / "tester" / "catalog.yaml").write_text(
|
||||
"categories:\n - id: smoke\n title: Smoke\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(cases_dir / "smoke.yaml").write_text(
|
||||
"category: smoke\ncases:\n - id: TC-1\n title: One\n what: one\n lifecycle: active\n tag: smoke\n risk: readonly\n how: {steps: [open]}\n expected: [{kind: visible-text, value: ready}]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = cases.export_cases_csv(root)
|
||||
with output.open(encoding="utf-8-sig", newline="") as handle:
|
||||
rows = list(csv.DictReader(handle))
|
||||
self.assertEqual(rows[0]["id"], "TC-1")
|
||||
self.assertNotIn("last_result", rows[0])
|
||||
self.assertNotIn("last_executed", rows[0])
|
||||
|
||||
def test_run_id_has_random_collision_suffix(self) -> None:
|
||||
runs = load_module("tester_run_csv", RUNS)
|
||||
first = runs.new_run_id("smoke")
|
||||
second = runs.new_run_id("smoke")
|
||||
self.assertNotEqual(first, second)
|
||||
self.assertRegex(first, r"^\d{8}T\d{6}Z-smoke-[0-9a-f]{6}$")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user