diff --git a/agents/prompts/architect.md b/agents/prompts/architect.md index e648385..e5780e1 100644 --- a/agents/prompts/architect.md +++ b/agents/prompts/architect.md @@ -15,7 +15,6 @@ You receive: **Normal mode** (default): -0. If PREVIOUS STEP OUTPUT contains a `context_packet` field — read it FIRST before opening any files or analyzing any other context. It contains the essential handoff: architecture decisions, critical file paths, constraints, and unknowns from the prior agent. 1. Read `DESIGN.md`, `core/models.py`, `core/db.py`, `agents/runner.py`, and any MODULES files relevant to the task 2. Understand the current architecture — what already exists and what needs to change 3. Design the solution: data model, interfaces, component interactions diff --git a/agents/prompts/backend_dev.md b/agents/prompts/backend_dev.md index 97b7218..3b4a97f 100644 --- a/agents/prompts/backend_dev.md +++ b/agents/prompts/backend_dev.md @@ -12,7 +12,6 @@ You receive: ## Working Mode -0. If PREVIOUS STEP OUTPUT contains a `context_packet` field — read it FIRST before opening any files or analyzing any other context. It contains the essential handoff: architecture decisions, critical file paths, constraints, and unknowns from the prior agent. 1. Read all relevant backend files before making any changes 2. Review `PREVIOUS STEP OUTPUT` if it contains an architect spec — follow it precisely 3. Implement the feature or fix as described in the task brief diff --git a/agents/prompts/department_head.md b/agents/prompts/department_head.md index 4da74f3..7f1a1f2 100644 --- a/agents/prompts/department_head.md +++ b/agents/prompts/department_head.md @@ -28,7 +28,6 @@ You receive: - Handoff notes clarity — the next department must be able to start without asking questions - Previous department handoff — build on their work, don't repeat it - Sub-pipeline length — keep it SHORT, 1-4 steps maximum -- Produce a `context_packet` with exactly 5 fields: `architecture_notes` (string — key arch decisions made), `key_files` (array of file paths critical for the next agent), `constraints` (array of hard technical/business limits discovered), `unknowns` (array of open risks or unresolved questions), `handoff_for` (string — role name of the first worker in sub_pipeline). In the brief for the FIRST worker in sub_pipeline, include this sentence verbatim: «IMPORTANT: Read the `context_packet` field in PREVIOUS STEP OUTPUT FIRST, before any other section or file.» **Department-specific guidance:** @@ -47,7 +46,6 @@ You receive: - Each worker brief is self-contained — no "see above" references - Artifacts list is complete and specific - Handoff notes are actionable for the next department -- `context_packet` is present with all 5 required fields; `handoff_for` is non-empty and matches the role in `sub_pipeline[0]`; first worker brief contains explicit instruction to read `context_packet` first ## Return Format @@ -74,14 +72,7 @@ Return ONLY valid JSON (no markdown, no explanation): "schemas": [], "notes": "Added feature with full test coverage. All tests pass." }, - "handoff_notes": "Backend implementation complete. Tests passing. Frontend needs to call POST /api/feature with {field: value} body.", - "context_packet": { - "architecture_notes": "Used existing models.py pattern, no ORM, raw sqlite3", - "key_files": ["core/models.py", "web/api.py"], - "constraints": ["All DB columns must have DEFAULT values", "No new Python deps"], - "unknowns": ["Frontend integration not yet verified"], - "handoff_for": "backend_dev" - } + "handoff_notes": "Backend implementation complete. Tests passing. Frontend needs to call POST /api/feature with {field: value} body." } ``` diff --git a/agents/prompts/frontend_dev.md b/agents/prompts/frontend_dev.md index cea39a7..3d2f29b 100644 --- a/agents/prompts/frontend_dev.md +++ b/agents/prompts/frontend_dev.md @@ -12,7 +12,6 @@ You receive: ## Working Mode -0. If PREVIOUS STEP OUTPUT contains a `context_packet` field — read it FIRST before opening any files or analyzing any other context. It contains the essential handoff: architecture decisions, critical file paths, constraints, and unknowns from the prior agent. 1. Read all relevant frontend files before making any changes 2. Review `PREVIOUS STEP OUTPUT` if it contains an architect spec — follow it precisely 3. Implement the feature or fix as described in the task brief diff --git a/agents/runner.py b/agents/runner.py index 066b77d..9ada7f8 100644 --- a/agents/runner.py +++ b/agents/runner.py @@ -1347,29 +1347,14 @@ def _execute_department_head_step( role = step["role"] dept_name = role.replace("_head", "") - # Extract context_packet (KIN-DOCS-004): fail-open if missing - context_packet = parsed.get("context_packet") - if context_packet is None and parent_pipeline_id: - try: - models.write_log( - conn, parent_pipeline_id, - f"Dept {step['role']}: context_packet missing from output — handoff quality degraded", - level="WARN", - extra={"role": step["role"]}, - ) - except Exception: - pass - - # Build initial context for workers: context_packet first, then dept head's plan - dept_plan_context_dict: dict = {} - if context_packet is not None: - dept_plan_context_dict["context_packet"] = context_packet - dept_plan_context_dict["department_head_plan"] = { - "department": dept_name, - "artifacts": parsed.get("artifacts", {}), - "handoff_notes": parsed.get("handoff_notes", ""), - } - dept_plan_context = json.dumps(dept_plan_context_dict, ensure_ascii=False) + # Build initial context for workers: dept head's plan + artifacts + dept_plan_context = json.dumps({ + "department_head_plan": { + "department": dept_name, + "artifacts": parsed.get("artifacts", {}), + "handoff_notes": parsed.get("handoff_notes", ""), + }, + }, ensure_ascii=False) # KIN-084: log sub-pipeline start if parent_pipeline_id: @@ -1444,7 +1429,6 @@ def _execute_department_head_step( decisions_made=decisions_made, blockers=[], status=handoff_status, - context_packet=context_packet, ) except Exception: pass # Handoff save errors must never block pipeline @@ -1454,7 +1438,6 @@ def _execute_department_head_step( "from_department": dept_name, "handoff_notes": parsed.get("handoff_notes", ""), "artifacts": parsed.get("artifacts", {}), - "context_packet": context_packet, "sub_pipeline_summary": { "steps_completed": sub_result.get("steps_completed", 0), "success": sub_result.get("success", False), diff --git a/agents/specialists.yaml b/agents/specialists.yaml index 6bef8bf..cd3af24 100644 --- a/agents/specialists.yaml +++ b/agents/specialists.yaml @@ -181,8 +181,6 @@ specialists: context_rules: decisions: all modules: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" frontend_head: name: "Frontend Department Head" @@ -195,8 +193,6 @@ specialists: context_rules: decisions: all modules: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" qa_head: name: "QA Department Head" @@ -208,8 +204,6 @@ specialists: permissions: read_only context_rules: decisions: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" security_head: name: "Security Department Head" @@ -221,8 +215,6 @@ specialists: permissions: read_only context_rules: decisions_category: security - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" infra_head: name: "Infrastructure Department Head" @@ -234,8 +226,6 @@ specialists: permissions: read_only context_rules: decisions: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" knowledge_synthesizer: name: "Knowledge Synthesizer" @@ -262,8 +252,6 @@ specialists: permissions: read_only context_rules: decisions: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" marketing_head: name: "Marketing Department Head" @@ -276,8 +264,6 @@ specialists: context_rules: decisions: all modules: all - output_schema: - context_packet: "{ architecture_notes: string, key_files: array, constraints: array, unknowns: array, handoff_for: string }" # Departments — PM uses these when routing complex cross-domain tasks to department heads departments: diff --git a/core/db.py b/core/db.py index e795fb6..5d8e47b 100644 --- a/core/db.py +++ b/core/db.py @@ -165,7 +165,6 @@ CREATE TABLE IF NOT EXISTS department_handoffs ( artifacts JSON, decisions_made JSON, blockers JSON, - context_packet JSON DEFAULT NULL, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -704,13 +703,6 @@ def _migrate(conn: sqlite3.Connection): """) conn.commit() - # Add context_packet column to department_handoffs (KIN-DOCS-004) - if "department_handoffs" in existing_tables: - handoff_cols = {r[1] for r in conn.execute("PRAGMA table_info(department_handoffs)").fetchall()} - if "context_packet" not in handoff_cols: - conn.execute("ALTER TABLE department_handoffs ADD COLUMN context_packet JSON DEFAULT NULL") - conn.commit() - # Add test_command column to projects (KIN-ARCH-008); NULL = auto-detect (KIN-101) projects_cols = {row["name"] for row in conn.execute("PRAGMA table_info(projects)")} if "test_command" not in projects_cols: diff --git a/core/models.py b/core/models.py index b52b9d1..d4694ce 100644 --- a/core/models.py +++ b/core/models.py @@ -42,7 +42,6 @@ _JSON_COLUMNS: frozenset[str] = frozenset({ "dependencies", "steps", "artifacts", "decisions_made", "blockers", - "context_packet", "extra_json", "pending_actions", }) @@ -1192,16 +1191,14 @@ def create_handoff( decisions_made: list | None = None, blockers: list | None = None, status: str = "pending", - context_packet: dict | list | None = None, ) -> dict: """Record a department handoff with artifacts for inter-department context.""" cur = conn.execute( """INSERT INTO department_handoffs - (pipeline_id, task_id, from_department, to_department, artifacts, decisions_made, blockers, context_packet, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (pipeline_id, task_id, from_department, to_department, artifacts, decisions_made, blockers, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", (pipeline_id, task_id, from_department, to_department, - _json_encode(artifacts), _json_encode(decisions_made), _json_encode(blockers), - _json_encode(context_packet), status), + _json_encode(artifacts), _json_encode(decisions_made), _json_encode(blockers), status), ) conn.commit() row = conn.execute( diff --git a/tests/test_kin_docs_004_regression.py b/tests/test_kin_docs_004_regression.py deleted file mode 100644 index 8e65965..0000000 --- a/tests/test_kin_docs_004_regression.py +++ /dev/null @@ -1,469 +0,0 @@ -"""Regression tests for KIN-DOCS-004 — context_packet как артефакт handoff. - -Acceptance criteria: -1. Структурный тест agents/prompts/department_head.md: context_packet присутствует - в ## Return Format с 5 обязательными полями, упомянут в ## Focus On и ## Quality Checks. -2. Структурный тест agents/specialists.yaml: все 7 dept head ролей имеют - output_schema.context_packet. -3. Структурные тесты промптов-получателей (architect.md, backend_dev.md, frontend_dev.md): - явная инструкция читать context_packet первым (шаг 0 в ## Working Mode). -4. Unit-тест models.create_handoff: context_packet сохраняется как JSON и декодируется обратно. -5. Runner-тесты _execute_department_head_step: context_packet включён в summary output, - размещён первым ключом в initial context для воркеров; fail-open при отсутствии. -""" - -import json -from pathlib import Path -from unittest.mock import patch, MagicMock, call - -import pytest -import yaml - -from core.db import init_db -from core import models - -PROMPTS_DIR = Path(__file__).parent.parent / "agents" / "prompts" -SPECIALISTS_YAML = Path(__file__).parent.parent / "agents" / "specialists.yaml" - -DEPT_HEAD_ROLES = [ - "backend_head", - "frontend_head", - "qa_head", - "security_head", - "infra_head", - "research_head", - "marketing_head", -] - -CONTEXT_PACKET_FIELDS = [ - "architecture_notes", - "key_files", - "constraints", - "unknowns", - "handoff_for", -] - - -def _load_yaml(): - return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8")) - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - -@pytest.fixture -def conn(): - c = init_db(":memory:") - models.create_project(c, "proj", "TestProject", "~/projects/test", - tech_stack=["python"]) - models.create_task(c, "PROJ-001", "proj", "Test task", - brief={"route_type": "dept_feature"}) - yield c - c.close() - - -def _success_sub_result(pipeline_id=99): - return { - "success": True, - "total_cost_usd": 0.01, - "total_tokens": 100, - "total_duration_seconds": 2.0, - "steps_completed": 1, - "results": [{"role": "backend_dev", "output": "done"}], - "pipeline_id": pipeline_id, - } - - -def _dept_head_raw_output(with_context_packet=True, extra_fields=None): - """Сборка raw_output для dept head, опционально с context_packet.""" - out = { - "status": "done", - "sub_pipeline": [ - {"role": "backend_dev", "model": "sonnet", "brief": "Implement feature"}, - ], - "artifacts": {"files_changed": ["core/models.py"], "notes": "done"}, - "handoff_notes": "Backend complete", - } - if with_context_packet: - out["context_packet"] = { - "architecture_notes": "Pure functions, raw sqlite3", - "key_files": ["core/models.py", "core/db.py"], - "constraints": ["All DB columns need DEFAULT values"], - "unknowns": ["Frontend integration pending"], - "handoff_for": "backend_dev", - } - if extra_fields: - out.update(extra_fields) - return {"raw_output": json.dumps(out)} - - -# =========================================================================== -# 1. Структурные тесты agents/prompts/department_head.md -# =========================================================================== - -class TestDepartmentHeadPromptStructure: - """department_head.md содержит context_packet во всех нужных секциях.""" - - def _content(self): - return (PROMPTS_DIR / "department_head.md").read_text(encoding="utf-8") - - def test_context_packet_in_return_format(self): - """department_head.md содержит 'context_packet' в секции ## Return Format.""" - content = self._content() - # Найти секцию Return Format и убедиться, что context_packet там есть - return_format_idx = content.find("## Return Format") - assert return_format_idx != -1, "department_head.md не содержит секцию '## Return Format'" - return_format_section = content[return_format_idx:] - assert "context_packet" in return_format_section, ( - "department_head.md не содержит 'context_packet' в ## Return Format" - ) - - @pytest.mark.parametrize("field", CONTEXT_PACKET_FIELDS) - def test_context_packet_required_field_present(self, field): - """department_head.md упоминает каждое из 5 обязательных полей context_packet.""" - content = self._content() - assert field in content, ( - f"department_head.md не содержит обязательного поля context_packet: {field!r}" - ) - - def test_context_packet_in_focus_on(self): - """department_head.md упоминает context_packet в секции ## Focus On.""" - content = self._content() - focus_on_idx = content.find("## Focus On") - assert focus_on_idx != -1, "department_head.md не содержит секцию '## Focus On'" - focus_on_section = content[focus_on_idx:] - quality_checks_idx = focus_on_section.find("## Quality Checks") - focus_on_body = focus_on_section[:quality_checks_idx] if quality_checks_idx != -1 else focus_on_section - assert "context_packet" in focus_on_body, ( - "department_head.md не упоминает 'context_packet' в ## Focus On" - ) - - def test_context_packet_in_quality_checks(self): - """department_head.md валидирует context_packet в ## Quality Checks.""" - content = self._content() - qc_idx = content.find("## Quality Checks") - assert qc_idx != -1, "department_head.md не содержит секцию '## Quality Checks'" - qc_section = content[qc_idx:] - assert "context_packet" in qc_section, ( - "department_head.md не упоминает 'context_packet' в ## Quality Checks" - ) - - -# =========================================================================== -# 2. Структурный тест agents/specialists.yaml — 7 dept head ролей -# =========================================================================== - -class TestSpecialistsYamlContextPacket: - """Все 7 dept head ролей имеют output_schema.context_packet в specialists.yaml.""" - - @pytest.mark.parametrize("role", DEPT_HEAD_ROLES) - def test_dept_head_has_output_schema_context_packet(self, role): - """specialists.yaml: роль {role} имеет output_schema.context_packet.""" - data = _load_yaml() - specialists = data.get("specialists", {}) - assert role in specialists, ( - f"Роль {role!r} не найдена в specialists.yaml" - ) - output_schema = specialists[role].get("output_schema", {}) - assert "context_packet" in output_schema, ( - f"specialists.yaml: {role} не имеет output_schema.context_packet" - ) - - def test_exactly_7_dept_heads_have_context_packet(self): - """Ровно 7 dept head ролей имеют output_schema.context_packet.""" - data = _load_yaml() - specialists = data.get("specialists", {}) - roles_with_cp = [ - role for role, spec in specialists.items() - if "context_packet" in spec.get("output_schema", {}) - ] - assert len(roles_with_cp) == 7, ( - f"Ожидалось 7 ролей с output_schema.context_packet, найдено {len(roles_with_cp)}: {roles_with_cp}" - ) - - -# =========================================================================== -# 3. Структурные тесты промптов-получателей context_packet -# =========================================================================== - -class TestWorkerPromptsContextPacketInstruction: - """architect.md, backend_dev.md, frontend_dev.md содержат инструкцию читать context_packet первым.""" - - @pytest.mark.parametrize("prompt_file", ["architect.md", "backend_dev.md", "frontend_dev.md"]) - def test_worker_prompt_has_context_packet_read_first_instruction(self, prompt_file): - """Промпт {prompt_file} содержит явную инструкцию читать context_packet первым.""" - content = (PROMPTS_DIR / prompt_file).read_text(encoding="utf-8") - assert "context_packet" in content, ( - f"{prompt_file} не содержит инструкцию про context_packet" - ) - - @pytest.mark.parametrize("prompt_file", ["architect.md", "backend_dev.md", "frontend_dev.md"]) - def test_worker_prompt_context_packet_instruction_in_working_mode(self, prompt_file): - """Инструкция про context_packet находится в секции ## Working Mode промпта.""" - content = (PROMPTS_DIR / prompt_file).read_text(encoding="utf-8") - wm_idx = content.find("## Working Mode") - assert wm_idx != -1, f"{prompt_file} не содержит секцию '## Working Mode'" - wm_section = content[wm_idx:] - # Ограничиваем до следующей секции - next_section_idx = wm_section.find("\n## ", 1) - wm_body = wm_section[:next_section_idx] if next_section_idx != -1 else wm_section - assert "context_packet" in wm_body, ( - f"{prompt_file}: инструкция читать context_packet отсутствует в ## Working Mode" - ) - - @pytest.mark.parametrize("prompt_file", ["architect.md", "backend_dev.md", "frontend_dev.md"]) - def test_worker_prompt_context_packet_instruction_is_step_0(self, prompt_file): - """Инструкция про context_packet пронумерована как шаг 0 в ## Working Mode.""" - content = (PROMPTS_DIR / prompt_file).read_text(encoding="utf-8") - wm_idx = content.find("## Working Mode") - wm_section = content[wm_idx:] - next_section_idx = wm_section.find("\n## ", 1) - wm_body = wm_section[:next_section_idx] if next_section_idx != -1 else wm_section - # Шаг 0 должен содержать context_packet - assert "0." in wm_body and "context_packet" in wm_body, ( - f"{prompt_file}: шаг 0 с инструкцией context_packet не найден в ## Working Mode" - ) - - -# =========================================================================== -# 4. Unit-тесты models.create_handoff с context_packet -# =========================================================================== - -class TestCreateHandoffContextPacket: - """Unit-тесты models.create_handoff — сохранение и чтение context_packet.""" - - def test_create_handoff_stores_context_packet_dict(self, conn): - """create_handoff с context_packet dict: возвращает его как dict.""" - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - cp = { - "architecture_notes": "Pure functions", - "key_files": ["core/models.py"], - "constraints": ["No ORM"], - "unknowns": [], - "handoff_for": "backend_dev", - } - handoff = models.create_handoff( - conn, pipeline["id"], "PROJ-001", - from_department="backend", - context_packet=cp, - ) - assert handoff["context_packet"] == cp, ( - "create_handoff не вернул context_packet как dict" - ) - - def test_create_handoff_context_packet_none_returns_none(self, conn): - """create_handoff без context_packet: поле возвращается как None.""" - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - handoff = models.create_handoff( - conn, pipeline["id"], "PROJ-001", - from_department="backend", - context_packet=None, - ) - assert handoff["context_packet"] is None, ( - "create_handoff без context_packet должен возвращать None, не что-то другое" - ) - - def test_create_handoff_context_packet_persisted_in_db(self, conn): - """create_handoff с context_packet: поле сохраняется в БД и декодируется при SELECT.""" - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - cp = {"architecture_notes": "test", "key_files": ["a.py"], "constraints": [], - "unknowns": ["q1"], "handoff_for": "backend_dev"} - handoff = models.create_handoff( - conn, pipeline["id"], "PROJ-001", - from_department="backend", - context_packet=cp, - ) - # Перечитать из БД через get_handoffs_for_task - handoffs = models.get_handoffs_for_task(conn, "PROJ-001") - assert len(handoffs) == 1 - assert handoffs[0]["context_packet"] == cp, ( - "context_packet не совпадает после повторного чтения из БД" - ) - - def test_context_packet_in_json_columns(self): - """'context_packet' присутствует в models._JSON_COLUMNS.""" - assert "context_packet" in models._JSON_COLUMNS, ( - "'context_packet' должен быть в models._JSON_COLUMNS для автодекодирования" - ) - - -# =========================================================================== -# 5. Runner-тесты _execute_department_head_step -# =========================================================================== - -class TestExecuteDeptHeadStepContextPacket: - """Тесты _execute_department_head_step: context_packet в output и initial context.""" - - def test_context_packet_present_in_summary_output(self, conn): - """Если dept head вернул context_packet — он включён в summary output для следующего шага.""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - with patch("agents.runner.run_pipeline", return_value=_success_sub_result(pipeline["id"])), \ - patch("agents.runner.models.create_handoff"): - result = _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=True), - next_department="frontend", - ) - - assert result["success"] is True - summary = json.loads(result["output"]) - assert "context_packet" in summary, ( - "context_packet должен быть в summary output для следующего dept head" - ) - cp = summary["context_packet"] - assert cp["handoff_for"] == "backend_dev" - assert "core/models.py" in cp["key_files"] - - def test_context_packet_placed_first_in_worker_initial_context(self, conn): - """Если context_packet есть — он первый ключ в initial_previous_output для воркеров.""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - captured_initial_output = {} - - def capture_run_pipeline(conn, task_id, steps, *args, **kwargs): - initial = kwargs.get("initial_previous_output", "{}") - captured_initial_output["value"] = json.loads(initial) - return _success_sub_result(pipeline["id"]) - - with patch("agents.runner.run_pipeline", side_effect=capture_run_pipeline), \ - patch("agents.runner.models.create_handoff"): - _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=True), - ) - - ctx = captured_initial_output.get("value", {}) - assert "context_packet" in ctx, ( - "context_packet отсутствует в initial_previous_output для воркеров" - ) - keys = list(ctx.keys()) - assert keys[0] == "context_packet", ( - f"context_packet должен быть первым ключом в initial context, но первый: {keys[0]!r}" - ) - - def test_missing_context_packet_does_not_block_pipeline(self, conn): - """Если context_packet отсутствует — pipeline продолжается (fail-open).""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - with patch("agents.runner.run_pipeline", return_value=_success_sub_result(pipeline["id"])), \ - patch("agents.runner.models.create_handoff"): - result = _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=False), - next_department="frontend", - ) - - assert result["success"] is True, ( - "Pipeline должен продолжаться при отсутствии context_packet (fail-open)" - ) - - def test_missing_context_packet_not_in_worker_initial_context(self, conn): - """Если context_packet отсутствует — его ключа нет в initial context для воркеров.""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - captured = {} - - def capture_run_pipeline(conn, task_id, steps, *args, **kwargs): - initial = kwargs.get("initial_previous_output", "{}") - captured["value"] = json.loads(initial) - return _success_sub_result(pipeline["id"]) - - with patch("agents.runner.run_pipeline", side_effect=capture_run_pipeline), \ - patch("agents.runner.models.create_handoff"): - _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=False), - ) - - ctx = captured.get("value", {}) - assert "context_packet" not in ctx, ( - "context_packet не должен быть в initial context, если dept head его не вернул" - ) - # Но department_head_plan должен всё равно присутствовать - assert "department_head_plan" in ctx, ( - "department_head_plan должен присутствовать в initial context даже без context_packet" - ) - - def test_context_packet_passed_to_create_handoff(self, conn): - """context_packet передаётся в models.create_handoff при сохранении хэндоффа.""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - with patch("agents.runner.run_pipeline", return_value=_success_sub_result(pipeline["id"])), \ - patch("agents.runner.models.create_handoff") as mock_handoff: - _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=True), - ) - - assert mock_handoff.called, "models.create_handoff должен быть вызван" - _, kwargs = mock_handoff.call_args - cp = kwargs.get("context_packet") - assert cp is not None, "context_packet должен быть передан в create_handoff" - assert cp["handoff_for"] == "backend_dev" - - def test_missing_context_packet_handoff_receives_none(self, conn): - """Если context_packet отсутствует — create_handoff вызывается с context_packet=None.""" - from agents.runner import _execute_department_head_step - - pipeline = models.create_pipeline( - conn, "PROJ-001", "proj", "dept_feature", [{"role": "backend_head"}] - ) - with patch("agents.runner.run_pipeline", return_value=_success_sub_result(pipeline["id"])), \ - patch("agents.runner.models.create_handoff") as mock_handoff: - _execute_department_head_step( - conn=conn, - task_id="PROJ-001", - project_id="proj", - parent_pipeline_id=pipeline["id"], - step={"role": "backend_head", "brief": "plan"}, - dept_head_result=_dept_head_raw_output(with_context_packet=False), - ) - - assert mock_handoff.called - _, kwargs = mock_handoff.call_args - assert kwargs.get("context_packet") is None, ( - "create_handoff должен получить context_packet=None, если dept head не вернул его" - )