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,391 @@
|
|
|
1
|
+
"""Tests for AutonomyService safety checks and auto-actions."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
|
|
7
|
+
from app.models.enums import ActorType
|
|
8
|
+
from app.models.evidence import Evidence, EvidenceKind
|
|
9
|
+
from app.models.goal import Goal
|
|
10
|
+
from app.models.ticket import Ticket
|
|
11
|
+
from app.models.ticket_event import TicketEvent
|
|
12
|
+
from app.services.autonomy_service import AutonomyService
|
|
13
|
+
from app.services.config_service import AutonomyConfig
|
|
14
|
+
from app.state_machine import TicketState
|
|
15
|
+
|
|
16
|
+
# ── Fixtures ──
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_goal(
|
|
20
|
+
autonomy_enabled: bool = True,
|
|
21
|
+
auto_approve_tickets: bool = True,
|
|
22
|
+
auto_approve_revisions: bool = True,
|
|
23
|
+
auto_merge: bool = True,
|
|
24
|
+
auto_approve_followups: bool = True,
|
|
25
|
+
max_auto_approvals: int | None = None,
|
|
26
|
+
auto_approval_count: int = 0,
|
|
27
|
+
) -> Goal:
|
|
28
|
+
"""Create a Goal with autonomy settings."""
|
|
29
|
+
return Goal(
|
|
30
|
+
id=str(uuid.uuid4()),
|
|
31
|
+
title="Test Goal",
|
|
32
|
+
description="Test goal for autonomy",
|
|
33
|
+
autonomy_enabled=autonomy_enabled,
|
|
34
|
+
auto_approve_tickets=auto_approve_tickets,
|
|
35
|
+
auto_approve_revisions=auto_approve_revisions,
|
|
36
|
+
auto_merge=auto_merge,
|
|
37
|
+
auto_approve_followups=auto_approve_followups,
|
|
38
|
+
max_auto_approvals=max_auto_approvals,
|
|
39
|
+
auto_approval_count=auto_approval_count,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def make_ticket(goal: Goal, state: str = "verifying") -> Ticket:
|
|
44
|
+
"""Create a Ticket linked to a goal."""
|
|
45
|
+
return Ticket(
|
|
46
|
+
id=str(uuid.uuid4()),
|
|
47
|
+
goal_id=goal.id,
|
|
48
|
+
title="Test Ticket",
|
|
49
|
+
description="Test ticket",
|
|
50
|
+
state=state,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_evidence(
|
|
55
|
+
ticket_id: str,
|
|
56
|
+
kind: str = EvidenceKind.VERIFY_META.value,
|
|
57
|
+
exit_code: int = 0,
|
|
58
|
+
stdout_path: str = "test.txt",
|
|
59
|
+
) -> Evidence:
|
|
60
|
+
"""Create an Evidence record."""
|
|
61
|
+
return Evidence(
|
|
62
|
+
id=str(uuid.uuid4()),
|
|
63
|
+
ticket_id=ticket_id,
|
|
64
|
+
job_id=str(uuid.uuid4()),
|
|
65
|
+
kind=kind,
|
|
66
|
+
command="test",
|
|
67
|
+
exit_code=exit_code,
|
|
68
|
+
stdout_path=stdout_path,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── can_auto_approve_ticket tests ──
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def test_auto_approve_ticket_all_checks_pass(db):
|
|
76
|
+
"""Auto-approve ticket when all goal settings are enabled."""
|
|
77
|
+
goal = make_goal()
|
|
78
|
+
db.add(goal)
|
|
79
|
+
await db.flush()
|
|
80
|
+
|
|
81
|
+
ticket = make_ticket(goal, state="proposed")
|
|
82
|
+
db.add(ticket)
|
|
83
|
+
await db.flush()
|
|
84
|
+
|
|
85
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
86
|
+
result = await service.can_auto_approve_ticket(db, ticket)
|
|
87
|
+
|
|
88
|
+
assert result.approved is True
|
|
89
|
+
assert "allowed" in result.reason
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def test_auto_approve_ticket_autonomy_disabled(db):
|
|
93
|
+
"""Reject when autonomy_enabled is False."""
|
|
94
|
+
goal = make_goal(autonomy_enabled=False)
|
|
95
|
+
db.add(goal)
|
|
96
|
+
await db.flush()
|
|
97
|
+
|
|
98
|
+
ticket = make_ticket(goal, state="proposed")
|
|
99
|
+
db.add(ticket)
|
|
100
|
+
await db.flush()
|
|
101
|
+
|
|
102
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
103
|
+
result = await service.can_auto_approve_ticket(db, ticket)
|
|
104
|
+
|
|
105
|
+
assert result.approved is False
|
|
106
|
+
assert "not enabled" in result.reason
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def test_auto_approve_ticket_feature_disabled(db):
|
|
110
|
+
"""Reject when auto_approve_tickets is False."""
|
|
111
|
+
goal = make_goal(auto_approve_tickets=False)
|
|
112
|
+
db.add(goal)
|
|
113
|
+
await db.flush()
|
|
114
|
+
|
|
115
|
+
ticket = make_ticket(goal, state="proposed")
|
|
116
|
+
db.add(ticket)
|
|
117
|
+
await db.flush()
|
|
118
|
+
|
|
119
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
120
|
+
result = await service.can_auto_approve_ticket(db, ticket)
|
|
121
|
+
|
|
122
|
+
assert result.approved is False
|
|
123
|
+
assert "not enabled" in result.reason
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def test_auto_approve_ticket_max_approvals_reached(db):
|
|
127
|
+
"""Reject when max_auto_approvals is reached."""
|
|
128
|
+
goal = make_goal(max_auto_approvals=2, auto_approval_count=2)
|
|
129
|
+
db.add(goal)
|
|
130
|
+
await db.flush()
|
|
131
|
+
|
|
132
|
+
ticket = make_ticket(goal, state="proposed")
|
|
133
|
+
db.add(ticket)
|
|
134
|
+
await db.flush()
|
|
135
|
+
|
|
136
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
137
|
+
result = await service.can_auto_approve_ticket(db, ticket)
|
|
138
|
+
|
|
139
|
+
assert result.approved is False
|
|
140
|
+
assert "Max auto-approvals" in result.reason
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def test_auto_approve_ticket_under_max_approvals(db):
|
|
144
|
+
"""Allow when auto_approval_count is under max."""
|
|
145
|
+
goal = make_goal(max_auto_approvals=5, auto_approval_count=3)
|
|
146
|
+
db.add(goal)
|
|
147
|
+
await db.flush()
|
|
148
|
+
|
|
149
|
+
ticket = make_ticket(goal, state="proposed")
|
|
150
|
+
db.add(ticket)
|
|
151
|
+
await db.flush()
|
|
152
|
+
|
|
153
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
154
|
+
result = await service.can_auto_approve_ticket(db, ticket)
|
|
155
|
+
|
|
156
|
+
assert result.approved is True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── can_auto_approve_revision tests ──
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def test_auto_approve_revision_all_checks_pass(db):
|
|
163
|
+
"""Auto-approve revision when all checks pass."""
|
|
164
|
+
goal = make_goal()
|
|
165
|
+
db.add(goal)
|
|
166
|
+
await db.flush()
|
|
167
|
+
|
|
168
|
+
ticket = make_ticket(goal)
|
|
169
|
+
db.add(ticket)
|
|
170
|
+
await db.flush()
|
|
171
|
+
|
|
172
|
+
# Add passing verification evidence
|
|
173
|
+
evidence = make_evidence(
|
|
174
|
+
ticket.id, kind=EvidenceKind.VERIFY_META.value, exit_code=0
|
|
175
|
+
)
|
|
176
|
+
db.add(evidence)
|
|
177
|
+
await db.flush()
|
|
178
|
+
|
|
179
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
180
|
+
result = await service.can_auto_approve_revision(db, ticket)
|
|
181
|
+
|
|
182
|
+
assert result.approved is True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def test_auto_approve_revision_verification_failed(db):
|
|
186
|
+
"""Reject when verification evidence has non-zero exit code."""
|
|
187
|
+
goal = make_goal()
|
|
188
|
+
db.add(goal)
|
|
189
|
+
await db.flush()
|
|
190
|
+
|
|
191
|
+
ticket = make_ticket(goal)
|
|
192
|
+
db.add(ticket)
|
|
193
|
+
await db.flush()
|
|
194
|
+
|
|
195
|
+
# Add failing verification evidence
|
|
196
|
+
evidence = make_evidence(
|
|
197
|
+
ticket.id, kind=EvidenceKind.VERIFY_META.value, exit_code=1
|
|
198
|
+
)
|
|
199
|
+
db.add(evidence)
|
|
200
|
+
await db.flush()
|
|
201
|
+
|
|
202
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
203
|
+
result = await service.can_auto_approve_revision(db, ticket)
|
|
204
|
+
|
|
205
|
+
assert result.approved is False
|
|
206
|
+
assert "Verification failed" in result.reason
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def test_auto_approve_revision_autonomy_disabled(db):
|
|
210
|
+
"""Reject when autonomy is not enabled on goal."""
|
|
211
|
+
goal = make_goal(autonomy_enabled=False)
|
|
212
|
+
db.add(goal)
|
|
213
|
+
await db.flush()
|
|
214
|
+
|
|
215
|
+
ticket = make_ticket(goal)
|
|
216
|
+
db.add(ticket)
|
|
217
|
+
await db.flush()
|
|
218
|
+
|
|
219
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
220
|
+
result = await service.can_auto_approve_revision(db, ticket)
|
|
221
|
+
|
|
222
|
+
assert result.approved is False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def test_auto_approve_revision_feature_disabled(db):
|
|
226
|
+
"""Reject when auto_approve_revisions is False."""
|
|
227
|
+
goal = make_goal(auto_approve_revisions=False)
|
|
228
|
+
db.add(goal)
|
|
229
|
+
await db.flush()
|
|
230
|
+
|
|
231
|
+
ticket = make_ticket(goal)
|
|
232
|
+
db.add(ticket)
|
|
233
|
+
await db.flush()
|
|
234
|
+
|
|
235
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
236
|
+
result = await service.can_auto_approve_revision(db, ticket)
|
|
237
|
+
|
|
238
|
+
assert result.approved is False
|
|
239
|
+
assert "not enabled" in result.reason
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def test_auto_approve_revision_max_approvals_reached(db):
|
|
243
|
+
"""Reject when max auto-approvals has been reached."""
|
|
244
|
+
goal = make_goal(max_auto_approvals=1, auto_approval_count=1)
|
|
245
|
+
db.add(goal)
|
|
246
|
+
await db.flush()
|
|
247
|
+
|
|
248
|
+
ticket = make_ticket(goal)
|
|
249
|
+
db.add(ticket)
|
|
250
|
+
await db.flush()
|
|
251
|
+
|
|
252
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
253
|
+
result = await service.can_auto_approve_revision(db, ticket)
|
|
254
|
+
|
|
255
|
+
assert result.approved is False
|
|
256
|
+
assert "Max auto-approvals" in result.reason
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ── Diff size and sensitive file tests (unit-level, no file I/O) ──
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_check_revision_approval_passes_basic():
|
|
263
|
+
"""Pure logic check: revision approval with passing goal and no evidence."""
|
|
264
|
+
goal = make_goal()
|
|
265
|
+
config = AutonomyConfig()
|
|
266
|
+
service = AutonomyService(config=config)
|
|
267
|
+
|
|
268
|
+
result = service._check_revision_approval(goal, [])
|
|
269
|
+
assert result.approved is True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_check_revision_approval_verification_fail():
|
|
273
|
+
"""Pure logic check: verify evidence with bad exit code blocks approval."""
|
|
274
|
+
goal = make_goal()
|
|
275
|
+
ticket = make_ticket(goal)
|
|
276
|
+
evidence = make_evidence(
|
|
277
|
+
ticket.id, kind=EvidenceKind.VERIFY_META.value, exit_code=1
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
config = AutonomyConfig(require_verification_pass=True)
|
|
281
|
+
service = AutonomyService(config=config)
|
|
282
|
+
|
|
283
|
+
result = service._check_revision_approval(goal, [evidence])
|
|
284
|
+
assert result.approved is False
|
|
285
|
+
assert "Verification failed" in result.reason
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_check_revision_approval_verification_pass_not_required():
|
|
289
|
+
"""When require_verification_pass is False, failing verify does not block."""
|
|
290
|
+
goal = make_goal()
|
|
291
|
+
ticket = make_ticket(goal)
|
|
292
|
+
evidence = make_evidence(
|
|
293
|
+
ticket.id, kind=EvidenceKind.VERIFY_META.value, exit_code=1
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
config = AutonomyConfig(require_verification_pass=False)
|
|
297
|
+
service = AutonomyService(config=config)
|
|
298
|
+
|
|
299
|
+
result = service._check_revision_approval(goal, [evidence])
|
|
300
|
+
assert result.approved is True
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_check_ticket_approval_all_enabled():
|
|
304
|
+
"""Pure logic: ticket approval passes with all flags on."""
|
|
305
|
+
goal = make_goal()
|
|
306
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
307
|
+
result = service._check_ticket_approval(goal)
|
|
308
|
+
assert result.approved is True
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_check_ticket_approval_max_exceeded():
|
|
312
|
+
"""Pure logic: ticket approval fails when count >= max."""
|
|
313
|
+
goal = make_goal(max_auto_approvals=3, auto_approval_count=3)
|
|
314
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
315
|
+
result = service._check_ticket_approval(goal)
|
|
316
|
+
assert result.approved is False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_check_ticket_approval_no_max():
|
|
320
|
+
"""Pure logic: no max means unlimited approvals."""
|
|
321
|
+
goal = make_goal(max_auto_approvals=None, auto_approval_count=100)
|
|
322
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
323
|
+
result = service._check_ticket_approval(goal)
|
|
324
|
+
assert result.approved is True
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ── record_auto_action tests ──
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def test_record_auto_action_creates_event(db):
|
|
331
|
+
"""Record auto-action creates a TicketEvent and increments counter."""
|
|
332
|
+
goal = make_goal(auto_approval_count=0)
|
|
333
|
+
db.add(goal)
|
|
334
|
+
await db.flush()
|
|
335
|
+
|
|
336
|
+
ticket = make_ticket(goal, state="verifying")
|
|
337
|
+
db.add(ticket)
|
|
338
|
+
await db.flush()
|
|
339
|
+
|
|
340
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
341
|
+
await service.record_auto_action(
|
|
342
|
+
db,
|
|
343
|
+
ticket,
|
|
344
|
+
action_type="approve_revision",
|
|
345
|
+
details={"reason": "All checks passed"},
|
|
346
|
+
from_state="verifying",
|
|
347
|
+
to_state="done",
|
|
348
|
+
)
|
|
349
|
+
await db.flush()
|
|
350
|
+
|
|
351
|
+
# Check event was created
|
|
352
|
+
result = await db.execute(
|
|
353
|
+
select(TicketEvent).where(TicketEvent.ticket_id == ticket.id)
|
|
354
|
+
)
|
|
355
|
+
events = list(result.scalars().all())
|
|
356
|
+
assert len(events) == 1
|
|
357
|
+
assert events[0].actor_type == ActorType.SYSTEM.value
|
|
358
|
+
assert events[0].actor_id == "autonomy_service"
|
|
359
|
+
assert "approve_revision" in events[0].reason
|
|
360
|
+
|
|
361
|
+
# Check counter incremented
|
|
362
|
+
await db.refresh(goal)
|
|
363
|
+
assert goal.auto_approval_count == 1
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def test_record_auto_action_increments_counter(db):
|
|
367
|
+
"""Multiple auto-actions increment the counter correctly."""
|
|
368
|
+
goal = make_goal(auto_approval_count=5)
|
|
369
|
+
db.add(goal)
|
|
370
|
+
await db.flush()
|
|
371
|
+
|
|
372
|
+
ticket = make_ticket(goal, state="proposed")
|
|
373
|
+
db.add(ticket)
|
|
374
|
+
await db.flush()
|
|
375
|
+
|
|
376
|
+
service = AutonomyService(config=AutonomyConfig())
|
|
377
|
+
await service.record_auto_action(db, ticket, "approve_ticket", {"reason": "test"})
|
|
378
|
+
await db.flush()
|
|
379
|
+
await db.refresh(goal)
|
|
380
|
+
|
|
381
|
+
assert goal.auto_approval_count == 6
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ── State machine transition test ──
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_verifying_to_done_transition_allowed():
|
|
388
|
+
"""VERIFYING -> DONE should be allowed in the state machine."""
|
|
389
|
+
from app.state_machine import validate_transition
|
|
390
|
+
|
|
391
|
+
assert validate_transition(TicketState.VERIFYING, TicketState.DONE) is True
|