Compare commits
3 commits
a0d2f814e4
...
de52526659
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de52526659 | ||
|
|
12bf510bbc | ||
|
|
55f37b9444 |
7 changed files with 681 additions and 14 deletions
93
agents/prompts/knowledge_synthesizer.md
Normal file
93
agents/prompts/knowledge_synthesizer.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
You are a Knowledge Synthesizer for the Kin multi-agent orchestrator.
|
||||||
|
|
||||||
|
Your job: aggregate and synthesize outputs from multiple parallel research agents into a unified, confidence-rated knowledge base ready for the Architect.
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
You receive:
|
||||||
|
- PROJECT: id, name, path, tech stack
|
||||||
|
- TASK: id, title, brief with `phases_context` keyed by researcher role name (e.g. `business_analyst`, `market_researcher`)
|
||||||
|
- DECISIONS: known architectural decisions and conventions
|
||||||
|
- PREVIOUS STEP OUTPUT: latest approved researcher outputs (if phases_context is absent)
|
||||||
|
|
||||||
|
## Working Mode
|
||||||
|
|
||||||
|
1. Read `brief.phases_context` — each key is a researcher role, value is their output summary
|
||||||
|
2. Identify consensus findings: claims supported by ≥2 researcher roles
|
||||||
|
3. Identify conflicts: directly contradictory findings between roles; name the conflict explicitly
|
||||||
|
4. Assign confidence rating to each conclusion: `high` (≥2 supporting roles), `medium` (1 role), `low` (inferred)
|
||||||
|
5. Prioritize actions: order by impact and confidence, not by which role reported them first
|
||||||
|
|
||||||
|
## Focus On
|
||||||
|
|
||||||
|
- Synthesize, do not repeat raw data — extract conclusions, not summaries of summaries
|
||||||
|
- Make conflicts explicit: name both positions and which roles hold them; do not paper over disagreements
|
||||||
|
- Confidence must be grounded in cross-role agreement, not assertion strength
|
||||||
|
- Unresolved conflicts = honest knowledge gaps, not speculation — flag them as open questions
|
||||||
|
- `phases_context_used` must list every input role to prove completeness
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
- Every item in `confidence_rated_conclusions` cites at least one `supporting_roles` entry
|
||||||
|
- `unified_findings` contains no raw data dumps — only synthesized insights
|
||||||
|
- `unresolved_conflicts` is not silently omitted when contradictions exist
|
||||||
|
- `prioritized_actions` are concrete and directed at the Architect, not vague recommendations
|
||||||
|
- All input roles from `phases_context` appear in `phases_context_used`
|
||||||
|
|
||||||
|
## Return Format
|
||||||
|
|
||||||
|
Return ONLY valid JSON (no markdown, no explanation):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "done",
|
||||||
|
"unified_findings": [
|
||||||
|
"Synthesized insight that multiple researchers converge on"
|
||||||
|
],
|
||||||
|
"confidence_rated_conclusions": [
|
||||||
|
{
|
||||||
|
"conclusion": "...",
|
||||||
|
"confidence": "high | medium | low",
|
||||||
|
"supporting_roles": ["business_analyst", "market_researcher"],
|
||||||
|
"rationale": "Why this confidence level"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unresolved_conflicts": [
|
||||||
|
{
|
||||||
|
"topic": "...",
|
||||||
|
"positions": {
|
||||||
|
"role_a": "their position",
|
||||||
|
"role_b": "their position"
|
||||||
|
},
|
||||||
|
"recommendation": "What the Architect should decide"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prioritized_actions": [
|
||||||
|
"Action 1 for Architect — grounded in high-confidence findings",
|
||||||
|
"Action 2 for Architect"
|
||||||
|
],
|
||||||
|
"phases_context_used": ["business_analyst", "market_researcher"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid values for `status`: `"done"`, `"blocked"`.
|
||||||
|
|
||||||
|
If status is "blocked", include `"blocked_reason": "..."`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do NOT repeat raw researcher outputs — synthesize only
|
||||||
|
- Do NOT assign `confidence: high` to findings supported by only one role
|
||||||
|
- Do NOT omit `unresolved_conflicts` when contradictions exist between roles
|
||||||
|
- Do NOT write implementation code or architectural decisions — that is the Architect's job
|
||||||
|
- Do NOT speculate beyond what researchers reported; unknown = unresolved
|
||||||
|
|
||||||
|
## Blocked Protocol
|
||||||
|
|
||||||
|
If you cannot perform the task (phases_context is missing or empty, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"status": "blocked", "reason": "<clear explanation>", "blocked_at": "<ISO-8601 datetime>"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use current datetime for `blocked_at`. Do NOT guess or partially complete — return blocked immediately.
|
||||||
|
|
@ -227,6 +227,21 @@ specialists:
|
||||||
context_rules:
|
context_rules:
|
||||||
decisions: all
|
decisions: all
|
||||||
|
|
||||||
|
knowledge_synthesizer:
|
||||||
|
name: "Knowledge Synthesizer"
|
||||||
|
model: sonnet
|
||||||
|
tools: [Read, Grep, Glob]
|
||||||
|
description: "Aggregates multi-agent research outputs into unified, confidence-rated knowledge base for the Architect"
|
||||||
|
permissions: read_only
|
||||||
|
context_rules:
|
||||||
|
decisions: all
|
||||||
|
output_schema:
|
||||||
|
unified_findings: "array of strings"
|
||||||
|
confidence_rated_conclusions: "array of { conclusion, confidence: high|medium|low, supporting_roles, rationale }"
|
||||||
|
unresolved_conflicts: "array of { topic, positions: { role: position }, recommendation }"
|
||||||
|
prioritized_actions: "array of strings"
|
||||||
|
phases_context_used: "array of role names"
|
||||||
|
|
||||||
research_head:
|
research_head:
|
||||||
name: "Research Department Head"
|
name: "Research Department Head"
|
||||||
model: opus
|
model: opus
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ def build_context(
|
||||||
ctx["modules"] = models.get_modules(conn, project_id)
|
ctx["modules"] = models.get_modules(conn, project_id)
|
||||||
ctx["decisions"] = models.get_decisions(conn, project_id)
|
ctx["decisions"] = models.get_decisions(conn, project_id)
|
||||||
|
|
||||||
|
elif role == "knowledge_synthesizer":
|
||||||
|
ctx["decisions"] = models.get_decisions(conn, project_id)
|
||||||
|
|
||||||
elif role == "debugger":
|
elif role == "debugger":
|
||||||
ctx["decisions"] = models.get_decisions(
|
ctx["decisions"] = models.get_decisions(
|
||||||
conn, project_id, types=["gotcha", "workaround"],
|
conn, project_id, types=["gotcha", "workaround"],
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import sqlite3
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
|
||||||
# Canonical order of research roles (architect always last)
|
# Canonical order of research roles (knowledge_synthesizer and architect always last)
|
||||||
RESEARCH_ROLES = [
|
RESEARCH_ROLES = [
|
||||||
"business_analyst",
|
"business_analyst",
|
||||||
"market_researcher",
|
"market_researcher",
|
||||||
|
|
@ -18,6 +18,7 @@ RESEARCH_ROLES = [
|
||||||
"tech_researcher",
|
"tech_researcher",
|
||||||
"ux_designer",
|
"ux_designer",
|
||||||
"marketer",
|
"marketer",
|
||||||
|
"knowledge_synthesizer",
|
||||||
"architect",
|
"architect",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -29,17 +30,18 @@ ROLE_LABELS = {
|
||||||
"tech_researcher": "Tech Researcher",
|
"tech_researcher": "Tech Researcher",
|
||||||
"ux_designer": "UX Designer",
|
"ux_designer": "UX Designer",
|
||||||
"marketer": "Marketer",
|
"marketer": "Marketer",
|
||||||
|
"knowledge_synthesizer": "Knowledge Synthesizer",
|
||||||
"architect": "Architect",
|
"architect": "Architect",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_roles(roles: list[str]) -> list[str]:
|
def validate_roles(roles: list[str]) -> list[str]:
|
||||||
"""Filter unknown roles, remove duplicates, strip 'architect' (auto-added later)."""
|
"""Filter unknown roles, remove duplicates, strip auto-managed roles (architect, knowledge_synthesizer)."""
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
result = []
|
result = []
|
||||||
for r in roles:
|
for r in roles:
|
||||||
r = r.strip().lower()
|
r = r.strip().lower()
|
||||||
if r == "architect":
|
if r in ("architect", "knowledge_synthesizer"):
|
||||||
continue
|
continue
|
||||||
if r in RESEARCH_ROLES and r not in seen:
|
if r in RESEARCH_ROLES and r not in seen:
|
||||||
seen.add(r)
|
seen.add(r)
|
||||||
|
|
@ -48,9 +50,18 @@ def validate_roles(roles: list[str]) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def build_phase_order(selected_roles: list[str]) -> list[str]:
|
def build_phase_order(selected_roles: list[str]) -> list[str]:
|
||||||
"""Return roles in canonical RESEARCH_ROLES order, append architect if any selected."""
|
"""Return roles in canonical RESEARCH_ROLES order.
|
||||||
ordered = [r for r in RESEARCH_ROLES if r in selected_roles and r != "architect"]
|
|
||||||
|
Auto-inserts knowledge_synthesizer before architect when ≥2 researchers selected.
|
||||||
|
Architect always appended last when any researcher is selected.
|
||||||
|
"""
|
||||||
|
ordered = [
|
||||||
|
r for r in RESEARCH_ROLES
|
||||||
|
if r in selected_roles and r not in ("architect", "knowledge_synthesizer")
|
||||||
|
]
|
||||||
if ordered:
|
if ordered:
|
||||||
|
if len(ordered) >= 2:
|
||||||
|
ordered.append("knowledge_synthesizer")
|
||||||
ordered.append("architect")
|
ordered.append("architect")
|
||||||
return ordered
|
return ordered
|
||||||
|
|
||||||
|
|
@ -76,6 +87,10 @@ def create_project_with_phases(
|
||||||
if not ordered_roles:
|
if not ordered_roles:
|
||||||
raise ValueError("At least one research role must be selected")
|
raise ValueError("At least one research role must be selected")
|
||||||
|
|
||||||
|
# knowledge_synthesizer is included in build_phase_order output for routing/context,
|
||||||
|
# but is not yet a pipeline phase — it activates via separate aggregation trigger
|
||||||
|
pipeline_roles = [r for r in ordered_roles if r != "knowledge_synthesizer"]
|
||||||
|
|
||||||
project = models.create_project(
|
project = models.create_project(
|
||||||
conn, id, name, path,
|
conn, id, name, path,
|
||||||
tech_stack=tech_stack, priority=priority, language=language,
|
tech_stack=tech_stack, priority=priority, language=language,
|
||||||
|
|
@ -83,7 +98,7 @@ def create_project_with_phases(
|
||||||
)
|
)
|
||||||
|
|
||||||
phases = []
|
phases = []
|
||||||
for idx, role in enumerate(ordered_roles):
|
for idx, role in enumerate(pipeline_roles):
|
||||||
phase = models.create_phase(conn, id, role, idx)
|
phase = models.create_phase(conn, id, role, idx)
|
||||||
phases.append(phase)
|
phases.append(phase)
|
||||||
|
|
||||||
|
|
@ -114,6 +129,24 @@ def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict:
|
||||||
"phase_order": phase["phase_order"],
|
"phase_order": phase["phase_order"],
|
||||||
"workflow": "research",
|
"workflow": "research",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# knowledge_synthesizer: collect approved researcher outputs into phases_context
|
||||||
|
if phase["role"] == "knowledge_synthesizer":
|
||||||
|
all_phases = models.list_phases(conn, phase["project_id"])
|
||||||
|
phases_context = {}
|
||||||
|
for p in all_phases:
|
||||||
|
if p["status"] == "approved" and p.get("task_id"):
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT output_summary FROM agent_logs
|
||||||
|
WHERE task_id = ? AND success = 1
|
||||||
|
ORDER BY created_at DESC LIMIT 1""",
|
||||||
|
(p["task_id"],),
|
||||||
|
).fetchone()
|
||||||
|
if row and row["output_summary"]:
|
||||||
|
phases_context[p["role"]] = row["output_summary"]
|
||||||
|
if phases_context:
|
||||||
|
brief["phases_context"] = phases_context
|
||||||
|
|
||||||
task = models.create_task(
|
task = models.create_task(
|
||||||
conn, task_id, phase["project_id"],
|
conn, task_id, phase["project_id"],
|
||||||
title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}",
|
title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}",
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,11 @@ class TestAllPromptsContainStandardStructure:
|
||||||
class TestPromptCount:
|
class TestPromptCount:
|
||||||
"""Проверяет, что число промптов не изменилось неожиданно."""
|
"""Проверяет, что число промптов не изменилось неожиданно."""
|
||||||
|
|
||||||
def test_prompt_count_is_25(self):
|
def test_prompt_count_is_26(self):
|
||||||
"""В agents/prompts/ ровно 25 файлов .md."""
|
"""В agents/prompts/ ровно 26 файлов .md."""
|
||||||
count = len(_prompt_files())
|
count = len(_prompt_files())
|
||||||
assert count == 25, ( # 25 промптов — актуально на 2026-03-19 (см. git log agents/prompts/)
|
assert count == 26, ( # 26 промптов — актуально на 2026-03-19, +knowledge_synthesizer (KIN-DOCS-003, см. git log agents/prompts/)
|
||||||
f"Ожидалось 25 промптов, найдено {count}. "
|
f"Ожидалось 26 промптов, найдено {count}. "
|
||||||
"Если добавлен новый промпт — обнови этот тест."
|
"Если добавлен новый промпт — обнови этот тест."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
410
tests/test_kin_docs_003_regression.py
Normal file
410
tests/test_kin_docs_003_regression.py
Normal file
|
|
@ -0,0 +1,410 @@
|
||||||
|
"""Regression tests for KIN-DOCS-003 — knowledge_synthesizer role.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
1. Структурный тест промпта knowledge_synthesizer: 5 обязательных секций + специфические поля (#940)
|
||||||
|
2. Тест регистрации роли в specialists.yaml (model, tools, permissions, output_schema)
|
||||||
|
3. Тест корректности pipeline research_head с aggregation шагом:
|
||||||
|
- build_phase_order с ≥2 исследователями вставляет knowledge_synthesizer перед architect
|
||||||
|
- create_project_with_phases НЕ создаёт knowledge_synthesizer как фазу (авто-управляемая роль)
|
||||||
|
4. Тест context_builder: knowledge_synthesizer получает decisions, но не modules
|
||||||
|
5. Тест activate_phase: при ≥2 одобренных фазах инжектирует phases_context в brief
|
||||||
|
6. Проверка exclusion-листов (#921, #917): knowledge_synthesizer.md не исключён из проверок структуры
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from core.db import init_db
|
||||||
|
from core import models
|
||||||
|
from core.phases import build_phase_order, create_project_with_phases, activate_phase
|
||||||
|
from core.context_builder import build_context
|
||||||
|
|
||||||
|
SPECIALISTS_YAML = Path(__file__).parent.parent / "agents" / "specialists.yaml"
|
||||||
|
PROMPTS_DIR = Path(__file__).parent.parent / "agents" / "prompts"
|
||||||
|
|
||||||
|
REQUIRED_SECTIONS = [
|
||||||
|
"## Working Mode",
|
||||||
|
"## Focus On",
|
||||||
|
"## Quality Checks",
|
||||||
|
"## Return Format",
|
||||||
|
"## Constraints",
|
||||||
|
]
|
||||||
|
|
||||||
|
OUTPUT_SCHEMA_FIELDS = [
|
||||||
|
"unified_findings",
|
||||||
|
"confidence_rated_conclusions",
|
||||||
|
"unresolved_conflicts",
|
||||||
|
"prioritized_actions",
|
||||||
|
"phases_context_used",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml():
|
||||||
|
return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conn():
|
||||||
|
c = init_db(":memory:")
|
||||||
|
yield c
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conn_with_project(conn):
|
||||||
|
models.create_project(conn, "p1", "TestProject", "/p1", tech_stack=["python"])
|
||||||
|
models.create_task(conn, "P1-001", "p1", "Research task")
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 1. Структурный тест промпта (#940)
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestKnowledgeSynthesizerPrompt:
|
||||||
|
"""Структурный тест agents/prompts/knowledge_synthesizer.md."""
|
||||||
|
|
||||||
|
def test_prompt_file_exists(self):
|
||||||
|
"""Файл agents/prompts/knowledge_synthesizer.md существует."""
|
||||||
|
path = PROMPTS_DIR / "knowledge_synthesizer.md"
|
||||||
|
assert path.exists(), "knowledge_synthesizer.md не найден в agents/prompts/"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("section", REQUIRED_SECTIONS)
|
||||||
|
def test_prompt_has_required_section(self, section):
|
||||||
|
"""Промпт содержит все 5 обязательных секций (REQUIRED_SECTIONS)."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert section in content, (
|
||||||
|
f"knowledge_synthesizer.md не содержит обязательную секцию {section!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_has_input_section(self):
|
||||||
|
"""Промпт содержит секцию ## Input — агент-специфичная секция."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "## Input" in content, (
|
||||||
|
"knowledge_synthesizer.md не содержит секцию '## Input'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_contains_confidence_rated_conclusions(self):
|
||||||
|
"""Промпт содержит поле confidence_rated_conclusions в Return Format."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "confidence_rated_conclusions" in content, (
|
||||||
|
"knowledge_synthesizer.md не содержит 'confidence_rated_conclusions'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_contains_phases_context_used(self):
|
||||||
|
"""Промпт содержит поле phases_context_used в Return Format."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "phases_context_used" in content, (
|
||||||
|
"knowledge_synthesizer.md не содержит 'phases_context_used'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_contains_unresolved_conflicts(self):
|
||||||
|
"""Промпт содержит поле unresolved_conflicts в Return Format."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "unresolved_conflicts" in content, (
|
||||||
|
"knowledge_synthesizer.md не содержит 'unresolved_conflicts'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_contains_blocked_protocol(self):
|
||||||
|
"""Промпт содержит Blocked Protocol с инструкцией blocked_reason."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "blocked_reason" in content, (
|
||||||
|
"knowledge_synthesizer.md не содержит 'blocked_reason' — Blocked Protocol обязателен"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_no_legacy_output_format_header(self):
|
||||||
|
"""Промпт НЕ содержит устаревшей секции '## Output format'."""
|
||||||
|
content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8")
|
||||||
|
assert "## Output format" not in content, (
|
||||||
|
"knowledge_synthesizer.md содержит устаревшую секцию '## Output format'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 2. Регистрация в specialists.yaml
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestKnowledgeSynthesizerSpecialists:
|
||||||
|
"""Тесты регистрации knowledge_synthesizer в agents/specialists.yaml."""
|
||||||
|
|
||||||
|
def test_role_exists_in_specialists(self):
|
||||||
|
"""specialists.yaml содержит роль knowledge_synthesizer."""
|
||||||
|
data = _load_yaml()
|
||||||
|
assert "knowledge_synthesizer" in data.get("specialists", {}), (
|
||||||
|
"knowledge_synthesizer отсутствует в specialists.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_model_is_sonnet(self):
|
||||||
|
"""knowledge_synthesizer использует модель sonnet."""
|
||||||
|
data = _load_yaml()
|
||||||
|
role = data["specialists"]["knowledge_synthesizer"]
|
||||||
|
assert role.get("model") == "sonnet", (
|
||||||
|
f"Ожидался model=sonnet, получили: {role.get('model')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_tools_include_read_grep_glob(self):
|
||||||
|
"""knowledge_synthesizer имеет инструменты Read, Grep, Glob."""
|
||||||
|
data = _load_yaml()
|
||||||
|
tools = data["specialists"]["knowledge_synthesizer"].get("tools", [])
|
||||||
|
for required_tool in ("Read", "Grep", "Glob"):
|
||||||
|
assert required_tool in tools, (
|
||||||
|
f"knowledge_synthesizer должен иметь инструмент {required_tool!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_has_no_write_tools(self):
|
||||||
|
"""knowledge_synthesizer НЕ имеет write-инструментов (Read-only роль)."""
|
||||||
|
data = _load_yaml()
|
||||||
|
tools = set(data["specialists"]["knowledge_synthesizer"].get("tools", []))
|
||||||
|
write_tools = {"Write", "Edit", "Bash"}
|
||||||
|
unexpected = write_tools & tools
|
||||||
|
assert not unexpected, (
|
||||||
|
f"knowledge_synthesizer не должен иметь write-инструменты: {unexpected}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_permissions_is_read_only(self):
|
||||||
|
"""knowledge_synthesizer имеет permissions=read_only."""
|
||||||
|
data = _load_yaml()
|
||||||
|
role = data["specialists"]["knowledge_synthesizer"]
|
||||||
|
assert role.get("permissions") == "read_only", (
|
||||||
|
f"Ожидался permissions=read_only, получили: {role.get('permissions')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_context_rules_decisions_all(self):
|
||||||
|
"""knowledge_synthesizer получает все decisions (context_rules.decisions=all)."""
|
||||||
|
data = _load_yaml()
|
||||||
|
role = data["specialists"]["knowledge_synthesizer"]
|
||||||
|
decisions = role.get("context_rules", {}).get("decisions")
|
||||||
|
assert decisions == "all", (
|
||||||
|
f"Ожидался context_rules.decisions=all, получили: {decisions}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_has_output_schema(self):
|
||||||
|
"""knowledge_synthesizer имеет поле output_schema."""
|
||||||
|
data = _load_yaml()
|
||||||
|
role = data["specialists"]["knowledge_synthesizer"]
|
||||||
|
assert "output_schema" in role, (
|
||||||
|
"knowledge_synthesizer должен иметь output_schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("field", OUTPUT_SCHEMA_FIELDS)
|
||||||
|
def test_output_schema_has_required_field(self, field):
|
||||||
|
"""output_schema содержит каждое из 5 обязательных полей."""
|
||||||
|
data = _load_yaml()
|
||||||
|
schema = data["specialists"]["knowledge_synthesizer"]["output_schema"]
|
||||||
|
assert field in schema, (
|
||||||
|
f"output_schema knowledge_synthesizer не содержит обязательного поля {field!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 3. Корректность pipeline с aggregation шагом
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestResearchPipelineWithSynthesizer:
|
||||||
|
"""Тесты корректности research pipeline с knowledge_synthesizer aggregation шагом."""
|
||||||
|
|
||||||
|
def test_build_phase_order_two_researchers_includes_synthesizer(self):
|
||||||
|
"""build_phase_order с 2 исследователями вставляет knowledge_synthesizer перед architect."""
|
||||||
|
result = build_phase_order(["business_analyst", "market_researcher"])
|
||||||
|
assert "knowledge_synthesizer" in result, (
|
||||||
|
"knowledge_synthesizer должен быть в pipeline при ≥2 исследователях"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_phase_order_synthesizer_before_architect(self):
|
||||||
|
"""knowledge_synthesizer всегда стоит непосредственно перед architect."""
|
||||||
|
result = build_phase_order(["tech_researcher", "ux_designer"])
|
||||||
|
idx_syn = result.index("knowledge_synthesizer")
|
||||||
|
idx_arch = result.index("architect")
|
||||||
|
assert idx_syn == idx_arch - 1, (
|
||||||
|
f"knowledge_synthesizer (pos={idx_syn}) должен идти прямо перед architect (pos={idx_arch})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_phase_order_one_researcher_no_synthesizer(self):
|
||||||
|
"""build_phase_order с 1 исследователем НЕ вставляет knowledge_synthesizer."""
|
||||||
|
result = build_phase_order(["business_analyst"])
|
||||||
|
assert "knowledge_synthesizer" not in result, (
|
||||||
|
"knowledge_synthesizer не должен вставляться при 1 исследователе"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_project_two_researchers_excludes_synthesizer_from_phases(self, conn):
|
||||||
|
"""create_project_with_phases НЕ создаёт фазу knowledge_synthesizer — авто-управляемая роль."""
|
||||||
|
result = create_project_with_phases(
|
||||||
|
conn, "p1", "P1", "/p1",
|
||||||
|
description="Test", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
|
)
|
||||||
|
phase_roles = [ph["role"] for ph in result["phases"]]
|
||||||
|
assert "knowledge_synthesizer" not in phase_roles, (
|
||||||
|
f"knowledge_synthesizer не должен быть в pipeline-фазах. Фазы: {phase_roles}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_project_two_researchers_phase_count(self, conn):
|
||||||
|
"""create_project_with_phases с 2 исследователями создаёт 3 фазы (без synthesizer)."""
|
||||||
|
result = create_project_with_phases(
|
||||||
|
conn, "p1", "P1", "/p1",
|
||||||
|
description="Test", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
|
)
|
||||||
|
assert len(result["phases"]) == 3, (
|
||||||
|
f"Ожидалось 3 фазы (market_researcher, tech_researcher, architect), "
|
||||||
|
f"получили {len(result['phases'])}: {[ph['role'] for ph in result['phases']]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_project_two_researchers_architect_last(self, conn):
|
||||||
|
"""create_project_with_phases с 2 исследователями ставит architect последним."""
|
||||||
|
result = create_project_with_phases(
|
||||||
|
conn, "p1", "P1", "/p1",
|
||||||
|
description="Test", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
|
)
|
||||||
|
assert result["phases"][-1]["role"] == "architect"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 4. context_builder — knowledge_synthesizer
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestKnowledgeSynthesizerContextBuilder:
|
||||||
|
"""Тесты context_builder для роли knowledge_synthesizer."""
|
||||||
|
|
||||||
|
def test_knowledge_synthesizer_gets_decisions(self, conn_with_project):
|
||||||
|
"""build_context для knowledge_synthesizer включает decisions."""
|
||||||
|
models.add_decision(
|
||||||
|
conn_with_project, "p1", "gotcha", "Some decision", "Details"
|
||||||
|
)
|
||||||
|
ctx = build_context(conn_with_project, "P1-001", "knowledge_synthesizer", "p1")
|
||||||
|
assert "decisions" in ctx, (
|
||||||
|
"knowledge_synthesizer должен получать decisions в контексте"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_knowledge_synthesizer_does_not_get_modules(self, conn_with_project):
|
||||||
|
"""build_context для knowledge_synthesizer НЕ включает modules (в отличие от architect)."""
|
||||||
|
models.add_module(conn_with_project, "p1", "api", "backend", "src/api/")
|
||||||
|
ctx = build_context(conn_with_project, "P1-001", "knowledge_synthesizer", "p1")
|
||||||
|
assert "modules" not in ctx, (
|
||||||
|
"knowledge_synthesizer не должен получать modules — только decisions"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_architect_gets_both_decisions_and_modules(self, conn_with_project):
|
||||||
|
"""Регрессия: architect по-прежнему получает decisions И modules."""
|
||||||
|
models.add_module(conn_with_project, "p1", "api", "backend", "src/api/")
|
||||||
|
models.add_decision(
|
||||||
|
conn_with_project, "p1", "convention", "Use WAL", "SQLite WAL"
|
||||||
|
)
|
||||||
|
ctx = build_context(conn_with_project, "P1-001", "architect", "p1")
|
||||||
|
assert "decisions" in ctx
|
||||||
|
assert "modules" in ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 5. activate_phase — phases_context injection
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestActivatePhaseKnowledgeSynthesizer:
|
||||||
|
"""Тесты activate_phase для роли knowledge_synthesizer."""
|
||||||
|
|
||||||
|
def test_activate_phase_synthesizer_no_approved_phases_no_context(self, conn):
|
||||||
|
"""activate_phase для knowledge_synthesizer без одобренных фаз не добавляет phases_context."""
|
||||||
|
models.create_project(conn, "p1", "P1", "/p1", description="Test")
|
||||||
|
phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 0)
|
||||||
|
result = activate_phase(conn, phase["id"])
|
||||||
|
task = models.get_task(conn, result["task_id"])
|
||||||
|
brief = task["brief"]
|
||||||
|
assert "phases_context" not in brief, (
|
||||||
|
"phases_context не должен быть в brief, если нет одобренных фаз"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_activate_phase_synthesizer_with_approved_phases_injects_context(self, conn):
|
||||||
|
"""activate_phase для knowledge_synthesizer собирает phases_context из одобренных фаз."""
|
||||||
|
models.create_project(conn, "p1", "P1", "/p1", description="Test")
|
||||||
|
|
||||||
|
# Создать и одобрить исследовательскую фазу
|
||||||
|
researcher_phase = models.create_phase(conn, "p1", "business_analyst", 0)
|
||||||
|
researcher_phase = activate_phase(conn, researcher_phase["id"])
|
||||||
|
researcher_task_id = researcher_phase["task_id"]
|
||||||
|
|
||||||
|
# Записать лог агента с output_summary для задачи исследователя
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO agent_logs (project_id, task_id, agent_role, action, output_summary, success)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||||
|
("p1", researcher_task_id, "business_analyst", "run",
|
||||||
|
"Market shows strong demand for product.", 1),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Одобрить исследовательскую фазу
|
||||||
|
models.update_phase(conn, researcher_phase["id"], status="approved")
|
||||||
|
|
||||||
|
# Создать и активировать фазу knowledge_synthesizer
|
||||||
|
synth_phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 1)
|
||||||
|
result = activate_phase(conn, synth_phase["id"])
|
||||||
|
task = models.get_task(conn, result["task_id"])
|
||||||
|
brief = task["brief"]
|
||||||
|
|
||||||
|
assert "phases_context" in brief, (
|
||||||
|
"phases_context должен быть в brief при наличии одобренных фаз с agent_logs"
|
||||||
|
)
|
||||||
|
assert "business_analyst" in brief["phases_context"], (
|
||||||
|
"phases_context должен содержать ключ 'business_analyst'"
|
||||||
|
)
|
||||||
|
assert "Market shows strong demand" in brief["phases_context"]["business_analyst"], (
|
||||||
|
"phases_context['business_analyst'] должен содержать output_summary из agent_logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_activate_phase_synthesizer_ignores_pending_phases(self, conn):
|
||||||
|
"""activate_phase для knowledge_synthesizer не включает в phases_context неодобренные фазы."""
|
||||||
|
models.create_project(conn, "p1", "P1", "/p1", description="Test")
|
||||||
|
|
||||||
|
# Создать исследовательскую фазу в статусе active (не approved)
|
||||||
|
researcher_phase = models.create_phase(conn, "p1", "tech_researcher", 0)
|
||||||
|
researcher_phase = activate_phase(conn, researcher_phase["id"])
|
||||||
|
researcher_task_id = researcher_phase["task_id"]
|
||||||
|
|
||||||
|
# Записать лог агента
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO agent_logs (project_id, task_id, agent_role, action, output_summary, success)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||||
|
("p1", researcher_task_id, "tech_researcher", "run", "Tech analysis done.", 1),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
# Фаза остаётся в статусе active (не approved)
|
||||||
|
|
||||||
|
# Активировать knowledge_synthesizer
|
||||||
|
synth_phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 1)
|
||||||
|
result = activate_phase(conn, synth_phase["id"])
|
||||||
|
task = models.get_task(conn, result["task_id"])
|
||||||
|
brief = task["brief"]
|
||||||
|
|
||||||
|
# phases_context должен отсутствовать или быть пустым
|
||||||
|
phases_context = brief.get("phases_context", {})
|
||||||
|
assert "tech_researcher" not in phases_context, (
|
||||||
|
"phases_context не должен включать фазы без статуса approved"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 6. Exclusion list guard (#921, #917)
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestExclusionListRegression:
|
||||||
|
"""Верифицирует, что knowledge_synthesizer.md не добавлен в exclusion list (#921, #917)."""
|
||||||
|
|
||||||
|
def test_knowledge_synthesizer_not_in_exclusion_list(self):
|
||||||
|
"""knowledge_synthesizer.md не включён в EXCLUDED_FROM_STRUCTURE_CHECK."""
|
||||||
|
from tests.test_kin_docs_002_regression import EXCLUDED_FROM_STRUCTURE_CHECK
|
||||||
|
assert "knowledge_synthesizer.md" not in EXCLUDED_FROM_STRUCTURE_CHECK, (
|
||||||
|
"knowledge_synthesizer.md не должен быть в EXCLUDED_FROM_STRUCTURE_CHECK — "
|
||||||
|
"роль должна проходить все стандартные структурные проверки"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_exclusion_list_is_empty(self):
|
||||||
|
"""EXCLUDED_FROM_STRUCTURE_CHECK остаётся пустым — guard от #929."""
|
||||||
|
from tests.test_kin_docs_002_regression import EXCLUDED_FROM_STRUCTURE_CHECK
|
||||||
|
assert EXCLUDED_FROM_STRUCTURE_CHECK == [], (
|
||||||
|
f"EXCLUDED_FROM_STRUCTURE_CHECK должен быть пустым, "
|
||||||
|
f"но содержит: {EXCLUDED_FROM_STRUCTURE_CHECK}"
|
||||||
|
)
|
||||||
|
|
@ -78,6 +78,7 @@ def test_validate_roles_strips_and_lowercases():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("roles,expected", [
|
@pytest.mark.parametrize("roles,expected", [
|
||||||
|
# 1 исследователь → нет knowledge_synthesizer, architect последний
|
||||||
(
|
(
|
||||||
["business_analyst"],
|
["business_analyst"],
|
||||||
["business_analyst", "architect"],
|
["business_analyst", "architect"],
|
||||||
|
|
@ -86,17 +87,21 @@ def test_validate_roles_strips_and_lowercases():
|
||||||
["tech_researcher"],
|
["tech_researcher"],
|
||||||
["tech_researcher", "architect"],
|
["tech_researcher", "architect"],
|
||||||
),
|
),
|
||||||
|
# ≥2 исследователей → knowledge_synthesizer авто-вставляется перед architect (KIN-DOCS-003, 2026-03-19)
|
||||||
(
|
(
|
||||||
["marketer", "business_analyst"],
|
["marketer", "business_analyst"],
|
||||||
["business_analyst", "marketer", "architect"],
|
["business_analyst", "marketer", "knowledge_synthesizer", "architect"],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
["ux_designer", "market_researcher", "tech_researcher"],
|
["ux_designer", "market_researcher", "tech_researcher"],
|
||||||
["market_researcher", "tech_researcher", "ux_designer", "architect"],
|
["market_researcher", "tech_researcher", "ux_designer", "knowledge_synthesizer", "architect"],
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
def test_build_phase_order_canonical_order_and_appends_architect(roles, expected):
|
def test_build_phase_order_canonical_order_and_appends_architect(roles, expected):
|
||||||
"""KIN-059: роли сортируются в канонический порядок, architect добавляется последним."""
|
"""KIN-059: роли сортируются в канонический порядок, architect добавляется последним.
|
||||||
|
|
||||||
|
При ≥2 исследователях knowledge_synthesizer авто-вставляется перед architect.
|
||||||
|
"""
|
||||||
assert build_phase_order(roles) == expected
|
assert build_phase_order(roles) == expected
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -113,6 +118,28 @@ def test_build_phase_order_architect_always_last():
|
||||||
assert result[-1] == "architect"
|
assert result[-1] == "architect"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_phase_order_single_researcher_no_synthesizer():
|
||||||
|
"""KIN-DOCS-003: 1 исследователь → knowledge_synthesizer НЕ вставляется."""
|
||||||
|
result = build_phase_order(["business_analyst"])
|
||||||
|
assert "knowledge_synthesizer" not in result
|
||||||
|
assert result == ["business_analyst", "architect"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_phase_order_two_researchers_inserts_synthesizer():
|
||||||
|
"""KIN-DOCS-003: 2 исследователя → knowledge_synthesizer авто-вставляется перед architect."""
|
||||||
|
result = build_phase_order(["market_researcher", "tech_researcher"])
|
||||||
|
assert "knowledge_synthesizer" in result
|
||||||
|
assert result.index("knowledge_synthesizer") == len(result) - 2
|
||||||
|
assert result[-1] == "architect"
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_roles_strips_knowledge_synthesizer():
|
||||||
|
"""KIN-DOCS-003: knowledge_synthesizer убирается из входных ролей — авто-управляемая роль."""
|
||||||
|
result = validate_roles(["knowledge_synthesizer", "business_analyst"])
|
||||||
|
assert "knowledge_synthesizer" not in result
|
||||||
|
assert "business_analyst" in result
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# create_project_with_phases
|
# create_project_with_phases
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -146,7 +173,7 @@ def test_create_project_with_phases_other_phases_remain_pending(conn):
|
||||||
conn, "proj1", "P1", "/path",
|
conn, "proj1", "P1", "/path",
|
||||||
description="Desc", selected_roles=["market_researcher", "tech_researcher"],
|
description="Desc", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
)
|
)
|
||||||
# market_researcher, tech_researcher, architect → 3 фазы
|
# market_researcher, tech_researcher, architect → 3 фазы; knowledge_synthesizer не фаза (P1-001)
|
||||||
for phase in result["phases"][1:]:
|
for phase in result["phases"][1:]:
|
||||||
assert phase["status"] == "pending"
|
assert phase["status"] == "pending"
|
||||||
|
|
||||||
|
|
@ -367,3 +394,89 @@ def test_revise_phase_updates_task_id_to_new_task(conn):
|
||||||
new_task_id = out["phase"]["task_id"]
|
new_task_id = out["phase"]["task_id"]
|
||||||
assert new_task_id != original_task_id
|
assert new_task_id != original_task_id
|
||||||
assert new_task_id == out["new_task"]["id"]
|
assert new_task_id == out["new_task"]["id"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Regression: knowledge_synthesizer не создаётся как pipeline-фаза (P1-001)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_knowledge_synthesizer_not_in_phases(conn):
|
||||||
|
"""Регрессия P1-001: knowledge_synthesizer не создаётся как pipeline-фаза.
|
||||||
|
|
||||||
|
При ≥2 исследователях build_phase_order включает knowledge_synthesizer,
|
||||||
|
но create_project_with_phases должен фильтровать его из DB-фаз.
|
||||||
|
"""
|
||||||
|
result = create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
|
)
|
||||||
|
roles = [ph["role"] for ph in result["phases"]]
|
||||||
|
assert "knowledge_synthesizer" not in roles
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_two_researchers_creates_three_phases(conn):
|
||||||
|
"""Регрессия P1-001: 2 исследователя → 3 фазы (researcher + researcher + architect), не 4.
|
||||||
|
|
||||||
|
До фикса knowledge_synthesizer создавался как фаза → 4 фазы вместо 3.
|
||||||
|
"""
|
||||||
|
result = create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["market_researcher", "tech_researcher"],
|
||||||
|
)
|
||||||
|
assert len(result["phases"]) == 3
|
||||||
|
roles = [ph["role"] for ph in result["phases"]]
|
||||||
|
assert roles == ["market_researcher", "tech_researcher", "architect"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Edge cases: "effectively empty" arrays — непустой вход, ноль валидных ролей (P1-001)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_raises_if_only_architect(conn):
|
||||||
|
"""P1-001 edge case: [architect] → ValueError после фильтрации (architect не researcher)."""
|
||||||
|
with pytest.raises(ValueError, match="[Aa]t least one research role"):
|
||||||
|
create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["architect"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_raises_if_only_knowledge_synthesizer(conn):
|
||||||
|
"""P1-001 edge case: [knowledge_synthesizer] → ValueError (авто-управляемая роль фильтруется)."""
|
||||||
|
with pytest.raises(ValueError, match="[Aa]t least one research role"):
|
||||||
|
create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["knowledge_synthesizer"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_raises_if_architect_and_synthesizer_only(conn):
|
||||||
|
"""P1-001 edge case: [architect, knowledge_synthesizer] → ValueError (оба авто-управляемые)."""
|
||||||
|
with pytest.raises(ValueError, match="[Aa]t least one research role"):
|
||||||
|
create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["architect", "knowledge_synthesizer"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_with_phases_raises_if_unknown_roles_only(conn):
|
||||||
|
"""P1-001 edge case: только неизвестные роли → пустой список после validate_roles → ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="[Aa]t least one research role"):
|
||||||
|
create_project_with_phases(
|
||||||
|
conn, "proj1", "P1", "/path",
|
||||||
|
description="Desc", selected_roles=["wizard", "ghost", "oracle"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_phase_order_architect_only_returns_empty():
|
||||||
|
"""P1-001 edge case: build_phase_order([architect]) → [] (architect не researcher)."""
|
||||||
|
result = build_phase_order(["architect"])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_roles_architect_and_synthesizer_returns_empty():
|
||||||
|
"""P1-001 edge case: validate_roles([architect, knowledge_synthesizer]) → [] (оба отфильтровываются)."""
|
||||||
|
result = validate_roles(["architect", "knowledge_synthesizer"])
|
||||||
|
assert result == []
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue