test(e2e): 添加 E2E 测试基础设施和线程记忆测试
- 配置 Playwright:baseURL 改为 localhost:2026,视频仅在 CI 保留 - 更新 .gitignore 排除 Playwright 报告/缓存 - 新增线程记忆 E2E 测试:验证发送消息后可加载 summary 且无日志报错 - thread-memory-panel 添加 data-testid 属性便于定位
This commit is contained in:
parent
a1eff2fa12
commit
29203a14b8
7
frontend/.gitignore
vendored
7
frontend/.gitignore
vendored
@ -47,3 +47,10 @@ test-results
|
||||
|
||||
# idea files
|
||||
.idea
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
||||
18
frontend/e2e/example.spec.ts
Normal file
18
frontend/e2e/example.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
});
|
||||
@ -5,11 +5,10 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
import { config as loadEnv } from "dotenv";
|
||||
|
||||
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
// Load local e2e env defaults from frontend/.env(.local), while keeping shell env highest priority.
|
||||
loadEnv({ path: path.resolve(configDir, ".env.local") });
|
||||
loadEnv({ path: path.resolve(configDir, ".env") });
|
||||
|
||||
const baseURL = process.env.FRONTEND_E2E_BASE_URL ?? "http://127.0.0.1:3000";
|
||||
const baseURL = process.env.FRONTEND_E2E_BASE_URL ?? "http://localhost:2026";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
@ -24,7 +23,7 @@ export default defineConfig({
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
video: process.env.CI ? "retain-on-failure" : "off",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
|
||||
@ -665,89 +665,105 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@ -902,24 +918,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
||||
@ -978,36 +998,42 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/simple-git-linux-arm64-musl@0.1.22':
|
||||
resolution: {integrity: sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/simple-git-linux-ppc64-gnu@0.1.22':
|
||||
resolution: {integrity: sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/simple-git-linux-s390x-gnu@0.1.22':
|
||||
resolution: {integrity: sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/simple-git-linux-x64-gnu@0.1.22':
|
||||
resolution: {integrity: sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/simple-git-linux-x64-musl@0.1.22':
|
||||
resolution: {integrity: sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/simple-git-win32-arm64-msvc@0.1.22':
|
||||
resolution: {integrity: sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==}
|
||||
@ -1057,24 +1083,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.7':
|
||||
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.7':
|
||||
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.7':
|
||||
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.7':
|
||||
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
|
||||
@ -2088,24 +2118,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@ -2507,41 +2541,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@ -4159,24 +4201,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
|
||||
@ -85,7 +85,10 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg">
|
||||
<div
|
||||
className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg"
|
||||
data-testid="thread-memory-panel"
|
||||
>
|
||||
<div className="text-sm font-semibold">
|
||||
<span className="hidden sm:inline">{t.threadMemoryPanel.title}</span>
|
||||
</div>
|
||||
@ -98,6 +101,7 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
||||
void handleLoadMemorySummary();
|
||||
}}
|
||||
disabled={loadingSummary}
|
||||
data-testid="thread-memory-load"
|
||||
>
|
||||
{loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load}
|
||||
</Button>
|
||||
@ -131,6 +135,7 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
||||
onChange={(e) => setMemorySummary(e.target.value)}
|
||||
placeholder={t.threadMemoryPanel.summaryPlaceholder}
|
||||
className="min-h-32 bg-white/80"
|
||||
data-testid="thread-memory-summary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
243
frontend/tests/e2e/thread-memory-log.spec.ts
Normal file
243
frontend/tests/e2e/thread-memory-log.spec.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import {
|
||||
expandComposer,
|
||||
newChatEntry,
|
||||
openChat,
|
||||
} from "./support/chat-helpers";
|
||||
|
||||
const LANGGRAPH_LOG_PATH = "/home/mt/Projects/deerflow2/logs/langgraph.log";
|
||||
const INPUT_TOOLS_TOUR_SEEN_KEY = "workspace.input_tools_tour_seen.v1";
|
||||
const MEMORY_ERROR_PATTERNS = [
|
||||
"Thread memory update failed",
|
||||
"json.decoder.JSONDecodeError",
|
||||
"JSONDecodeError",
|
||||
];
|
||||
|
||||
async function readLogTail(startOffset: number) {
|
||||
const handle = await fs.open(LANGGRAPH_LOG_PATH, "r");
|
||||
try {
|
||||
const stats = await handle.stat();
|
||||
const length = Math.max(0, stats.size - startOffset);
|
||||
if (length === 0) {
|
||||
return "";
|
||||
}
|
||||
const buffer = Buffer.alloc(length);
|
||||
await handle.read(buffer, 0, length, startOffset);
|
||||
return buffer.toString("utf8");
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
function e2eLog(message: string, extra?: unknown) {
|
||||
if (extra === undefined) {
|
||||
console.log(`[DF-MEM-001] ${message}`);
|
||||
return;
|
||||
}
|
||||
console.log(`[DF-MEM-001] ${message}`, extra);
|
||||
}
|
||||
|
||||
async function completeInputToolsTour(page: Parameters<typeof openChat>[0]) {
|
||||
e2eLog("checking input tools tour");
|
||||
const tourRoot = page.locator(".workspace-input-tools-tour");
|
||||
const nextButton = page
|
||||
.locator(".workspace-input-tools-tour .ant-tour-next-btn")
|
||||
.getByText(/下一步|完成/);
|
||||
|
||||
if (!(await tourRoot.isVisible().catch(() => false))) {
|
||||
e2eLog("input tools tour not visible, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let step = 0; step < 4; step += 1) {
|
||||
e2eLog(`input tools tour step ${step + 1}`);
|
||||
await expect(nextButton.first()).toBeVisible();
|
||||
await nextButton.first().click();
|
||||
if (!(await tourRoot.isVisible().catch(() => false))) {
|
||||
e2eLog("input tools tour completed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
e2eLog("input tools tour still visible after max steps");
|
||||
}
|
||||
|
||||
async function waitForResolvedThreadId(
|
||||
page: Parameters<typeof openChat>[0],
|
||||
threadId: string,
|
||||
) {
|
||||
e2eLog("waiting for resolved thread id", threadId);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
(storageKey) => window.sessionStorage.getItem(storageKey),
|
||||
"workspace.thread_id",
|
||||
),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
)
|
||||
.toBe(threadId);
|
||||
e2eLog("resolved thread id is ready", threadId);
|
||||
}
|
||||
|
||||
test.describe("线程记忆 / 前端加载与日志校验", () => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
test("DF-MEM-001 发送消息后可从线程记忆面板加载 summary,且新增日志无记忆报错", async ({
|
||||
page,
|
||||
}) => {
|
||||
const threadId = uuid();
|
||||
const logStats = await fs.stat(LANGGRAPH_LOG_PATH);
|
||||
const message =
|
||||
`请记住:我常用 TypeScript、React 和 Playwright,偏好中文且直接回答重点。本次 e2e 线程标识 ${threadId.slice(0, 8)}。`;
|
||||
e2eLog("test started", { threadId, initialLogSize: logStats.size });
|
||||
|
||||
await page.addInitScript(
|
||||
({ key, currentThreadId }: { key: string; currentThreadId: string }) => {
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
seen: true,
|
||||
threadIds: [currentThreadId],
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
key: INPUT_TOOLS_TOUR_SEEN_KEY,
|
||||
currentThreadId: threadId,
|
||||
},
|
||||
);
|
||||
e2eLog("seeded localStorage for input tools tour");
|
||||
|
||||
await openChat(page, newChatEntry(threadId));
|
||||
e2eLog("opened chat page", await page.url());
|
||||
await completeInputToolsTour(page);
|
||||
await waitForResolvedThreadId(page, threadId);
|
||||
e2eLog("composer page state ready");
|
||||
const observedRequests: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes("/stream") || url.includes("/threads")) {
|
||||
observedRequests.push(`${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
e2eLog("registered network observer");
|
||||
const textarea = page.locator("textarea[name='message']");
|
||||
const submit = page.locator("button[aria-label='Submit']");
|
||||
await textarea.click();
|
||||
await textarea.pressSequentially(message);
|
||||
e2eLog("filled textarea", {
|
||||
messageLength: message.length,
|
||||
textareaValueLength: (await textarea.inputValue()).length,
|
||||
});
|
||||
await expect(textarea).toHaveValue(message);
|
||||
e2eLog("textarea value confirmed");
|
||||
e2eLog("submit button state before click", {
|
||||
visible: await submit.isVisible(),
|
||||
enabled: await submit.isEnabled(),
|
||||
});
|
||||
await submit.evaluate((button) => {
|
||||
(button as HTMLButtonElement).click();
|
||||
});
|
||||
e2eLog("submit clicked via evaluate");
|
||||
await expect
|
||||
.poll(
|
||||
async () => ({
|
||||
url: page.url(),
|
||||
userCount: await page.locator(".is-user").count(),
|
||||
assistantCount: await page.locator(".is-assistant").count(),
|
||||
textareaValue: await textarea.inputValue(),
|
||||
observedRequests: observedRequests.slice(-10),
|
||||
}),
|
||||
{
|
||||
timeout: 30_000,
|
||||
intervals: [500, 1_000, 2_000],
|
||||
},
|
||||
)
|
||||
.toMatchObject({
|
||||
url: expect.stringMatching(new RegExp(`/workspace/chats/${threadId}\\?is_chatting=true`)),
|
||||
});
|
||||
e2eLog("post-submit state reached", {
|
||||
currentUrl: await page.url(),
|
||||
observedRequests: observedRequests.slice(-10),
|
||||
});
|
||||
|
||||
await expect(textarea).toHaveValue("");
|
||||
e2eLog("textarea cleared after submit");
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/workspace/chats/${threadId}\\?is_chatting=true`),
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
e2eLog("navigated to active thread page", await page.url());
|
||||
await expect
|
||||
.poll(async () => await page.locator(".is-user").count(), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
e2eLog("user message rendered", await page.locator(".is-user").count());
|
||||
await expect(page.locator(".is-user").last()).toContainText(
|
||||
"TypeScript",
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
e2eLog("user message content contains TypeScript");
|
||||
await expect
|
||||
.poll(async () => await page.locator(".is-assistant").count(), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
e2eLog("assistant message rendered", await page.locator(".is-assistant").count());
|
||||
|
||||
await expandComposer(page);
|
||||
e2eLog("composer expanded for memory button");
|
||||
const memoryButton = page.getByTestId("thread-memory-trigger");
|
||||
e2eLog("memory button visibility precheck", {
|
||||
count: await memoryButton.count(),
|
||||
});
|
||||
await expect(memoryButton).toBeVisible();
|
||||
await memoryButton.click();
|
||||
e2eLog("memory button clicked");
|
||||
|
||||
const loadButton = page.getByTestId("thread-memory-load");
|
||||
await expect(loadButton).toBeVisible();
|
||||
e2eLog("memory load button visible");
|
||||
|
||||
let latestSummary = "";
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await loadButton.click();
|
||||
latestSummary = await page
|
||||
.getByTestId("thread-memory-summary")
|
||||
.inputValue();
|
||||
e2eLog("memory summary polled", {
|
||||
length: latestSummary.length,
|
||||
preview: latestSummary.slice(0, 80),
|
||||
});
|
||||
return latestSummary;
|
||||
},
|
||||
{
|
||||
timeout: 75_000,
|
||||
intervals: [1_000, 2_000, 3_000, 5_000],
|
||||
},
|
||||
)
|
||||
.not.toEqual("");
|
||||
e2eLog("memory summary loaded", {
|
||||
length: latestSummary.length,
|
||||
preview: latestSummary.slice(0, 120),
|
||||
});
|
||||
|
||||
const logTail = await readLogTail(logStats.size);
|
||||
e2eLog("new langgraph log tail length", logTail.length);
|
||||
for (const pattern of MEMORY_ERROR_PATTERNS) {
|
||||
e2eLog(`checking log pattern absence: ${pattern}`);
|
||||
expect(logTail).not.toContain(pattern);
|
||||
}
|
||||
e2eLog("test finished successfully");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user