mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Add memory settings UX tests
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
f4ce06638f
commit
22b4d8a176
2 files changed
+308
No files matched your search
@@ -0,0 +1,173 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { MemoryProviderDescriptor } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MemoryProviderConfigForm } from "./MemoryProviderConfigForm";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const provider: MemoryProviderDescriptor = {
|
||||
key: "qmd_memory",
|
||||
displayName: "QMD Memory",
|
||||
description: "Markdown memory",
|
||||
kind: "plugin",
|
||||
pluginId: "paperclip.qmd-memory",
|
||||
capabilities: {
|
||||
browse: true,
|
||||
correction: false,
|
||||
asyncIngestion: false,
|
||||
providerManagedExtraction: false,
|
||||
},
|
||||
configSchema: null,
|
||||
configMetadata: {
|
||||
suggestedConfig: {
|
||||
searchMode: "query",
|
||||
topK: 5,
|
||||
apiToken: "",
|
||||
autoIndexOnWrite: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
key: "searchMode",
|
||||
label: "Search mode",
|
||||
description: "How QMD should retrieve matching memory files.",
|
||||
input: "select",
|
||||
options: [
|
||||
{ value: "query", label: "Query" },
|
||||
{ value: "search", label: "Search" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "topK",
|
||||
label: "Hydration snippets",
|
||||
description: "Maximum snippets to include in a run preamble.",
|
||||
input: "number",
|
||||
min: 1,
|
||||
max: 25,
|
||||
},
|
||||
{
|
||||
key: "apiToken",
|
||||
label: "API token",
|
||||
input: "secret",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "autoIndexOnWrite",
|
||||
label: "Auto-index on write",
|
||||
input: "boolean",
|
||||
},
|
||||
],
|
||||
pathSuggestions: [
|
||||
{
|
||||
key: "store",
|
||||
label: "Binding directory",
|
||||
path: "/tmp/paperclip/qmd/default",
|
||||
description: "Markdown records are stored here.",
|
||||
},
|
||||
],
|
||||
healthChecks: [
|
||||
{
|
||||
key: "qmd",
|
||||
label: "QMD binary",
|
||||
status: "warning",
|
||||
message: "qmd was not found on PATH.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe("MemoryProviderConfigForm", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("renders provider metadata fields, secret inputs, paths, health, and advanced JSON", async () => {
|
||||
const onChange = vi.fn();
|
||||
const onValidationChange = vi.fn();
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MemoryProviderConfigForm
|
||||
provider={provider}
|
||||
value={{
|
||||
searchMode: "query",
|
||||
topK: 5,
|
||||
apiToken: "secret-token",
|
||||
autoIndexOnWrite: true,
|
||||
}}
|
||||
onChange={onChange}
|
||||
onValidationChange={onValidationChange}
|
||||
/>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Search mode");
|
||||
expect(container.textContent).toContain("Hydration snippets");
|
||||
expect(container.textContent).toContain("Binding directory");
|
||||
expect(container.textContent).toContain("/tmp/paperclip/qmd/default");
|
||||
expect(container.textContent).toContain("QMD binary");
|
||||
expect(container.textContent).toContain("warning");
|
||||
expect(container.querySelector('input[type="password"]')).not.toBeNull();
|
||||
|
||||
const advancedButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Advanced JSON")
|
||||
);
|
||||
expect(advancedButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
advancedButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.querySelector("textarea")?.value).toContain('"topK": 5');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows field-specific validation and reports invalid config state", async () => {
|
||||
const onChange = vi.fn();
|
||||
const onValidationChange = vi.fn();
|
||||
const root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MemoryProviderConfigForm
|
||||
provider={provider}
|
||||
value={{
|
||||
searchMode: "missing",
|
||||
topK: 99,
|
||||
apiToken: "",
|
||||
autoIndexOnWrite: "yes",
|
||||
}}
|
||||
onChange={onChange}
|
||||
onValidationChange={onValidationChange}
|
||||
/>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Search mode must be one of the listed options.");
|
||||
expect(container.textContent).toContain("Hydration snippets must be at most 25.");
|
||||
expect(container.textContent).toContain("API token is required.");
|
||||
expect(container.textContent).toContain("Auto-index on write must be on or off.");
|
||||
expect(onValidationChange).toHaveBeenLastCalledWith(false);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { MemoryBinding, MemoryResolvedBinding } from "@paperclipai/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ToastProvider } from "../context/ToastContext";
|
||||
import { ProjectMemorySettings } from "./ProjectMemorySettings";
|
||||
|
||||
const memoryApiMocks = vi.hoisted(() => ({
|
||||
listBindings: vi.fn(),
|
||||
getProjectBinding: vi.fn(),
|
||||
setProjectBinding: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../api/memory", () => ({
|
||||
memoryApi: memoryApiMocks,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function tick() {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function createBinding(overrides: Partial<MemoryBinding> = {}): MemoryBinding {
|
||||
return {
|
||||
id: "binding-qmd",
|
||||
companyId: "company-1",
|
||||
key: "qmd-default",
|
||||
name: "QMD default",
|
||||
providerKey: "qmd_memory",
|
||||
config: {},
|
||||
enabled: true,
|
||||
createdAt: new Date("2026-04-20T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-20T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createResolvedBinding(overrides: Partial<MemoryResolvedBinding> = {}): MemoryResolvedBinding {
|
||||
return {
|
||||
companyId: "company-1",
|
||||
targetType: "company",
|
||||
targetId: "company-1",
|
||||
binding: createBinding(),
|
||||
source: "company_default",
|
||||
checkedTargetTypes: ["project", "company"],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ProjectMemorySettings", () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
memoryApiMocks.listBindings.mockReset();
|
||||
memoryApiMocks.getProjectBinding.mockReset();
|
||||
memoryApiMocks.setProjectBinding.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
container.remove();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("shows inherited company memory and saves a project override", async () => {
|
||||
const binding = createBinding();
|
||||
memoryApiMocks.listBindings.mockResolvedValue([binding]);
|
||||
memoryApiMocks.getProjectBinding.mockResolvedValue(createResolvedBinding());
|
||||
memoryApiMocks.setProjectBinding.mockResolvedValue({
|
||||
id: "target-project",
|
||||
companyId: "company-1",
|
||||
bindingId: binding.id,
|
||||
targetType: "project",
|
||||
targetId: "project-1",
|
||||
createdAt: new Date("2026-04-20T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-20T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<ProjectMemorySettings companyId="company-1" projectId="project-1" />
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await tick();
|
||||
await tick();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("QMD default (qmd_memory)");
|
||||
expect(container.textContent).toContain("Source: Company default");
|
||||
|
||||
const select = container.querySelector("select");
|
||||
expect(select).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
select!.value = binding.id;
|
||||
select!.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
const saveButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Save override")
|
||||
);
|
||||
expect(saveButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await tick();
|
||||
});
|
||||
|
||||
expect(memoryApiMocks.setProjectBinding).toHaveBeenCalledWith("project-1", binding.id);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user