pi-mono/packages/coding-agent/test/interactive-mode-status.test.ts
2026-05-07 15:59:42 +02:00

859 lines
28 KiB
TypeScript

import { homedir } from "node:os";
import * as path from "node:path";
import { type AutocompleteProvider, CombinedAutocompleteProvider, Container } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, test, vi } from "vitest";
import type { AutocompleteProviderFactory } from "../src/core/extensions/types.js";
import type { SourceInfo } from "../src/core/source-info.js";
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");
}
function normalizeRenderedOutput(container: Container, width = 220): string {
return renderAll(container, width)
.replace(/\u001b\[[0-9;]*m/g, "")
.replace(/\\/g, "/")
.split("\n")
.map((line) => line.replace(/\s+$/g, ""))
.join("\n")
.trim();
}
type ExtensionFixture = {
path: string;
sourceInfo?: SourceInfo;
};
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.setToolsExpanded", () => {
test("applies expansion state to the active header and chat entries", () => {
const header = { setExpanded: vi.fn() };
const chatChild = { setExpanded: vi.fn() };
const fakeThis: any = {
toolOutputExpanded: false,
customHeader: undefined,
builtInHeader: header,
chatContainer: { children: [chatChild] },
ui: { requestRender: vi.fn() },
};
(InteractiveMode as any).prototype.setToolsExpanded.call(fakeThis, true);
expect(fakeThis.toolOutputExpanded).toBe(true);
expect(header.setExpanded).toHaveBeenCalledWith(true);
expect(chatChild.setExpanded).toHaveBeenCalledWith(true);
expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1);
});
});
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.createExtensionUIContext addAutocompleteProvider", () => {
test("stores wrapper factories and rebuilds autocomplete immediately", () => {
const wrapper: AutocompleteProviderFactory = (current) => current;
const fakeThis = {
autocompleteProviderWrappers: [] as AutocompleteProviderFactory[],
setupAutocompleteProvider: vi.fn(),
};
const uiContext = (InteractiveMode as any).prototype.createExtensionUIContext.call(fakeThis);
uiContext.addAutocompleteProvider(wrapper);
expect(fakeThis.autocompleteProviderWrappers).toEqual([wrapper]);
expect(fakeThis.setupAutocompleteProvider).toHaveBeenCalledTimes(1);
});
});
describe("InteractiveMode.setupAutocompleteProvider", () => {
test("stacks wrapper factories over a fresh base provider", () => {
const defaultEditor = { setAutocompleteProvider: vi.fn() };
const customEditor = { setAutocompleteProvider: vi.fn() };
const calls: string[] = [];
const wrap1: AutocompleteProviderFactory = (current): AutocompleteProvider => ({
async getSuggestions(lines, cursorLine, cursorCol, options) {
calls.push("getSuggestions:wrap1");
return current.getSuggestions(lines, cursorLine, cursorCol, options);
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
calls.push("applyCompletion:wrap1");
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
calls.push("shouldTrigger:wrap1");
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
});
const wrap2: AutocompleteProviderFactory = (current): AutocompleteProvider => ({
async getSuggestions(lines, cursorLine, cursorCol, options) {
calls.push("getSuggestions:wrap2");
return current.getSuggestions(lines, cursorLine, cursorCol, options);
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
calls.push("applyCompletion:wrap2");
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
calls.push("shouldTrigger:wrap2");
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
});
const fakeThis = {
createBaseAutocompleteProvider: () => new CombinedAutocompleteProvider([], "/tmp/project", undefined),
defaultEditor,
editor: customEditor,
autocompleteProviderWrappers: [wrap1, wrap2],
};
(InteractiveMode as any).prototype.setupAutocompleteProvider.call(fakeThis);
expect(defaultEditor.setAutocompleteProvider).toHaveBeenCalledTimes(1);
expect(customEditor.setAutocompleteProvider).toHaveBeenCalledTimes(1);
const provider = defaultEditor.setAutocompleteProvider.mock.calls[0]?.[0] as AutocompleteProvider;
expect(provider).toBe(customEditor.setAutocompleteProvider.mock.calls[0]?.[0]);
expect(provider.shouldTriggerFileCompletion?.(["foo"], 0, 3)).toBe(true);
expect(calls).toEqual(["shouldTrigger:wrap2", "shouldTrigger:wrap1"]);
});
});
describe("InteractiveMode.showLoadedResources", () => {
beforeAll(() => {
initTheme("dark");
});
function createShowLoadedResourcesThis(options: {
quietStartup: boolean;
verbose?: boolean;
toolOutputExpanded?: boolean;
cwd?: string;
contextFiles?: Array<{ path: string; content?: string }>;
extensions?: ExtensionFixture[];
skills?: Array<{ filePath: string; name: string }>;
skillDiagnostics?: Array<{ type: "warning" | "error" | "collision"; message: string }>;
useRealScopeGroups?: boolean;
}) {
const fakeThis: any = {
options: { verbose: options.verbose ?? false },
toolOutputExpanded: options.toolOutputExpanded ?? false,
chatContainer: new Container(),
settingsManager: {
getQuietStartup: () => options.quietStartup,
},
sessionManager: {
getCwd: () => options.cwd ?? "/tmp/project",
},
session: {
promptTemplates: [],
extensionRunner: {
getCommandDiagnostics: () => [],
getShortcutDiagnostics: () => [],
},
resourceLoader: {
getPathMetadata: () => new Map(),
getAgentsFiles: () => ({ agentsFiles: options.contextFiles ?? [] }),
getSkills: () => ({
skills: options.skills ?? [],
diagnostics: options.skillDiagnostics ?? [],
}),
getPrompts: () => ({ prompts: [], diagnostics: [] }),
getExtensions: () => ({ extensions: options.extensions ?? [], errors: [], runtime: {} }),
getThemes: () => ({ themes: [], diagnostics: [] }),
},
},
formatDisplayPath: (p: string) => (InteractiveMode as any).prototype.formatDisplayPath.call(fakeThis, p),
formatExtensionDisplayPath: (p: string) =>
(InteractiveMode as any).prototype.formatExtensionDisplayPath.call(fakeThis, p),
formatContextPath: (p: string) => (InteractiveMode as any).prototype.formatContextPath.call(fakeThis, p),
getStartupExpansionState: () => (InteractiveMode as any).prototype.getStartupExpansionState.call(fakeThis),
buildScopeGroups: () => [],
formatScopeGroups: () => "resource-list",
isPackageSource: (sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.isPackageSource.call(fakeThis, sourceInfo),
getShortPath: (p: string, sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.getShortPath.call(fakeThis, p, sourceInfo),
getCompactPathLabel: (p: string, sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.getCompactPathLabel.call(fakeThis, p, sourceInfo),
getCompactPackageSourceLabel: (sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.getCompactPackageSourceLabel.call(fakeThis, sourceInfo),
getCompactExtensionLabel: (p: string, sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.getCompactExtensionLabel.call(fakeThis, p, sourceInfo),
getCompactDisplayPathSegments: (p: string) =>
(InteractiveMode as any).prototype.getCompactDisplayPathSegments.call(fakeThis, p),
getCompactNonPackageExtensionLabel: (
p: string,
index: number,
allPaths: Array<{ path: string; segments: string[] }>,
) => (InteractiveMode as any).prototype.getCompactNonPackageExtensionLabel.call(fakeThis, p, index, allPaths),
getCompactExtensionLabels: (extensions: ExtensionFixture[]) =>
(InteractiveMode as any).prototype.getCompactExtensionLabels.call(fakeThis, extensions),
formatDiagnostics: () => "diagnostics",
getBuiltInCommandConflictDiagnostics: () => [],
};
if (options.useRealScopeGroups) {
fakeThis.getScopeGroup = (sourceInfo?: SourceInfo) =>
(InteractiveMode as any).prototype.getScopeGroup.call(fakeThis, sourceInfo);
fakeThis.buildScopeGroups = (items: Array<{ path: string; sourceInfo?: SourceInfo }>) =>
(InteractiveMode as any).prototype.buildScopeGroups.call(fakeThis, items);
fakeThis.formatScopeGroups = (groups: unknown, formatOptions: unknown) =>
(InteractiveMode as any).prototype.formatScopeGroups.call(fakeThis, groups, formatOptions);
}
return fakeThis;
}
function createSourceInfo(
filePath: string,
options: {
source: string;
scope: "user" | "project" | "temporary";
origin: "package" | "top-level";
baseDir?: string;
},
): SourceInfo {
return {
path: filePath,
source: options.source,
scope: options.scope,
origin: options.origin,
baseDir: options.baseDir,
};
}
function createExtensionFixtures(): ExtensionFixture[] {
return [
{
path: "/tmp/project/.pi/extensions/answer.ts",
sourceInfo: createSourceInfo("/tmp/project/.pi/extensions/answer.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/project/.pi/extensions",
}),
},
{
path: "/tmp/project/.pi/extensions/local-index/index.ts",
sourceInfo: createSourceInfo("/tmp/project/.pi/extensions/local-index/index.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/project/.pi/extensions",
}),
},
{
path: "/tmp/agent/extensions/user-index/index.ts",
sourceInfo: createSourceInfo("/tmp/agent/extensions/user-index/index.ts", {
source: "local",
scope: "user",
origin: "top-level",
baseDir: "/tmp/agent/extensions",
}),
},
{
path: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts",
sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", {
source: "npm:pi-markdown-preview",
scope: "project",
origin: "package",
baseDir: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview",
}),
},
{
path: "/tmp/project/.pi/npm/node_modules/@scope/pi-scoped/extensions/index.ts",
sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/@scope/pi-scoped/extensions/index.ts", {
source: "npm:@scope/pi-scoped",
scope: "project",
origin: "package",
baseDir: "/tmp/project/.pi/npm/node_modules/@scope/pi-scoped",
}),
},
{
path: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/index.ts",
sourceInfo: createSourceInfo(
"/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/index.ts",
{
source: "git:github.com/HazAT/pi-interactive-subagents",
scope: "project",
origin: "package",
baseDir: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents",
},
),
},
{
path: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/subagents/index.ts",
sourceInfo: createSourceInfo(
"/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/subagents/index.ts",
{
source: "git:github.com/HazAT/pi-interactive-subagents",
scope: "project",
origin: "package",
baseDir: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents",
},
),
},
{
path: "/tmp/temp/cli-extension.ts",
sourceInfo: createSourceInfo("/tmp/temp/cli-extension.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/temp",
}),
},
];
}
test("shows a compact resource listing by default", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Skills]");
expect(output).toContain("commit");
expect(output).not.toContain("resource-list");
});
test("shows full resource listing when expanded", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
toolOutputExpanded: true,
skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Skills]");
expect(output).toContain("resource-list");
expect(output).not.toContain("commit");
});
test("shows full resource listing on verbose startup even when tool output is collapsed", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
verbose: true,
toolOutputExpanded: false,
skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Skills]");
expect(output).toContain("resource-list");
expect(output).not.toContain("commit");
});
test("abbreviates extensions in compact listing", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions: [{ path: "/tmp/extensions/answer.ts" }, { path: "/tmp/extensions/btw.ts" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Extensions]");
expect(output).toContain("answer.ts, btw.ts");
expect(output).not.toContain("extensions/answer.ts");
});
test("captures mixed extension layouts in compact output", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions: createExtensionFixtures(),
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
@scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`);
});
test("adds more parent folders until local extension labels are unique", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/alpha/one/index.ts",
sourceInfo: createSourceInfo("/tmp/alpha/one/index.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/alpha",
}),
},
{
path: "/tmp/beta/one/index.ts",
sourceInfo: createSourceInfo("/tmp/beta/one/index.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/beta",
}),
},
{
path: "/tmp/gamma/one/index.ts",
sourceInfo: createSourceInfo("/tmp/gamma/one/index.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/gamma",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
alpha/one, beta/one, gamma/one"`);
});
test("strips index.ts from local extension label, showing parent dir", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/extensions/plan-mode/index.ts",
sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
plan-mode"`);
});
test("strips index.js from local extension label, showing parent dir", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/extensions/plan-mode/index.js",
sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.js", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
plan-mode"`);
});
test("mixed single-file and subdirectory index.ts extensions strip index.ts", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/extensions/webfetch.ts",
sourceInfo: createSourceInfo("/tmp/extensions/webfetch.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
{
path: "/tmp/extensions/plan-mode/index.ts",
sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
plan-mode, webfetch.ts"`);
});
test("multiple index.ts with unique parent dirs need no disambiguation", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/extensions/foo/index.ts",
sourceInfo: createSourceInfo("/tmp/extensions/foo/index.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
{
path: "/tmp/extensions/bar/index.ts",
sourceInfo: createSourceInfo("/tmp/extensions/bar/index.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
bar, foo"`);
});
test("multiple index.ts with same parent dir name disambiguated with grandparent", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/alpha/tools/index.ts",
sourceInfo: createSourceInfo("/tmp/alpha/tools/index.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/alpha",
}),
},
{
path: "/tmp/beta/tools/index.ts",
sourceInfo: createSourceInfo("/tmp/beta/tools/index.ts", {
source: "cli",
scope: "temporary",
origin: "top-level",
baseDir: "/tmp/beta",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
alpha/tools, beta/tools"`);
});
test("non-index file in subdirectory stays as filename", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/extensions/my-ext/main.ts",
sourceInfo: createSourceInfo("/tmp/extensions/my-ext/main.ts", {
source: "local",
scope: "project",
origin: "top-level",
baseDir: "/tmp/extensions",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
main.ts"`);
});
test("package extensions still strip index.ts correctly (regression guard)", () => {
const extensions: ExtensionFixture[] = [
{
path: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts",
sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", {
source: "npm:pi-markdown-preview",
scope: "project",
origin: "package",
baseDir: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview",
}),
},
];
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
extensions,
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
pi-markdown-preview"`);
});
test("captures mixed extension layouts in expanded output", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
toolOutputExpanded: true,
extensions: createExtensionFixtures(),
useRealScopeGroups: true,
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`
"[Extensions]
project
/tmp/project/.pi/extensions/answer.ts
/tmp/project/.pi/extensions/local-index
git:github.com/HazAT/pi-interactive-subagents
extensions
extensions/subagents
npm:@scope/pi-scoped
extensions
npm:pi-markdown-preview
extensions
user
/tmp/agent/extensions/user-index
path
/tmp/temp/cli-extension.ts"`);
});
test("shows context paths relative to cwd while preserving full external paths", () => {
const home = homedir();
const cwd = path.join(home, "Development", "pi-mono");
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
cwd,
contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/");
expect(output).toContain("[Context]");
expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md");
expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`);
});
test("shows full context paths when expanded", () => {
const home = homedir();
const cwd = path.join(home, "Development", "pi-mono");
const fakeThis = createShowLoadedResourcesThis({
quietStartup: false,
toolOutputExpanded: true,
cwd,
contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
});
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/");
expect(output).toContain("[Context]");
expect(output).toContain("~/.pi/agent/AGENTS.md");
expect(output).toContain("~/Development/pi-mono/AGENTS.md");
expect(output).not.toContain("~/.pi/agent/AGENTS.md, AGENTS.md");
});
test("does not show verbose listing on quiet startup during reload", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
extensions: [{ path: "/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", name: "commit" }],
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]");
});
});