mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
142 lines
5.7 KiB
TypeScript
142 lines
5.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { AppServerClient } from "../../../src/app-server/connection/client";
|
|
import type { ArchiveThreadResult } from "../../../src/app-server/services/thread-archive";
|
|
import type { ArchiveExportDestination } from "../../../src/app-server/services/thread-archive-markdown";
|
|
import { createThreadOperations, type ThreadOperationsHost } from "../../../src/features/threads/thread-operations";
|
|
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
|
|
|
|
const archiveMock = vi.hoisted(() => ({
|
|
archiveThreadOnAppServer: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../../src/app-server/services/thread-archive", () => ({
|
|
archiveThreadOnAppServer: archiveMock.archiveThreadOnAppServer,
|
|
}));
|
|
|
|
describe("ThreadOperations", () => {
|
|
it("renames a thread and notifies shared surfaces after success", async () => {
|
|
const { operations, client, catalog } = operationsFixture();
|
|
|
|
await expect(operations.renameThread("thread", " Saved title ")).resolves.toBe(true);
|
|
|
|
expect(client?.setThreadName).toHaveBeenCalledWith("thread", "Saved title");
|
|
expect(catalog.apply).toHaveBeenCalledWith({ type: "thread-renamed", threadId: "thread", name: "Saved title" });
|
|
});
|
|
|
|
it("can skip rename publication when the caller invalidates the save", async () => {
|
|
const { operations, catalog } = operationsFixture();
|
|
|
|
await operations.renameThread("thread", "Generated title", { shouldPublish: () => false });
|
|
|
|
expect(catalog.apply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("archives a thread, reports exported markdown, and notifies shared surfaces", async () => {
|
|
const { operations, catalog, notice } = operationsFixture();
|
|
archiveMock.archiveThreadOnAppServer.mockResolvedValueOnce({ exportedPath: "Archive/thread.md" } satisfies ArchiveThreadResult);
|
|
|
|
await expect(operations.archiveThread("thread", { saveMarkdown: true, closeOpenPanels: true })).resolves.toEqual({
|
|
exportedPath: "Archive/thread.md",
|
|
});
|
|
|
|
expect(notice).toHaveBeenCalledWith("Saved archived thread to Archive/thread.md.");
|
|
expect(catalog.apply).toHaveBeenCalledWith({
|
|
type: "thread-archived",
|
|
threadId: "thread",
|
|
options: { closeOpenPanels: true },
|
|
});
|
|
});
|
|
|
|
it("does not notify surfaces when an operation has no current client", async () => {
|
|
const { operations, catalog } = operationsFixture({ client: null });
|
|
|
|
await expect(operations.renameThread("thread", "Title")).rejects.toThrow("No current client.");
|
|
await expect(operations.archiveThread("thread")).rejects.toThrow("No current client.");
|
|
|
|
expect(catalog.apply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not publish stale rename results after the current client changes", async () => {
|
|
const firstClient = clientMock();
|
|
const secondClient = clientMock();
|
|
let currentClient: MockClient | null = firstClient;
|
|
const { operations, catalog } = operationsFixture({ client: () => currentClient });
|
|
firstClient.setThreadName.mockImplementationOnce(async () => {
|
|
currentClient = secondClient;
|
|
});
|
|
|
|
await expect(operations.renameThread("thread", "Title")).rejects.toThrow("Client changed.");
|
|
|
|
expect(catalog.apply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not publish stale archive results after the current client changes", async () => {
|
|
const firstClient = clientMock();
|
|
const secondClient = clientMock();
|
|
let currentClient: MockClient | null = firstClient;
|
|
const { operations, catalog } = operationsFixture({ client: () => currentClient });
|
|
archiveMock.archiveThreadOnAppServer.mockImplementationOnce(async () => {
|
|
currentClient = secondClient;
|
|
return { exportedPath: null } satisfies ArchiveThreadResult;
|
|
});
|
|
|
|
await expect(operations.archiveThread("thread")).rejects.toThrow("Client changed.");
|
|
|
|
expect(catalog.apply).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function operationsFixture(options: { client?: MockClient | null | (() => MockClient | null) } = {}) {
|
|
archiveMock.archiveThreadOnAppServer.mockReset();
|
|
archiveMock.archiveThreadOnAppServer.mockResolvedValue({ exportedPath: null } satisfies ArchiveThreadResult);
|
|
const configuredClient = options.client === undefined ? clientMock() : options.client;
|
|
const currentClient = typeof configuredClient === "function" ? configuredClient : () => configuredClient;
|
|
const catalog = {
|
|
apply: vi.fn(),
|
|
};
|
|
const notice = vi.fn();
|
|
const host: ThreadOperationsHost = {
|
|
clientAccess: {
|
|
withClient: async (operation) => {
|
|
const client = currentClient() as AppServerClient | null;
|
|
if (!client) throw new Error("No current client.");
|
|
const result = await operation(client);
|
|
if ((currentClient() as AppServerClient | null) !== client) throw new Error("Client changed.");
|
|
return result;
|
|
},
|
|
},
|
|
archiveExport: {
|
|
settings: () => ({
|
|
archiveExportFolderTemplate: DEFAULT_SETTINGS.archiveExportFolderTemplate,
|
|
archiveExportFilenameTemplate: DEFAULT_SETTINGS.archiveExportFilenameTemplate,
|
|
archiveExportTags: DEFAULT_SETTINGS.archiveExportTags,
|
|
}),
|
|
enabled: () => false,
|
|
vaultPath: "/vault",
|
|
vaultConfigDir: "vault-config",
|
|
},
|
|
archiveDestination: () => archiveDestinationMock(),
|
|
catalog,
|
|
notice,
|
|
};
|
|
return { operations: createThreadOperations(host), client: currentClient(), catalog, notice };
|
|
}
|
|
|
|
type MockClient = ReturnType<typeof clientMock>;
|
|
|
|
function clientMock() {
|
|
return {
|
|
setThreadName: vi.fn().mockResolvedValue({}),
|
|
archiveThread: vi.fn().mockResolvedValue({}),
|
|
};
|
|
}
|
|
|
|
function archiveDestinationMock(): ArchiveExportDestination {
|
|
return {
|
|
normalizePath: (path) => path,
|
|
exists: vi.fn().mockResolvedValue(false),
|
|
createFolder: vi.fn().mockResolvedValue(undefined),
|
|
createMarkdownFile: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
}
|