This commit is contained in:
2026-09-09 10:29:02 +08:00
parent 2aa49b5263
commit ccb12dad7c
102 changed files with 7879 additions and 559 deletions

View File

View File

@@ -0,0 +1,25 @@
"""pytest 全局 fixture:把 ``scripts/`` 注入 sys.path 并隔离数据目录。"""
from __future__ import annotations
import os
import sys
import tempfile
import pytest
_SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
if _SCRIPTS_DIR not in sys.path:
sys.path.insert(0, _SCRIPTS_DIR)
@pytest.fixture
def isolated_env(tmp_path, monkeypatch):
"""隔离的数据根目录,避免污染本机真实数据。"""
data_root = tmp_path / "data_root"
data_root.mkdir()
monkeypatch.setenv("JIANGCHANG_DATA_ROOT", str(data_root))
monkeypatch.setenv("JIANGCHANG_USER_ID", "test_user")
# 重置 config mock 的缓存,让下一次 get 重新读 env
from jiangchang_skill_core import config
config.reset_cache()
return data_root

View File

@@ -0,0 +1,168 @@
"""冒烟测试:覆盖 demo 关键路径。
不依赖 pytest,可用 ``python -m unittest discover -s tests`` 跑。
但 fixture 用法对齐 pytest;这里把 fixture 用 ``setUp`` 模拟。
"""
from __future__ import annotations
import io
import json
import os
import sys
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
# 注入 scripts/ 到 sys.path
_HERE = os.path.dirname(os.path.abspath(__file__))
_SCRIPTS_DIR = os.path.join(os.path.dirname(_HERE), "scripts")
if _SCRIPTS_DIR not in sys.path:
sys.path.insert(0, _SCRIPTS_DIR)
# 与 main.py 同样的 mock 注入:没有真实平台库时启用 mock
if not os.environ.get("TODO_SKILL_USE_REAL_PLATFORM"):
try:
import jiangchang_skill_core # noqa: F401
except ImportError:
_mock_platform = os.path.join(_SCRIPTS_DIR, "_mock_platform")
if _mock_platform not in sys.path:
sys.path.insert(0, _mock_platform)
class IsolatedEnv:
"""临时数据根目录,测试结束自动清理。"""
def __init__(self):
self._tmp = tempfile.mkdtemp(prefix="todo_test_")
self._prev_root = os.environ.get("JIANGCHANG_DATA_ROOT")
self._prev_user = os.environ.get("JIANGCHANG_USER_ID")
os.environ["JIANGCHANG_DATA_ROOT"] = os.path.join(self._tmp, "data")
os.environ["JIANGCHANG_USER_ID"] = "test_user"
os.makedirs(os.environ["JIANGCHANG_DATA_ROOT"], exist_ok=True)
def reset_config(self):
from jiangchang_skill_core import config
config.reset_cache()
def __enter__(self):
self.reset_config()
return self
def __exit__(self, *exc):
if self._prev_root is None:
os.environ.pop("JIANGCHANG_DATA_ROOT", None)
else:
os.environ["JIANGCHANG_DATA_ROOT"] = self._prev_root
if self._prev_user is None:
os.environ.pop("JIANGCHANG_USER_ID", None)
else:
os.environ["JIANGCHANG_USER_ID"] = self._prev_user
return False
def _run_cli(argv: list[str]) -> tuple[int, str, str]:
"""调用 cli.app.main,返回 (rc, stdout, stderr)。"""
from cli.app import main
out = io.StringIO()
err = io.StringIO()
with redirect_stdout(out), redirect_stderr(err):
rc = main(argv)
return rc, out.getvalue(), err.getvalue()
class TestCliSmoke(unittest.TestCase):
def test_empty_argv_shows_usage_and_nonzero(self):
rc, out, _ = _run_cli([])
self.assertEqual(rc, 1)
self.assertIn("待办事项 demo", out)
def test_version_json(self):
with IsolatedEnv():
rc, out, _ = _run_cli(["version"])
self.assertEqual(rc, 0)
payload = json.loads(out.strip())
self.assertEqual(payload["skill"], "todo-skill")
self.assertIn("version", payload)
def test_init_db_creates_tables_and_is_idempotent(self):
with IsolatedEnv():
rc, out, _ = _run_cli(["init-db"])
self.assertEqual(rc, 0)
payload = json.loads(out.strip())
self.assertTrue(payload["ok"])
self.assertTrue(os.path.isfile(payload["db_path"]))
# 再来一次应仍成功
rc2, out2, _ = _run_cli(["init-db"])
self.assertEqual(rc2, 0)
self.assertEqual(json.loads(out2.strip())["db_path"], payload["db_path"])
def test_add_list_done_stats_full_flow(self):
with IsolatedEnv():
_run_cli(["init-db"])
rc, out, _ = _run_cli(["todo", "add", "任务A", "-p", "high"])
self.assertEqual(rc, 0)
item = json.loads(out.strip())
self.assertEqual(item["title"], "任务A")
self.assertEqual(item["status"], "pending")
todo_id = item["id"]
rc, out, _ = _run_cli(["todo", "list"])
self.assertEqual(rc, 0)
listing = json.loads(out.strip())
self.assertEqual(listing["count"], 1)
rc, out, _ = _run_cli(["todo", "done", str(todo_id)])
self.assertEqual(rc, 0)
result = json.loads(out.strip())
self.assertTrue(result["items"][0]["ok"])
self.assertEqual(result["items"][0]["todo"]["status"], "done")
rc, out, _ = _run_cli(["todo", "stats"])
stats = json.loads(out.strip())
self.assertEqual(stats, {"total": 1, "pending": 0, "done": 1})
def test_done_batch_with_comma_ids(self):
with IsolatedEnv():
_run_cli(["init-db"])
_run_cli(["todo", "add", "T1"])
_run_cli(["todo", "add", "T2"])
rc, out, _ = _run_cli(["todo", "done", "1,2"])
self.assertEqual(rc, 0)
result = json.loads(out.strip())
self.assertEqual(result["count"], 2)
self.assertTrue(all(r["ok"] for r in result["items"]))
def test_done_invalid_id_returns_error(self):
with IsolatedEnv():
_run_cli(["init-db"])
rc, out, _ = _run_cli(["todo", "done", "999"])
self.assertEqual(rc, 1)
result = json.loads(out.strip())
self.assertFalse(result["items"][0]["ok"])
def test_remind_async_pattern_emits_progress(self):
with IsolatedEnv():
_run_cli(["init-db"])
_run_cli(["todo", "add", "T1"])
rc, out, _ = _run_cli(["remind", "-s", "1"])
self.assertEqual(rc, 0)
# stdout 应含 emit 进度事件(mock 把 activity 打到 stdout)
self.assertIn('"event": "emit"', out)
self.assertIn('"event": "finish"', out)
self.assertIn('"status": "ok"', out)
# 最后一行是 CLI 结果 JSON
last_line = [l for l in out.strip().splitlines() if l.startswith("{")][-1]
payload = json.loads(last_line)
self.assertTrue(payload["ok"])
self.assertEqual(payload["task_type"], "remind")
def test_health_returns_zero(self):
with IsolatedEnv():
rc, out, _ = _run_cli(["health"])
self.assertEqual(rc, 0)
self.assertIn("todo-skill health: ok", out)
self.assertIn("platform_kit_version:", out)
if __name__ == "__main__":
unittest.main()