FN-6116: clear terminal event buffers after subscriber delivery
Prevent stale terminal connection metadata from replaying after it has already been delivered. - only retain scrollback and connected payloads when no listeners are registered yet - clear buffered terminal events after live subscribers receive them - add regression coverage for scrollback and connected replay behavior Files changed: .../app/hooks/__tests__/useTerminal.test.ts | 50 ++++++++++++++++++++++ packages/dashboard/app/hooks/useTerminal.ts | 23 ++++++---- 2 files changed, 65 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-6116 Fusion-Task-Lineage: 5876fe24-2e90-4ef1-9e8b-5abce40555b4
This commit is contained in:
@@ -402,6 +402,56 @@ describe("useTerminal", () => {
|
||||
expect(onData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("clears scrollback buffer after direct delivery to subscribers", () => {
|
||||
const { result } = renderHook(() => useTerminal("test-session-123"));
|
||||
const firstSubscription = vi.fn();
|
||||
const secondSubscription = vi.fn();
|
||||
|
||||
let unsubscribeFirst: (() => void) | undefined;
|
||||
act(() => {
|
||||
unsubscribeFirst = result.current.onScrollback(firstSubscription);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
MockWebSocket.instances[0].emitMessage({ type: "scrollback", data: "initial prompt$ " });
|
||||
});
|
||||
|
||||
expect(firstSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(firstSubscription).toHaveBeenCalledWith("initial prompt$ ");
|
||||
|
||||
act(() => {
|
||||
unsubscribeFirst?.();
|
||||
result.current.onScrollback(secondSubscription);
|
||||
});
|
||||
|
||||
expect(secondSubscription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears connected buffer after direct delivery to subscribers", () => {
|
||||
const { result } = renderHook(() => useTerminal("test-session-123"));
|
||||
const firstSubscription = vi.fn();
|
||||
const secondSubscription = vi.fn();
|
||||
|
||||
let unsubscribeFirst: (() => void) | undefined;
|
||||
act(() => {
|
||||
unsubscribeFirst = result.current.onConnect(firstSubscription);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
MockWebSocket.instances[0].emitMessage({ type: "connected", shell: "/bin/bash", cwd: "/project" });
|
||||
});
|
||||
|
||||
expect(firstSubscription).toHaveBeenCalledTimes(1);
|
||||
expect(firstSubscription).toHaveBeenCalledWith({ shell: "/bin/bash", cwd: "/project" });
|
||||
|
||||
act(() => {
|
||||
unsubscribeFirst?.();
|
||||
result.current.onConnect(secondSubscription);
|
||||
});
|
||||
|
||||
expect(secondSubscription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears buffered scrollback after first replay to prevent duplicate delivery", () => {
|
||||
const { result } = renderHook(() => useTerminal("test-session-123"));
|
||||
|
||||
|
||||
@@ -353,18 +353,25 @@ export function useTerminal(sessionId: string | null, projectId?: string): UseTe
|
||||
break;
|
||||
case "scrollback":
|
||||
if (msg.data) {
|
||||
// Buffer scrollback for late subscribers
|
||||
buffer.scrollback = msg.data;
|
||||
onScrollbackCallbacksRef.current.forEach((cb) => cb(msg.data!));
|
||||
// Buffer scrollback only when no subscribers are registered yet
|
||||
if (onScrollbackCallbacksRef.current.size === 0) {
|
||||
buffer.scrollback = msg.data;
|
||||
} else {
|
||||
onScrollbackCallbacksRef.current.forEach((cb) => cb(msg.data!));
|
||||
buffer.scrollback = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "connected":
|
||||
if (msg.shell && msg.cwd) {
|
||||
// Buffer connected info for late subscribers
|
||||
buffer.connected = { shell: msg.shell!, cwd: msg.cwd! };
|
||||
onConnectCallbacksRef.current.forEach((cb) =>
|
||||
cb({ shell: msg.shell!, cwd: msg.cwd! })
|
||||
);
|
||||
const connectedInfo = { shell: msg.shell!, cwd: msg.cwd! };
|
||||
// Buffer connected info only when no subscribers are registered yet
|
||||
if (onConnectCallbacksRef.current.size === 0) {
|
||||
buffer.connected = connectedInfo;
|
||||
} else {
|
||||
onConnectCallbacksRef.current.forEach((cb) => cb(connectedInfo));
|
||||
buffer.connected = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "exit":
|
||||
|
||||
Reference in New Issue
Block a user