196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
import { resetSettingsForTest, Settings, settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { InteractiveMode } from "@oh-my-pi/pi-coding-agent/modes/interactive-mode";
|
|
import { initTheme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
|
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
|
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
|
import * as sessionColor from "@oh-my-pi/pi-coding-agent/utils/session-color";
|
|
import type { Container, NativeScrollbackLiveRegion } from "@oh-my-pi/pi-tui";
|
|
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
|
|
type Harness = {
|
|
mode: InteractiveMode;
|
|
sessionManager: SessionManager;
|
|
tempDir: TempDir;
|
|
};
|
|
|
|
let harnesses: Harness[] = [];
|
|
|
|
function defined<T>(value: T | undefined): T {
|
|
expect(value).toBeDefined();
|
|
return value as T;
|
|
}
|
|
|
|
async function createHarness(sessionName: string): Promise<Harness> {
|
|
const tempDir = TempDir.createSync("@pi-working-accent-");
|
|
await Settings.init({ inMemory: true, cwd: tempDir.path() });
|
|
await initTheme(false);
|
|
const sessionManager = SessionManager.inMemory(tempDir.path());
|
|
await sessionManager.setSessionName(sessionName, "user");
|
|
const session = {
|
|
sessionManager,
|
|
settings,
|
|
agent: {
|
|
state: { tools: [] },
|
|
metadataForProvider: () => undefined,
|
|
},
|
|
customCommands: [],
|
|
skills: [],
|
|
autoCompactionEnabled: true,
|
|
messages: [],
|
|
systemPrompt: [],
|
|
state: { model: undefined },
|
|
model: undefined,
|
|
thinkingLevel: undefined,
|
|
} as unknown as AgentSession;
|
|
const mode = new InteractiveMode(session, "test");
|
|
const harness = { mode, sessionManager, tempDir };
|
|
harnesses.push(harness);
|
|
return harness;
|
|
}
|
|
|
|
function startStableLoader(mode: InteractiveMode): void {
|
|
mode.ensureLoadingAnimation();
|
|
mode.loadingAnimation?.stop();
|
|
}
|
|
|
|
function renderLoader(mode: InteractiveMode): string {
|
|
return mode.statusContainer.render(120).join("\n");
|
|
}
|
|
|
|
function shadowAccentSurfaceLuminance(value: number | undefined): () => void {
|
|
Object.defineProperty(theme, "accentSurfaceLuminance", {
|
|
configurable: true,
|
|
get: () => value,
|
|
});
|
|
return () => {
|
|
delete (theme as unknown as { accentSurfaceLuminance?: number }).accentSurfaceLuminance;
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const harness of harnesses) {
|
|
harness.mode.stop();
|
|
harness.tempDir.removeSync();
|
|
}
|
|
harnesses = [];
|
|
vi.restoreAllMocks();
|
|
resetSettingsForTest();
|
|
});
|
|
|
|
describe("InteractiveMode working-message session accent cache", () => {
|
|
it("reports a live seam only while status content is mounted", async () => {
|
|
const { mode } = await createHarness("Live status");
|
|
const statusContainer = mode.statusContainer as Container & NativeScrollbackLiveRegion;
|
|
|
|
// Empty: no seam — the engine may commit freely past the container.
|
|
expect(statusContainer.getNativeScrollbackLiveRegionStart()).toBeUndefined();
|
|
// Loader mounted: every row is live, so the seam sits at 0 and keeps
|
|
// the animating loader out of immutable native scrollback.
|
|
startStableLoader(mode);
|
|
expect(statusContainer.getNativeScrollbackLiveRegionStart()).toBe(0);
|
|
});
|
|
|
|
it("reuses one computed accent across loader spinner and message colorizers", async () => {
|
|
const { mode } = await createHarness("Cached session");
|
|
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
|
const getAnsi = vi.spyOn(sessionColor, "getSessionAccentAnsi");
|
|
|
|
startStableLoader(mode);
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(getAnsi).toHaveBeenCalledTimes(2);
|
|
|
|
mode.loadingAnimation?.setMessage("Still working");
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(getAnsi).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("recomputes for session renames and keeps the main ANSI path status-line equivalent", async () => {
|
|
const initialName = "Alpha session";
|
|
const renamedName = "Beta session";
|
|
const { mode, sessionManager } = await createHarness(initialName);
|
|
const initialAnsi = defined(
|
|
sessionColor.getSessionAccentAnsi(
|
|
sessionColor.getSessionAccentHex(
|
|
initialName,
|
|
theme.getMajorThemeColorHexes(),
|
|
theme.accentSurfaceLuminance,
|
|
),
|
|
),
|
|
);
|
|
const renamedAnsi = defined(
|
|
sessionColor.getSessionAccentAnsi(
|
|
sessionColor.getSessionAccentHex(
|
|
renamedName,
|
|
theme.getMajorThemeColorHexes(),
|
|
theme.accentSurfaceLuminance,
|
|
),
|
|
),
|
|
);
|
|
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
|
|
|
startStableLoader(mode);
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(renderLoader(mode)).toContain(initialAnsi);
|
|
|
|
await sessionManager.setSessionName(renamedName, "user");
|
|
mode.loadingAnimation?.setMessage("Renamed session");
|
|
expect(getHex).toHaveBeenCalledTimes(2);
|
|
expect(renderLoader(mode)).toContain(renamedAnsi);
|
|
});
|
|
|
|
it("keys cached accents by theme accent-surface luminance", async () => {
|
|
const sessionName = "Luminance session";
|
|
const { mode } = await createHarness(sessionName);
|
|
const restoreInitial = shadowAccentSurfaceLuminance(undefined);
|
|
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
|
|
|
try {
|
|
startStableLoader(mode);
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(getHex.mock.calls[0]).toEqual([sessionName, theme.getMajorThemeColorHexes(), undefined]);
|
|
|
|
restoreInitial();
|
|
const restoreLight = shadowAccentSurfaceLuminance(0.72);
|
|
try {
|
|
mode.loadingAnimation?.setMessage("Light theme");
|
|
expect(getHex).toHaveBeenCalledTimes(2);
|
|
expect(getHex.mock.calls[1]).toEqual([sessionName, theme.getMajorThemeColorHexes(), 0.72]);
|
|
} finally {
|
|
restoreLight();
|
|
}
|
|
} finally {
|
|
restoreInitial();
|
|
}
|
|
});
|
|
|
|
it("caches disabled session accents and recomputes when the setting is enabled again", async () => {
|
|
const sessionName = "Toggle session";
|
|
const { mode } = await createHarness(sessionName);
|
|
const accentAnsi = defined(
|
|
sessionColor.getSessionAccentAnsi(
|
|
sessionColor.getSessionAccentHex(
|
|
sessionName,
|
|
theme.getMajorThemeColorHexes(),
|
|
theme.accentSurfaceLuminance,
|
|
),
|
|
),
|
|
);
|
|
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
|
|
|
startStableLoader(mode);
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(renderLoader(mode)).toContain(accentAnsi);
|
|
|
|
settings.set("statusLine.sessionAccent", false);
|
|
mode.loadingAnimation?.setMessage("Accent disabled");
|
|
expect(getHex).toHaveBeenCalledTimes(1);
|
|
expect(renderLoader(mode)).not.toContain(accentAnsi);
|
|
|
|
settings.set("statusLine.sessionAccent", true);
|
|
mode.loadingAnimation?.setMessage("Accent enabled");
|
|
expect(getHex).toHaveBeenCalledTimes(2);
|
|
expect(renderLoader(mode)).toContain(accentAnsi);
|
|
});
|
|
});
|