"""#11 Bootstrap 四段式拼装单元测试(v2.1) 覆盖: - T1: build(task, role) 4 段结构 - T2: token 估算 + 预算告警 - T3: 缺失组件降级 - T4: build_for_task 便捷方法 - T5: _read_skill fallback - T6: ROLE_SKILL_MAP 覆盖 """ import pytest pytestmark = pytest.mark.unit from unittest.mock import patch from pathlib import Path from src.daemon.bootstrap import BootstrapBuilder, estimate_tokens @pytest.fixture def builder(): return BootstrapBuilder(max_tokens=4096) # --------------------------------------------------------------------------- # T1: build(task, role) 4 段结构 # --------------------------------------------------------------------------- class TestFourSectionBuild: def test_basic_executor_build(self, builder): b = builder.build( 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", ) 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", ) assert "Plan sprint" in b def test_depends_on_outputs_injected(self, builder): b = builder.build( 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", ) assert "前序产出" not in b # --------------------------------------------------------------------------- # T2: token 估算 + 预算告警 # --------------------------------------------------------------------------- class TestTokenEstimation: def test_estimate_tokens_basic(self): assert estimate_tokens("hello") > 0 def test_estimate_tokens_long_text(self): text = "a" * 1000 tokens = estimate_tokens(text) assert 200 < tokens < 400 # --------------------------------------------------------------------------- # T3: 缺失组件降级 # --------------------------------------------------------------------------- class TestGracefulDegradation: def test_empty_task(self, builder): b = builder.build(task={}, role="executor") assert b # 不为空 assert "硬约束" in b def test_partial_task(self, builder): b = builder.build(task={"title": "Only title"}, role="executor") assert "Only title" in b # --------------------------------------------------------------------------- # T4: build_for_task 便捷方法 # --------------------------------------------------------------------------- class TestBuildForTask: def test_build_for_task_object(self, builder): """用 mock task 对象测试 build_for_task""" class MockTask: id = "t1" title = "Build Feature" description = "Implement X with tests" must_haves = "Unit tests, Documentation" 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 忽略旧参数""" class MockTask: id = "t1" title = "T" description = "" must_haves = "" status = "" task = MockTask() # 旧参数 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