"""单元测试:§24 v4 trajectory prompt.submitted compact 检测 测试 _check_compact_in_progress_trajectory 方法。 用 tmp_path 构造 mock trajectory jsonl 文件。 """ import json from datetime import datetime, timedelta, timezone from pathlib import Path import pytest from src.daemon.spawner import AgentSpawner # ── helpers ── _SESSION_ID = "sess-abc123" def _make_trajectory_event(event_type: str, ts: str = None, **kwargs) -> dict: """构造 trajectory jsonl 事件""" obj = {"type": event_type} if ts: obj["ts"] = ts obj.update(kwargs) return obj def _write_trajectory(tmp_path: Path, session_id: str, turns: list[list[dict]]): """写入 trajectory jsonl,按 turns 分组。 每个 turn 是一个 list of events。 自动在每组前加 session.started(如果该 turn 没有的话)。 """ traj_file = tmp_path / f"{session_id}.trajectory.jsonl" with open(traj_file, "w") as f: for turn_events in turns: # 如果 turn 第一个事件不是 session.started,自动加一个 if not turn_events or turn_events[0].get("type") != "session.started": started = _make_trajectory_event( "session.started", ts=turn_events[0].get("ts") if turn_events else None, ) f.write(json.dumps(started, ensure_ascii=False) + "\n") for evt in turn_events: f.write(json.dumps(evt, ensure_ascii=False) + "\n") def _utc_now_str() -> str: """返回当前 UTC 时间的 ISO 字符串(带 Z 后缀)""" return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.") + \ f"{datetime.now(timezone.utc).microsecond // 1000:03d}Z" def _utc_past_str(minutes_ago: int) -> str: """返回过去 N 分钟的 UTC ISO 字符串""" ts = datetime.now(timezone.utc) - timedelta(minutes=minutes_ago) return ts.strftime("%Y-%m-%dT%H:%M:%S.") + \ f"{ts.microsecond // 1000:03d}Z" # ── 测试用例 ── class TestCheckCompactInProgressTrajectory: """§24 v4: _check_compact_in_progress_trajectory 单元测试""" def test_tc1_normal_turn_with_submitted_returns_false(self, tmp_path): """TC1: 正常 turn(有 prompt.submitted)→ False""" now = _utc_now_str() turns = [[ _make_trajectory_event("session.started", ts=now), _make_trajectory_event("context.compiled", ts=now), _make_trajectory_event("prompt.submitted", ts=now), _make_trajectory_event("model.completed", ts=now), ]] _write_trajectory(tmp_path, _SESSION_ID, turns) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc2_compact_turn_returns_true(self, tmp_path): """TC2: compact turn(无 prompt.submitted,有 context.compiled + model.completed)→ True""" now = _utc_now_str() turns = [[ _make_trajectory_event("session.started", ts=now), _make_trajectory_event("context.compiled", ts=now), _make_trajectory_event("model.completed", ts=now), ]] _write_trajectory(tmp_path, _SESSION_ID, turns) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is True def test_tc3_skipped_prompt_returns_false(self, tmp_path): """TC3: 空白 prompt(有 prompt.skipped)→ False""" now = _utc_now_str() turns = [[ _make_trajectory_event("session.started", ts=now), _make_trajectory_event("context.compiled", ts=now), _make_trajectory_event("prompt.skipped", ts=now), _make_trajectory_event("model.completed", ts=now), ]] _write_trajectory(tmp_path, _SESSION_ID, turns) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc4_timeout_fallback_returns_false(self, tmp_path): """TC4: 超过 30min 兜底 → False""" old = _utc_past_str(35) turns = [[ _make_trajectory_event("session.started", ts=old), _make_trajectory_event("context.compiled", ts=old), _make_trajectory_event("model.completed", ts=old), ]] _write_trajectory(tmp_path, _SESSION_ID, turns) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc5_trajectory_not_exists_returns_false(self, tmp_path): """TC5: trajectory 不存在 → False""" session_file = str(tmp_path / "nonexistent-session") assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc6_empty_trajectory_returns_false(self, tmp_path): """TC6: 空 trajectory → False""" traj_file = tmp_path / f"{_SESSION_ID}.trajectory.jsonl" traj_file.write_text("") session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc7_multi_turn_last_normal_returns_false(self, tmp_path): """TC7: 多 turn 尾部只看最后一个(最后一个正常但之前有 compact)→ False""" old = _utc_past_str(10) now = _utc_now_str() turn1 = [ _make_trajectory_event("session.started", ts=old), _make_trajectory_event("context.compiled", ts=old), _make_trajectory_event("model.completed", ts=old), # compact turn, no prompt ] turn2 = [ _make_trajectory_event("session.started", ts=now), _make_trajectory_event("prompt.submitted", ts=now), _make_trajectory_event("model.completed", ts=now), # normal turn ] _write_trajectory(tmp_path, _SESSION_ID, [turn1, turn2]) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is False def test_tc8_multi_turn_last_abnormal_returns_true(self, tmp_path): """TC8: 多 turn 尾部最后一个非正常 → True""" old = _utc_past_str(5) now = _utc_now_str() turn1 = [ _make_trajectory_event("session.started", ts=old), _make_trajectory_event("prompt.submitted", ts=old), _make_trajectory_event("model.completed", ts=old), # normal turn ] turn2 = [ _make_trajectory_event("session.started", ts=now), _make_trajectory_event("context.compiled", ts=now), _make_trajectory_event("model.completed", ts=now), # compact turn, no prompt ] _write_trajectory(tmp_path, _SESSION_ID, [turn1, turn2]) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is True def test_tc9_null_session_file_returns_false(self): """TC9: session_file 为空字符串 → False""" assert AgentSpawner._check_compact_in_progress_trajectory("") is False def test_tc10_none_session_file_returns_false(self): """TC10: session_file 为 None → False""" assert AgentSpawner._check_compact_in_progress_trajectory(None) is False def test_tc11_events_without_ts_returns_true(self, tmp_path): """TC11: 事件有 type 但无 ts 字段 → 无法判断超时 → True(skip)""" turns = [[ _make_trajectory_event("session.started"), # 无 ts _make_trajectory_event("context.compiled"), # 无 ts _make_trajectory_event("model.completed"), # 无 ts ]] _write_trajectory(tmp_path, _SESSION_ID, turns) session_file = str(tmp_path / _SESSION_ID) assert AgentSpawner._check_compact_in_progress_trajectory(session_file) is True