diff --git a/e2e/specs/agent-documents.spec.ts b/e2e/specs/agent-documents.spec.ts new file mode 100644 index 0000000..40f527b --- /dev/null +++ b/e2e/specs/agent-documents.spec.ts @@ -0,0 +1,191 @@ +import { test, expect } from "@playwright/test"; +import { E2E_ADMIN, FIXED_CAPTCHA } from "../fixtures/test-users"; + +async function loginViaUI(page: import("@playwright/test").Page) { + await page.goto("/auth/login"); + await page.fill("#login-username", E2E_ADMIN.username); + await page.fill("#login-password", E2E_ADMIN.password); + await page.fill("#login-captcha", FIXED_CAPTCHA); + await page.click('button[type="submit"]'); + await page.waitForURL("/", { timeout: 15000 }); +} + +async function createDocViaAPI( + request: import("@playwright/test").APIRequestContext, + body: Record, +) { + const res = await request.post("/api/agent-documents", { data: body }); + return res; +} + +test.describe("Agent 文档", () => { + test("列表页可访问(空状态)", async ({ page, request }) => { + await loginViaUI(page); + await page.goto("/agent-documents"); + await expect(page.locator(".agent-documents-page")).toBeVisible({ timeout: 10000 }); + }); + + test("通过 API 创建文档后列表显示", async ({ page, request }) => { + await loginViaUI(page); + + const title = `E2E测试文档_${Date.now()}`; + const res = await createDocViaAPI(request, { + title, + content: "## 测试内容\n这是一个E2E测试创建的文档。", + type: "document", + tags: ["e2e", "test"], + }); + expect(res.ok()).toBeTruthy(); + const json = await res.json(); + expect(json.code).toBe(0); + const docId = json.data.id; + + await page.goto("/agent-documents"); + await expect(page.locator(".document-card").first()).toBeVisible({ timeout: 10000 }); + await expect(page.locator(".document-card").first()).toContainText(title); + + await request.delete(`/api/agent-documents/${docId}`); + }); + + test("详情页显示文档内容", async ({ page, request }) => { + await loginViaUI(page); + + const title = `E2E详情测试_${Date.now()}`; + const content = "## 详情页测试\nMarkdown渲染验证。"; + const res = await createDocViaAPI(request, { + title, + content, + type: "document", + tags: ["detail"], + }); + const json = await res.json(); + const docId = json.data.id; + + await page.goto(`/agent-documents/${docId}`); + await expect(page.locator(".detail-title")).toContainText(title, { timeout: 10000 }); + await expect(page.locator(".detail-content")).toBeVisible(); + + await request.delete(`/api/agent-documents/${docId}`); + }); + + test("类型筛选工作正常", async ({ page, request }) => { + await loginViaUI(page); + + const docRes = await createDocViaAPI(request, { + title: `E2E文档类型_${Date.now()}`, + content: "文档类型测试", + type: "document", + }); + const memRes = await createDocViaAPI(request, { + title: `E2E记忆类型_${Date.now()}`, + content: "记忆类型测试", + type: "memory", + }); + const docId = (await docRes.json()).data.id; + const memId = (await memRes.json()).data.id; + + await page.goto("/agent-documents"); + await page.click(".filter-tab", { text: "文档" }); + await page.waitForTimeout(500); + const docCards = page.locator(".document-card"); + const count1 = await docCards.count(); + expect(count1).toBeGreaterThan(0); + for (let i = 0; i < count1; i++) { + await expect(docCards.nth(i).locator(".type-badge")).toHaveClass(/document/); + } + + await page.click(".filter-tab", { text: "记忆" }); + await page.waitForTimeout(500); + const memCards = page.locator(".document-card"); + const count2 = await memCards.count(); + expect(count2).toBeGreaterThan(0); + for (let i = 0; i < count2; i++) { + await expect(memCards.nth(i).locator(".type-badge")).toHaveClass(/memory/); + } + + await request.delete(`/api/agent-documents/${docId}`); + await request.delete(`/api/agent-documents/${memId}`); + }); + + test("关键词搜索工作正常", async ({ page, request }) => { + await loginViaUI(page); + + const unique = `E2E搜索关键词_${Date.now()}`; + const res = await createDocViaAPI(request, { + title: unique, + content: "搜索测试内容", + type: "document", + }); + const docId = (await res.json()).data.id; + + await page.goto("/agent-documents"); + await page.fill(".search-input", unique); + await page.waitForTimeout(600); + + const cards = page.locator(".document-card"); + await expect(cards.first()).toBeVisible({ timeout: 10000 }); + const count = await cards.count(); + expect(count).toBeGreaterThanOrEqual(1); + await expect(cards.first()).toContainText(unique); + + await request.delete(`/api/agent-documents/${docId}`); + }); + + test("详情页编辑文档", async ({ page, request }) => { + await loginViaUI(page); + + const res = await createDocViaAPI(request, { + title: `E2E编辑前_${Date.now()}`, + content: "编辑前内容", + type: "document", + }); + const docId = (await res.json()).data.id; + + await page.goto(`/agent-documents/${docId}`); + await page.click("button.action-btn", { text: "编辑" }); + await expect(page.locator(".form-dialog")).toBeVisible({ timeout: 5000 }); + + await page.fill(".form-input", `E2E编辑后_${Date.now()}`); + await page.click("button.btn-save"); + + await expect(page.locator(".Toastify__toast").first()).toBeVisible({ timeout: 10000 }); + await page.waitForTimeout(500); + + await request.delete(`/api/agent-documents/${docId}`); + }); + + test("详情页删除文档", async ({ page, request }) => { + await loginViaUI(page); + + const res = await createDocViaAPI(request, { + title: `E2E删除测试_${Date.now()}`, + content: "待删除", + type: "document", + }); + const docId = (await res.json()).data.id; + + await page.goto(`/agent-documents/${docId}`); + await page.click("button.action-btn.danger"); + await expect(page.locator(".confirm-dialog")).toBeVisible({ timeout: 5000 }); + await page.click(".confirm-dialog .btn-save.danger"); + + await expect(page).toHaveURL(/\/agent-documents$/, { timeout: 10000 }); + }); + + test("未登录访问列表页重定向到登录", async ({ page }) => { + await page.goto("/agent-documents"); + await expect(page).toHaveURL(/\/auth\/login/); + }); + + test("未登录 API 返回 401", async ({ request }) => { + const res = await request.get("/api/agent-documents"); + expect(res.status()).toBe(401); + }); + + test("导航栏包含文档入口", async ({ page }) => { + await loginViaUI(page); + await page.goto("/"); + const docLink = page.locator(".nav-links a", { hasText: "文档" }); + await expect(docLink).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/server/api/agent-documents/index.post.ts b/server/api/agent-documents/index.post.ts new file mode 100644 index 0000000..a102c67 --- /dev/null +++ b/server/api/agent-documents/index.post.ts @@ -0,0 +1,31 @@ +import { createDocument, deserializeTags, type AgentDocumentType } from "../../service/agent-document"; +import { requireUser } from "#server/utils/context"; +import { validate } from "#server/utils/validation"; +import { z } from "zod"; + +const createSchema = z.object({ + title: z.string().min(1).max(200), + content: z.string().min(1).max(50000), + type: z.enum(["memory", "document"]).optional(), + tags: z.array(z.string()).optional(), + sessionId: z.string().optional(), +}); + +export default defineWrappedResponseHandler(async (event) => { + const user = await requireUser(event); + const body = await validate(createSchema, await readBody(event)); + + const created = await createDocument({ + userId: user.id, + sessionId: body.sessionId ?? null, + type: body.type as AgentDocumentType | undefined, + title: body.title, + content: body.content, + tags: body.tags ?? null, + }); + + return R.success({ + ...created, + tags: deserializeTags(created.tags), + }); +});