Files
hermes-agent/web/src/lib/dashboard-auth-reload.test.ts
izumi0uu cab8673ea6 fix(dashboard): reload loopback tabs after stale session-token closes
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.
2026-08-04 15:26:37 +05:30

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();
});
});