chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
144
third_party/pi/packages/coding-agent/test/interactive-mode-tree-navigation.test.ts
vendored
Normal file
144
third_party/pi/packages/coding-agent/test/interactive-mode-tree-navigation.test.ts
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Container } from "@earendil-works/pi-tui";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SessionManager } from "../src/core/session-manager.ts";
|
||||
import { SettingsManager } from "../src/core/settings-manager.ts";
|
||||
import type { StatusIndicator } from "../src/modes/interactive/components/status-indicator.ts";
|
||||
import type { TreeSelectorComponent } from "../src/modes/interactive/components/tree-selector.ts";
|
||||
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
import { assistantMsg, userMsg } from "./utilities.ts";
|
||||
|
||||
const busyMessage = "Wait for the current compaction or tree navigation to finish before navigating the session tree.";
|
||||
|
||||
function createTreeUI() {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const targetId = sessionManager.appendMessage(userMsg("first"));
|
||||
sessionManager.appendMessage(assistantMsg("reply"));
|
||||
let selector: TreeSelectorComponent | undefined;
|
||||
const onEscape = vi.fn();
|
||||
const ui = {
|
||||
sessionManager,
|
||||
settingsManager: SettingsManager.inMemory(),
|
||||
session: {
|
||||
isStreaming: false,
|
||||
isCompacting: false,
|
||||
abort: vi.fn(async () => {
|
||||
ui.session.isStreaming = false;
|
||||
}),
|
||||
abortBranchSummary: vi.fn(),
|
||||
navigateTree: vi.fn(async () => {
|
||||
if (ui.session.isCompacting) throw new Error(busyMessage);
|
||||
return { cancelled: false };
|
||||
}),
|
||||
},
|
||||
defaultEditor: { onEscape },
|
||||
editor: { getText: () => "", setText: vi.fn() },
|
||||
chatContainer: new Container(),
|
||||
isInitialized: true,
|
||||
footer: { invalidate: vi.fn() },
|
||||
ui: { terminal: { rows: 24, setProgress: vi.fn() }, requestRender: vi.fn() },
|
||||
showSelector: (
|
||||
create: (done: () => void) => { component: TreeSelectorComponent; focus: TreeSelectorComponent },
|
||||
) => {
|
||||
selector = create(vi.fn()).component;
|
||||
},
|
||||
showExtensionSelector: vi.fn(async () => "No summary"),
|
||||
// Dispose newly created indicators so a failing regression cannot leak spinner timers.
|
||||
showStatusIndicator: vi.fn((indicator: StatusIndicator) => indicator.dispose()),
|
||||
clearStatusIndicator: vi.fn(),
|
||||
restoreQueuedMessagesToEditor: vi.fn(),
|
||||
renderInitialMessages: vi.fn(),
|
||||
showStatus: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
flushCompactionQueue: vi.fn(async () => {}),
|
||||
};
|
||||
const showTreeSelector = Reflect.get(InteractiveMode.prototype, "showTreeSelector") as (this: typeof ui) => void;
|
||||
showTreeSelector.call(ui);
|
||||
|
||||
return {
|
||||
ui,
|
||||
onEscape,
|
||||
targetId,
|
||||
async select() {
|
||||
expect(selector).toBeDefined();
|
||||
await selector!.getTreeList().onSelect!(targetId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("InteractiveMode tree navigation availability", () => {
|
||||
beforeEach(() => initTheme("dark"));
|
||||
|
||||
// Regression for #9178 / PR #9179: rejection must not replace the active operation's UI.
|
||||
it.each(["Summarize", "No summary"])("preserves operation UI when choosing %s while busy", async (choice) => {
|
||||
const { ui, onEscape, select } = createTreeUI();
|
||||
const originalLeafId = ui.sessionManager.getLeafId();
|
||||
ui.showExtensionSelector.mockImplementation(async () => {
|
||||
// Compaction or another navigation can start while the dialog is open.
|
||||
ui.session.isCompacting = true;
|
||||
return choice;
|
||||
});
|
||||
|
||||
await select();
|
||||
|
||||
expect(ui.showError).toHaveBeenCalledWith(busyMessage);
|
||||
expect(ui.showStatusIndicator).not.toHaveBeenCalled();
|
||||
expect(ui.clearStatusIndicator).not.toHaveBeenCalled();
|
||||
expect(ui.defaultEditor.onEscape).toBe(onEscape);
|
||||
expect(ui.session.navigateTree).not.toHaveBeenCalled();
|
||||
expect(ui.session.abort).not.toHaveBeenCalled();
|
||||
expect(ui.sessionManager.getLeafId()).toBe(originalLeafId);
|
||||
});
|
||||
|
||||
it("allows navigation when compaction finishes while the dialog is open", async () => {
|
||||
const { ui, targetId, select } = createTreeUI();
|
||||
ui.session.isCompacting = true;
|
||||
ui.showExtensionSelector.mockImplementation(async () => {
|
||||
ui.session.isCompacting = false;
|
||||
return "No summary";
|
||||
});
|
||||
|
||||
await select();
|
||||
|
||||
expect(ui.session.navigateTree).toHaveBeenCalledWith(targetId, {
|
||||
summarize: false,
|
||||
customInstructions: undefined,
|
||||
});
|
||||
expect(ui.showError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still aborts an active response before navigating", async () => {
|
||||
const { ui, select } = createTreeUI();
|
||||
ui.session.isStreaming = true;
|
||||
ui.session.navigateTree.mockImplementation(async () => {
|
||||
expect(ui.session.isStreaming).toBe(false);
|
||||
expect(ui.restoreQueuedMessagesToEditor).toHaveBeenCalledOnce();
|
||||
return { cancelled: false };
|
||||
});
|
||||
|
||||
await select();
|
||||
|
||||
expect(ui.session.abort).toHaveBeenCalledOnce();
|
||||
expect(ui.session.navigateTree).toHaveBeenCalledOnce();
|
||||
expect(ui.showError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rechecks availability after the response abort settles", async () => {
|
||||
const { ui, onEscape, select } = createTreeUI();
|
||||
ui.session.isStreaming = true;
|
||||
ui.showExtensionSelector.mockResolvedValue("Summarize");
|
||||
ui.session.abort.mockImplementation(async () => {
|
||||
ui.session.isStreaming = false;
|
||||
ui.session.isCompacting = true;
|
||||
});
|
||||
|
||||
await select();
|
||||
|
||||
expect(ui.session.abort).toHaveBeenCalledOnce();
|
||||
expect(ui.showError).toHaveBeenCalledWith(busyMessage);
|
||||
expect(ui.showStatusIndicator).not.toHaveBeenCalled();
|
||||
expect(ui.clearStatusIndicator).not.toHaveBeenCalled();
|
||||
expect(ui.defaultEditor.onEscape).toBe(onEscape);
|
||||
expect(ui.session.navigateTree).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user