Files
2026-07-13 13:39:12 +08:00

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);
});
});