Files
kandev/apps/web/lib/state/slices/session/remove-task-session.test.ts
T
José Almeida 3b2e1396fa perf: fix board lag, memory growth, and rebase-driven CPU spikes (#1427)
* perf(kanban): memoize derived board data to stop re-render storms

The board rebuilt the `tasks` array (and `stepOptions`/`sharedProps`) on every
store change, producing fresh object identities that defeated downstream memos
and re-rendered every card on each WebSocket message. Wrap them in useMemo so
the board only re-renders when its inputs actually change.

* perf(session-runtime): bound runtime memory and dedupe git-status compare

Three runtime-store fixes for the crash a heavy rebase triggered:

- Cap shell + terminal output buffers at 2MB (matching the existing process
  buffer) so a chatty shell can't grow the store without limit.
- Purge per-session runtime state (messages, turns, shell/process buffers, and
  the bySessionId maps) when a session is removed; env-scoped maps are dropped
  only once no other session shares the environment.
- Run setGitStatus's deep per-file diff comparison once per update and return
  whether anything changed, instead of recomputing it 2-3x. Under a massive
  rebase (thousands of files, frequent updates) that comparison was the
  dominant CPU cost and drove the render storm.

* refactor(session-runtime): drop now-unused gitStatusWouldMutate

The git-status handler no longer pre-checks for changes (setGitStatus now
returns whether it changed), leaving gitStatusWouldMutate with no callers.

* fix(session-runtime): enforce terminal cap on first and oversized chunks

appendTerminalChunk only evicted when more than one chunk was buffered, so a
single chunk larger than the 2MB cap slipped through untrimmed; the new-terminal
branch also pushed the first chunk without going through the cap at all. Clamp
each chunk to the tail of the cap and route the new-terminal path through
appendTerminalChunk. Addresses CodeRabbit review.
2026-06-18 13:30:41 +01:00

55 lines
2.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { createSessionSlice } from "./session-slice";
import { createSessionRuntimeSlice } from "../session-runtime/session-runtime-slice";
import type { SessionSlice } from "./types";
import type { SessionRuntimeSlice } from "../session-runtime/types";
type CombinedSlice = SessionSlice & SessionRuntimeSlice;
function makeStore() {
return create<CombinedSlice>()(
immer((set) => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(createSessionSlice as any)(set),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(createSessionRuntimeSlice as any)(set),
})),
);
}
const TASK_ID = "task-1";
const SESSION_ID = "session-1";
describe("removeTaskSession cleanup cascade", () => {
let store: ReturnType<typeof makeStore>;
beforeEach(() => {
store = makeStore();
});
it("purges messages, turns, and runtime buffers for the removed session", () => {
const s = store.getState();
s.registerSessionEnvironment(SESSION_ID, "env-1");
s.appendShellOutput(SESSION_ID, "shell output");
s.setContextWindow(SESSION_ID, { size: 1, used: 1, remaining: 0, efficiency: 1 });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
s.addMessage({ id: "m1", session_id: SESSION_ID, role: "user", content: "hi" } as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
s.addTurn({ id: "t1", session_id: SESSION_ID } as any);
expect(store.getState().messages.bySession[SESSION_ID]).toHaveLength(1);
expect(store.getState().turns.bySession[SESSION_ID]).toHaveLength(1);
store.getState().removeTaskSession(TASK_ID, SESSION_ID);
const after = store.getState();
expect(after.messages.bySession[SESSION_ID]).toBeUndefined();
expect(after.turns.bySession[SESSION_ID]).toBeUndefined();
expect(after.contextWindow.bySessionId[SESSION_ID]).toBeUndefined();
expect(after.shell.outputs["env-1"]).toBeUndefined();
expect(after.environmentIdBySessionId[SESSION_ID]).toBeUndefined();
});
});