| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- import json
- import logging
- import sys
- from pathlib import Path
- def _load_module():
- scripts_dir = Path(__file__).resolve().parents[2]
- if str(scripts_dir) not in sys.path:
- sys.path.insert(0, str(scripts_dir))
- import workflow_manager
- return workflow_manager
- def test_workflow_lifecycle_and_trace(tmp_path, monkeypatch):
- module = _load_module()
- monkeypatch.setattr(module, "find_project_root", lambda: tmp_path)
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- module.start_task("webnovel-write", {"chapter_num": 7})
- module.start_step("Step 1", "Context")
- module.complete_step("Step 1", json.dumps({"state_json_modified": True}, ensure_ascii=False))
- module.complete_task(json.dumps({"review_completed": True}, ensure_ascii=False))
- state = module.load_state()
- assert state["current_task"] is None
- assert state["history"][-1]["status"] == module.TASK_STATUS_COMPLETED
- assert state["last_stable_state"]["artifacts"]["review_completed"] is True
- trace_path = module.get_call_trace_path()
- assert trace_path.exists()
- lines = trace_path.read_text(encoding="utf-8").strip().splitlines()
- events = [json.loads(line)["event"] for line in lines if line.strip()]
- assert "task_started" in events
- assert "step_started" in events
- assert "step_completed" in events
- assert "task_completed" in events
- def test_start_task_reentry_increments_retry(tmp_path, monkeypatch):
- module = _load_module()
- monkeypatch.setattr(module, "find_project_root", lambda: tmp_path)
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- module.start_task("webnovel-write", {"chapter_num": 8})
- module.start_task("webnovel-write", {"chapter_num": 8})
- state = module.load_state()
- task = state["current_task"]
- assert task is not None
- assert task["status"] == module.TASK_STATUS_RUNNING
- assert int(task.get("retry_count", 0)) >= 1
- def test_complete_step_rejects_mismatch_step_id(tmp_path, monkeypatch):
- module = _load_module()
- monkeypatch.setattr(module, "find_project_root", lambda: tmp_path)
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- module.start_task("webnovel-write", {"chapter_num": 9})
- module.start_step("Step 2A", "Draft")
- module.complete_step("Step 2B")
- state = module.load_state()
- current_step = state["current_task"]["current_step"]
- assert current_step is not None
- assert current_step["id"] == "Step 2A"
- assert current_step["status"] == module.STEP_STATUS_RUNNING
- def test_workflow_step_owner_and_order_violation_trace(tmp_path, monkeypatch):
- module = _load_module()
- monkeypatch.setattr(module, "find_project_root", lambda: tmp_path)
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- assert module.expected_step_owner("webnovel-write", "Step 1") == "context-agent"
- assert module.expected_step_owner("webnovel-write", "Step 5") == "data-agent"
- module.start_task("webnovel-write", {"chapter_num": 12})
- module.start_step("Step 3", "Review")
- trace_path = module.get_call_trace_path()
- lines = [json.loads(line) for line in trace_path.read_text(encoding="utf-8").splitlines() if line.strip()]
- events = [row.get("event") for row in lines]
- assert "step_order_violation" in events
- step_started = [row for row in lines if row.get("event") == "step_started"]
- assert step_started
- assert step_started[-1].get("payload", {}).get("expected_owner") == "review-agents"
- def test_safe_append_call_trace_logs_failure(monkeypatch, caplog):
- module = _load_module()
- def _raise_trace_error(event, payload=None):
- raise RuntimeError("trace failure")
- monkeypatch.setattr(module, "append_call_trace", _raise_trace_error)
- with caplog.at_level(logging.WARNING):
- module.safe_append_call_trace("unit_test_event", {"ok": True})
- message_text = "\n".join(record.getMessage() for record in caplog.records)
- assert "failed to append call trace" in message_text
- assert "unit_test_event" in message_text
- def test_workflow_reentry_does_not_duplicate_history(tmp_path, monkeypatch):
- module = _load_module()
- monkeypatch.setattr(module, "find_project_root", lambda: tmp_path)
- webnovel_dir = tmp_path / ".webnovel"
- webnovel_dir.mkdir(parents=True, exist_ok=True)
- module.start_task("webnovel-write", {"chapter_num": 20})
- module.start_task("webnovel-write", {"chapter_num": 20})
- module.start_task("webnovel-write", {"chapter_num": 20})
- state = module.load_state()
- assert isinstance(state.get("history"), list)
- assert len(state.get("history")) == 0
- task = state.get("current_task") or {}
- assert int(task.get("retry_count", 0)) >= 2
|