clanker-agent/packages/coding-agent/test/session-selector-path-delete.test.ts
Harivansh Rathi 0250f72976 move pi-mono into companion-cloud as apps/companion-os
- Copy all pi-mono source into apps/companion-os/
- Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases
- Update deploy-staging.yml to build pi from source (bun compile) before Docker build
- Add apps/companion-os/** to path triggers
- No more cross-repo dispatch needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:22:50 -08:00

207 lines
6.1 KiB
TypeScript

import {
DEFAULT_EDITOR_KEYBINDINGS,
EditorKeybindingsManager,
setEditorKeybindings,
} from "@mariozechner/pi-tui";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import { KeybindingsManager } from "../src/core/keybindings.js";
import type { SessionInfo } from "../src/core/session-manager.js";
import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
type Deferred<T> = {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (err: unknown) => void;
};
function createDeferred<T>(): Deferred<T> {
let resolve: (value: T) => void = () => {};
let reject: (err: unknown) => void = () => {};
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
async function flushPromises(): Promise<void> {
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
}
function makeSession(
overrides: Partial<SessionInfo> & { id: string },
): SessionInfo {
return {
path: overrides.path ?? `/tmp/${overrides.id}.jsonl`,
id: overrides.id,
cwd: overrides.cwd ?? "",
name: overrides.name,
created: overrides.created ?? new Date(0),
modified: overrides.modified ?? new Date(0),
messageCount: overrides.messageCount ?? 1,
firstMessage: overrides.firstMessage ?? "hello",
allMessagesText: overrides.allMessagesText ?? "hello",
};
}
const CTRL_D = "\x04";
const CTRL_BACKSPACE = "\x1b[127;5u";
describe("session selector path/delete interactions", () => {
const keybindings = KeybindingsManager.inMemory();
beforeEach(() => {
// Ensure test isolation: editor keybindings are a global singleton
setEditorKeybindings(
new EditorKeybindingsManager(DEFAULT_EDITOR_KEYBINDINGS),
);
});
beforeAll(() => {
// session selector uses the global theme instance
initTheme("dark");
});
it("does not treat Ctrl+Backspace as delete when search query is non-empty", async () => {
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
const selector = new SessionSelectorComponent(
async () => sessions,
async () => [],
() => {},
() => {},
() => {},
() => {},
{ keybindings },
);
await flushPromises();
const list = selector.getSessionList();
const confirmationChanges: Array<string | null> = [];
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
list.handleInput("a");
list.handleInput(CTRL_BACKSPACE);
expect(confirmationChanges).toEqual([]);
});
it("enters confirmation mode on Ctrl+D even with a non-empty search query", async () => {
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
const selector = new SessionSelectorComponent(
async () => sessions,
async () => [],
() => {},
() => {},
() => {},
() => {},
{ keybindings },
);
await flushPromises();
const list = selector.getSessionList();
const confirmationChanges: Array<string | null> = [];
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
list.handleInput("a");
list.handleInput(CTRL_D);
expect(confirmationChanges).toEqual([sessions[0]!.path]);
});
it("enters confirmation mode on Ctrl+Backspace when search query is empty", async () => {
const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
const selector = new SessionSelectorComponent(
async () => sessions,
async () => [],
() => {},
() => {},
() => {},
() => {},
{ keybindings },
);
await flushPromises();
const list = selector.getSessionList();
const confirmationChanges: Array<string | null> = [];
list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
let deletedPath: string | null = null;
list.onDeleteSession = async (sessionPath) => {
deletedPath = sessionPath;
};
list.handleInput(CTRL_BACKSPACE);
expect(confirmationChanges).toEqual([sessions[0]!.path]);
list.handleInput("\r");
expect(confirmationChanges).toEqual([sessions[0]!.path, null]);
expect(deletedPath).toBe(sessions[0]!.path);
});
it("does not switch scope back to All when All load resolves after toggling back to Current", async () => {
const currentSessions = [makeSession({ id: "current" })];
const allDeferred = createDeferred<SessionInfo[]>();
let allLoadCalls = 0;
const selector = new SessionSelectorComponent(
async () => currentSessions,
async () => {
allLoadCalls++;
return allDeferred.promise;
},
() => {},
() => {},
() => {},
() => {},
{ keybindings },
);
await flushPromises();
const list = selector.getSessionList();
list.handleInput("\t"); // current -> all (starts async load)
list.handleInput("\t"); // all -> current
allDeferred.resolve([makeSession({ id: "all" })]);
await flushPromises();
expect(allLoadCalls).toBe(1);
const output = selector.render(120).join("\n");
expect(output).toContain("Resume Session (Current Folder)");
expect(output).not.toContain("Resume Session (All)");
});
it("does not start redundant All loads when toggling scopes while All is already loading", async () => {
const currentSessions = [makeSession({ id: "current" })];
const allDeferred = createDeferred<SessionInfo[]>();
let allLoadCalls = 0;
const selector = new SessionSelectorComponent(
async () => currentSessions,
async () => {
allLoadCalls++;
return allDeferred.promise;
},
() => {},
() => {},
() => {},
() => {},
{ keybindings },
);
await flushPromises();
const list = selector.getSessionList();
list.handleInput("\t"); // current -> all (starts async load)
list.handleInput("\t"); // all -> current
list.handleInput("\t"); // current -> all again while load pending
expect(allLoadCalls).toBe(1);
allDeferred.resolve([makeSession({ id: "all" })]);
await flushPromises();
});
});