213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
"""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) <= 20 # 接近限制
|
||
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
|