evOo-local/apps/web/e2e/knowledge-panel.spec.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

205 lines
8.4 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { expect, test } from "@playwright/test";
import type { MemoryDocument } from "@rakazo/contracts";
import {
activeBotId,
captureScreenshot,
completeOnboarding,
openUserSettings,
rpc,
signup,
} from "./helpers";
test("memory and skills are readable and editable in the app", async ({ page }, testInfo) => {
const stamp = Date.now();
const userName = `Knowledge ${stamp}`;
await signup(page, `knowledge-${stamp}@rakazo.test`, "password12", userName);
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
// Space-wide documents live in Settings → Memory. Open that before bot
// settings so the Knowledge Memory tab cannot steal this click.
await openUserSettings(page, "memory");
await expect(page.getByLabel("Close memory settings")).toBeVisible();
const spaceDocs = page.getByTestId("space-memory-documents");
await expect(spaceDocs.getByText("Shared documents")).toBeVisible();
const memoryRow = spaceDocs.getByRole("button", { name: /MEMORY\.md/ });
await expect(memoryRow).toBeVisible();
await memoryRow.click();
const docEditor = spaceDocs.locator("textarea");
const marker = `Edited in e2e ${stamp}`;
await docEditor.fill(`# Memory\n\n${marker}\n`);
await captureScreenshot(page, testInfo, "83-space-memory-editor");
let releaseSave!: () => void;
const saveGate = new Promise<void>((resolve) => {
releaseSave = resolve;
});
await page.route(
"**/rpc/memory/update",
async (route) => {
await saveGate;
await route.continue();
},
{ times: 1 },
);
await spaceDocs.getByRole("button", { name: "Save", exact: true }).click();
try {
await expect(docEditor).toBeDisabled();
await expect(memoryRow).toBeDisabled();
await expect(spaceDocs.getByRole("button", { name: "Cancel", exact: true })).toBeDisabled();
} finally {
releaseSave();
}
await expect(spaceDocs.getByText("rev 2")).toBeVisible();
// The save persisted: reopen the document and find the marker.
await memoryRow.click();
await expect(docEditor).toHaveValue(new RegExp(marker));
await captureScreenshot(page, testInfo, "84-space-memory-saved");
const sharedDocuments = await rpc<MemoryDocument[]>(page, "memory/list", { scope: "user" });
expect(sharedDocuments).toContainEqual(
expect.objectContaining({ content: `# Memory\n\n${marker}\n`, revision: 2 }),
);
// Export must fetch fresh content and exclude the bot's private document.
const sharedDocument = sharedDocuments.find((doc) => doc.path === "MEMORY.md")!;
const latestMarker = `Latest shared memory ${stamp}`;
await rpc(page, "memory/update", { documentId: sharedDocument.id, content: latestMarker });
const downloadPromise = page.waitForEvent("download");
await spaceDocs.getByRole("button", { name: "Download as markdown" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("space-memory.md");
const exported = await readFile((await download.path())!, "utf8");
expect(exported).toContain(latestMarker);
expect(exported).not.toContain(marker);
expect(exported).not.toContain("# Chief");
await page.getByLabel("Close memory settings").click();
await expect(page.getByLabel("Close memory settings")).toHaveCount(0);
// The bot's Knowledge section lives under Advanced in its settings panel.
await page
.locator("main")
.getByRole("button", { name: /^Chief/ })
.click();
const settings = page.getByTestId("bot-settings");
await expect(settings.getByRole("button", { name: "Save", exact: true })).toBeVisible();
await settings.getByText("Advanced", { exact: true }).click();
const knowledge = settings.getByTestId("bot-knowledge");
await expect(knowledge).toBeVisible();
await expect(knowledge.getByRole("tablist", { name: "Knowledge" })).toBeVisible();
// Bot creation seeds MEMORY.md (`# Chief`); edit it and assert the revision bumps.
const botMemory = knowledge.getByTestId("bot-knowledge-memory");
const botMemoryRow = botMemory.getByRole("button", { name: /MEMORY\.md/ });
await expect(botMemoryRow).toBeVisible();
await botMemoryRow.click();
const botDocEditor = botMemory.locator("textarea");
await expect(botDocEditor).toHaveValue(/# Chief/);
const botMarker = `Bot memory e2e ${stamp}`;
await botDocEditor.fill(`# Chief\n\n${botMarker}\n`);
await botMemory.getByRole("button", { name: "Save", exact: true }).scrollIntoViewIfNeeded();
await captureScreenshot(page, testInfo, "80-knowledge-bot-memory");
await botMemory.getByRole("button", { name: "Save", exact: true }).click();
await expect(botMemory.getByText("rev 2")).toBeVisible();
expect(
await rpc<MemoryDocument[]>(page, "memory/list", {
botId: activeBotId(page),
scope: "bot",
}),
).toContainEqual(expect.objectContaining({ content: `# Chief\n\n${botMarker}\n`, revision: 2 }));
await botMemoryRow.click();
await expect(botDocEditor).toHaveValue(new RegExp(botMarker));
await botMemory.getByRole("button", { name: "Cancel", exact: true }).click();
// Skills: create one through the editor, reopen it, edit, then delete it.
// Builtin catalog is currently empty; user skills still cover create/edit/delete.
await knowledge.getByRole("tab", { name: "Skills", exact: true }).click();
await knowledge.getByRole("button", { name: "New skill", exact: true }).click();
const editor = knowledge.locator("textarea");
await editor.fill(
[
"---",
"name: greet-politely",
"description: Say hello before anything else.",
"---",
"",
"Always open with a greeting.",
].join("\n"),
);
await captureScreenshot(page, testInfo, "81-knowledge-skill-editor");
await knowledge.getByRole("button", { name: "Save", exact: true }).click();
const skillRow = knowledge.getByRole("button", { name: /greet-politely/ });
await expect(skillRow).toBeVisible();
await expect(knowledge.getByText("Say hello before anything else.")).toBeVisible();
await captureScreenshot(page, testInfo, "82-knowledge-skill-listed");
const composer = page.getByRole("combobox", { name: /^Message/ });
await composer.fill("/");
await expect(
page.getByRole("button", { name: "Skill greet-politely", exact: true }),
).toBeVisible();
await composer.fill("");
// A provider-owned skill uses the same viewer without mutation controls.
await page.route(
"**/rpc/agentSkills/get",
async (route) => {
const response = await route.fetch();
const body = await response.json();
await route.fulfill({
response,
json: { ...body, json: { ...body.json, readOnly: true, source: "plugin" } },
});
},
{ times: 1 },
);
await skillRow.click();
await expect(editor).toHaveAttribute("readonly", "");
await expect(knowledge.getByRole("button", { name: "Save", exact: true })).toHaveCount(0);
await expect(knowledge.getByRole("button", { name: "Delete", exact: true })).toHaveCount(0);
await knowledge.getByRole("button", { name: "Close", exact: true }).click();
await skillRow.click();
await expect(editor).toHaveValue(/Always open with a greeting/);
await editor.fill(
[
"---",
"name: greet-politely",
"description: Say hello before anything else.",
"---",
"",
"Open with a warm greeting.",
].join("\n"),
);
let releaseSkillRefresh!: () => void;
const skillRefreshGate = new Promise<void>((resolve) => {
releaseSkillRefresh = resolve;
});
await page.route(
"**/rpc/agentSkills/list",
async (route) => {
await skillRefreshGate;
await route.continue();
},
{ times: 1 },
);
await knowledge.getByRole("button", { name: "Save", exact: true }).click();
try {
await expect(editor).toBeHidden();
await expect(skillRow).toBeDisabled();
await captureScreenshot(page, testInfo, "85-skill-refresh-pending");
} finally {
releaseSkillRefresh();
}
await skillRow.click();
await expect(editor).toHaveValue(/warm greeting/);
await knowledge.getByRole("button", { name: "Delete", exact: true }).click();
await knowledge.getByRole("button", { name: "Confirm delete", exact: true }).click();
await expect(skillRow).toBeHidden();
await composer.fill("/");
await expect(page.getByRole("button", { name: "Skill greet-politely", exact: true })).toHaveCount(
0,
);
expect(await rpc<Array<{ name: string }>>(page, "agentSkills/list", {})).not.toContainEqual(
expect.objectContaining({ name: "greet-politely" }),
);
});