clanker-agent/packages/coding-agent/test/interactive-mode-status.test.ts
Harivansh Rathi 536241053c refactor: finish companion rename migration
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures.

Verification:
- semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references
- npm run check in apps/companion-os (fails in this worktree: biome not found)

Co-authored-by: Codex <noreply@openai.com>
2026-03-10 07:39:32 -05:00

194 lines
6.3 KiB
TypeScript

import { Container } from "@mariozechner/companion-tui";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
function renderLastLine(container: Container, width = 120): string {
const last = container.children[container.children.length - 1];
if (!last) return "";
return last.render(width).join("\n");
}
function renderAll(container: Container, width = 120): string {
return container.children.flatMap((child) => child.render(width)).join("\n");
}
describe("InteractiveMode.showStatus", () => {
beforeAll(() => {
// showStatus uses the global theme instance
initTheme("dark");
});
test("coalesces immediately-sequential status messages", () => {
const fakeThis: any = {
chatContainer: new Container(),
ui: { requestRender: vi.fn() },
lastStatusSpacer: undefined,
lastStatusText: undefined,
};
(InteractiveMode as any).prototype.showStatus.call(fakeThis, "STATUS_ONE");
expect(fakeThis.chatContainer.children).toHaveLength(2);
expect(renderLastLine(fakeThis.chatContainer)).toContain("STATUS_ONE");
(InteractiveMode as any).prototype.showStatus.call(fakeThis, "STATUS_TWO");
// second status updates the previous line instead of appending
expect(fakeThis.chatContainer.children).toHaveLength(2);
expect(renderLastLine(fakeThis.chatContainer)).toContain("STATUS_TWO");
expect(renderLastLine(fakeThis.chatContainer)).not.toContain("STATUS_ONE");
});
test("appends a new status line if something else was added in between", () => {
const fakeThis: any = {
chatContainer: new Container(),
ui: { requestRender: vi.fn() },
lastStatusSpacer: undefined,
lastStatusText: undefined,
};
(InteractiveMode as any).prototype.showStatus.call(fakeThis, "STATUS_ONE");
expect(fakeThis.chatContainer.children).toHaveLength(2);
// Something else gets added to the chat in between status updates
fakeThis.chatContainer.addChild({
render: () => ["OTHER"],
invalidate: () => {},
});
expect(fakeThis.chatContainer.children).toHaveLength(3);
(InteractiveMode as any).prototype.showStatus.call(fakeThis, "STATUS_TWO");
// adds spacer + text
expect(fakeThis.chatContainer.children).toHaveLength(5);
expect(renderLastLine(fakeThis.chatContainer)).toContain("STATUS_TWO");
});
});
describe("InteractiveMode.createExtensionUIContext setTheme", () => {
test("persists theme changes to settings manager", () => {
initTheme("dark");
let currentTheme = "dark";
const settingsManager = {
getTheme: vi.fn(() => currentTheme),
setTheme: vi.fn((theme: string) => {
currentTheme = theme;
}),
};
const fakeThis: any = {
session: { settingsManager },
settingsManager,
ui: { requestRender: vi.fn() },
};
const uiContext = (
InteractiveMode as any
).prototype.createExtensionUIContext.call(fakeThis);
const result = uiContext.setTheme("light");
expect(result.success).toBe(true);
expect(settingsManager.setTheme).toHaveBeenCalledWith("light");
expect(currentTheme).toBe("light");
expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1);
});
test("does not persist invalid theme names", () => {
initTheme("dark");
const settingsManager = {
getTheme: vi.fn(() => "dark"),
setTheme: vi.fn(),
};
const fakeThis: any = {
session: { settingsManager },
settingsManager,
ui: { requestRender: vi.fn() },
};
const uiContext = (
InteractiveMode as any
).prototype.createExtensionUIContext.call(fakeThis);
const result = uiContext.setTheme("__missing_theme__");
expect(result.success).toBe(false);
expect(settingsManager.setTheme).not.toHaveBeenCalled();
expect(fakeThis.ui.requestRender).not.toHaveBeenCalled();
});
});
describe("InteractiveMode.showLoadedResources", () => {
beforeAll(() => {
initTheme("dark");
});
function createShowLoadedResourcesThis(options: {
quietStartup: boolean;
verbose?: boolean;
skills?: Array<{ filePath: string }>;
skillDiagnostics?: Array<{
type: "warning" | "error" | "collision";
message: string;
}>;
}) {
const fakeThis: any = {
options: { verbose: options.verbose ?? false },
chatContainer: new Container(),
settingsManager: {
getQuietStartup: () => options.quietStartup,
},
session: {
promptTemplates: [],
extensionRunner: undefined,
resourceLoader: {
getPathMetadata: () => new Map(),
getAgentsFiles: () => ({ agentsFiles: [] }),
getSkills: () => ({
skills: options.skills ?? [],
diagnostics: options.skillDiagnostics ?? [],
}),
getPrompts: () => ({ prompts: [], diagnostics: [] }),
getExtensions: () => ({ errors: [] }),
getThemes: () => ({ themes: [], diagnostics: [] }),
},
},
formatDisplayPath: (p: string) => p,
buildScopeGroups: () => [],
formatScopeGroups: () => "resource-list",
getShortPath: (p: string) => p,
formatDiagnostics: () => "diagnostics",
};
return fakeThis;
}
test("does not show verbose listing on quiet startup during reload", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
skills: [{ filePath: "/tmp/skill/SKILL.md" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
extensionPaths: ["/tmp/ext/index.ts"],
force: false,
showDiagnosticsWhenQuiet: true,
});
expect(fakeThis.chatContainer.children).toHaveLength(0);
});
test("still shows diagnostics on quiet startup when requested", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
skills: [{ filePath: "/tmp/skill/SKILL.md" }],
skillDiagnostics: [{ type: "warning", message: "duplicate skill name" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
showDiagnosticsWhenQuiet: true,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Skill conflicts]");
expect(output).not.toContain("[Skills]");
});
});