kin/tests/test_context_builder.py
johnfrum1234 c129cf9d95 Fix output truncation bug, add language support for agent responses
Bug 1 — Output truncation:
  _run_claude() was replacing raw stdout with parsed sub-field which
  could be a dict (not string). run_agent() then saved dict.__repr__
  to DB instead of full JSON. Fixed: _run_claude() always returns
  string output; run_agent() ensures string before DB write.
  Added tests: full_output_saved_to_db, dict_output_saved_as_json_string.

Bug 2 — Language support:
  Added projects.language column (TEXT DEFAULT 'ru').
  Auto-migration for existing DBs (ALTER TABLE ADD COLUMN).
  context_builder passes language in project context.
  format_prompt() appends "## Language\nALWAYS respond in {language}"
  at the end of every prompt.
  CLI: kin project add --language ru (default: ru).
  Tests: language in prompt for ru/en, project creation, context.

112 tests, all passing. ~/.kin/kin.db migrated (vdol: language=ru).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:39:33 +02:00

163 lines
7 KiB
Python

"""Tests for core/context_builder.py — context assembly per role."""
import pytest
from core.db import init_db
from core import models
from core.context_builder import build_context, format_prompt
@pytest.fixture
def conn():
c = init_db(":memory:")
# Seed project, modules, decisions, tasks
models.create_project(c, "vdol", "ВДОЛЬ и ПОПЕРЕК", "~/projects/vdolipoperek",
tech_stack=["vue3", "typescript", "nodejs"])
models.add_module(c, "vdol", "search", "frontend", "src/search/")
models.add_module(c, "vdol", "api", "backend", "src/api/")
models.add_decision(c, "vdol", "gotcha", "Safari bug",
"position:fixed breaks", category="ui", tags=["ios"])
models.add_decision(c, "vdol", "workaround", "API rate limit",
"10 req/s max", category="api")
models.add_decision(c, "vdol", "convention", "Use WAL mode",
"Always use WAL for SQLite", category="architecture")
models.add_decision(c, "vdol", "decision", "Auth required",
"All endpoints need auth", category="security")
models.create_task(c, "VDOL-001", "vdol", "Fix search filters",
brief={"module": "search", "route_type": "debug"})
models.create_task(c, "VDOL-002", "vdol", "Add payments",
status="in_progress")
yield c
c.close()
class TestBuildContext:
def test_pm_gets_everything(self, conn):
ctx = build_context(conn, "VDOL-001", "pm", "vdol")
assert ctx["task"]["id"] == "VDOL-001"
assert ctx["project"]["id"] == "vdol"
assert len(ctx["modules"]) == 2
assert len(ctx["decisions"]) == 4 # all decisions
assert len(ctx["active_tasks"]) == 1 # VDOL-002 in_progress
assert "pm" in ctx["available_specialists"]
def test_architect_gets_all_decisions_and_modules(self, conn):
ctx = build_context(conn, "VDOL-001", "architect", "vdol")
assert len(ctx["modules"]) == 2
assert len(ctx["decisions"]) == 4
def test_debugger_gets_only_gotcha_workaround(self, conn):
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
types = {d["type"] for d in ctx["decisions"]}
assert types <= {"gotcha", "workaround"}
assert "convention" not in types
assert "decision" not in types
assert ctx["module_hint"] == "search"
def test_frontend_dev_gets_gotcha_workaround_convention(self, conn):
ctx = build_context(conn, "VDOL-001", "frontend_dev", "vdol")
types = {d["type"] for d in ctx["decisions"]}
assert "gotcha" in types
assert "workaround" in types
assert "convention" in types
assert "decision" not in types # plain decisions excluded
def test_backend_dev_same_as_frontend(self, conn):
ctx = build_context(conn, "VDOL-001", "backend_dev", "vdol")
types = {d["type"] for d in ctx["decisions"]}
assert types == {"gotcha", "workaround", "convention"}
def test_reviewer_gets_only_conventions(self, conn):
ctx = build_context(conn, "VDOL-001", "reviewer", "vdol")
types = {d["type"] for d in ctx["decisions"]}
assert types == {"convention"}
def test_tester_gets_minimal_context(self, conn):
ctx = build_context(conn, "VDOL-001", "tester", "vdol")
assert ctx["task"] is not None
assert ctx["project"] is not None
assert "decisions" not in ctx
assert "modules" not in ctx
def test_security_gets_security_decisions(self, conn):
ctx = build_context(conn, "VDOL-001", "security", "vdol")
categories = {d.get("category") for d in ctx["decisions"]}
assert categories == {"security"}
def test_unknown_role_gets_fallback(self, conn):
ctx = build_context(conn, "VDOL-001", "unknown_role", "vdol")
assert "decisions" in ctx
assert len(ctx["decisions"]) > 0
class TestFormatPrompt:
def test_format_with_template(self, conn):
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
prompt = format_prompt(ctx, "debugger", "You are a debugger. Find bugs.")
assert "You are a debugger" in prompt
assert "VDOL-001" in prompt
assert "Fix search filters" in prompt
assert "vdol" in prompt
assert "vue3" in prompt
def test_format_includes_decisions(self, conn):
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
prompt = format_prompt(ctx, "debugger", "Debug this.")
assert "Safari bug" in prompt
assert "API rate limit" in prompt
# Convention should NOT be here (debugger doesn't get it)
assert "WAL mode" not in prompt
def test_format_pm_includes_specialists(self, conn):
ctx = build_context(conn, "VDOL-001", "pm", "vdol")
prompt = format_prompt(ctx, "pm", "You are PM.")
assert "Available specialists" in prompt
assert "debugger" in prompt
assert "Active tasks" in prompt
assert "VDOL-002" in prompt
def test_format_with_previous_output(self, conn):
ctx = build_context(conn, "VDOL-001", "tester", "vdol")
ctx["previous_output"] = "Found race condition in useSearch.ts"
prompt = format_prompt(ctx, "tester", "Write tests.")
assert "Previous step output" in prompt
assert "race condition" in prompt
def test_format_loads_prompt_file(self, conn):
ctx = build_context(conn, "VDOL-001", "pm", "vdol")
prompt = format_prompt(ctx, "pm") # Should load from agents/prompts/pm.md
assert "decompose" in prompt.lower() or "pipeline" in prompt.lower()
def test_format_missing_prompt_file(self, conn):
ctx = build_context(conn, "VDOL-001", "analyst", "vdol")
prompt = format_prompt(ctx, "analyst") # No analyst.md exists
assert "analyst" in prompt.lower()
def test_format_includes_language_ru(self, conn):
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
prompt = format_prompt(ctx, "debugger", "Debug.")
assert "## Language" in prompt
assert "Russian" in prompt
assert "ALWAYS respond in Russian" in prompt
def test_format_includes_language_en(self, conn):
# Update project language to en
conn.execute("UPDATE projects SET language='en' WHERE id='vdol'")
conn.commit()
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
prompt = format_prompt(ctx, "debugger", "Debug.")
assert "ALWAYS respond in English" in prompt
class TestLanguageInProject:
def test_project_has_language_default(self, conn):
p = models.get_project(conn, "vdol")
assert p["language"] == "ru"
def test_create_project_with_language(self, conn):
p = models.create_project(conn, "en-proj", "English Project", "/en",
language="en")
assert p["language"] == "en"
def test_context_carries_language(self, conn):
ctx = build_context(conn, "VDOL-001", "pm", "vdol")
assert ctx["project"]["language"] == "ru"