kin: KIN-091 Улучшения из исследования рынка: (1) Revise button с feedback loop, (2) auto-test before review — агент сам прогоняет тесты и фиксит до review, (3) spec-driven workflow для новых проектов — constitution → spec → plan → tasks, (4) git worktrees для параллельных агентов без конфликтов, (5) auto-trigger pipeline при создании задачи с label auto
This commit is contained in:
parent
0cc063d47a
commit
0ccd451b4b
14 changed files with 1660 additions and 18 deletions
|
|
@ -448,11 +448,12 @@ class TestAttachmentsInContext:
|
|||
assert "mockup.jpg" in filenames
|
||||
assert "/tmp/prj/.kin/attachments/PRJ-001/screenshot.png" in paths
|
||||
|
||||
def test_build_context_no_attachments_key_when_empty(self, conn):
|
||||
"""KIN-090: ключ 'attachments' отсутствует в контексте, если вложений нет."""
|
||||
def test_build_context_attachments_key_always_present(self, conn):
|
||||
"""KIN-094 #213: ключ 'attachments' всегда присутствует в контексте (пустой список если нет вложений)."""
|
||||
# conn fixture has no attachments
|
||||
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
|
||||
assert "attachments" not in ctx
|
||||
assert "attachments" in ctx
|
||||
assert ctx["attachments"] == []
|
||||
|
||||
def test_all_roles_get_attachments(self, conn_with_attachments):
|
||||
"""KIN-090: AC2 — все роли (debugger, pm, tester, reviewer) получают вложения."""
|
||||
|
|
@ -473,3 +474,193 @@ class TestAttachmentsInContext:
|
|||
ctx = build_context(conn, "VDOL-001", "debugger", "vdol")
|
||||
prompt = format_prompt(ctx, "debugger", "Debug this.")
|
||||
assert "## Attachments" not in prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KIN-094: Attachments — ctx["attachments"] always present + inline text content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAttachmentsKIN094:
|
||||
"""KIN-094: AC3 — PM и другие агенты всегда получают ключ attachments в контексте;
|
||||
текстовые файлы <= 32 KB вставляются inline в промпт."""
|
||||
|
||||
@pytest.fixture
|
||||
def conn_no_attachments(self):
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", "/tmp/prj")
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@pytest.fixture
|
||||
def conn_text_attachment(self, tmp_path):
|
||||
"""Проект с текстовым вложением <= 32 KB на диске."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
txt_file = tmp_path / "spec.txt"
|
||||
txt_file.write_text("Привет, это спека задачи", encoding="utf-8")
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "spec.txt", str(txt_file), "text/plain", txt_file.stat().st_size,
|
||||
)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@pytest.fixture
|
||||
def conn_md_attachment(self, tmp_path):
|
||||
"""Проект с .md вложением (text/markdown или определяется по расширению)."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
md_file = tmp_path / "README.md"
|
||||
md_file.write_text("# Title\n\nContent of readme", encoding="utf-8")
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "README.md", str(md_file), "text/markdown", md_file.stat().st_size,
|
||||
)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@pytest.fixture
|
||||
def conn_json_attachment(self, tmp_path):
|
||||
"""Проект с JSON-вложением (application/json)."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
json_file = tmp_path / "config.json"
|
||||
json_file.write_text('{"key": "value"}', encoding="utf-8")
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "config.json", str(json_file), "application/json", json_file.stat().st_size,
|
||||
)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@pytest.fixture
|
||||
def conn_large_text_attachment(self, tmp_path):
|
||||
"""Проект с текстовым вложением > 32 KB (не должно инлайниться)."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
big_file = tmp_path / "big.txt"
|
||||
big_file.write_text("x" * (32 * 1024 + 1), encoding="utf-8")
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "big.txt", str(big_file), "text/plain", big_file.stat().st_size,
|
||||
)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
@pytest.fixture
|
||||
def conn_image_attachment(self, tmp_path):
|
||||
"""Проект с бинарным PNG-вложением (не должно инлайниться)."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
png_file = tmp_path / "screen.png"
|
||||
png_file.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 64)
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "screen.png", str(png_file), "image/png", png_file.stat().st_size,
|
||||
)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ctx["attachments"] always present
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_pm_context_attachments_empty_list_when_no_attachments(self, conn_no_attachments):
|
||||
"""KIN-094: PM получает пустой список attachments, а не отсутствующий ключ."""
|
||||
ctx = build_context(conn_no_attachments, "PRJ-001", "pm", "prj")
|
||||
assert "attachments" in ctx
|
||||
assert ctx["attachments"] == []
|
||||
|
||||
def test_all_roles_attachments_key_present_when_empty(self, conn_no_attachments):
|
||||
"""KIN-094: все роли получают ключ attachments (пустой список) даже без вложений."""
|
||||
for role in ("pm", "debugger", "tester", "reviewer", "backend_dev", "frontend_dev", "architect"):
|
||||
ctx = build_context(conn_no_attachments, "PRJ-001", role, "prj")
|
||||
assert "attachments" in ctx, f"Role '{role}' missing 'attachments' key"
|
||||
assert isinstance(ctx["attachments"], list), f"Role '{role}': attachments is not a list"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Inline content for small text files
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_format_prompt_inlines_small_text_file_content(self, conn_text_attachment):
|
||||
"""KIN-094: содержимое текстового файла <= 32 KB вставляется inline в промпт."""
|
||||
ctx = build_context(conn_text_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert "Привет, это спека задачи" in prompt
|
||||
|
||||
def test_format_prompt_inlines_text_file_in_code_block(self, conn_text_attachment):
|
||||
"""KIN-094: inline-контент обёрнут в блок кода (``` ... ```)."""
|
||||
ctx = build_context(conn_text_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert "```" in prompt
|
||||
|
||||
def test_format_prompt_inlines_md_file_by_extension(self, conn_md_attachment):
|
||||
"""KIN-094: .md файл определяется по расширению и вставляется inline."""
|
||||
ctx = build_context(conn_md_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert "# Title" in prompt
|
||||
assert "Content of readme" in prompt
|
||||
|
||||
def test_format_prompt_inlines_json_file_by_mime(self, conn_json_attachment):
|
||||
"""KIN-094: application/json файл вставляется inline по MIME-типу."""
|
||||
ctx = build_context(conn_json_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert '"key": "value"' in prompt
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# NOT inlined: binary and large files
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_format_prompt_does_not_inline_image_file(self, conn_image_attachment):
|
||||
"""KIN-094: бинарный PNG файл НЕ вставляется inline."""
|
||||
ctx = build_context(conn_image_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
# File is listed in ## Attachments section but no ``` block with binary content
|
||||
assert "screen.png" in prompt # listed
|
||||
assert "image/png" in prompt
|
||||
# Should not contain raw binary or ``` code block for the PNG
|
||||
# We verify the file content (PNG header) is NOT inlined
|
||||
assert "\x89PNG" not in prompt
|
||||
|
||||
def test_format_prompt_does_not_inline_large_text_file(self, conn_large_text_attachment):
|
||||
"""KIN-094: текстовый файл > 32 KB НЕ вставляется inline."""
|
||||
ctx = build_context(conn_large_text_attachment, "PRJ-001", "pm", "prj")
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert "big.txt" in prompt # listed
|
||||
# Content should NOT be inlined (32KB+1 of 'x' chars)
|
||||
assert "x" * 100 not in prompt
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Resilience: missing file on disk
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_format_prompt_handles_missing_file_gracefully(self, tmp_path):
|
||||
"""KIN-094: если файл отсутствует на диске, format_prompt не падает."""
|
||||
c = init_db(":memory:")
|
||||
models.create_project(c, "prj", "Prj", str(tmp_path))
|
||||
models.create_task(c, "PRJ-001", "prj", "Task")
|
||||
# Register attachment pointing to non-existent file
|
||||
models.create_attachment(
|
||||
c, "PRJ-001", "missing.txt",
|
||||
str(tmp_path / "missing.txt"),
|
||||
"text/plain", 100,
|
||||
)
|
||||
ctx = build_context(c, "PRJ-001", "pm", "prj")
|
||||
# Should not raise — exception is caught silently
|
||||
prompt = format_prompt(ctx, "pm", "You are PM.")
|
||||
assert "missing.txt" in prompt # still listed
|
||||
c.close()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PM pipeline: attachments available in brief context
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_pm_context_includes_attachment_paths_for_pipeline(self, conn_text_attachment):
|
||||
"""KIN-094: PM-агент получает пути к вложениям в контексте для старта pipeline."""
|
||||
ctx = build_context(conn_text_attachment, "PRJ-001", "pm", "prj")
|
||||
assert len(ctx["attachments"]) == 1
|
||||
att = ctx["attachments"][0]
|
||||
assert att["filename"] == "spec.txt"
|
||||
assert att["mime_type"] == "text/plain"
|
||||
assert "path" in att
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue