diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py new file mode 100644 index 0000000..1a3358e --- /dev/null +++ b/tests/test_bootstrap.py @@ -0,0 +1,212 @@ +"""F11 Bootstrap 拼装单元测试 + +按 test-plan-v2.6.md §F11: +- T1: 各 role 拼装(P0) +- T2: token 估算(P0) +- T3: 缺失组件降级(P1) +- T4: 模板变量替换(P1) +""" + +import pytest +from pathlib import Path + +from src.blackboard.models import Task +from src.daemon.bootstrap import BootstrapBuilder, estimate_tokens + + +@pytest.fixture +def builder(): + return BootstrapBuilder(max_tokens=4096) + + +@pytest.fixture +def builder_with_templates(tmp_path): + template_dir = tmp_path / "templates" + template_dir.mkdir() + (template_dir / "executor.md").write_text("# Executor Role\nYou execute tasks.") + (template_dir / "reviewer.md").write_text("# Reviewer Role\nYou review code.") + (template_dir / "planner.md").write_text("# Planner Role\nYou plan tasks.") + return BootstrapBuilder(template_dir=template_dir, max_tokens=4096) + + +# --------------------------------------------------------------------------- +# T1: 各 role 拼装 +# --------------------------------------------------------------------------- + +class TestRoleBootstrap: + def test_executor_bootstrap(self, builder): + b = builder.build( + role="executor", + task_context={"task_id": "t1", "title": "Write tests"}, + ) + assert "Write tests" in b + assert "t1" in b + + def test_reviewer_bootstrap(self, builder): + b = builder.build( + role="reviewer", + task_context={"task_id": "t2", "title": "Review PR"}, + ) + assert "Review PR" in b + + def test_planner_bootstrap(self, builder): + b = builder.build( + role="planner", + task_context={"task_id": "t3", "title": "Plan sprint"}, + ) + assert "Plan sprint" in b + + def test_executor_with_guardrail(self, builder): + b = builder.build( + role="executor", + guardrail_rules="## Guardrail\nNo dangerous ops", + ) + assert "Guardrail" in b + + def test_reviewer_no_guardrail(self, builder): + b = builder.build( + role="reviewer", + guardrail_rules="## Guardrail\nNo dangerous ops", + ) + assert "Guardrail" not in b # reviewer 不注入 guardrail + + def test_executor_with_review_protocol(self, builder): + b = builder.build( + role="executor", + review_protocols="## Review Protocol\nCheck tests", + ) + assert "Review Protocol" in b + + def test_reviewer_with_review_protocol(self, builder): + b = builder.build( + role="reviewer", + review_protocols="## Review Protocol\nCheck quality", + ) + assert "Review Protocol" in b + + def test_with_template(self, builder_with_templates): + b = builder_with_templates.build(role="executor") + assert "Executor Role" in b + assert "You execute tasks" 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 # ~333 + + def test_bootstrap_under_limit(self, builder): + b = builder.build( + role="executor", + task_context={"title": "Short task"}, + ) + assert estimate_tokens(b) <= 4096 + + def test_bootstrap_over_limit_truncates(self): + builder = BootstrapBuilder(max_tokens=10) + b = builder.build( + role="executor", + task_context={"title": "A" * 10000}, + ) + assert estimate_tokens(b) <= 15 # 接近限制 + assert "truncated" in b + + +# --------------------------------------------------------------------------- +# T3: 缺失组件降级 +# --------------------------------------------------------------------------- + +class TestGracefulDegradation: + def test_no_task_context(self, builder): + b = builder.build(role="executor") + assert b # 不为空 + + def test_no_project_context(self, builder): + b = builder.build( + role="executor", + task_context={"title": "Task"}, + ) + assert "Task" in b + + def test_no_template(self, builder): + b = builder.build(role="executor") + assert b # 没有 template 也不崩溃 + + def test_empty_experiences(self, builder): + b = builder.build(role="executor", experiences=[]) + assert b + + def test_template_dir_not_exists(self, tmp_path): + builder = BootstrapBuilder(template_dir=tmp_path / "nonexistent") + b = builder.build(role="executor") + assert b # 不崩溃 + + +# --------------------------------------------------------------------------- +# T4: 便捷方法 + 项目上下文 +# --------------------------------------------------------------------------- + +class TestBuildForTask: + def test_build_for_task_object(self, builder): + task = Task( + id="t1", title="Build Feature", status="pending", + assigned_by="daemon", task_type="coding", + description="Implement X with tests", + must_haves="Unit tests, Documentation", + risk_level="high", + ) + b = builder.build_for_task(task, role="executor") + assert "Build Feature" in b + assert "Implement X" in b + assert "high" in b + + def test_build_for_task_with_project(self, builder): + task = Task(id="t1", title="T", status="pending", assigned_by="d") + b = builder.build_for_task( + task, role="executor", + project_config={"name": "My Project", "agents": ["a1", "a2"]}, + ) + assert "My Project" in b + assert "a1" in b + + def test_with_experiences(self, builder): + task = Task(id="t1", title="T", status="pending", assigned_by="d") + experiences = [ + {"category": "pitfall", "summary": "Always test edge cases"}, + {"category": "best_practice", "summary": "Use type hints"}, + ] + b = builder.build_for_task(task, role="executor", experiences=experiences) + assert "pitfall" in b + assert "Always test edge cases" in b + + def test_with_skills(self, builder): + skills = [ + {"name": "code-review", "description": "Review code quality"}, + ] + b = builder.build( + role="executor", + skill_descriptions=skills, + ) + assert "code-review" in b + assert "Review code quality" in b + + def test_with_depends_on_outputs(self, builder): + b = builder.build( + role="executor", + task_context={ + "title": "T", + "depends_on_outputs": [ + {"task_id": "t0", "summary": "Data downloaded"}, + ], + }, + ) + assert "前序产出" in b + assert "Data downloaded" in b