feat(webhook): pull reject reason from latest comment
handle_verdict(rejected): the reason is now pulled from the issue latest Plane comment (_latest_comment_reason: GET comments, newest by created_at, HTML stripped) instead of a fixed stub. Slava writes the reason in a comment before flipping the status to Rejected. Falls back to a fixed note when there is no comment / the API call fails. tests: add test_status_only_verdict.py (test_inreview_comment_does_not_revert [bug 3 root], test_any_comment_no_pipeline_action, test_approved_status_advances_without_inprogress_reset, test_rejected_status_pulls_reason_from_comment) and test_inprogress_from_needs_input_relaunches_analyst in test_status_trigger.py. Rewrote the comment-based tests (test_verdict_status, test_plane_approved/ rejected in test_webhooks) under the status-only model: comments are no-ops, verdicts come from status changes.
This commit is contained in:
@@ -95,27 +95,32 @@ def test_plane_webhook_generates_sequential_ids(mock_docs, mock_branch):
|
||||
assert ids[1] == "ET-002"
|
||||
|
||||
|
||||
APPROVED_STATE = "a519a341-dada-4a91-8910-7604f82b79c5"
|
||||
REJECTED_STATE = "ba958f3c-5db5-461d-8f82-89425e413b97"
|
||||
|
||||
|
||||
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
||||
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
||||
@patch("src.webhooks.plane.launcher")
|
||||
def test_plane_approved_advances_stage(mock_launcher, mock_docs, mock_branch, tmp_path, monkeypatch):
|
||||
"""Comment :approved: at stage=analysis → advance to architecture."""
|
||||
"""Status-only model: Approved STATUS at stage=analysis -> advance to
|
||||
architecture. A comment never triggers this.
|
||||
"""
|
||||
# Patch repos_dir for QG check
|
||||
monkeypatch.setattr("src.qg.checks.settings.repos_dir", str(tmp_path))
|
||||
|
||||
# Create task first
|
||||
client.post("/webhook/plane", json={
|
||||
"event": "work_item.created",
|
||||
"data": {"id": "adv-001", "name": "Advance test", "project": "proj-1"}
|
||||
})
|
||||
|
||||
# Get the task to find work_item_id
|
||||
# Seed an analysis task directly (creation no longer makes a task post-PR#11).
|
||||
conn = get_db()
|
||||
task = conn.execute("SELECT * FROM tasks WHERE plane_id = 'adv-001'").fetchone()
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, plane_issue_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
("adv-001", "ET-001", "enduro-trails", "feature/ET-001-x", "analysis", "adv-001"),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
work_item_id = task["work_item_id"]
|
||||
work_item_id = "ET-001"
|
||||
|
||||
# Create required analysis files
|
||||
# Create required analysis files so the analysis QG passes.
|
||||
wi_dir = tmp_path / "enduro-trails" / "docs" / "work-items" / work_item_id
|
||||
wi_dir.mkdir(parents=True)
|
||||
(wi_dir / "01-brd.md").write_text("# BRD")
|
||||
@@ -123,16 +128,15 @@ def test_plane_approved_advances_stage(mock_launcher, mock_docs, mock_branch, tm
|
||||
(wi_dir / "03-acceptance-criteria.md").write_text("# AC")
|
||||
(wi_dir / "04-test-plan.yaml").write_text("tests: []")
|
||||
|
||||
# Mock launcher
|
||||
mock_launcher.launch.return_value = 1
|
||||
|
||||
# Send approved comment
|
||||
# Send Approved STATUS change.
|
||||
resp = client.post("/webhook/plane", json={
|
||||
"event": "comment.created",
|
||||
"event": "issue", "action": "updated",
|
||||
"data": {
|
||||
"work_item_id": "adv-001",
|
||||
"comment": "Looks good :approved:"
|
||||
}
|
||||
"id": "adv-001", "name": "Advance test", "project": "proj-1",
|
||||
"state": {"id": APPROVED_STATE, "name": "Approved", "group": "completed"},
|
||||
},
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
@@ -143,29 +147,39 @@ def test_plane_approved_advances_stage(mock_launcher, mock_docs, mock_branch, tm
|
||||
assert task["stage"] == "architecture"
|
||||
|
||||
|
||||
@patch("src.webhooks.plane.httpx.get")
|
||||
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
||||
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
||||
def test_plane_rejected_rolls_back(mock_docs, mock_branch):
|
||||
"""Comment :rejected: rolls back stage."""
|
||||
# Create task
|
||||
client.post("/webhook/plane", json={
|
||||
"event": "work_item.created",
|
||||
"data": {"id": "rej-001", "name": "Reject test", "project": "proj-1"}
|
||||
})
|
||||
def test_plane_rejected_rolls_back(mock_docs, mock_branch, mock_get):
|
||||
"""Status-only model: Rejected STATUS rolls back stage. A comment never
|
||||
triggers this; the reason is pulled from the latest comment.
|
||||
"""
|
||||
class _R:
|
||||
status_code = 200
|
||||
@staticmethod
|
||||
def json():
|
||||
return {"results": [
|
||||
{"comment_stripped": "missing ADR", "created_at": "2026-06-03T10:00:00Z"}
|
||||
]}
|
||||
mock_get.return_value = _R()
|
||||
|
||||
# Manually set stage to architecture
|
||||
# Seed an architecture task directly.
|
||||
conn = get_db()
|
||||
conn.execute("UPDATE tasks SET stage = 'architecture' WHERE plane_id = 'rej-001'")
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, plane_issue_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
("rej-001", "ET-002", "enduro-trails", "feature/ET-002-x", "architecture", "rej-001"),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Send rejected comment
|
||||
# Send Rejected STATUS change.
|
||||
resp = client.post("/webhook/plane", json={
|
||||
"event": "comment.created",
|
||||
"event": "issue", "action": "updated",
|
||||
"data": {
|
||||
"work_item_id": "rej-001",
|
||||
"comment": "Not ready :rejected:"
|
||||
}
|
||||
"id": "rej-001", "name": "Reject test", "project": "proj-1",
|
||||
"state": {"id": REJECTED_STATE, "name": "Rejected", "group": "cancelled"},
|
||||
},
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
Reference in New Issue
Block a user