diff --git a/tests/e2e/test_e2e_v27.py b/tests/e2e/test_e2e_v27.py index c9f3128..cb6f74a 100644 --- a/tests/e2e/test_e2e_v27.py +++ b/tests/e2e/test_e2e_v27.py @@ -2,6 +2,11 @@ import pytest pytestmark = pytest.mark.e2e +skip_no_integration = pytest.mark.skipif( + not __import__("os").environ.get("RUN_INTEGRATION"), + reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon", +) + """v2.7 端到端测试 — 全链路真实环境 覆盖:项目管理 → Task CRUD → SubTask → Stage进度 → 状态聚合 → 依赖链 → 超时 → Mail → 真实Agent调度 @@ -62,6 +67,7 @@ def _tid() -> str: # E1: 项目管理 # =================================================================== +@skip_no_integration class TestE1ProjectManagement: """E1: 项目创建、列表、归档""" @@ -117,6 +123,7 @@ class TestE1ProjectManagement: # E2: Task CRUD + 状态机 # =================================================================== +@skip_no_integration class TestE2TaskCRUD: """E2: Task 创建、查询、状态转换""" @@ -198,6 +205,7 @@ class TestE2TaskCRUD: # E3: SubTask 父子关系 # =================================================================== +@skip_no_integration class TestE3SubTask: """E3: 父子 Task 关系""" @@ -257,6 +265,7 @@ class TestE3SubTask: # E4: Stage 进度 # =================================================================== +@skip_no_integration class TestE4StageProgress: """E4: stages_json + stage 分组统计""" @@ -325,6 +334,7 @@ class TestE4StageProgress: # E5: 父 Task 状态聚合 # =================================================================== +@skip_no_integration class TestE5ParentAggregation: """E5: compute_parent_status 聚合逻辑""" @@ -418,6 +428,7 @@ class TestE5ParentAggregation: # E6: 依赖链 # =================================================================== +@skip_no_integration class TestE6DependencyChain: """E6: depends_on 依赖推进""" @@ -475,6 +486,7 @@ class TestE6DependencyChain: # E7: 超时回收 # =================================================================== +@skip_no_integration class TestE7Timeout: """E7: claimed/working 超时回收""" @@ -537,6 +549,7 @@ class TestE7Timeout: # E8: Mail Tab 6 端点 # =================================================================== +@skip_no_integration class TestE8MailTab: """E8: Mail 端到端""" @@ -769,6 +782,7 @@ def _poll_task(pid, tid, timeout, terminal_states=None): @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE9RealAgentDispatch: """E9: 真实 Agent 调度测试 @@ -915,6 +929,7 @@ class TestE9RealAgentDispatch: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE10FullChain: """E10: 项目 → 父子Task → 生产Ticker → 聚合 → 依赖 → Mail → 前端API @@ -1116,6 +1131,7 @@ class TestE10FullChain: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE11AcquireFirstE2E: """E11: #07.1 Acquire-First Phase 1-4 真实 Agent E2E @@ -1284,6 +1300,7 @@ class TestE11AcquireFirstE2E: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE12TimeoutsUnifiedE2E: """E12: #07.2 _check_timeouts 统一超时 + crash_limit + updated_at fallback @@ -1420,6 +1437,7 @@ class TestE12TimeoutsUnifiedE2E: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE13CompactHangingE2E: """E13: compact_hanging outcome → 任务保持 working(不标 failed) @@ -1513,6 +1531,7 @@ class TestE13CompactHangingE2E: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE14RollbackE2E: """E14: crash 后 current_agent 回退验证 diff --git a/tests/e2e/test_e2e_v31.py b/tests/e2e/test_e2e_v31.py index 47efaba..c0cac5b 100644 --- a/tests/e2e/test_e2e_v31.py +++ b/tests/e2e/test_e2e_v31.py @@ -2,6 +2,11 @@ import pytest pytestmark = pytest.mark.e2e +skip_no_integration = pytest.mark.skipif( + not __import__("os").environ.get("RUN_INTEGRATION"), + reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon", +) + """v3.1 端到端测试 — 新增场景覆盖 覆盖 v3.1 新增功能: @@ -169,6 +174,7 @@ def _patch_db_claimed_at(pid: str, tid: str, claimed_at: str): @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE94BroadcastClaim: """E9-4: 无 assignee 任务 → 广播认领 → Agent 执行 → done""" @@ -230,6 +236,7 @@ class TestE94BroadcastClaim: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE95PauseResume: """E9-5: 手动推状态到 working → paused → 恢复 → 验证 resumed_from""" @@ -322,6 +329,7 @@ class TestE95PauseResume: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE96CancelledRestart: """E9-6: cancelled → pending(重新启动)→ Agent 执行 → done""" @@ -388,6 +396,7 @@ class TestE96CancelledRestart: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE97ClaimedTimeout: """E9-7: claimed 超时 → pending (assignee 清空)""" @@ -453,6 +462,7 @@ class TestE97ClaimedTimeout: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE98CacheHeaders: """E9-8: 验证 CachedStaticFiles 缓存头""" @@ -524,6 +534,7 @@ class TestE98CacheHeaders: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE10cRetryChain: """E10c: failed → pending(手动重试)→ 广播 → 认领 → done""" @@ -598,6 +609,7 @@ class TestE10cRetryChain: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE10dFullLifecycle: """E10d: 无 assignee → 广播认领 → claimed → working → review → done @@ -690,6 +702,7 @@ class TestE10dFullLifecycle: @pytest.mark.integration @pytest.mark.skipif(not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run real agent tests") +@skip_no_integration class TestE15PromptV3Broadcast: """E15: Prompt v3.0 广播认领三级响应 E2E diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py index 87f33a2..cd26cf3 100644 --- a/tests/unit/test_bootstrap.py +++ b/tests/unit/test_bootstrap.py @@ -1,11 +1,12 @@ """#11 Bootstrap 四段式拼装单元测试(v2.1) 覆盖: -- T1: build(role, task_context) 4 段结构 +- T1: build(task, role) 4 段结构 - T2: token 估算 + 预算告警 - T3: 缺失组件降级 - T4: build_for_task 便捷方法 - +- T5: _read_skill fallback +- T6: ROLE_SKILL_MAP 覆盖 """ import pytest @@ -23,52 +24,57 @@ def builder(): # --------------------------------------------------------------------------- -# T1: build(role, task_context) 4 段结构 +# T1: build(task, role) 4 段结构 # --------------------------------------------------------------------------- class TestFourSectionBuild: def test_basic_executor_build(self, builder): b = builder.build( - role="executor", - task_context={"task_id": "t1", "title": "Write tests", "description": "Write unit tests", + task={"task_id": "t1", "title": "Write tests", "description": "Write unit tests", "must_haves": "100% coverage", "status": "claimed"}, + role="executor", ) - # 段: 任务上下文 + # 段 1: 任务上下文 assert "Write tests" in b assert "t1" in b assert "100% coverage" in b + # 段 4: 硬约束 + assert "review" in b + assert "handoff" in b def test_basic_reviewer_build(self, builder): b = builder.build( + task={"task_id": "t2", "title": "Review PR"}, role="reviewer", - task_context={"task_id": "t2", "title": "Review PR"}, ) assert "Review PR" in b + # 段 4: reviewer 硬约束 + assert "pass/fail" in b or "pass" in b def test_planner_build(self, builder): b = builder.build( + task={"task_id": "t3", "title": "Plan sprint"}, role="planner", - task_context={"task_id": "t3", "title": "Plan sprint"}, ) assert "Plan sprint" in b def test_depends_on_outputs_injected(self, builder): b = builder.build( - role="executor", - task_context={ + task={ "title": "T", "depends_on_outputs": [ {"task_id": "t0", "summary": "Data downloaded"}, ], }, + role="executor", ) assert "前序产出" in b assert "Data downloaded" in b def test_no_depends_on_omitted(self, builder): b = builder.build( + task={"title": "T"}, role="executor", - task_context={"title": "T"}, ) assert "前序产出" not in b @@ -93,12 +99,12 @@ class TestTokenEstimation: class TestGracefulDegradation: def test_empty_task(self, builder): - b = builder.build(role="executor", task_context={}) + b = builder.build(task={}, role="executor") assert b # 不为空 - assert "# Role: executor" in b + assert "硬约束" in b def test_partial_task(self, builder): - b = builder.build(role="executor", task_context={"title": "Only title"}) + b = builder.build(task={"title": "Only title"}, role="executor") assert "Only title" in b @@ -114,30 +120,72 @@ class TestBuildForTask: title = "Build Feature" description = "Implement X with tests" must_haves = "Unit tests, Documentation" - task_type = "coding" - risk_level = "low" + status = "claimed" task = MockTask() b = builder.build_for_task(task, role="executor") assert "Build Feature" in b assert "Implement X" in b def test_build_for_task_ignores_kwargs(self, builder): - """build_for_task accepts project_config and experiences""" + """build_for_task 忽略旧参数""" class MockTask: id = "t1" title = "T" description = "" must_haves = "" - task_type = "coding" - risk_level = "low" + status = "" task = MockTask() - # project_config/experiences are valid kwargs for build_for_task + # 旧参数 project_config/experiences 不应报错 b = builder.build_for_task( task, role="executor", project_config={"name": "Old"}, experiences=[{"x": 1}], ) assert "T" in b + # 不应出现旧参数内容 + assert "Old" not in b +# --------------------------------------------------------------------------- +# T5: _read_skill fallback +# --------------------------------------------------------------------------- +class TestReadSkillFallback: + def test_missing_skill_file_returns_empty(self, builder): + """Skill 文件不存在时返回空字符串,不抛异常""" + result = builder._read_skill("nonexistent-skill-xyz") + assert result == "" + + def test_existing_skill_file_read(self, builder): + """能读取实际存在的 Skill 文件""" + # blackboard-executor 应该存在(P1 已创建) + result = builder._read_skill("blackboard-executor") + assert "执行" in result or "executor" in result.lower() + + +# --------------------------------------------------------------------------- +# T6: ROLE_SKILL_MAP 覆盖 +# --------------------------------------------------------------------------- + +class TestRoleSkillMap: + def test_all_roles_mapped(self): + assert set(BootstrapBuilder.ROLE_SKILL_MAP.keys()) == { + "executor", "reviewer", "reviewer-simayi", + "reviewer-pangtong", "planner", "claim", + } + + def test_unknown_role_warns(self, builder): + """未映射的 role 输出 warning""" + import logging + with patch("src.daemon.bootstrap.logger") as mock_logger: + builder.build(task={"title": "T"}, role="unknown_role") + mock_logger.warning.assert_called_with( + "No skill mapping for role: %s", "unknown_role" + ) + + def test_discussion_role_no_warning(self, builder, caplog): + """discussion 角色不应触发 warning""" + import logging + with caplog.at_level(logging.WARNING, logger="moziplus-v2.bootstrap"): + builder.build(task={"title": "T"}, role="discussion") + assert "No skill mapping" not in caplog.text