194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
|
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
|
|
|
const NAVIGATION_TIMEOUT_MS = 300_000;
|
|
|
|
type MemoryConfig = {
|
|
enabled: boolean;
|
|
maxTokens: number;
|
|
retentionDays: number;
|
|
strategy: "recent" | "semantic" | "hybrid";
|
|
skillsEnabled: boolean;
|
|
};
|
|
|
|
type MemoryEntry = {
|
|
id: string;
|
|
apiKeyId: string;
|
|
sessionId: string | null;
|
|
type: "factual" | "episodic" | "procedural" | "semantic";
|
|
key: string;
|
|
content: string;
|
|
metadata: Record<string, unknown>;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
expiresAt: string | null;
|
|
};
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function setRangeValue(page: Page, testId: string, value: number) {
|
|
await page.getByTestId(testId).evaluate((element, nextValue) => {
|
|
const input = element as HTMLInputElement;
|
|
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
valueSetter?.call(input, String(nextValue));
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}, value);
|
|
}
|
|
|
|
test.describe("Memory settings", () => {
|
|
test.setTimeout(600_000);
|
|
|
|
test("updates memory config in settings and deletes stored memory entries", async ({ page }) => {
|
|
const state: {
|
|
config: MemoryConfig;
|
|
settings: { skillsmpApiKey: string };
|
|
memories: MemoryEntry[];
|
|
updateCalls: number;
|
|
deleteCalls: number;
|
|
} = {
|
|
config: {
|
|
enabled: false,
|
|
maxTokens: 2000,
|
|
retentionDays: 30,
|
|
strategy: "hybrid",
|
|
skillsEnabled: false,
|
|
},
|
|
settings: {
|
|
skillsmpApiKey: "",
|
|
},
|
|
memories: [
|
|
{
|
|
id: "mem-1",
|
|
apiKeyId: "key-1",
|
|
sessionId: "session-a",
|
|
type: "factual",
|
|
key: "preferred_language",
|
|
content: "The user prefers answers in Portuguese.",
|
|
metadata: {},
|
|
createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(),
|
|
updatedAt: new Date("2026-04-05T20:00:00.000Z").toISOString(),
|
|
expiresAt: null,
|
|
},
|
|
],
|
|
updateCalls: 0,
|
|
deleteCalls: 0,
|
|
};
|
|
|
|
await page.route(/\/api\/settings$/, async (route) => {
|
|
const method = route.request().method();
|
|
if (method === "GET") {
|
|
await fulfillJson(route, state.settings);
|
|
return;
|
|
}
|
|
if (method === "PATCH") {
|
|
const payload = (route.request().postDataJSON() as Record<string, unknown>) || {};
|
|
state.settings = {
|
|
...state.settings,
|
|
...(typeof payload.skillsmpApiKey === "string"
|
|
? { skillsmpApiKey: payload.skillsmpApiKey }
|
|
: {}),
|
|
};
|
|
await fulfillJson(route, state.settings);
|
|
return;
|
|
}
|
|
await fulfillJson(route, { error: "Method not allowed in settings stub" }, 405);
|
|
});
|
|
|
|
await page.route(/\/api\/settings\/memory$/, async (route) => {
|
|
const method = route.request().method();
|
|
if (method === "GET") {
|
|
await fulfillJson(route, state.config);
|
|
return;
|
|
}
|
|
if (method === "PUT") {
|
|
state.updateCalls += 1;
|
|
const payload = (route.request().postDataJSON() as Partial<MemoryConfig>) || {};
|
|
state.config = {
|
|
...state.config,
|
|
...payload,
|
|
};
|
|
await fulfillJson(route, state.config);
|
|
return;
|
|
}
|
|
await fulfillJson(route, { error: "Method not allowed in memory settings stub" }, 405);
|
|
});
|
|
|
|
await page.route(/\/api\/memory(?:\?.*)?$/, async (route) => {
|
|
await fulfillJson(route, {
|
|
data: state.memories,
|
|
total: state.memories.length,
|
|
totalPages: 1,
|
|
stats: {
|
|
total: state.memories.length,
|
|
tokensUsed: state.memories.length * 24,
|
|
hitRate: state.memories.length > 0 ? 0.75 : 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.route(/\/api\/memory\/[^/]+$/, async (route) => {
|
|
state.deleteCalls += 1;
|
|
const memoryId = route.request().url().split("/").pop() || "";
|
|
state.memories = state.memories.filter((memory) => memory.id !== memoryId);
|
|
await fulfillJson(route, { success: true });
|
|
});
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/settings/ai", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
let settingsHydrationRetries = 0;
|
|
await expect(async () => {
|
|
if (settingsHydrationRetries++ > 0) {
|
|
await page.reload({ waitUntil: "commit" }).catch(() => {});
|
|
}
|
|
await expect(page.getByTestId("memory-enabled-switch")).toBeVisible({ timeout: 15000 });
|
|
}).toPass({ timeout: 45_000, intervals: [1000, 2500, 5000] });
|
|
await expect(page.getByTestId("memory-enabled-switch")).toHaveAttribute(
|
|
"aria-checked",
|
|
"false"
|
|
);
|
|
|
|
await page.getByTestId("memory-enabled-switch").click();
|
|
await expect(page.getByTestId("memory-enabled-switch")).toHaveAttribute("aria-checked", "true");
|
|
await expect.poll(() => state.config.enabled).toBe(true);
|
|
|
|
await setRangeValue(page, "memory-retention-slider", 45);
|
|
await expect.poll(() => state.config.retentionDays).toBe(45);
|
|
|
|
await page.getByTestId("memory-strategy-recent").click();
|
|
await expect.poll(() => state.config.strategy).toBe("recent");
|
|
await expect.poll(() => state.updateCalls).toBeGreaterThanOrEqual(3);
|
|
|
|
await page.getByTestId("memory-enabled-switch").click();
|
|
await expect(page.getByTestId("memory-enabled-switch")).toHaveAttribute(
|
|
"aria-checked",
|
|
"false"
|
|
);
|
|
await expect.poll(() => state.config.enabled).toBe(false);
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/memory", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
let memoryHydrationRetries = 0;
|
|
await expect(async () => {
|
|
if (memoryHydrationRetries++ > 0) {
|
|
await page.reload({ waitUntil: "commit" }).catch(() => {});
|
|
}
|
|
await expect(page.getByText("preferred_language")).toBeVisible({ timeout: 15000 });
|
|
}).toPass({ timeout: 45_000, intervals: [1000, 2500, 5000] });
|
|
await page.getByRole("button", { name: /delete/i }).click();
|
|
|
|
await expect.poll(() => state.deleteCalls).toBe(1);
|
|
await expect(page.getByText("preferred_language")).toHaveCount(0);
|
|
});
|
|
});
|