fix(boot): keep active-profile recovery on one redirect path (#5001)
This commit is contained in:
1 parent
44dacc5cf5
commit
4d727c2e24
3 files changed
+105
-18
No files matched your search
+24
-1
@@ -2261,7 +2261,30 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
||||
api(_checkUrl,{method:_testUpdates?'GET':'POST',body:_testUpdates?undefined:JSON.stringify({force:false})}).then(d=>{if(!_testUpdates)sessionStorage.setItem('hermes-update-checked','1');if((d.webui&&d.webui.behind>0)||(d.agent&&d.agent.behind>0))_showUpdateBanner(d);}).catch(()=>{});
|
||||
}
|
||||
async function _resolveActiveProfileBootstrapState({
|
||||
loadActiveProfile = () => api('/api/profile/active'),
|
||||
loadActiveProfile = async () => {
|
||||
const rel = 'api/profile/active';
|
||||
const url = new URL(rel, document.baseURI || location.href);
|
||||
const res = await fetch(url.href, {
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
if (res.status === 401) return undefined;
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let message = text;
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
message = j.error || j.message || text;
|
||||
} catch (_) {}
|
||||
const err = new Error(message);
|
||||
err.status = res.status;
|
||||
err.statusText = res.statusText;
|
||||
err.body = text;
|
||||
throw err;
|
||||
}
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
return ct.includes('application/json') ? await res.json() : await res.text();
|
||||
},
|
||||
getNextUrl = () => window.location.pathname + window.location.search,
|
||||
redirectToLogin = (nextUrl) => {
|
||||
window.location.href = 'login?next=' + encodeURIComponent(nextUrl);
|
||||
|
||||
@@ -91,6 +91,30 @@ function makeAttempt(attempt) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeFetchResponse(attempt) {
|
||||
const status = attempt.status === undefined ? 200 : attempt.status;
|
||||
const payload = Object.prototype.hasOwnProperty.call(attempt, 'payload')
|
||||
? attempt.payload
|
||||
: {name: 'default', is_default: true};
|
||||
const textBody = Object.prototype.hasOwnProperty.call(attempt, 'text')
|
||||
? attempt.text
|
||||
: JSON.stringify(payload);
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: attempt.statusText || '',
|
||||
headers: {
|
||||
get(name) {
|
||||
return String(name).toLowerCase() === 'content-type'
|
||||
? attempt.contentType || 'application/json'
|
||||
: '';
|
||||
},
|
||||
},
|
||||
json: async () => payload,
|
||||
text: async () => textBody,
|
||||
};
|
||||
}
|
||||
|
||||
eval(extractFunction(bootSrc, '_resolveActiveProfileBootstrapState'));
|
||||
const bootActiveProfileBlock = extractBlock(
|
||||
bootSrc,
|
||||
@@ -120,15 +144,52 @@ return (async () => {
|
||||
const storageHistory = [];
|
||||
|
||||
for (const attempt of attempts) {
|
||||
const state = await _resolveActiveProfileBootstrapState({
|
||||
loadActiveProfile: makeAttempt(attempt),
|
||||
markerStorage: storage,
|
||||
markerKey,
|
||||
getNextUrl: () => attempt.nextUrl || '/',
|
||||
redirectToLogin: (nextUrl) => {
|
||||
redirectUrls.push(`login?next=${encodeURIComponent(nextUrl)}`);
|
||||
},
|
||||
});
|
||||
const nextUrl = attempt.nextUrl || '/';
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalWindow = globalThis.window;
|
||||
const originalLocation = globalThis.location;
|
||||
const originalDocument = globalThis.document;
|
||||
let state;
|
||||
|
||||
try {
|
||||
if (scenario.useDefaultLoader) {
|
||||
const location = {
|
||||
href: `http://example.test${nextUrl}`,
|
||||
pathname: nextUrl,
|
||||
search: '',
|
||||
};
|
||||
globalThis.fetch = async () => makeFetchResponse(attempt);
|
||||
globalThis.location = location;
|
||||
globalThis.document = {baseURI: 'http://example.test/'};
|
||||
globalThis.window = {location};
|
||||
state = await _resolveActiveProfileBootstrapState({
|
||||
markerStorage: storage,
|
||||
markerKey,
|
||||
getNextUrl: () => nextUrl,
|
||||
redirectToLogin: (value) => {
|
||||
const href = `login?next=${encodeURIComponent(value)}`;
|
||||
redirectUrls.push(href);
|
||||
location.href = href;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
state = await _resolveActiveProfileBootstrapState({
|
||||
loadActiveProfile: makeAttempt(attempt),
|
||||
markerStorage: storage,
|
||||
markerKey,
|
||||
getNextUrl: () => nextUrl,
|
||||
redirectToLogin: (value) => {
|
||||
redirectUrls.push(`login?next=${encodeURIComponent(value)}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
globalThis.window = originalWindow;
|
||||
globalThis.location = originalLocation;
|
||||
globalThis.document = originalDocument;
|
||||
}
|
||||
|
||||
const bootState = {};
|
||||
let applyBotNameCalls = 0;
|
||||
const bootResult = await runBootActiveProfileBlock(
|
||||
@@ -194,9 +255,10 @@ def test_active_profile_boot_recovery_is_one_shot_and_bounded(driver_path):
|
||||
driver_path,
|
||||
{
|
||||
"markerKey": "test-5001-active-profile-recovery",
|
||||
"useDefaultLoader": True,
|
||||
"attempts": [
|
||||
{"type": "undefined", "nextUrl": "/"},
|
||||
{"type": "undefined", "nextUrl": "/"},
|
||||
{"status": 401, "payload": {"error": "Authentication required"}, "nextUrl": "/"},
|
||||
{"status": 401, "payload": {"error": "Authentication required"}, "nextUrl": "/"},
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -255,8 +317,9 @@ def test_active_profile_boot_non_401_errors_fallback_without_redirect(driver_pat
|
||||
driver_path,
|
||||
{
|
||||
"markerKey": "test-5001-active-profile-recovery-non-401",
|
||||
"useDefaultLoader": True,
|
||||
"attempts": [
|
||||
{"type": "error", "status": 500, "nextUrl": "/"},
|
||||
{"status": 500, "payload": {"error": "server failure"}, "nextUrl": "/"},
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -278,8 +341,9 @@ def test_active_profile_boot_invalid_payload_falls_back_without_redirect(driver_
|
||||
driver_path,
|
||||
{
|
||||
"markerKey": "test-5001-active-profile-recovery-invalid-payload",
|
||||
"useDefaultLoader": True,
|
||||
"attempts": [
|
||||
{"type": "return", "value": {"is_default": False}, "nextUrl": "/"},
|
||||
{"status": 200, "payload": {"is_default": False}, "nextUrl": "/"},
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -301,10 +365,10 @@ def test_active_profile_success_path_applies_boot_state_and_continues(driver_pat
|
||||
driver_path,
|
||||
{
|
||||
"markerKey": "test-5001-active-profile-recovery-success",
|
||||
"simulateBootContinue": True,
|
||||
"useDefaultLoader": True,
|
||||
"attempts": [
|
||||
{
|
||||
"type": "success",
|
||||
"status": 200,
|
||||
"payload": {"name": "team-profile", "is_default": False},
|
||||
}
|
||||
],
|
||||
|
||||
@@ -53,8 +53,8 @@ class TestComposerPlaceholderProfile:
|
||||
def test_boot_applies_placeholder_after_active_profile_loads(self):
|
||||
"""Boot must set the composer placeholder after S.activeProfile is known."""
|
||||
src = _src("boot.js")
|
||||
fetch_idx = src.find("api('/api/profile/active')")
|
||||
assert fetch_idx >= 0, "boot.js should fetch the active profile during boot"
|
||||
fetch_idx = src.find("const activeProfileState = await _resolveActiveProfileBootstrapState();")
|
||||
assert fetch_idx >= 0, "boot.js should resolve the active profile during boot"
|
||||
label_idx = src.find("const profileLabel=$('profileChipLabel');", fetch_idx)
|
||||
assert label_idx >= 0, "profile chip sync should follow active profile fetch"
|
||||
assert "applyBotName();" in src[fetch_idx:label_idx], (
|
||||
|
||||
Reference in new issue
Block a user