fix(tab-guard): name the company the other tab switched to (#2328)

The cross-tab guard dialog said "another tab switched the active company"
without saying to which one, so the two exits read as "your company" versus
"the new one". Resolve the observed company id against the memberships the
shell already ships to the client (switcher list plus the foreign-host
signpost list; no request at the moment the tab is told to stop) and say
"en annan flik har bytt till Demo AB" and "Ladda om som Demo AB". Unknown
ids keep the unnamed wording.

Founder re-confirmed the blocking two-exit design (WL-09) today after a
forensic pass on a real firing; this is copy only, no behaviour change.


Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-05 17:27:08 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 473b1fd2eb
commit b0b995c77c
6 changed files with 75 additions and 6 deletions
+23
View File
@@ -8,6 +8,7 @@ import {
markCompanySwitchInFlight,
markSelfSwitchTarget,
requestHasNextActionHeader,
resolveObservedCompanyName,
shouldBlockMutation,
} from '../tab-guard'
@@ -244,3 +245,25 @@ describe('guardBrowserWrite', () => {
expect(guardBrowserWrite()).toBe(true)
})
})
describe('resolveObservedCompanyName', () => {
const companies = [
{ company: { id: 'c-arcim', name: 'Arcim Technology AB' } },
{ company: { id: 'c-demo', name: 'Demo AB' } },
]
const foreign = [{ id: 'c-byra', name: 'Klientbolaget AB' }]
it('names a company from the switcher list', () => {
expect(resolveObservedCompanyName('c-demo', companies, foreign)).toBe('Demo AB')
})
it('names a company homed on another host from the signpost list', () => {
expect(resolveObservedCompanyName('c-byra', companies, foreign)).toBe('Klientbolaget AB')
})
it('is null for unknown ids and for no observation, so the dialog keeps its unnamed wording', () => {
expect(resolveObservedCompanyName('c-elsewhere', companies, foreign)).toBeNull()
expect(resolveObservedCompanyName(null, companies, foreign)).toBeNull()
expect(resolveObservedCompanyName(undefined, companies)).toBeNull()
})
})
+20
View File
@@ -161,6 +161,26 @@ export function isTabMismatch(
return observedCompanyId !== tabCompanyId
}
/**
* Name of the company another tab switched to, for the guard dialog. Looks
* through the memberships the shell already handed the client (the switcher
* list and the foreign-host signpost list), so no request is needed at the
* moment the tab is being told to stop. Null when the id is not among them
* (a company this login cannot see on this host): the dialog then falls back
* to its unnamed wording rather than guessing.
*/
export function resolveObservedCompanyName(
observedCompanyId: string | null | undefined,
companies: readonly { company: { id: string; name: string } }[],
foreignCompanies: readonly { id: string; name: string }[] = [],
): string | null {
if (!observedCompanyId) return null
const local = companies.find((entry) => entry.company.id === observedCompanyId)
if (local?.company.name) return local.company.name
const foreign = foreignCompanies.find((entry) => entry.id === observedCompanyId)
return foreign?.name || null
}
/**
* Whether a fetch about to leave this tab must be blocked. Guards mutating
* same-origin requests on two shapes: