fec51bdbb6
A new entry can be proposed into a specific project; it lands in that project's
content repo and shows under that project's proposals. A non-default project is
no longer read-only.
- api.py: POST /api/projects/{pid}/rfcs/propose (propose body extracted into a
project-parameterized helper; unscoped /api/rfcs/propose kept as default
compat). Slug uniqueness, idea-PR reservation, landing state, and the
proposed_use_cases row scoped to the target project. GET
/api/projects/{pid}/proposals.
- cache.py: refresh_meta_pulls loops every project's content_repo, stamping
cached_prs.project_id; projects.content_repo(pid) helper.
- frontend: proposeRFC(projectId,…)/listProposals(projectId); ProposeModal
takes projectId; App resolves current project from the /p/<id>/ URL; Catalog
lists that project's proposals.
- tests: test_project_scoped_propose.py (lands scoped + gated 404). 447 backend
+ 11 Vitest green; clean build.
Known limitation: branch/PR/graduation edit flows + default-id re-stamp not yet
scoped (next slice). Per docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.7 KiB
Python
65 lines
2.7 KiB
Python
"""§22.4 (Plan B write): proposing a new entry into a *specific* project lands
|
|
it in that project's content repo and surfaces under that project's proposals,
|
|
isolated from the default project."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from test_propose_vertical import ( # noqa: F401
|
|
app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as,
|
|
)
|
|
|
|
|
|
def _register_ecomm(fake):
|
|
from app import db
|
|
db.conn().execute(
|
|
"INSERT OR IGNORE INTO projects (id, name, type, content_repo, visibility, initial_state) "
|
|
"VALUES ('ecomm', 'Ecomm', 'document', 'ecomm-content', 'public', 'super-draft')"
|
|
)
|
|
fake._seed_repo("wiggleverse", "ecomm-content")
|
|
|
|
|
|
def test_propose_into_second_project_lands_scoped(app_with_fake_gitea):
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
_register_ecomm(fake)
|
|
provision_user_row(user_id=3, login="alice", role="contributor")
|
|
sign_in_as(client, user_id=3, gitea_login="alice", display_name="Alice",
|
|
role="contributor", email="alice@test")
|
|
r = client.post("/api/projects/ecomm/rfcs/propose", json={
|
|
"title": "Cart", "slug": "cart", "pitch": "why a cart", "tags": [],
|
|
})
|
|
assert r.status_code == 200, r.text
|
|
|
|
# The idea PR shows under ecomm's proposals, not the default's.
|
|
e = {i["slug"] for i in client.get("/api/projects/ecomm/proposals").json()["items"]}
|
|
d = {i["slug"] for i in client.get("/api/projects/default/proposals").json()["items"]}
|
|
assert "cart" in e
|
|
assert "cart" not in d
|
|
|
|
# It landed in ecomm's content repo, not the default 'meta' repo.
|
|
assert ("wiggleverse", "ecomm-content") in {
|
|
(o, rp) for (o, rp) in fake.branches if rp == "ecomm-content"
|
|
}
|
|
assert any(
|
|
br.startswith("propose/cart")
|
|
for br in fake.branches.get(("wiggleverse", "ecomm-content"), {})
|
|
)
|
|
|
|
|
|
def test_propose_into_gated_project_404s_for_non_member(app_with_fake_gitea):
|
|
app, _ = app_with_fake_gitea
|
|
from app import db
|
|
with TestClient(app) as client:
|
|
db.conn().execute(
|
|
"INSERT OR IGNORE INTO projects (id, name, type, content_repo, visibility, initial_state) "
|
|
"VALUES ('secret', 'Secret', 'document', 'secret-content', 'gated', 'super-draft')"
|
|
)
|
|
provision_user_row(user_id=4, login="bob", role="contributor")
|
|
sign_in_as(client, user_id=4, gitea_login="bob", display_name="Bob",
|
|
role="contributor", email="bob@test")
|
|
r = client.post("/api/projects/secret/rfcs/propose", json={
|
|
"title": "X", "slug": "x", "pitch": "p", "tags": [],
|
|
})
|
|
assert r.status_code == 404
|