mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Report idle, working, blocked, done, and error to terminals that support the Program Status Protocol. ProcessTerminal sends the support query with the keyboard query and reports only after the terminal answers before DA1; PI_PROGRAM_STATUS=1|0 overrides detection. Interactive mode reports runs, compaction, extension dialogs, and logins. agent_settled now carries aborted so cancelled runs report idle. closes #10607
276 lines
9.1 KiB
TypeScript
276 lines
9.1 KiB
TypeScript
import type { Usage } from "@earendil-works/pi-ai";
|
|
import { Container } from "@earendil-works/pi-tui";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
import type { SessionEntry } from "../src/core/session-manager.ts";
|
|
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
|
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
|
import { stripAnsi } from "../src/utils/ansi.ts";
|
|
|
|
describe("InteractiveMode compaction events", () => {
|
|
test("uses the cache miss notice setting for compaction and branch summary costs", () => {
|
|
const usage: Usage = {
|
|
input: 10,
|
|
output: 20,
|
|
cacheRead: 30,
|
|
cacheWrite: 40,
|
|
totalTokens: 100,
|
|
cost: { input: 0.01, output: 0.02, cacheRead: 0.03, cacheWrite: 0.065, total: 0.125 },
|
|
};
|
|
const addCompactionCostNotice = Reflect.get(InteractiveMode.prototype, "addCompactionCostNotice") as (
|
|
this: { chatContainer: Container; settingsManager: { getShowCacheMissNotices(): boolean } },
|
|
notice: {
|
|
type: "compaction_cost";
|
|
kind: "compaction" | "branch_summary";
|
|
usage: Usage;
|
|
},
|
|
) => void;
|
|
|
|
initTheme("dark");
|
|
const enabled = {
|
|
chatContainer: new Container(),
|
|
settingsManager: { getShowCacheMissNotices: () => true },
|
|
};
|
|
addCompactionCostNotice.call(enabled, { type: "compaction_cost", kind: "compaction", usage });
|
|
addCompactionCostNotice.call(enabled, {
|
|
type: "compaction_cost",
|
|
kind: "branch_summary",
|
|
usage,
|
|
});
|
|
const output = stripAnsi(enabled.chatContainer.render(120).join("\n"));
|
|
expect(output).toContain("Compaction: 100 tokens billed (~$0.13)");
|
|
expect(output).toContain("Branch summary: 100 tokens billed (~$0.13)");
|
|
|
|
const disabled = {
|
|
chatContainer: new Container(),
|
|
settingsManager: { getShowCacheMissNotices: () => false },
|
|
};
|
|
addCompactionCostNotice.call(disabled, { type: "compaction_cost", kind: "compaction", usage });
|
|
expect(disabled.chatContainer.children).toHaveLength(0);
|
|
});
|
|
|
|
test("renders each compaction cost after its summary", () => {
|
|
const currentUsage: Usage = {
|
|
input: 10,
|
|
output: 20,
|
|
cacheRead: 30,
|
|
cacheWrite: 40,
|
|
totalTokens: 100,
|
|
cost: { input: 0.01, output: 0.02, cacheRead: 0.03, cacheWrite: 0.04, total: 0.1 },
|
|
};
|
|
const previousUsage: Usage = {
|
|
input: 1,
|
|
output: 2,
|
|
cacheRead: 3,
|
|
cacheWrite: 4,
|
|
totalTokens: 10,
|
|
cost: { input: 0.001, output: 0.002, cacheRead: 0.003, cacheWrite: 0.004, total: 0.01 },
|
|
};
|
|
const entries: SessionEntry[] = [
|
|
{
|
|
type: "compaction",
|
|
id: "current",
|
|
parentId: "previous",
|
|
timestamp: "2025-01-02T00:00:00Z",
|
|
summary: "current summary",
|
|
firstKeptEntryId: "kept",
|
|
tokensBefore: 200,
|
|
usage: currentUsage,
|
|
},
|
|
{
|
|
type: "compaction",
|
|
id: "previous",
|
|
parentId: null,
|
|
timestamp: "2025-01-01T00:00:00Z",
|
|
summary: "previous summary",
|
|
firstKeptEntryId: "kept",
|
|
tokensBefore: 100,
|
|
usage: previousUsage,
|
|
},
|
|
];
|
|
const fakeThis = { renderSessionItems: vi.fn() };
|
|
const renderSessionEntries = Reflect.get(InteractiveMode.prototype, "renderSessionEntries") as (
|
|
this: typeof fakeThis,
|
|
entries: SessionEntry[],
|
|
) => void;
|
|
|
|
renderSessionEntries.call(fakeThis, entries);
|
|
|
|
expect(fakeThis.renderSessionItems).toHaveBeenCalledWith(
|
|
[
|
|
expect.objectContaining({ role: "compactionSummary", summary: "current summary" }),
|
|
{ type: "compaction_cost", kind: "compaction", usage: currentUsage },
|
|
expect.objectContaining({ role: "compactionSummary", summary: "previous summary" }),
|
|
{ type: "compaction_cost", kind: "compaction", usage: previousUsage },
|
|
],
|
|
{},
|
|
);
|
|
});
|
|
|
|
test("renders retained entries and appends the latest summary cost at the bottom", async () => {
|
|
const usage: Usage = {
|
|
input: 10,
|
|
output: 20,
|
|
cacheRead: 30,
|
|
cacheWrite: 40,
|
|
totalTokens: 100,
|
|
cost: { input: 0.01, output: 0.02, cacheRead: 0.03, cacheWrite: 0.065, total: 0.125 },
|
|
};
|
|
const latestCompaction: SessionEntry = {
|
|
type: "compaction",
|
|
id: "latest",
|
|
parentId: "previous",
|
|
timestamp: "2025-01-02T00:00:00Z",
|
|
summary: "summary",
|
|
firstKeptEntryId: "kept",
|
|
tokensBefore: 123,
|
|
usage,
|
|
};
|
|
const previousCompaction: SessionEntry = {
|
|
type: "compaction",
|
|
id: "previous",
|
|
parentId: null,
|
|
timestamp: "2025-01-01T00:00:00Z",
|
|
summary: "previous summary",
|
|
firstKeptEntryId: "kept",
|
|
tokensBefore: 100,
|
|
usage,
|
|
};
|
|
const fakeThis = {
|
|
isInitialized: true,
|
|
footer: { invalidate: vi.fn() },
|
|
programStatus: { handleEvent: vi.fn() },
|
|
autoCompactionEscapeHandler: undefined as (() => void) | undefined,
|
|
autoCompactionLoader: undefined,
|
|
defaultEditor: {},
|
|
statusContainer: { clear: vi.fn() },
|
|
chatContainer: { clear: vi.fn() },
|
|
sessionManager: { buildContextEntries: vi.fn().mockReturnValue([latestCompaction, previousCompaction]) },
|
|
renderSessionEntries: vi.fn(),
|
|
addMessageToChat: vi.fn(),
|
|
addCompactionCostNotice: vi.fn(),
|
|
showError: vi.fn(),
|
|
showStatus: vi.fn(),
|
|
clearStatusIndicator: vi.fn(),
|
|
flushCompactionQueue: vi.fn().mockResolvedValue(undefined),
|
|
settingsManager: { getShowTerminalProgress: () => false },
|
|
ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } },
|
|
};
|
|
|
|
const handleEvent = Reflect.get(InteractiveMode.prototype, "handleEvent") as (
|
|
this: typeof fakeThis,
|
|
event: {
|
|
type: "compaction_end";
|
|
reason: "manual" | "threshold" | "overflow";
|
|
result: { tokensBefore: number; summary: string; usage?: Usage } | undefined;
|
|
aborted: boolean;
|
|
willRetry: boolean;
|
|
errorMessage?: string;
|
|
},
|
|
) => Promise<void>;
|
|
|
|
await handleEvent.call(fakeThis, {
|
|
type: "compaction_end",
|
|
reason: "manual",
|
|
result: {
|
|
tokensBefore: 123,
|
|
summary: "summary",
|
|
usage,
|
|
},
|
|
aborted: false,
|
|
willRetry: false,
|
|
});
|
|
|
|
expect(fakeThis.chatContainer.clear).toHaveBeenCalledTimes(1);
|
|
expect(fakeThis.renderSessionEntries).toHaveBeenCalledWith([previousCompaction]);
|
|
expect(fakeThis.addMessageToChat).toHaveBeenCalledTimes(1);
|
|
expect(fakeThis.addMessageToChat).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
role: "compactionSummary",
|
|
tokensBefore: 123,
|
|
summary: "summary",
|
|
}),
|
|
);
|
|
expect(fakeThis.addCompactionCostNotice).toHaveBeenCalledWith({
|
|
type: "compaction_cost",
|
|
kind: "compaction",
|
|
usage,
|
|
});
|
|
expect(fakeThis.flushCompactionQueue).toHaveBeenCalledWith({ willRetry: false });
|
|
});
|
|
|
|
test("updates the working state when the same agent run resumes after compaction", async () => {
|
|
const fakeThis = {
|
|
isInitialized: true,
|
|
footer: { invalidate: vi.fn() },
|
|
programStatus: { handleEvent: vi.fn() },
|
|
activeStatusIndicator: undefined,
|
|
workingVisible: true,
|
|
showWorkingStatusIndicator: vi.fn(),
|
|
clearStatusIndicator: vi.fn(),
|
|
settingsManager: { getShowTerminalProgress: () => true },
|
|
ui: { requestRender: vi.fn(), terminal: { setProgress: vi.fn() } },
|
|
};
|
|
const handleEvent = Reflect.get(InteractiveMode.prototype, "handleEvent") as (
|
|
this: typeof fakeThis,
|
|
event: { type: "turn_start" },
|
|
) => Promise<void>;
|
|
|
|
await handleEvent.call(fakeThis, { type: "turn_start" });
|
|
|
|
expect(fakeThis.ui.terminal.setProgress).toHaveBeenCalledWith(true);
|
|
expect(fakeThis.showWorkingStatusIndicator).toHaveBeenCalledTimes(1);
|
|
expect(fakeThis.clearStatusIndicator).not.toHaveBeenCalled();
|
|
expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1);
|
|
|
|
fakeThis.workingVisible = false;
|
|
await handleEvent.call(fakeThis, { type: "turn_start" });
|
|
|
|
expect(fakeThis.showWorkingStatusIndicator).toHaveBeenCalledTimes(1);
|
|
expect(fakeThis.clearStatusIndicator).toHaveBeenCalledTimes(1);
|
|
expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
// Regression test for #9340.
|
|
test("routes interactive response aborts through AgentSession", () => {
|
|
const abort = vi.fn(async () => {});
|
|
const ui = {
|
|
clearAllQueues: () => ({ steering: [], followUp: [] }),
|
|
updatePendingMessagesDisplay: vi.fn(),
|
|
session: { abort },
|
|
};
|
|
const restoreQueuedMessagesToEditor = Reflect.get(InteractiveMode.prototype, "restoreQueuedMessagesToEditor") as (
|
|
this: typeof ui,
|
|
options?: { abort?: boolean },
|
|
) => number;
|
|
|
|
restoreQueuedMessagesToEditor.call(ui, { abort: true });
|
|
|
|
expect(abort).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
test("preserves steering behavior when flushing into an active agent run", async () => {
|
|
const fakeThis = {
|
|
compactionQueuedMessages: [{ text: "change direction", mode: "steer" as const }],
|
|
session: {
|
|
clearQueue: vi.fn(),
|
|
prompt: vi.fn().mockResolvedValue(undefined),
|
|
steer: vi.fn().mockResolvedValue(undefined),
|
|
followUp: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
isExtensionCommand: vi.fn().mockReturnValue(false),
|
|
updatePendingMessagesDisplay: vi.fn(),
|
|
showError: vi.fn(),
|
|
};
|
|
|
|
const flushCompactionQueue = Reflect.get(InteractiveMode.prototype, "flushCompactionQueue") as (
|
|
this: typeof fakeThis,
|
|
options?: { willRetry?: boolean },
|
|
) => Promise<void>;
|
|
|
|
await flushCompactionQueue.call(fakeThis, { willRetry: false });
|
|
|
|
expect(fakeThis.session.prompt).toHaveBeenCalledWith("change direction", { streamingBehavior: "steer" });
|
|
expect(fakeThis.compactionQueuedMessages).toEqual([]);
|
|
expect(fakeThis.showError).not.toHaveBeenCalled();
|
|
});
|
|
});
|