BE-PR-010: update proposal tests for feat_task_id deprecation

- Accept tests now create Essentials (required by BE-PR-007)
- Accept tests assert feat_task_id is None (no longer written)
- Added _add_essential helper for test convenience
- Updated test docstrings with BE-PR-010 references
This commit is contained in:
zhi
2026-03-30 12:50:00 +00:00
parent 9cc561e5d5
commit b505fa7b35
2 changed files with 63 additions and 21 deletions

View File

@@ -3,11 +3,11 @@
Covers: Covers:
- CRUD: create, list, get, update - CRUD: create, list, get, update
- propose_code per-project incrementing - propose_code per-project incrementing
- accept → auto-generate feature story task + feat_task_id - accept → auto-generate story tasks from Essentials (feat_task_id deprecated per BE-PR-010)
- accept with non-open milestone → fail - accept with non-open milestone → fail
- reject → status change - reject → status change
- rejected → reopen back to open - rejected → reopen back to open
- feat_task_id cannot be set manually - feat_task_id cannot be set manually (deprecated, read-only)
- edit restrictions (only open proposals editable) - edit restrictions (only open proposals editable)
- permission checks for accept/reject/reopen - permission checks for accept/reject/reopen
- Legacy /proposes endpoint still works - Legacy /proposes endpoint still works
@@ -33,6 +33,27 @@ def _legacy_propose_url(project_id: int, propose_id: int | None = None) -> str:
return f"{base}/{propose_id}" if propose_id else base return f"{base}/{propose_id}" if propose_id else base
def _essential_url(project_id: int, proposal_id: int) -> str:
"""Essential CRUD URL under a Proposal."""
return f"/projects/{project_id}/proposals/{proposal_id}/essentials"
def _add_essential(client, project_id: int, proposal_id: int, headers, *,
title: str = "Default Essential", type: str = "feature",
description: str | None = None) -> dict:
"""Helper: create an Essential under a Proposal (required for accept)."""
body = {"title": title, "type": type}
if description:
body["description"] = description
resp = client.post(
_essential_url(project_id, proposal_id),
json=body,
headers=headers,
)
assert resp.status_code == 201, f"Failed to create essential: {resp.text}"
return resp.json()
# =========================================================================== # ===========================================================================
# CRUD # CRUD
# =========================================================================== # ===========================================================================
@@ -58,7 +79,7 @@ class TestProposalCRUD:
assert data["title"] == "New Feature Idea" assert data["title"] == "New Feature Idea"
assert data["status"] == "open" assert data["status"] == "open"
assert data["propose_code"].startswith("PROJ:P") assert data["propose_code"].startswith("PROJ:P")
assert data["feat_task_id"] is None assert data["feat_task_id"] is None # DEPRECATED (BE-PR-010): always None for new proposals
def test_list_proposals( def test_list_proposals(
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header, self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
@@ -171,6 +192,10 @@ class TestAccept:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# New accept flow requires at least one Essential (BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr),
title="Cool Feature", type="feature", description="Do something cool")
resp = client.post( resp = client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
json={"milestone_id": ms.id}, json={"milestone_id": ms.id},
@@ -179,19 +204,12 @@ class TestAccept:
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["status"] == "accepted" assert data["status"] == "accepted"
assert data["feat_task_id"] is not None # BE-PR-010: feat_task_id is no longer written by new accept flow
assert data["feat_task_id"] is None
# Verify the generated task exists # Tasks are tracked via generated_tasks (source_proposal_id)
from app.models.task import Task assert "generated_tasks" in data
task = db.query(Task).filter(Task.id == int(data["feat_task_id"])).first() assert len(data["generated_tasks"]) >= 1
assert task is not None
assert task.title == "Cool Feature"
assert task.description == "Do something cool"
assert task.task_type == "story"
assert task.task_subtype == "feature"
task_status = task.status.value if hasattr(task.status, "value") else task.status
assert task_status == "pending"
assert task.milestone_id == ms.id
def test_accept_non_open_milestone_fails( def test_accept_non_open_milestone_fails(
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header, self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
@@ -210,6 +228,9 @@ class TestAccept:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr))
resp = client.post( resp = client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
json={"milestone_id": ms.id}, json={"milestone_id": ms.id},
@@ -233,6 +254,9 @@ class TestAccept:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr))
# First accept # First accept
client.post( client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
@@ -248,9 +272,14 @@ class TestAccept:
) )
assert resp.status_code == 400 assert resp.status_code == 400
def test_accept_auto_fills_feat_task_id( def test_accept_does_not_write_feat_task_id(
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header, self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
): ):
"""BE-PR-010: new accept flow does NOT populate feat_task_id.
feat_task_id is deprecated; tasks are now tracked via
Task.source_proposal_id / source_essential_id.
"""
admin_role, mgr_role, dev_role = seed_roles_and_permissions admin_role, mgr_role, dev_role = seed_roles_and_permissions
mgr = make_user() mgr = make_user()
project = make_project(owner_id=mgr.id) project = make_project(owner_id=mgr.id)
@@ -263,17 +292,21 @@ class TestAccept:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr))
resp = client.post( resp = client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
json={"milestone_id": ms.id}, json={"milestone_id": ms.id},
headers=auth_header(mgr), headers=auth_header(mgr),
) )
data = resp.json() data = resp.json()
assert data["feat_task_id"] is not None # feat_task_id should remain None — deprecated field
assert data["feat_task_id"] is None
# Re-fetch to confirm persistence # Re-fetch to confirm persistence
get_resp = client.get(_proposal_url(project.id, proposal_id), headers=auth_header(mgr)) get_resp = client.get(_proposal_url(project.id, proposal_id), headers=auth_header(mgr))
assert get_resp.json()["feat_task_id"] == data["feat_task_id"] assert get_resp.json()["feat_task_id"] is None
def test_accept_no_permission_fails( def test_accept_no_permission_fails(
self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header, self, client, db, make_user, make_project, make_milestone, seed_roles_and_permissions, make_member, auth_header,
@@ -294,6 +327,9 @@ class TestAccept:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(dev_user))
# Dev tries to accept — should fail # Dev tries to accept — should fail
resp = client.post( resp = client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
@@ -346,6 +382,9 @@ class TestReject:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr))
# Accept first # Accept first
client.post( client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",
@@ -470,11 +509,11 @@ class TestReopen:
# =========================================================================== # ===========================================================================
# feat_task_id protection # feat_task_id protection (DEPRECATED per BE-PR-010)
# =========================================================================== # ===========================================================================
class TestFeatTaskIdProtection: class TestFeatTaskIdProtection:
"""feat_task_id is server-side only, cannot be set by client.""" """feat_task_id is deprecated and read-only; cannot be set by client."""
def test_update_cannot_set_feat_task_id( def test_update_cannot_set_feat_task_id(
self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header, self, client, db, make_user, make_project, seed_roles_and_permissions, make_member, auth_header,
@@ -496,7 +535,7 @@ class TestFeatTaskIdProtection:
headers=auth_header(user), headers=auth_header(user),
) )
assert resp.status_code == 200 assert resp.status_code == 200
# feat_task_id should still be None (server ignores it) # feat_task_id should still be None — deprecated, read-only (BE-PR-010)
assert resp.json()["feat_task_id"] is None assert resp.json()["feat_task_id"] is None
@@ -522,6 +561,9 @@ class TestEditRestrictions:
) )
proposal_id = create_resp.json()["id"] proposal_id = create_resp.json()["id"]
# Add Essential (required for accept per BE-PR-007)
_add_essential(client, project.id, proposal_id, auth_header(mgr))
# Accept # Accept
client.post( client.post(
_proposal_url(project.id, proposal_id) + "/accept", _proposal_url(project.id, proposal_id) + "/accept",