test_journey.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import json
  2. from app.journey import build_journey
  3. from app.narrator import narrate_contract
  4. def _contract(task_id: str, task_kind: str, goal_ids: list[str]) -> dict:
  5. return {
  6. "task_id": task_id,
  7. "contract_ref": f"script-build://task-contracts/sha256/{task_id}",
  8. "task_kind": task_kind,
  9. "objective": "组织目标并形成可执行的内容结构",
  10. "goal_ids": goal_ids,
  11. "input_decision_refs": [
  12. {
  13. "decision_id": "direction-accepted",
  14. "expected_task_kind": "direction",
  15. "artifact_ref": {
  16. "uri": "script-build://artifact-versions/9",
  17. "kind": "direction",
  18. },
  19. }
  20. ],
  21. "base_artifact_ref": None,
  22. "criteria": [
  23. {"criterion_id": "complete", "description": "结构覆盖所有核心目标", "hard": True}
  24. ],
  25. }
  26. def test_contract_narrator_uses_only_frozen_contract_facts() -> None:
  27. narration = narrate_contract(_contract("task-1", "structure", ["goal-1", "goal-2"]))
  28. assert narration.action == "创建整体结构 Task"
  29. assert narration.goals == ["goal-1", "goal-2"]
  30. assert narration.inputs == ["已接受的创作方向"]
  31. assert narration.output == "整体结构"
  32. assert narration.criteria == ["结构覆盖所有核心目标"]
  33. def test_journey_links_real_lifecycle_and_marks_retry_and_parallel_work() -> None:
  34. source = {
  35. "snapshot": {
  36. "root_trace_id": "root-7",
  37. "root_objective": "为选题生成一份可发布的脚本",
  38. "tasks": [
  39. {
  40. "task_id": "root-task",
  41. "parent_task_id": None,
  42. "status": "running",
  43. "created_at": "2026-07-21T01:00:00+00:00",
  44. "current_spec": {"objective": "root", "context_refs": []},
  45. "child_task_ids": ["task-1", "task-2"],
  46. },
  47. {
  48. "task_id": "task-1",
  49. "parent_task_id": "root-task",
  50. "status": "needs_replan",
  51. "created_at": "2026-07-21T01:01:00+00:00",
  52. "current_spec": {"objective": "structure", "context_refs": []},
  53. "child_task_ids": [],
  54. },
  55. {
  56. "task_id": "task-2",
  57. "parent_task_id": "root-task",
  58. "status": "running",
  59. "created_at": "2026-07-21T01:01:00+00:00",
  60. "current_spec": {"objective": "paragraph", "context_refs": []},
  61. "child_task_ids": [],
  62. },
  63. ],
  64. "attempts": [
  65. {
  66. "attempt_id": "attempt-1",
  67. "task_id": "task-1",
  68. "worker_trace_id": "worker-1",
  69. "status": "submitted",
  70. "operation_id": "operation-1",
  71. "started_at": "2026-07-21T01:02:00+00:00",
  72. "duration_ms": 2000,
  73. "execution_stats": {"primary_model": "qwen", "total_tokens": 120},
  74. "submission": {
  75. "artifact_refs": [
  76. {
  77. "uri": "script-build://artifact-versions/11",
  78. "kind": "structure",
  79. }
  80. ]
  81. },
  82. },
  83. {
  84. "attempt_id": "attempt-2",
  85. "task_id": "task-2",
  86. "worker_trace_id": "worker-2",
  87. "status": "running",
  88. "operation_id": "operation-1",
  89. "started_at": "2026-07-21T01:02:00+00:00",
  90. "execution_stats": {},
  91. "submission": None,
  92. },
  93. ],
  94. "validations": [
  95. {
  96. "validation_id": "validation-1",
  97. "task_id": "task-1",
  98. "attempt_id": "attempt-1",
  99. "status": "completed",
  100. "verdict": "failed",
  101. "started_at": "2026-07-21T01:03:00+00:00",
  102. "duration_ms": 500,
  103. "validation_plan": {
  104. "mode": "deterministic",
  105. "preflight_rule_ids": ["goal-coverage"],
  106. },
  107. "criterion_results": [
  108. {
  109. "criterion_id": "complete",
  110. "verdict": "failed",
  111. "reason": "缺少 goal-2",
  112. }
  113. ],
  114. "execution_stats": {"total_tokens": 0},
  115. }
  116. ],
  117. "decisions": [
  118. {
  119. "decision_id": "decision-1",
  120. "task_id": "task-1",
  121. "attempt_id": "attempt-1",
  122. "validation_id": "validation-1",
  123. "action": "retry",
  124. "reason": "补齐 goal-2 后重新生成",
  125. "created_at": "2026-07-21T01:04:00+00:00",
  126. }
  127. ],
  128. "operations": [
  129. {
  130. "operation_id": "operation-1",
  131. "attempt_ids": ["attempt-1", "attempt-2"],
  132. }
  133. ],
  134. },
  135. "input_summary": {
  136. "topic": "一次反转如何改变信念",
  137. "persona": {"account_name": "每天心理学", "point_count": 3},
  138. "strategies": [{"name": "先冲突后解法"}],
  139. "input_snapshot_id": "11",
  140. "input_digest": "sha256:input",
  141. },
  142. "contracts": {
  143. "task-1": _contract("task-1", "structure", ["goal-1", "goal-2"]),
  144. "task-2": _contract("task-2", "paragraph", ["goal-2"]),
  145. },
  146. "messages": {
  147. "root-7": [
  148. {
  149. "role": "assistant",
  150. "sequence": 1,
  151. "content": {
  152. "text": "当前缺少整体结构,需要组织目标。",
  153. "tool_calls": [
  154. {
  155. "id": "call-1",
  156. "function": {
  157. "name": "plan_script_tasks",
  158. "arguments": json.dumps(
  159. {"contracts": [{"task_kind": "structure"}]}
  160. ),
  161. },
  162. }
  163. ],
  164. },
  165. },
  166. {
  167. "role": "tool",
  168. "tool_call_id": "call-1",
  169. "content": {"result": json.dumps({"task_ids": ["task-1", "task-2"]})},
  170. },
  171. ],
  172. "worker-1": [
  173. {
  174. "role": "assistant",
  175. "content": {
  176. "text": "我先建立结构骨架。",
  177. "tool_calls": [
  178. {"function": {"name": "create_script_structure", "arguments": "{}"}}
  179. ],
  180. },
  181. }
  182. ],
  183. "worker-2": [],
  184. },
  185. "events": [
  186. {"sequence": 10, "payload": {"task_ids": ["task-1", "task-2"]}},
  187. {"sequence": 20, "payload": {"attempt_id": "attempt-1"}},
  188. {"sequence": 21, "payload": {"attempt_id": "attempt-2"}},
  189. {"sequence": 30, "payload": {"validation_id": "validation-1"}},
  190. {"sequence": 40, "payload": {"decision_id": "decision-1"}},
  191. ],
  192. }
  193. journey = build_journey(7, source)
  194. assert [step.step_type for step in journey.steps].count("plan") == 2
  195. execute = next(step for step in journey.steps if step.step_id == "execute:attempt-1")
  196. assert execute.parallel_total == 2
  197. assert execute.reasoning_source == "observable"
  198. validation = next(step for step in journey.steps if step.step_type == "validate")
  199. assert "缺少 goal-2" in validation.reasoning
  200. loop = next(edge for edge in journey.edges if edge.relation == "loop")
  201. assert loop.source == "decide:decision-1"
  202. assert loop.target == "execute:attempt-1"
  203. assert any(edge.relation == "validates" for edge in journey.edges)
  204. assert [step.sequence for step in journey.steps] == list(range(1, len(journey.steps) + 1))