fix(routes): preserve live cards on cross-profile 409 collisions
Gate follow-up for #7710. Three front-end handlers treated EVERY 409 as a domain-specific stale/expired signal, so the new session_profile_mismatch response would have: - messages.js: hidden a LIVE clarify card and mislabelled it expired - ui.js: retired a LIVE compression-recovery card with a false note - commands.js: rendered a false 'Compression failed' error state Each now routes through _sessionProfileMismatchFromError() first and leaves the card standing. Also captures the status in the import_cli test mock so a wrong status can no longer pass (the mock swallowed it into *_).
This commit is contained in:
1 parent
d698fb4772
commit
0378ed4e34
4 files changed
+38
-4
No files matched your search
@@ -991,6 +991,12 @@ async function resumeManualCompressionForSession(sid){
|
|||||||
// No active compression job or transient server error — not a real failure.
|
// No active compression job or transient server error — not a real failure.
|
||||||
// 404: route missed or session gone; 5xx: backend exception during status check.
|
// 404: route missed or session gone; 5xx: backend exception during status check.
|
||||||
if(e&&(!e.status||e.status===404||e.status>=500)) return;
|
if(e&&(!e.status||e.status===404||e.status>=500)) return;
|
||||||
|
// #7710: a cross-profile refusal now arrives as 409
|
||||||
|
// (``session_profile_mismatch``) where it used to be a 404 that hit the
|
||||||
|
// benign early-return above. It is not a compression failure, so do not
|
||||||
|
// render the error state or locally settle the compression UI.
|
||||||
|
if(e&&e.status===409&&typeof _sessionProfileMismatchFromError==='function'
|
||||||
|
&&_sessionProfileMismatchFromError(e)) return;
|
||||||
if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){
|
if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){
|
||||||
const visibleMessages=_manualCompressionVisibleMessages();
|
const visibleMessages=_manualCompressionVisibleMessages();
|
||||||
setCompressionUi({
|
setCompressionUi({
|
||||||
|
|||||||
@@ -8988,6 +8988,19 @@ async function respondClarify(response) {
|
|||||||
// not tear B down on A's late 409. The SSE/poll path will re-render the
|
// not tear B down on A's late 409. The SSE/poll path will re-render the
|
||||||
// next prompt's card from scratch via ``showClarifyCard`` either way.
|
// next prompt's card from scratch via ``showClarifyCard`` either way.
|
||||||
if (e && e.status === 409) {
|
if (e && e.status === 409) {
|
||||||
|
// #7710: a cross-profile refusal now also arrives as 409
|
||||||
|
// (``session_profile_mismatch``). The prompt is NOT expired — the write
|
||||||
|
// was refused because the session belongs to another profile. Treating
|
||||||
|
// it as expired would hide a live clarification card and mislabel the
|
||||||
|
// cause, so leave the card standing and report the real reason.
|
||||||
|
if (typeof _sessionProfileMismatchFromError === 'function'
|
||||||
|
&& _sessionProfileMismatchFromError(e)) {
|
||||||
|
_clarifySetControlsDisabled(false, false);
|
||||||
|
if (typeof setStatus === "function") {
|
||||||
|
setStatus("Clarify: session belongs to a different profile");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_clarifyId === clarifyId) {
|
if (_clarifyId === clarifyId) {
|
||||||
// Same card still showing — dismiss it and rescue the typed draft.
|
// Same card still showing — dismiss it and rescue the typed draft.
|
||||||
// Order matters: ``_stashClarifyDraft`` (called from
|
// Order matters: ``_stashClarifyDraft`` (called from
|
||||||
|
|||||||
@@ -508,6 +508,16 @@ async function startCompressionRecovery(btn){
|
|||||||
const composer=$('msg');
|
const composer=$('msg');
|
||||||
if(composer&&typeof composer.focus==='function') composer.focus();
|
if(composer&&typeof composer.focus==='function') composer.focus();
|
||||||
}catch(e){
|
}catch(e){
|
||||||
|
// #7710: a cross-profile refusal now also arrives as 409
|
||||||
|
// (``session_profile_mismatch``). That is NOT a stale recovery action —
|
||||||
|
// the card is still valid, the request was simply refused because the
|
||||||
|
// session belongs to another profile. Retiring it would hide a live card
|
||||||
|
// and show a false "conversation already moved on" note.
|
||||||
|
if(e&&e.status===409&&typeof _sessionProfileMismatchFromError==='function'
|
||||||
|
&&_sessionProfileMismatchFromError(e)){
|
||||||
|
if(typeof setStatus==='function') setStatus('Session belongs to a different profile');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// A 409 means this session no longer has an active recovery action (the
|
// A 409 means this session no longer has an active recovery action (the
|
||||||
// session already moved on — e.g. a substantive prompt cleared it). The
|
// session already moved on — e.g. a substantive prompt cleared it). The
|
||||||
// persisted card in the transcript is stale, so retire it and show a neutral
|
// persisted card in the transcript is stale, so retire it and show a neutral
|
||||||
|
|||||||
@@ -44,10 +44,14 @@ def _capture(monkeypatch):
|
|||||||
# #7710: the real ``j`` signature is ``j(handler, payload, status=200,
|
# #7710: the real ``j`` signature is ``j(handler, payload, status=200,
|
||||||
# extra_headers=None, *, pretty=True)``; the mock has to accept the
|
# extra_headers=None, *, pretty=True)``; the mock has to accept the
|
||||||
# status keyword so the helper's 409 ``session_profile_mismatch``
|
# status keyword so the helper's 409 ``session_profile_mismatch``
|
||||||
# payload can be captured here.
|
# payload can be captured here. Capture the STATUS too — swallowing it
|
||||||
monkeypatch.setattr(
|
# into ``*_`` would let a wrong status (200/404) pass this test.
|
||||||
routes, "j", lambda h, o, *_, **__: (cap.__setitem__("ok", o), True)[1]
|
def _fake_j(h, o, status=200, *_, **__):
|
||||||
)
|
cap["ok"] = o
|
||||||
|
cap["ok_status"] = status
|
||||||
|
return True
|
||||||
|
|
||||||
|
monkeypatch.setattr(routes, "j", _fake_j)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
routes,
|
routes,
|
||||||
"bad",
|
"bad",
|
||||||
@@ -98,6 +102,7 @@ def test_import_cli_existing_foreign_profile_unqualified_request_404(monkeypatch
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert "ok" in cap, f"expected 409 session_profile_mismatch, got {cap}"
|
assert "ok" in cap, f"expected 409 session_profile_mismatch, got {cap}"
|
||||||
|
assert cap["ok_status"] == 409, f"expected status 409, got {cap.get('ok_status')}"
|
||||||
assert cap["ok"] == {
|
assert cap["ok"] == {
|
||||||
"error": "Session belongs to a different profile",
|
"error": "Session belongs to a different profile",
|
||||||
"code": "session_profile_mismatch",
|
"code": "session_profile_mismatch",
|
||||||
|
|||||||
Reference in new issue
Block a user