summaryrefslogtreecommitdiff
path: root/src/integration/assets/opencode/herdr-tui-session.test.ts
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-10 14:57:44 +0300
committerGitHub <noreply@github.com>2026-08-10 14:57:44 +0300
commitccccda54c21f6ef1649a44bb098d7c498724404d (patch)
tree86f28b85086ef36fd7ebc0f1d928b7606d76c487 /src/integration/assets/opencode/herdr-tui-session.test.ts
parent2e8cc9060d723a5ac32a2325e695fd8750ff60fe (diff)
fix: track opencode tui session selection (#2455)
refs #2450 Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/integration/assets/opencode/herdr-tui-session.test.ts')
-rw-r--r--src/integration/assets/opencode/herdr-tui-session.test.ts181
1 files changed, 181 insertions, 0 deletions
diff --git a/src/integration/assets/opencode/herdr-tui-session.test.ts b/src/integration/assets/opencode/herdr-tui-session.test.ts
new file mode 100644
index 00000000..1bcb1f81
--- /dev/null
+++ b/src/integration/assets/opencode/herdr-tui-session.test.ts
@@ -0,0 +1,181 @@
+import { afterEach, beforeEach, expect, mock, test } from "bun:test";
+
+const requests: unknown[] = [];
+const activeDisposers: Array<() => void> = [];
+const requestWaiters: Array<() => void> = [];
+let importCounter = 0;
+
+mock.module("node:net", () => ({
+ default: {
+ createConnection(_path: string, onConnect: () => void) {
+ const handlers = new Map<string, () => void>();
+ const client = {
+ write(input: string) {
+ requests.push(JSON.parse(input.trim()));
+ requestWaiters.shift()?.();
+ queueMicrotask(() => client.emit("data"));
+ },
+ setTimeout() {},
+ on(event: string, handler: () => void) {
+ handlers.set(event, handler);
+ },
+ destroy() {},
+ emit(event: string) {
+ handlers.get(event)?.();
+ },
+ };
+ queueMicrotask(onConnect);
+ return client;
+ },
+ },
+}));
+
+beforeEach(() => {
+ requests.length = 0;
+ requestWaiters.length = 0;
+ process.env.HERDR_ENV = "1";
+ process.env.HERDR_SOCKET_PATH = "test.sock";
+ process.env.HERDR_PANE_ID = "test:p1";
+});
+
+afterEach(() => {
+ for (const dispose of activeDisposers.splice(0)) {
+ dispose();
+ }
+});
+
+async function loadPlugin() {
+ importCounter += 1;
+ const module = await import(`./herdr-tui-session.js?test=${importCounter}`);
+ return module.default;
+}
+
+function fakeApi() {
+ const sessions = new Map<string, { id: string; parentID?: string }>();
+ let current: { name: string; params?: { sessionID: string } } = { name: "home" };
+ let dispose: (() => void) | undefined;
+ activeDisposers.push(() => dispose?.());
+
+ return {
+ api: {
+ route: {
+ get current() {
+ return current;
+ },
+ },
+ state: {
+ session: {
+ get(sessionID: string) {
+ return sessions.get(sessionID);
+ },
+ },
+ },
+ lifecycle: {
+ onDispose(handler: () => void) {
+ dispose = handler;
+ return () => {};
+ },
+ },
+ },
+ addSession(session: { id: string; parentID?: string }) {
+ sessions.set(session.id, session);
+ },
+ select(sessionID: string) {
+ current = { name: "session", params: { sessionID } };
+ },
+ dispose() {
+ dispose?.();
+ },
+ };
+}
+
+function waitForNextRequest(): Promise<void> {
+ return new Promise((resolve) => requestWaiters.push(resolve));
+}
+
+test("reports a root session when only the local route changes", async () => {
+ const plugin = await loadPlugin();
+ const tui = fakeApi();
+ tui.addSession({ id: "session-a" });
+ await plugin.tui(tui.api);
+
+ const dispatched = waitForNextRequest();
+ tui.select("session-a");
+ await dispatched;
+
+ expect(requests).toHaveLength(1);
+ expect(requestParam(requests[0], "agent_session_id")).toBe("session-a");
+ expect(requestParam(requests[0], "session_start_source")).toBe("select");
+ expect(requestParam(requests[0], "seq")).toBeUndefined();
+});
+
+test("retries an initial selection while Herdr detects the process", async () => {
+ const plugin = await loadPlugin();
+ const tui = fakeApi();
+ tui.addSession({ id: "session-a" });
+ tui.select("session-a");
+
+ await plugin.tui(tui.api);
+ await new Promise((resolve) => setTimeout(resolve, 125));
+
+ expect(requests.map((request) => requestParam(request, "agent_session_id"))).toEqual([
+ "session-a",
+ "session-a",
+ ]);
+});
+
+test("does not report root sessions not selected by this TUI", async () => {
+ const plugin = await loadPlugin();
+ const tui = fakeApi();
+ tui.addSession({ id: "session-a" });
+ tui.addSession({ id: "session-b" });
+ tui.select("session-a");
+ await plugin.tui(tui.api);
+
+ await new Promise((resolve) => setTimeout(resolve, 125));
+
+ expect(requests.length).toBeGreaterThan(0);
+ expect(requests.every((request) => requestParam(request, "agent_session_id") === "session-a")).toBe(
+ true,
+ );
+});
+
+test("does not replace the root session with a selected child session", async () => {
+ const plugin = await loadPlugin();
+ const tui = fakeApi();
+ tui.addSession({ id: "root-session" });
+ tui.addSession({ id: "child-session", parentID: "root-session" });
+ tui.select("root-session");
+ await plugin.tui(tui.api);
+ expect(requests).toHaveLength(1);
+
+ tui.select("child-session");
+ await new Promise((resolve) => setTimeout(resolve, 125));
+
+ expect(requests).toHaveLength(1);
+ expect(requestParam(requests[0], "agent_session_id")).toBe("root-session");
+});
+
+test("stops route polling when the TUI plugin is disposed", async () => {
+ const plugin = await loadPlugin();
+ const tui = fakeApi();
+ tui.addSession({ id: "session-a" });
+ await plugin.tui(tui.api);
+ tui.dispose();
+ tui.select("session-a");
+
+ await new Promise((resolve) => setTimeout(resolve, 125));
+
+ expect(requests).toHaveLength(0);
+});
+
+function requestParam(request: unknown, name: string): unknown {
+ if (!isRecord(request) || !isRecord(request.params)) {
+ return undefined;
+ }
+ return request.params[name];
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return typeof value === "object" && value !== null;
+}