Files
oh-my-pi/packages/coding-agent/test/interactive-mode-shutdown.test.ts
T
roboomp 734d424e5f fix(tui): fixed shell cursor handoff on quit
Avoided the forced teardown repaint during interactive shutdown so disposed UI state cannot clear the viewport before Bash resumes.

Moved TUI stop cursor placement to the first row after short rendered content without adding a blank line.

Fixes #1620
2026-06-01 04:14:36 +00:00

68 lines
2.8 KiB
TypeScript

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
import * as path from "node:path";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { InteractiveMode } from "@oh-my-pi/pi-coding-agent/modes/interactive-mode";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { postmortem, TempDir } from "@oh-my-pi/pi-utils";
describe("InteractiveMode shutdown", () => {
let authStorage: AuthStorage;
let mode: InteractiveMode;
let session: AgentSession;
let tempDir: TempDir;
beforeAll(() => {
initTheme();
});
beforeEach(async () => {
resetSettingsForTest();
tempDir = TempDir.createSync("@pi-shutdown-");
await Settings.init({ inMemory: true, cwd: tempDir.path() });
authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
const modelRegistry = new ModelRegistry(authStorage);
const model = modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected claude-sonnet-4-5 test model");
session = new AgentSession({
agent: new Agent({ initialState: { model, systemPrompt: ["Test"], tools: [], messages: [] } }),
sessionManager: SessionManager.create(tempDir.path(), tempDir.path()),
settings: Settings.isolated(),
modelRegistry,
});
mode = new InteractiveMode(session, "test");
});
afterEach(async () => {
mode?.stop();
vi.restoreAllMocks();
await session?.dispose();
authStorage?.close();
tempDir?.removeSync();
resetSettingsForTest();
});
it("stops from the last committed TUI frame without forcing a teardown repaint", async () => {
const requestRenderSpy = vi.spyOn(mode.ui, "requestRender").mockImplementation(() => {});
const stopSpy = vi.spyOn(mode.ui, "stop").mockImplementation(() => {});
const drainSpy = vi.spyOn(mode.ui.terminal, "drainInput").mockResolvedValue(undefined);
const disposeSpy = vi.spyOn(session, "dispose").mockResolvedValue(undefined);
const quitSpy = vi.spyOn(postmortem, "quit").mockResolvedValue(undefined);
vi.spyOn(session.sessionManager, "getSessionId").mockReturnValue("");
mode.isInitialized = true;
await mode.shutdown();
expect(disposeSpy).toHaveBeenCalled();
expect(requestRenderSpy.mock.calls.some(call => call[0] === true)).toBe(false);
expect(drainSpy).toHaveBeenCalledWith(1000);
expect(stopSpy).toHaveBeenCalled();
expect(quitSpy).toHaveBeenCalledWith(0);
});
});