Loopback dashboard tabs now share one one-shot stale-token recovery path across REST 401s, the PTY socket, the structured event socket, and the shared JSON-RPC gateway wrapper. The shared client exposes only an optional close-event interception hook; the dashboard remains responsible for deciding that loopback 4401 means reload. Constraint: Current main delegates the web gateway to apps/shared JsonRpcGatewayClient, and #54022 review requires a shared-client-compatible close-code hook plus direct ChatSidebar event-socket coverage. Rejected: Restore the dashboard's old direct WebSocket implementation | stale against the shared JSON-RPC client and would duplicate transport behavior. Confidence: high Scope-risk: moderate Directive: Keep stale-token policy dashboard-specific; the shared JSON-RPC client should expose close events without learning dashboard auth semantics. Tested: npm --workspace web test (21 files, 106 tests); focused stale-token tests (5 files, 14 tests); npm --workspace web run typecheck; npm --workspace @hermes/shared run lint; npm --workspace @hermes/shared run typecheck; focused web eslint; git diff --check. Not-tested: Manual browser smoke test across a real dashboard restart.
71 lines
2.0 KiB
TypeScript
71 lines
2.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
attemptDashboardTokenReloadOnce,
|
|
clearDashboardTokenReloadAttempt,
|
|
maybeReloadForLoopbackWsAuthFailure,
|
|
} from "./dashboard-auth-reload";
|
|
|
|
function makeStorage() {
|
|
const values = new Map<string, string>();
|
|
return {
|
|
getItem(key: string) {
|
|
return values.get(key) ?? null;
|
|
},
|
|
removeItem(key: string) {
|
|
values.delete(key);
|
|
},
|
|
setItem(key: string, value: string) {
|
|
values.set(key, value);
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("attemptDashboardTokenReloadOnce", () => {
|
|
it("reloads once and latches the attempt", () => {
|
|
const storage = makeStorage();
|
|
const reload = vi.fn();
|
|
|
|
expect(attemptDashboardTokenReloadOnce(storage, reload)).toBe(true);
|
|
expect(reload).toHaveBeenCalledTimes(1);
|
|
|
|
expect(attemptDashboardTokenReloadOnce(storage, reload)).toBe(false);
|
|
expect(reload).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("clears the latch when asked", () => {
|
|
const storage = makeStorage();
|
|
const reload = vi.fn();
|
|
|
|
expect(attemptDashboardTokenReloadOnce(storage, reload)).toBe(true);
|
|
clearDashboardTokenReloadAttempt(storage);
|
|
expect(attemptDashboardTokenReloadOnce(storage, reload)).toBe(true);
|
|
expect(reload).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("maybeReloadForLoopbackWsAuthFailure", () => {
|
|
it("reloads once for loopback 4401 closes", () => {
|
|
const storage = makeStorage();
|
|
const reload = vi.fn();
|
|
|
|
expect(
|
|
maybeReloadForLoopbackWsAuthFailure(4401, false, storage, reload),
|
|
).toBe(true);
|
|
expect(reload).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not reload in gated mode or for other close codes", () => {
|
|
const storage = makeStorage();
|
|
const reload = vi.fn();
|
|
|
|
expect(
|
|
maybeReloadForLoopbackWsAuthFailure(4401, true, storage, reload),
|
|
).toBe(false);
|
|
expect(
|
|
maybeReloadForLoopbackWsAuthFailure(4403, false, storage, reload),
|
|
).toBe(false);
|
|
expect(reload).not.toHaveBeenCalled();
|
|
});
|
|
});
|