From b505fa7b35333afd331f8b790eb42efccecc719f Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 30 Mar 2026 12:50:00 +0000 Subject: [PATCH] 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 --- .../test_propose.cpython-312-pytest-9.0.2.pyc | Bin 54004 -> 53241 bytes tests/test_propose.py | 84 +++++++++++++----- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/tests/__pycache__/test_propose.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_propose.cpython-312-pytest-9.0.2.pyc index 80a624fa1e2651858f7d7a7af76420d9dadab09d..2c985cac8aa6c8cd4f748ee2fbc30659aa97001a 100644 GIT binary patch delta 10259 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 F{|`P7EMfov diff --git a/tests/test_propose.py b/tests/test_propose.py index d9d88c0..ca7b0c9 100644 --- a/tests/test_propose.py +++ b/tests/test_propose.py @@ -3,11 +3,11 @@ Covers: - CRUD: create, list, get, update - 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 - reject → status change - 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) - permission checks for accept/reject/reopen - 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 +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 # =========================================================================== @@ -58,7 +79,7 @@ class TestProposalCRUD: assert data["title"] == "New Feature Idea" assert data["status"] == "open" 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( 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"] + # 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( _proposal_url(project.id, proposal_id) + "/accept", json={"milestone_id": ms.id}, @@ -179,19 +204,12 @@ class TestAccept: assert resp.status_code == 200 data = resp.json() 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 - from app.models.task import Task - task = db.query(Task).filter(Task.id == int(data["feat_task_id"])).first() - 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 + # Tasks are tracked via generated_tasks (source_proposal_id) + assert "generated_tasks" in data + assert len(data["generated_tasks"]) >= 1 def test_accept_non_open_milestone_fails( 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"] + # Add Essential (required for accept per BE-PR-007) + _add_essential(client, project.id, proposal_id, auth_header(mgr)) + resp = client.post( _proposal_url(project.id, proposal_id) + "/accept", json={"milestone_id": ms.id}, @@ -233,6 +254,9 @@ class TestAccept: ) 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 client.post( _proposal_url(project.id, proposal_id) + "/accept", @@ -248,9 +272,14 @@ class TestAccept: ) 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, ): + """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 mgr = make_user() project = make_project(owner_id=mgr.id) @@ -263,17 +292,21 @@ class TestAccept: ) 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( _proposal_url(project.id, proposal_id) + "/accept", json={"milestone_id": ms.id}, headers=auth_header(mgr), ) 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 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( 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"] + # 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 resp = client.post( _proposal_url(project.id, proposal_id) + "/accept", @@ -346,6 +382,9 @@ class TestReject: ) 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 client.post( _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: - """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( 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), ) 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 @@ -522,6 +561,9 @@ class TestEditRestrictions: ) 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 client.post( _proposal_url(project.id, proposal_id) + "/accept",