fix: guard slot-less set_project; repoint stale boundary tests (#5201) - #5216
fix: guard slot-less set_project; repoint stale boundary tests (#5201)#5216bolichen97 wants to merge 1 commit into
Conversation
Main went red after #3543 (37089b1) moved set_project out of _DASHBOARD_ONLY_DIRECTIVES: test_driver_session_directives.py's TestChannelApplierBoundary still pinned the pre-#3543 invariant, and the failure exposed a real defect — a slot-less channel caller (TurnDriver always passes slot=None) passes the new user-surface gate and crashes at slot.project with AttributeError, swallowed by the generic handler into an opaque 'Error applying set_project: ...' audited as 'error'. Fix: - _set_project now refuses a slot-less caller up front, before ANY mutation (the clear path writes slot state too), with an explicit refusal. It raises _DirectiveDenied so the wrapper audits 'denied': a missing slot is a session-shape condition — the class the _DirectiveDenied contract names ('an unsupported session type') and the same outcome this caller shape audited before #3543. - Remove set_project from the two stale dashboard-only boundary tests (kept suggest_followup/ask_question pinned unchanged) and note the #3543 narrowing so it is not 'restored' later. - New tests pin the post-#3543 semantics: channel key + slot applies the project; slot=None (set and clear paths) returns the clean refusal, audits 'denied', and mutates nothing. Verified: both previously-failing tests now pass; full backend suite 61777 passed; isort/flake8/mypy/black clean.
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Minimal fail-closed guard at the layer that owns the slot, with audit continuity ( [DESIGN-REVIEWED] a58af62 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified against the repo: the guard sits in First-Principles-Verdict: PASS Guards the exact slot-dereference #3543 exposed, at the mechanism, with the pre-#3543 What this change shipsIntent: unbreak red
No unfixed siblings: Subtractions
[FIRST-PRINCIPLES-REVIEWED] a58af62 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe discovery pass found no candidates. I've independently verified the changed code: the No findings. [OPUS-REVIEWED] a58af62 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
🤖 Kiro Crew Auto-Pipeline [operator: bolichen97#bb3ad1ca] Superseded by #5212 (merged 63705e6 while this PR's first CI run was in flight), which fixes the identical defect for the parallel issue #5208 with equivalent semantics: slot-less refusal before any mutation, SEL outcome The small deltas this branch carried beyond #5212 (three stale-comment fixes and two extra test pins, all validated by a dual-model review) are filed as #5222 with this branch referenced as a ready-made implementation. Closing rather than rebasing: re-landing a duplicate guard would add a second refusal site for the same condition. |
Closes #5201
What broke
mainis RED at 410972a: every open PR's merge-refBackend Testsshard fails on two tests intest/test_driver_session_directives.py::TestChannelApplierBoundary, regardless of the PR's own diff.PR #3543 (37089b1) deliberately moved
set_projectout of_DASHBOARD_ONLY_DIRECTIVES(any user-facing surface may retarget its own slot; the gate became the positive_has_user_surfacepredicate) and updatedtest/test_mcp_core_set_project.pyaccordingly — but never touchedtest_driver_session_directives.py, which still pinned the pre-#3543 invariant withset_projectparametrized in the dashboard-only set.The failures also exposed a real defect, not just a stale expectation: the channel
TurnDriverconsumer always passesslot=None(messaging/dispatch.py:208). Post-#3543 such a caller passes the user-surface gate (its key IS a channel key) and crashes atslot.project = rpwithAttributeError: 'NoneType' object has no attribute 'project', swallowed by the generic handler into an opaqueError applying set_project: ...audited aserror. The clear path (slot.project = "") had the same hole.The fix (minimal, main-red unblock scope)
_set_project, before ANY mutation (the clear path writes slot state too — fail closed): an explicit refusal naming the condition (... this slot-less channel turn holds no slot. Nothing was changed.).set_projectremoved from the two dashboard-only boundary tests (suggest_followup/ask_questionstay pinned unchanged — that messaging TurnDriver never consumes session-directive markers (set_project/monitor trio silently no-op on standalone transports) #4540 invariant is not weakened); docstrings now record that feat: allow set_project on non-dashboard surfaces (Telegram, Slack) #3543 removedset_projectdeliberately, so it is not "restored" later.slot=None(set-path AND clear, no-tab AND open-tab) → the clean refusal string, auditeddenied, zero pushes, noNoneTypeleak.Why
raise _DirectiveDenied(auditeddenied) and not a plainError:return (auditederror)The task allowed either and asked for the reasoning. The module's own audit contract decides it:
_DirectiveDenied's docstring names "an unsupported session type" as its case. A slot-less caller is a session/transport-shape condition (TurnDriver holds no slot), not an argument-validation failure likenot a directory(which stays a plain return)._DirectiveDenied, andtest_not_supported_paths_audit_denied_never_successpinsoutcome=deniedfor exactly that class.set_project) was refused by the dashboard-only gate and auditeddenied. Keepingdeniedmeans the same refused attempt does not silently reclassify toerroracross feat: allow set_project on non-dashboard surfaces (Telegram, Slack) #3543.Not done here (scope discipline)
test/test_mcp_core_set_project.pyuntouched — feat: allow set_project on non-dashboard surfaces (Telegram, Slack) #3543 updated it deliberately; it remains the authority on the new intent and passes as-is.session_directive_apply.py, PR refactor: extract shared session-directive forgery-gate predicate (#5122) #5167 editssession_directive.py. This guard is a small localized hunk; landing this first unblocks their merge-ref CI too.Verified
slot.project = rpin the traceback).test_driver_session_directives.py+test_mcp_core_set_project.py+test_session_directive.py= 100 passed._pending_reset_history_keyassertion, removal of needlessis_sensitive_pathmocks).