draft-board 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Tests for task_dispatch module - SQLite-backed task enqueue."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from app.services.task_dispatch import TaskHandle, enqueue_task
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Fixture: temp SQLite DB with job_queue table
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def sqlite_db(tmp_path):
|
|
18
|
+
"""Create a temporary SQLite DB with job_queue table."""
|
|
19
|
+
db_path = str(tmp_path / "test_dispatch.db")
|
|
20
|
+
conn = sqlite3.connect(db_path)
|
|
21
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
22
|
+
conn.execute("""
|
|
23
|
+
CREATE TABLE job_queue (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
task_name TEXT NOT NULL,
|
|
26
|
+
args_json TEXT NOT NULL DEFAULT '[]',
|
|
27
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
28
|
+
claimed_by TEXT,
|
|
29
|
+
claimed_at TIMESTAMP,
|
|
30
|
+
completed_at TIMESTAMP,
|
|
31
|
+
result_json TEXT,
|
|
32
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
34
|
+
)
|
|
35
|
+
""")
|
|
36
|
+
conn.commit()
|
|
37
|
+
conn.close()
|
|
38
|
+
|
|
39
|
+
with patch("app.sqlite_kv._DB_PATH", db_path):
|
|
40
|
+
yield db_path
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ===========================================================================
|
|
44
|
+
# TaskHandle tests
|
|
45
|
+
# ===========================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestTaskHandle:
|
|
49
|
+
def test_has_id_attribute(self):
|
|
50
|
+
handle = TaskHandle("abc-123")
|
|
51
|
+
assert handle.id == "abc-123"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ===========================================================================
|
|
55
|
+
# SQLite enqueue tests
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestEnqueueSQLite:
|
|
60
|
+
def test_enqueue_inserts_row(self, sqlite_db):
|
|
61
|
+
handle = enqueue_task("execute_ticket", args=["job-1"])
|
|
62
|
+
assert handle.id is not None
|
|
63
|
+
|
|
64
|
+
conn = sqlite3.connect(sqlite_db)
|
|
65
|
+
row = conn.execute(
|
|
66
|
+
"SELECT task_name, args_json, status FROM job_queue WHERE id = ?",
|
|
67
|
+
(handle.id,),
|
|
68
|
+
).fetchone()
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
assert row is not None
|
|
72
|
+
assert row[0] == "execute_ticket"
|
|
73
|
+
assert json.loads(row[1]) == ["job-1"]
|
|
74
|
+
assert row[2] == "pending"
|
|
75
|
+
|
|
76
|
+
def test_enqueue_default_args(self, sqlite_db):
|
|
77
|
+
handle = enqueue_task("verify_ticket")
|
|
78
|
+
|
|
79
|
+
conn = sqlite3.connect(sqlite_db)
|
|
80
|
+
row = conn.execute(
|
|
81
|
+
"SELECT args_json FROM job_queue WHERE id = ?",
|
|
82
|
+
(handle.id,),
|
|
83
|
+
).fetchone()
|
|
84
|
+
conn.close()
|
|
85
|
+
|
|
86
|
+
assert json.loads(row[0]) == []
|
|
87
|
+
|
|
88
|
+
def test_enqueue_returns_unique_ids(self, sqlite_db):
|
|
89
|
+
h1 = enqueue_task("execute_ticket", args=["j1"])
|
|
90
|
+
h2 = enqueue_task("execute_ticket", args=["j2"])
|
|
91
|
+
assert h1.id != h2.id
|
|
92
|
+
|
|
93
|
+
def test_enqueue_multiple_tasks(self, sqlite_db):
|
|
94
|
+
for i in range(5):
|
|
95
|
+
enqueue_task("execute_ticket", args=[f"job-{i}"])
|
|
96
|
+
|
|
97
|
+
conn = sqlite3.connect(sqlite_db)
|
|
98
|
+
count = conn.execute("SELECT COUNT(*) FROM job_queue").fetchone()[0]
|
|
99
|
+
conn.close()
|
|
100
|
+
assert count == 5
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Tests for ticket validation feature in TicketGenerationService."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from app.models.goal import Goal
|
|
10
|
+
from app.services.config_service import PlannerConfig
|
|
11
|
+
from app.services.context_gatherer import GatherStats, RepoContext
|
|
12
|
+
from app.services.llm_service import LLMResponse
|
|
13
|
+
from app.services.ticket_generation_service import TicketGenerationService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_db():
|
|
18
|
+
"""Create a mock database session."""
|
|
19
|
+
return AsyncMock(spec=AsyncSession)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_llm_service():
|
|
24
|
+
"""Create a mock LLM service."""
|
|
25
|
+
service = Mock()
|
|
26
|
+
service.call_completion = Mock()
|
|
27
|
+
service.safe_parse_json = Mock()
|
|
28
|
+
return service
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_config():
|
|
33
|
+
"""Create a mock planner config."""
|
|
34
|
+
return PlannerConfig()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def sample_goal():
|
|
39
|
+
"""Create a sample goal for testing."""
|
|
40
|
+
goal = Goal(
|
|
41
|
+
id="test-goal-id",
|
|
42
|
+
title="Add authentication system",
|
|
43
|
+
description="Implement JWT-based authentication",
|
|
44
|
+
board_id="test-board-id",
|
|
45
|
+
)
|
|
46
|
+
return goal
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def sample_context():
|
|
51
|
+
"""Create a sample repo context."""
|
|
52
|
+
context = RepoContext(
|
|
53
|
+
file_structure=["src/app.py", "src/auth.py"],
|
|
54
|
+
readme_excerpt="Sample project",
|
|
55
|
+
todos=["TODO: Implement login"],
|
|
56
|
+
stats=GatherStats(),
|
|
57
|
+
)
|
|
58
|
+
return context
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestTicketValidation:
|
|
62
|
+
"""Test suite for ticket validation feature."""
|
|
63
|
+
|
|
64
|
+
def test_build_validation_system_prompt(
|
|
65
|
+
self, mock_db, mock_llm_service, mock_config
|
|
66
|
+
):
|
|
67
|
+
"""Test that validation system prompt is properly built."""
|
|
68
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
69
|
+
|
|
70
|
+
prompt = service._build_ticket_validation_system_prompt()
|
|
71
|
+
|
|
72
|
+
assert "technical code reviewer" in prompt.lower()
|
|
73
|
+
assert "is_valid" in prompt
|
|
74
|
+
assert "validation_result" in prompt
|
|
75
|
+
assert "appropriate" in prompt
|
|
76
|
+
assert "already_implemented" in prompt
|
|
77
|
+
assert "not_relevant" in prompt
|
|
78
|
+
|
|
79
|
+
def test_build_validation_user_prompt(
|
|
80
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
81
|
+
):
|
|
82
|
+
"""Test that validation user prompt includes all necessary info."""
|
|
83
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
84
|
+
|
|
85
|
+
ticket = {
|
|
86
|
+
"title": "Implement JWT authentication",
|
|
87
|
+
"description": "Add JWT token generation and validation",
|
|
88
|
+
"priority_bucket": "P1",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
prompt = service._build_ticket_validation_user_prompt(
|
|
92
|
+
ticket=ticket,
|
|
93
|
+
goal_title=sample_goal.title,
|
|
94
|
+
goal_description=sample_goal.description,
|
|
95
|
+
context_summary="Files: src/app.py, src/auth.py",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert sample_goal.title in prompt
|
|
99
|
+
assert sample_goal.description in prompt
|
|
100
|
+
assert ticket["title"] in prompt
|
|
101
|
+
assert ticket["description"] in prompt
|
|
102
|
+
assert "src/auth.py" in prompt
|
|
103
|
+
|
|
104
|
+
def test_validate_ticket_appropriate(
|
|
105
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
106
|
+
):
|
|
107
|
+
"""Test validation of an appropriate ticket."""
|
|
108
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
109
|
+
|
|
110
|
+
# Mock LLM response indicating ticket is appropriate
|
|
111
|
+
mock_llm_service.call_completion.return_value = LLMResponse(
|
|
112
|
+
content='{"is_valid": true, "confidence": "high", "validation_result": "appropriate", "reasoning": "Ticket aligns with goal"}',
|
|
113
|
+
model="gpt-4o-mini",
|
|
114
|
+
usage={"prompt_tokens": 100, "completion_tokens": 50},
|
|
115
|
+
)
|
|
116
|
+
mock_llm_service.safe_parse_json.return_value = {
|
|
117
|
+
"is_valid": True,
|
|
118
|
+
"confidence": "high",
|
|
119
|
+
"validation_result": "appropriate",
|
|
120
|
+
"reasoning": "Ticket aligns with goal",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ticket = {"title": "Implement login endpoint", "description": "Add POST /login"}
|
|
124
|
+
|
|
125
|
+
result = service._validate_ticket_against_codebase(
|
|
126
|
+
ticket=ticket,
|
|
127
|
+
goal=sample_goal,
|
|
128
|
+
context_summary="Files: src/app.py",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
assert result["is_valid"] is True
|
|
132
|
+
assert result["validation_result"] == "appropriate"
|
|
133
|
+
assert result["confidence"] == "high"
|
|
134
|
+
|
|
135
|
+
def test_validate_ticket_already_implemented(
|
|
136
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
137
|
+
):
|
|
138
|
+
"""Test validation of a ticket for already implemented feature."""
|
|
139
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
140
|
+
|
|
141
|
+
# Mock LLM response indicating feature already exists
|
|
142
|
+
mock_llm_service.call_completion.return_value = LLMResponse(
|
|
143
|
+
content='{"is_valid": false, "confidence": "high", "validation_result": "already_implemented", "reasoning": "auth.py already has login"}',
|
|
144
|
+
model="gpt-4o-mini",
|
|
145
|
+
usage={"prompt_tokens": 100, "completion_tokens": 50},
|
|
146
|
+
)
|
|
147
|
+
mock_llm_service.safe_parse_json.return_value = {
|
|
148
|
+
"is_valid": False,
|
|
149
|
+
"confidence": "high",
|
|
150
|
+
"validation_result": "already_implemented",
|
|
151
|
+
"reasoning": "auth.py already has login",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
ticket = {"title": "Implement login endpoint", "description": "Add POST /login"}
|
|
155
|
+
|
|
156
|
+
result = service._validate_ticket_against_codebase(
|
|
157
|
+
ticket=ticket,
|
|
158
|
+
goal=sample_goal,
|
|
159
|
+
context_summary="Files: src/app.py, src/auth.py with login_user() function",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert result["is_valid"] is False
|
|
163
|
+
assert result["validation_result"] == "already_implemented"
|
|
164
|
+
|
|
165
|
+
def test_validate_ticket_not_relevant(
|
|
166
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
167
|
+
):
|
|
168
|
+
"""Test validation of a ticket that's not relevant to the goal."""
|
|
169
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
170
|
+
|
|
171
|
+
# Mock LLM response indicating ticket is not relevant
|
|
172
|
+
mock_llm_service.safe_parse_json.return_value = {
|
|
173
|
+
"is_valid": False,
|
|
174
|
+
"confidence": "medium",
|
|
175
|
+
"validation_result": "not_relevant",
|
|
176
|
+
"reasoning": "Database optimization doesn't relate to authentication goal",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ticket = {
|
|
180
|
+
"title": "Optimize database queries",
|
|
181
|
+
"description": "Add database indexes",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result = service._validate_ticket_against_codebase(
|
|
185
|
+
ticket=ticket,
|
|
186
|
+
goal=sample_goal,
|
|
187
|
+
context_summary="Files: src/app.py, src/auth.py",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert result["is_valid"] is False
|
|
191
|
+
assert result["validation_result"] == "not_relevant"
|
|
192
|
+
|
|
193
|
+
def test_validate_ticket_error_handling(
|
|
194
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
195
|
+
):
|
|
196
|
+
"""Test that validation errors fail open (accept ticket)."""
|
|
197
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
198
|
+
|
|
199
|
+
# Mock LLM service to raise an exception
|
|
200
|
+
mock_llm_service.call_completion.side_effect = Exception("LLM API error")
|
|
201
|
+
|
|
202
|
+
ticket = {"title": "Some ticket", "description": "Some description"}
|
|
203
|
+
|
|
204
|
+
result = service._validate_ticket_against_codebase(
|
|
205
|
+
ticket=ticket,
|
|
206
|
+
goal=sample_goal,
|
|
207
|
+
context_summary="Files: src/app.py",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Should fail open and accept the ticket
|
|
211
|
+
assert result["is_valid"] is True
|
|
212
|
+
assert result["validation_result"] == "unclear"
|
|
213
|
+
assert "error" in result["reasoning"].lower()
|
|
214
|
+
|
|
215
|
+
@pytest.mark.asyncio
|
|
216
|
+
async def test_generate_from_goal_with_validation(
|
|
217
|
+
self, mock_db, mock_llm_service, mock_config, sample_goal
|
|
218
|
+
):
|
|
219
|
+
"""Test that generate_from_goal filters tickets based on validation."""
|
|
220
|
+
with (
|
|
221
|
+
patch.object(
|
|
222
|
+
TicketGenerationService, "_call_agent_for_tickets"
|
|
223
|
+
) as mock_agent,
|
|
224
|
+
patch.object(
|
|
225
|
+
TicketGenerationService, "_get_existing_tickets"
|
|
226
|
+
) as mock_existing,
|
|
227
|
+
):
|
|
228
|
+
# Setup mock responses
|
|
229
|
+
mock_agent.return_value = json.dumps(
|
|
230
|
+
{
|
|
231
|
+
"tickets": [
|
|
232
|
+
{
|
|
233
|
+
"title": "Implement login endpoint",
|
|
234
|
+
"description": "Add POST /login",
|
|
235
|
+
"priority_bucket": "P1",
|
|
236
|
+
"priority_rationale": "Core feature",
|
|
237
|
+
"verification": ["curl http://localhost/login"],
|
|
238
|
+
"blocked_by": None,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"title": "Add existing feature",
|
|
242
|
+
"description": "This already exists",
|
|
243
|
+
"priority_bucket": "P2",
|
|
244
|
+
"priority_rationale": "Nice to have",
|
|
245
|
+
"verification": [],
|
|
246
|
+
"blocked_by": None,
|
|
247
|
+
},
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
mock_existing.return_value = []
|
|
253
|
+
|
|
254
|
+
# Mock database operations
|
|
255
|
+
mock_db.execute = AsyncMock()
|
|
256
|
+
mock_db.flush = AsyncMock()
|
|
257
|
+
mock_db.refresh = AsyncMock()
|
|
258
|
+
mock_db.commit = AsyncMock()
|
|
259
|
+
|
|
260
|
+
# Mock goal lookup
|
|
261
|
+
mock_result = AsyncMock()
|
|
262
|
+
mock_result.scalar_one_or_none.return_value = sample_goal
|
|
263
|
+
mock_db.execute.return_value = mock_result
|
|
264
|
+
|
|
265
|
+
service = TicketGenerationService(mock_db, mock_llm_service, mock_config)
|
|
266
|
+
|
|
267
|
+
# Mock context gatherer
|
|
268
|
+
with patch.object(service.context_gatherer, "gather") as mock_gather:
|
|
269
|
+
mock_context = Mock()
|
|
270
|
+
mock_context.to_prompt_string.return_value = (
|
|
271
|
+
"Files: src/app.py, src/auth.py"
|
|
272
|
+
)
|
|
273
|
+
mock_gather.return_value = mock_context
|
|
274
|
+
|
|
275
|
+
# Mock validation: first ticket appropriate, second already implemented
|
|
276
|
+
service._validate_ticket_against_codebase = Mock(
|
|
277
|
+
side_effect=[
|
|
278
|
+
{
|
|
279
|
+
"is_valid": True,
|
|
280
|
+
"confidence": "high",
|
|
281
|
+
"validation_result": "appropriate",
|
|
282
|
+
"reasoning": "Good ticket",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"is_valid": False,
|
|
286
|
+
"confidence": "high",
|
|
287
|
+
"validation_result": "already_implemented",
|
|
288
|
+
"reasoning": "Feature exists",
|
|
289
|
+
},
|
|
290
|
+
]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Note: This will fail because we need to mock more DB operations
|
|
294
|
+
# This is just to show the structure of the test
|
|
295
|
+
# In a real test, you'd need to mock Ticket creation and all DB operations
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_validation_config_default(mock_config):
|
|
299
|
+
"""Test that validation is disabled by default in config (matches YAML intent)."""
|
|
300
|
+
assert mock_config.features.validate_tickets is False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
pytest.main([__file__, "-v"])
|