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>
237 lines
9.2 KiB
TypeScript
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" }),
|
|
);
|
|
});
|