evOo-local/apps/web/e2e/knowledge-panel.spec.ts
cynarAI b2fb517286
Simplify the memory settings: what is remembered first, provider setup under Advanced (#164)
The memory pane opened with an untranslated provider description, a
provider picker, a scope toggle and API key fields, and pushed what the
agents actually remember to the bottom. Most people never connect a
semantic-recall provider, so that setup now sits behind a collapsed
Advanced disclosure (open while a provider is connected), and the
remembered documents come first.

- Drop the provider descriptions, the "Shared documents" heading and
  the per-document revision label.
- Replace the Isolated/Shared toggle pair with one "Share between bots"
  switch.
- Remove the unused standalone dialog mode of the memory pane.
- German: call the section "Gedächtnis" so it no longer reads like
  reminders ("Erinnerungen").


Claude-Session: https://claude.ai/code/session_01LWzKXyE6uDmmmzuod4oXNQ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 20:30:30 +02:00

237 lines
9.2 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");
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(docEditor).toHaveCount(0);
// 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` }),
);
expect(
sharedDocuments.find((doc) => doc.content.includes(marker))?.revision,
).toBeGreaterThanOrEqual(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(botDocEditor).toHaveCount(0);
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();
// Remembering appends a dated note and keeps the edit above.
await rpc(page, "threads/send", {
botId: activeBotId(page),
text: `remember that the tester ${stamp} prefers tea`,
});
await expect
.poll(
async () =>
(
await rpc<MemoryDocument[]>(page, "memory/list", {
botId: activeBotId(page),
scope: "bot",
})
).find((doc) => doc.path === "MEMORY.md")?.content,
{ timeout: 60_000 },
)
.toMatch(
new RegExp(
`^# Chief\\n\\n${botMarker}\\n\\n- \\d{4}-\\d{2}-\\d{2}: .*tester ${stamp} prefers tea\\n$`,
),
);
await expect
.poll(
async () =>
(await rpc<{ run?: unknown }>(page, "threads/get", { botId: activeBotId(page) })).run ??
null,
{ timeout: 60_000 },
)
.toBeNull();
// 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" }),
);
});