fix(prompter): batch confirm ignores a vestigial top-level project slug (#380)

* fix(prompter): batch confirm ignores a vestigial top-level project slug

The MegaTask confirm-batch 400'd with "Invalid project_id UUID: roboco-api".
The intake agent authors each draft's project as the repo slug it read, and the
panel only nulls the top-level project_id when the CEO toggles that draft's
picker — so an untouched draft carried the slug through to create_task_from_draft,
whose eager _resolve_uuid_field(project_id) raised on the non-UUID and rejected
the whole batch (guard-release from the prior fix surfaced it as a clean 400
instead of a wedged 500).

A batch root-subtask targets its repos via the the_work per-cell map (the panel
fills those with real project UUIDs); the top-level project_id/product_id is
vestigial for it. Strip it from a sub-draft when its cell map carries the real
target — a legacy no-the_work draft keeps its panel-filled top-level UUID, and
scope validation (which already runs off the cell map) is unchanged.

Adds a repro test with two cell-map drafts that also carry leftover top-level
slugs (roboco-api / roboco-panel); they now confirm instead of 400-ing.

* fix(tests): conventions PR integration test honors the #375 workspace-scope guard

#375 added a containment guard to open_conventions_pr (workspace_path must sit
under {workspaces_root}/{slug}); the unit test was updated but this integration
test still seeded a bare tmp_path/repo, so open_conventions_pr returned None and
test_open_conventions_pr_commits_locally_without_remote failed on master. Anchor
workspaces_root at the test dir and place the repo under the project's slug.

* refactor(prompter): extract batch sub-draft sanitize (xenon rank B)

The inline vestigial-target strip pushed _build_confirm_batch to cyclomatic
rank C (over the --max-absolute B gate). Move the assigned_to + top-level
project/product stripping into a pure _batch_subtask_draft helper; behavior is
unchanged, _build_confirm_batch drops back under the limit.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-10 03:59:13 +02:00
committed by GitHub
co-authored by Renn F
parent 2297d448f3
commit f601e32788
2 changed files with 78 additions and 6 deletions
@@ -73,6 +73,59 @@ async def test_confirm_live_batch_panel_the_work_shape(db_session: Any) -> None:
assert len(ids) == len(drafts)
@pytest.mark.asyncio
async def test_confirm_live_batch_ignores_vestigial_top_level_slug(
db_session: Any,
) -> None:
"""A batch draft whose the_work cell-map carries the real project UUID but
that also still has the intake agent's top-level project_id SLUG (the panel
only nulls it on a manual picker toggle) must NOT 400 the whole batch on the
UUID parse — the cell map is authoritative. Repro for the live 400
"Invalid project_id UUID: roboco-api"."""
project1, ceo_id = await _seed_project_and_ceo(db_session)
project2 = await _seed_second_project(db_session, ceo_id)
service = get_prompter_service(db=db_session)
drafts: list[dict[str, Any]] = [
{
"title": "A: backend work",
"acceptance_criteria": ["a"],
"project_id": "roboco-api", # agent's slug, panel left it in place
"the_work": [
{
"team": "backend",
"summary": "s",
"items": ["u"],
"project_id": str(project1),
},
],
},
{
"title": "B: frontend work",
"acceptance_criteria": ["b"],
"project_id": "roboco-panel", # slug on the other draft too
"the_work": [
{
"team": "frontend",
"summary": "s",
"items": ["u"],
"project_id": str(project2),
},
],
},
]
with patch("roboco.services.prompter.redis.from_url", return_value=_FakeRedis()):
result = await service.confirm_live_batch(
"Slug batch",
drafts,
ceo_id,
project_ids=[project1, project2],
route="main_pm",
session_id=f"sess-slug-{uuid4().hex[:8]}",
)
assert len(result["root_subtask_ids"]) == len(drafts)
@pytest.mark.asyncio
async def test_confirm_live_batch_panel_multicell_root_subtask(db_session: Any) -> None:
"""A batch draft that targets TWO cells (backend+frontend, one repo each) →