mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Own executable- and vault-bound resources in one disposable runtime. On executable changes, detach stable view shells, dispose all bound work, and publish a fully attached replacement runtime.
218 lines
7.8 KiB
TypeScript
218 lines
7.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { FileSystemAdapter } from "obsidian";
|
|
import { vi } from "vitest";
|
|
|
|
import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS } from "../../src/constants";
|
|
import type { Thread } from "../../src/domain/threads/model";
|
|
import { createThreadGoalOperationCoordinator } from "../../src/features/chat/application/threads/goal-actions";
|
|
import type { CodexChatHost } from "../../src/features/chat/host/contracts";
|
|
import type { CodexChatView } from "../../src/features/chat/host/view.obsidian";
|
|
import { createThreadNameMutationCoordinator } from "../../src/features/threads/workflows/thread-name-mutation-coordinator";
|
|
import type CodexPanelPlugin from "../../src/main";
|
|
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../src/settings/model";
|
|
import { createKeyedOperationQueue } from "../../src/shared/runtime/keyed-operation-queue";
|
|
import { chatPanelSettingsAccess } from "../features/chat/support/settings";
|
|
|
|
export async function pluginWithLeaves(leaves: TestLeaf[], options: { threadsLeaves?: TestLeaf[] } = {}): Promise<CodexPanelPlugin> {
|
|
const { default: CodexPanelPlugin } = await import("../../src/main");
|
|
const adapter = new FileSystemAdapter();
|
|
vi.spyOn(adapter, "getBasePath").mockReturnValue("/vault");
|
|
const plugin = new CodexPanelPlugin(
|
|
{
|
|
vault: { adapter },
|
|
workspace: {
|
|
getLeavesOfType: vi.fn((type: string) => {
|
|
if (type === VIEW_TYPE_CODEX_PANEL) return leaves;
|
|
if (type === VIEW_TYPE_CODEX_THREADS) return options.threadsLeaves ?? [];
|
|
return [];
|
|
}),
|
|
revealLeaf: vi.fn().mockResolvedValue(undefined),
|
|
getRightLeaf: vi.fn(() => null),
|
|
createLeafInParent: vi.fn(() => null),
|
|
getMostRecentLeaf: vi.fn(() => null),
|
|
getActiveViewOfType: vi.fn(() => null),
|
|
ensureSideLeaf: vi.fn(() => Promise.reject(new Error("Unexpected ensureSideLeaf call."))),
|
|
on: vi.fn(() => ({})),
|
|
activeLeaf: null,
|
|
rightSplit: {},
|
|
},
|
|
} as never,
|
|
{} as never,
|
|
);
|
|
plugin.settings = { ...DEFAULT_SETTINGS };
|
|
plugin.vaultPath = "/vault";
|
|
plugin.runtime.initialize();
|
|
return plugin;
|
|
}
|
|
|
|
export async function publishCodexPath(plugin: CodexPanelPlugin, codexPath: string): Promise<void> {
|
|
await plugin.runtime.settingTabHost().publishSettings({ ...plugin.settings, codexPath });
|
|
}
|
|
|
|
export interface TestLeaf {
|
|
view: unknown;
|
|
getViewState: ReturnType<typeof vi.fn>;
|
|
getRoot: ReturnType<typeof vi.fn>;
|
|
parent: object;
|
|
setViewState: ReturnType<typeof vi.fn>;
|
|
loadIfDeferred: ReturnType<typeof vi.fn>;
|
|
detach: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
export function leaf(options: { state?: Record<string, unknown> } = {}): TestLeaf {
|
|
return {
|
|
view: null,
|
|
getViewState: vi.fn(() => ({ type: VIEW_TYPE_CODEX_PANEL, state: options.state ?? {} })),
|
|
getRoot: vi.fn(() => ({})),
|
|
parent: {},
|
|
setViewState: vi.fn().mockResolvedValue(undefined),
|
|
loadIfDeferred: vi.fn().mockResolvedValue(undefined),
|
|
detach: vi.fn(),
|
|
};
|
|
}
|
|
|
|
export function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf): CodexChatView {
|
|
const containerEl = document.createElement("div");
|
|
containerEl.createDiv();
|
|
containerEl.createDiv();
|
|
const view = new CodexChatViewCtor(
|
|
{
|
|
...leaf,
|
|
app: {
|
|
workspace: {
|
|
getActiveFile: vi.fn(() => null),
|
|
getLastOpenFiles: vi.fn(() => []),
|
|
on: vi.fn(() => ({})),
|
|
openLinkText: vi.fn(),
|
|
requestSaveLayout: vi.fn(),
|
|
},
|
|
vault: {
|
|
on: vi.fn(() => ({})),
|
|
offref: vi.fn(),
|
|
getFiles: vi.fn(() => []),
|
|
getMarkdownFiles: vi.fn(() => []),
|
|
getAbstractFileByPath: vi.fn(() => null),
|
|
},
|
|
metadataCache: {
|
|
on: vi.fn(() => ({})),
|
|
offref: vi.fn(),
|
|
getFirstLinkpathDest: vi.fn(() => null),
|
|
fileToLinktext: vi.fn((_file: unknown, _sourcePath: string) => ""),
|
|
getFileCache: vi.fn(() => null),
|
|
},
|
|
},
|
|
containerEl,
|
|
} as never,
|
|
{
|
|
attachChatView: (runtimeView) => {
|
|
runtimeView.attachRuntime(chatHostFixture());
|
|
runtimeView.activateRuntime();
|
|
},
|
|
detachChatView: (runtimeView) => {
|
|
runtimeView.detachRuntime();
|
|
},
|
|
},
|
|
);
|
|
const workspace = view.app.workspace as unknown as {
|
|
getActiveViewOfType?: ReturnType<typeof vi.fn>;
|
|
};
|
|
workspace.getActiveViewOfType ??= vi.fn();
|
|
workspace.getActiveViewOfType.mockReturnValue(view);
|
|
return view;
|
|
}
|
|
|
|
function chatHostFixture(): CodexChatHost {
|
|
const settings: CodexPanelSettings = { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" };
|
|
return {
|
|
appServerClientAccess: {
|
|
withClient: vi.fn(() => Promise.reject(new Error("Unexpected app-server client request."))),
|
|
},
|
|
appServerContext: { codexPath: settings.codexPath, vaultPath: "/vault" },
|
|
threadNameMutations: createThreadNameMutationCoordinator(),
|
|
threadTitleTransport: {
|
|
persistedContext: vi.fn().mockResolvedValue(null),
|
|
generateTitle: vi.fn().mockResolvedValue(null),
|
|
},
|
|
threadGoalOperations: createThreadGoalOperationCoordinator(),
|
|
runtimeSettingsCommitQueue: createKeyedOperationQueue(),
|
|
settingsRef: {
|
|
settings: chatPanelSettingsAccess(settings),
|
|
vaultPath: "/vault",
|
|
},
|
|
workspace: {
|
|
openThreadInNewView: vi.fn(),
|
|
openThreadFromPanel: vi.fn(),
|
|
focusThreadInOpenView: vi.fn(),
|
|
threadHasPendingOrRunningPanel: vi.fn(() => false),
|
|
openTurnDiff: vi.fn(),
|
|
notifyPanelActivityChanged: vi.fn(),
|
|
openSideChat: vi.fn(),
|
|
},
|
|
appServerQueries: {
|
|
appServerMetadataSnapshot: vi.fn(() => null),
|
|
refreshAppServerMetadata: vi.fn(() => Promise.resolve()),
|
|
refreshSkills: vi.fn(() => Promise.resolve()),
|
|
refreshRateLimits: vi.fn(() => Promise.resolve()),
|
|
modelsSnapshot: vi.fn(() => null),
|
|
fetchModels: vi.fn(() => Promise.resolve([])),
|
|
refreshModels: vi.fn(() => Promise.resolve([])),
|
|
observeAppServerMetadataResources: vi.fn(() => () => undefined),
|
|
observeModelsResult: vi.fn(() => () => undefined),
|
|
},
|
|
threadCatalog: {
|
|
apply: vi.fn(),
|
|
loadActive: vi.fn(() => Promise.resolve([])),
|
|
refreshActive: vi.fn(() => Promise.resolve([])),
|
|
activeSnapshot: vi.fn(() => null),
|
|
recentActiveSnapshot: vi.fn(() => null),
|
|
hasMoreActive: vi.fn(() => false),
|
|
loadMoreActive: vi.fn(() => Promise.resolve([])),
|
|
observeActive: vi.fn(() => () => undefined),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function threadListClient(fetchThreads: () => Promise<readonly Thread[]>): never {
|
|
return {
|
|
request: async (method: string) => {
|
|
if (method !== "thread/list") throw new Error(`Unexpected app-server request: ${method}`);
|
|
return { data: await fetchThreads(), nextCursor: null };
|
|
},
|
|
} as never;
|
|
}
|
|
|
|
export async function flushMicrotasks(): Promise<void> {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
|
|
export function thread(id: string): Thread {
|
|
return {
|
|
id,
|
|
preview: id,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
name: null,
|
|
archived: false,
|
|
provenance: { kind: "interactive" },
|
|
};
|
|
}
|
|
|
|
export function panelSnapshot(overrides: PanelSnapshotFixtureOverrides = {}): ReturnType<CodexChatView["surface"]["openPanelSnapshot"]> {
|
|
const { pendingApprovals, pendingUserInputs, pendingMcpElicitations, ...snapshotOverrides } = overrides;
|
|
return {
|
|
viewId: "view",
|
|
threadId: "thread",
|
|
turnBusy: false,
|
|
pending: (pendingApprovals ?? 0) + (pendingUserInputs ?? 0) + (pendingMcpElicitations ?? 0) > 0,
|
|
hasComposerDraft: false,
|
|
connected: true,
|
|
...snapshotOverrides,
|
|
};
|
|
}
|
|
|
|
type PanelSnapshotFixtureOverrides = Partial<ReturnType<CodexChatView["surface"]["openPanelSnapshot"]>> & {
|
|
pendingApprovals?: number;
|
|
pendingUserInputs?: number;
|
|
pendingMcpElicitations?: number;
|
|
};
|