import { describe, expect, it, vi } from "vite-plus/test"; import { EnvironmentId, ThreadId, type TerminalSummary } from "@t3tools/contracts"; import { selectRunningSubprocessTerminalIds } from "@t3tools/client-runtime/state/terminal"; import { selectKnownTerminalSessions } from "./terminalSessions"; vi.mock("./terminal", () => ({ terminalEnvironment: {} })); const environmentA = EnvironmentId.make("environment-a"); const environmentB = EnvironmentId.make("environment-b"); const threadA = ThreadId.make("thread-a"); const threadB = ThreadId.make("thread-b"); function summary( threadId: string, terminalId: string, changes: Partial = {}, ): TerminalSummary { return { threadId, terminalId, cwd: "/repo", worktreePath: null, status: "running", pid: 123, exitCode: null, exitSignal: null, hasRunningSubprocess: true, label: "Terminal", updatedAt: "2026-09-04T00:00:00.000Z", ...changes, }; } describe("selectKnownTerminalSessions", () => { it("preserves numeric terminal order and stable picker ties across threads", () => { const metadata = [ summary(threadB, "terminal-2"), summary(threadA, "terminal-10"), summary(threadB, "terminal-1"), summary(threadA, "terminal-1"), summary(threadA, "terminal-01"), ]; const all = selectKnownTerminalSessions(metadata, environmentA, null); const selected = selectKnownTerminalSessions(metadata, environmentA, threadB); expect(all.map((session) => `${session.target.threadId}/${session.target.terminalId}`)).toEqual( [ "thread-b/terminal-1", "thread-a/terminal-1", "thread-a/terminal-01", "thread-b/terminal-2", "thread-a/terminal-10", ], ); expect(selected.map((session) => session.target.terminalId)).toEqual([ "terminal-1", "terminal-2", ]); const repeated = [...metadata]; expect(selectKnownTerminalSessions(repeated, environmentA, null)).toBe(all); expect(selectKnownTerminalSessions(repeated, environmentA, threadB)).toBe(selected); const reordered = metadata.toReversed(); expect(selectKnownTerminalSessions(reordered, environmentA, threadB)).toBe(selected); expect( selectKnownTerminalSessions(reordered, environmentA, null).map( (session) => `${session.target.threadId}/${session.target.terminalId}`, ), ).toEqual([ "thread-a/terminal-01", "thread-a/terminal-1", "thread-b/terminal-1", "thread-b/terminal-2", "thread-a/terminal-10", ]); }); it("changes only the affected group and preserves earlier snapshots", () => { const first = summary(threadA, "terminal-1"); const second = summary(threadA, "terminal-2"); const other = summary(threadB, "terminal-1"); const metadata = [first, second, other]; const beforeA = selectKnownTerminalSessions(metadata, environmentA, threadA); const beforeB = selectKnownTerminalSessions(metadata, environmentA, threadB); Object.freeze(beforeA); Object.freeze(beforeB); const exited = { ...second, status: "exited" as const, pid: null, exitCode: 0, hasRunningSubprocess: false, label: "Finished", updatedAt: "2026-09-04T00:00:01.000Z", }; const next = [first, other, exited]; const afterA = selectKnownTerminalSessions(next, environmentA, threadA); expect(selectKnownTerminalSessions(next, environmentA, threadB)).toBe(beforeB); expect(afterA).not.toBe(beforeA); expect(afterA[0]).toBe(beforeA[0]); expect(afterA[1]?.state).toMatchObject({ status: "exited", hasRunningSubprocess: false, summary: { label: "Finished", pid: null, exitCode: 0 }, }); expect(selectRunningSubprocessTerminalIds(afterA)).toEqual(["terminal-1"]); expect(beforeA[1]?.state).toMatchObject({ status: "running", hasRunningSubprocess: true, summary: { label: "Terminal", pid: 123 }, }); expect(selectKnownTerminalSessions(metadata, environmentA, threadA)).toBe(beforeA); const renamed = [first, exited, { ...other, label: "Renamed" }]; expect(selectKnownTerminalSessions(renamed, environmentA, threadA)).toBe(afterA); expect( selectKnownTerminalSessions(renamed, environmentA, threadB)[0]?.state.summary?.label, ).toBe("Renamed"); expect(selectKnownTerminalSessions([other], environmentA, threadA)).toEqual([]); expect(selectKnownTerminalSessions([other], environmentA, threadB)).toBe(beforeB); }); it("keeps environment targets separate when snapshots share the same objects", () => { const metadata = [summary(threadA, "terminal-1")]; const first = selectKnownTerminalSessions(metadata, environmentA, threadA); const second = selectKnownTerminalSessions(metadata, environmentB, threadA); expect(first[0]?.target.environmentId).toBe(environmentA); expect(second[0]?.target.environmentId).toBe(environmentB); expect(first).not.toBe(second); expect(first[0]).not.toBe(second[0]); expect(selectKnownTerminalSessions([...metadata], environmentA, threadA)).toBe(first); expect(selectKnownTerminalSessions([...metadata], environmentB, threadA)).toBe(second); }); it("returns stable empty results without a connected environment or matching sessions", () => { const metadata = [summary(threadA, "terminal-1")]; const empty = selectKnownTerminalSessions(null, environmentA, threadA); expect(selectKnownTerminalSessions(metadata, null, threadA)).toBe(empty); expect(selectKnownTerminalSessions([], environmentA, null)).toBe(empty); expect(selectKnownTerminalSessions(metadata, environmentA, threadB)).toBe(empty); }); it("does not materialize unrelated raw thread identifiers", () => { const metadata = [summary(" untrimmed-thread ", "terminal-1"), summary(threadA, "terminal-2")]; expect( selectKnownTerminalSessions(metadata, environmentA, threadA).map( (session) => session.target.threadId, ), ).toEqual([threadA]); }); it("reads the metadata array once for all thread selectors", () => { const source = Array.from({ length: 40 }, (_, index) => summary(`thread-${index}`, "terminal-1"), ); let reads = 0; const metadata = new Proxy(source, { get(target, property, receiver) { if (typeof property === "string" && /^(0|[1-9]\d*)$/.test(property)) reads += 1; return Reflect.get(target, property, receiver); }, }); for (const item of source) { expect( selectKnownTerminalSessions(metadata, environmentA, ThreadId.make(item.threadId)), ).toHaveLength(1); } expect(selectKnownTerminalSessions(metadata, environmentA, null)).toHaveLength(source.length); expect(reads).toBe(source.length); }); });