Files
dingtalk-workspace-cli/test/scripts/todo_skill_scripts_test.py
T
瑞达 2168cae031 Revert "fix(todo): confirm batch creation before writes"
This reverts commit 053b6372a3e8d82d4de3384dac9f672f732a15ac.
2026-08-19 11:01:59 +08:00

208 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Regression tests for the Todo Skill Golden Routes and deterministic scripts."""
import contextlib
import importlib.util
import io
import json
import re
import sys
import tempfile
import unittest
from datetime import timedelta
from pathlib import Path
from unittest import mock
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
TODO_ROOT = ROOT / "skills" / "multi" / "dingtalk-todo"
def load_script(filename):
path = TODO_ROOT / "scripts" / filename
spec = importlib.util.spec_from_file_location(f"todo_test_{path.stem}", path)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
DAILY = load_script("todo_daily_summary.py")
OVERDUE = load_script("todo_overdue_check.py")
BATCH = load_script("todo_batch_create.py")
class TodoSkillAlignmentTest(unittest.TestCase):
def test_golden_routes_prefer_verified_shortcuts(self):
skill = (TODO_ROOT / "SKILL.md").read_text(encoding="utf-8")
for route in (
"todo +remind",
"todo +assign",
"todo +create",
"todo +get-my-tasks",
"todo +search",
"todo +complete",
"todo +update",
"todo +reminder",
):
with self.subTest(route=route):
self.assertIn(route, skill)
self.assertIn("## Golden Routes", skill)
self.assertIn("一次最多加载一个操作 Reference", skill)
self.assertLessEqual(len(skill.encode("utf-8")), 16000)
def test_references_are_todo_only_and_reminder_contract_is_consistent(self):
combined = "\n".join(
path.read_text(encoding="utf-8")
for path in (TODO_ROOT / "references").rglob("*.md")
)
self.assertNotIn("## #7 听记与会后", combined)
self.assertNotIn("minutes list all", combined)
self.assertNotIn("当前不支持单独的 `reminder`", combined)
self.assertIn("提醒写入目前没有对应的查询接口", combined)
def test_markdown_links_resolve(self):
missing = []
pattern = re.compile(r"\[[^\]]+\]\(([^)#]+)(?:#[^)]+)?\)")
for document in TODO_ROOT.rglob("*.md"):
for target in pattern.findall(document.read_text(encoding="utf-8")):
if "://" not in target and not (document.parent / target).resolve().exists():
missing.append(f"{document.relative_to(ROOT)} -> {target}")
self.assertEqual([], missing)
class TodoDailySummaryTest(unittest.TestCase):
def test_uses_bounded_shortcut_and_excludes_missing_due(self):
start, _ = DAILY.date_range("today")
inside = int((start + timedelta(hours=9)).timestamp() * 1000)
outside = int((start + timedelta(days=2)).timestamp() * 1000)
calls = []
def fake_run(args, dws="dws"):
calls.append(args)
return {
"ok": True,
"outcome": "success",
"data": {
"complete": True,
"count": 3,
"todos": [
{"taskId": "in", "subject": "inside", "dueTime": inside},
{"taskId": "none", "subject": "no due"},
{"taskId": "out", "subject": "outside", "dueTime": outside},
],
},
}
stdout = io.StringIO()
with mock.patch.object(DAILY, "run_dws_json", side_effect=fake_run):
with contextlib.redirect_stdout(stdout):
code = DAILY.run(["today"])
self.assertEqual(0, code)
self.assertEqual(1, len(calls))
self.assertEqual(["todo", "+get-my-tasks"], calls[0][:2])
self.assertIn("--all", calls[0])
self.assertIn("--plan-finish-start", calls[0])
self.assertEqual(
["in"], [item["taskId"] for item in json.loads(stdout.getvalue())["todos"]]
)
def test_incomplete_traversal_fails_closed(self):
payload = {"ok": True, "data": {"complete": False, "todos": []}}
stdout = io.StringIO()
with mock.patch.object(DAILY, "run_dws_json", return_value=payload):
with contextlib.redirect_stdout(stdout):
code = DAILY.run(["today"])
self.assertEqual(2, code)
self.assertFalse(json.loads(stdout.getvalue())["complete"])
class TodoOverdueTest(unittest.TestCase):
def test_uses_overdue_shortcut_and_empty_is_success(self):
calls = []
def fake_run(args, dws="dws"):
calls.append(args)
return {"ok": True, "outcome": "success", "data": {"overdue": []}}
stdout = io.StringIO()
with mock.patch.object(OVERDUE, "run_dws_json", side_effect=fake_run):
with contextlib.redirect_stdout(stdout):
code = OVERDUE.run([])
self.assertEqual(0, code)
self.assertEqual(["todo", "+overdue", "--format", "json"], calls[0])
self.assertEqual(0, json.loads(stdout.getvalue())["count"])
class TodoBatchCreateTest(unittest.TestCase):
def test_batch_uses_iso_due_captures_id_and_reads_back(self):
with tempfile.TemporaryDirectory() as raw:
source = Path(raw) / "todos.json"
source.write_text(
json.dumps(
[
{
"title": "reviewed task",
"executors": "user1",
"priority": 40,
"due": "2026-08-18",
}
]
),
encoding="utf-8",
)
calls = []
def fake_run(args, dws="dws", **kwargs):
calls.append(args)
if args[:3] == ["todo", "task", "create"]:
return {"result": {"taskId": "task-1"}}
return {
"ok": True,
"data": {
"todoDetailModel": {
"taskId": "task-1",
"subject": "reviewed task",
}
},
}
stdout = io.StringIO()
with mock.patch.object(BATCH, "run_dws_json", side_effect=fake_run):
with contextlib.redirect_stdout(stdout):
code = BATCH.run([str(source)])
self.assertEqual(0, code)
self.assertEqual(2, len(calls))
due = calls[0][calls[0].index("--due") + 1]
self.assertIn("T23:59:59+08:00", due)
self.assertEqual(
["todo", "task", "get", "--task-id", "task-1"], calls[1][:5]
)
payload = json.loads(stdout.getvalue())
self.assertTrue(payload["complete"])
self.assertEqual("verified", payload["ledger"][0]["status"])
def test_possible_commit_is_preserved_as_unknown(self):
with tempfile.TemporaryDirectory() as raw:
source = Path(raw) / "todos.json"
source.write_text(
'[{"title":"x","executors":"u"}]', encoding="utf-8"
)
failure = BATCH.ScriptError("timeout", commit_unknown=True)
stdout = io.StringIO()
with mock.patch.object(BATCH, "run_dws_json", side_effect=failure):
with contextlib.redirect_stdout(stdout):
code = BATCH.run([str(source)])
self.assertEqual(2, code)
payload = json.loads(stdout.getvalue())
self.assertEqual(1, payload["unknownCount"])
self.assertEqual("unknown", payload["ledger"][0]["status"])
if __name__ == "__main__":
unittest.main()