Compare commits
No commits in common. "5027a9231415debb9e0c0e771b4938ef4cb8f6bd" and "c17ba298fb6a3fbcd5739f8973c4b962c84cd32b" have entirely different histories.
5027a92314
...
c17ba298fb
@ -1,95 +0,0 @@
|
||||
"""JSON extraction helpers for LLM-generated memory payloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
def strip_code_fence(text: str) -> str:
|
||||
cleaned = text.strip()
|
||||
if not cleaned.startswith("```"):
|
||||
return cleaned
|
||||
lines = cleaned.split("\n")
|
||||
return "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]).strip()
|
||||
|
||||
|
||||
def escape_inner_quotes_in_json_strings(text: str) -> str:
|
||||
"""Heuristically repair unescaped inner double quotes inside JSON strings."""
|
||||
out: list[str] = []
|
||||
in_string = False
|
||||
escape = False
|
||||
n = len(text)
|
||||
i = 0
|
||||
while i < n:
|
||||
ch = text[i]
|
||||
if not in_string:
|
||||
out.append(ch)
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if escape:
|
||||
out.append(ch)
|
||||
escape = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == "\\":
|
||||
out.append(ch)
|
||||
escape = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '"':
|
||||
j = i + 1
|
||||
while j < n and text[j].isspace():
|
||||
j += 1
|
||||
next_char = text[j] if j < n else ""
|
||||
if next_char in {":", ",", "}", "]", ""}:
|
||||
out.append(ch)
|
||||
in_string = False
|
||||
else:
|
||||
out.append('\\"')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(ch)
|
||||
i += 1
|
||||
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def extract_json_object(text: str) -> dict[str, Any] | None:
|
||||
cleaned = strip_code_fence(text)
|
||||
try:
|
||||
parsed = json.loads(cleaned)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
repaired = escape_inner_quotes_in_json_strings(cleaned)
|
||||
if repaired != cleaned:
|
||||
try:
|
||||
parsed = json.loads(repaired)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
candidate = match.group(0)
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
repaired = escape_inner_quotes_in_json_strings(candidate)
|
||||
if repaired != candidate:
|
||||
try:
|
||||
parsed = json.loads(repaired)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
@ -4,14 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
from deerflow.agents.memory.json_utils import (
|
||||
escape_inner_quotes_in_json_strings as _escape_inner_quotes_in_json_strings,
|
||||
)
|
||||
from deerflow.agents.memory.json_utils import extract_json_object as _extract_json_object
|
||||
from deerflow.agents.memory.json_utils import strip_code_fence as _strip_code_fence
|
||||
from deerflow.agents.memory.thread_prompt import create_empty_thread_memory
|
||||
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
||||
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
||||
@ -78,6 +74,106 @@ def _get_summary_model():
|
||||
config = get_thread_memory_config()
|
||||
return create_chat_model(name=config.model_name, thinking_enabled=False, stream_usage=False)
|
||||
|
||||
|
||||
def _strip_code_fence(text: str) -> str:
|
||||
cleaned = text.strip()
|
||||
if not cleaned.startswith("```"):
|
||||
return cleaned
|
||||
lines = cleaned.split("\n")
|
||||
return "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]).strip()
|
||||
|
||||
|
||||
def _extract_json_object(text: str) -> dict[str, Any] | None:
|
||||
cleaned = _strip_code_fence(text)
|
||||
try:
|
||||
parsed = json.loads(cleaned)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
repaired = _escape_inner_quotes_in_json_strings(cleaned)
|
||||
if repaired != cleaned:
|
||||
try:
|
||||
parsed = json.loads(repaired)
|
||||
if isinstance(parsed, dict):
|
||||
logger.warning("THREAD_SUMMARY_DEBUG parse_repaired mode=full_text")
|
||||
return parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(match.group(0))
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
candidate = match.group(0)
|
||||
repaired = _escape_inner_quotes_in_json_strings(candidate)
|
||||
if repaired != candidate:
|
||||
try:
|
||||
parsed = json.loads(repaired)
|
||||
if isinstance(parsed, dict):
|
||||
logger.warning("THREAD_SUMMARY_DEBUG parse_repaired mode=regex_object")
|
||||
return parsed
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _escape_inner_quotes_in_json_strings(text: str) -> str:
|
||||
"""Heuristically repair unescaped inner double quotes inside JSON strings.
|
||||
|
||||
If a quote appears while inside a string but the next non-space character is
|
||||
not a valid string terminator (comma, object/array close, or key colon), it is
|
||||
treated as content and escaped.
|
||||
"""
|
||||
out: list[str] = []
|
||||
in_string = False
|
||||
escape = False
|
||||
n = len(text)
|
||||
i = 0
|
||||
while i < n:
|
||||
ch = text[i]
|
||||
if not in_string:
|
||||
out.append(ch)
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if escape:
|
||||
out.append(ch)
|
||||
escape = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == "\\":
|
||||
out.append(ch)
|
||||
escape = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '"':
|
||||
j = i + 1
|
||||
while j < n and text[j].isspace():
|
||||
j += 1
|
||||
next_char = text[j] if j < n else ""
|
||||
# Valid JSON string terminators in context:
|
||||
# - key string: :
|
||||
# - value string: , } ]
|
||||
if next_char in {":", ",", "}", "]", ""}:
|
||||
out.append(ch)
|
||||
in_string = False
|
||||
else:
|
||||
out.append('\\"')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(ch)
|
||||
i += 1
|
||||
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _merge_summary_patch(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
||||
merged = {"ownerId": base.get("ownerId"), **create_empty_thread_memory()}
|
||||
merged["user"] = dict(base.get("user", {})) if isinstance(base.get("user"), dict) else merged["user"]
|
||||
|
||||
@ -9,7 +9,6 @@ import uuid
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from deerflow.agents.memory.json_utils import extract_json_object
|
||||
from deerflow.agents.memory.updater import _extract_text
|
||||
from deerflow.agents.memory.thread_prompt import build_thread_memory_prompt, create_empty_thread_memory
|
||||
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
||||
@ -129,9 +128,10 @@ class ThreadMemoryUpdater:
|
||||
try:
|
||||
response = self._get_model().invoke(prompt)
|
||||
response_text = _extract_text(response.content).strip()
|
||||
parsed = extract_json_object(response_text)
|
||||
if not isinstance(parsed, dict):
|
||||
raise json.JSONDecodeError("No valid JSON object found", response_text, 0)
|
||||
if response_text.startswith("```"):
|
||||
lines = response_text.split("\n")
|
||||
response_text = "\n".join(lines[1:-1] if lines[-1] == "```" else lines[1:])
|
||||
parsed = json.loads(response_text)
|
||||
cleaned = self._scrub_sensitive(parsed, thread_id)
|
||||
|
||||
expected_version = 0 if current is None else int(current.get("memoryVersion", 0))
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
||||
|
||||
|
||||
@ -20,65 +18,3 @@ def test_scrub_sensitive_tolerates_non_numeric_confidence():
|
||||
assert len(cleaned["facts"]) == 2
|
||||
assert cleaned["facts"][0]["confidence"] == 0.5
|
||||
assert cleaned["facts"][1]["confidence"] == 0.5
|
||||
|
||||
|
||||
def test_update_memory_repairs_model_json_with_unescaped_inner_quotes():
|
||||
class _Storage:
|
||||
def __init__(self):
|
||||
self.saved = None
|
||||
|
||||
def load(self, _thread_id):
|
||||
return None
|
||||
|
||||
def save(self, _thread_id, data, expected_version=None):
|
||||
self.saved = {
|
||||
"thread_id": _thread_id,
|
||||
"data": data,
|
||||
"expected_version": expected_version,
|
||||
}
|
||||
return True
|
||||
|
||||
fake_storage = _Storage()
|
||||
fake_model = type(
|
||||
"M",
|
||||
(),
|
||||
{
|
||||
"invoke": lambda self, prompt: type(
|
||||
"R",
|
||||
(),
|
||||
{
|
||||
"content": """
|
||||
{
|
||||
"user": {
|
||||
"topOfMind": {
|
||||
"summary": "反感“作为 AI"这种句式,认为回答不用寒暄直接说重点。",
|
||||
"updatedAt": "2026-06-11T07:13:11Z"
|
||||
}
|
||||
},
|
||||
"history": {},
|
||||
"facts": [
|
||||
{
|
||||
"content": "偏好直接回答,不喜欢“作为 AI"式开场",
|
||||
"category": "preference",
|
||||
"confidence": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
""".strip()
|
||||
},
|
||||
)()
|
||||
},
|
||||
)()
|
||||
messages = [type("Msg", (), {"type": "human", "content": "请直接回答重点,不要寒暄。"})()]
|
||||
|
||||
with (
|
||||
patch("deerflow.agents.memory.thread_updater.get_thread_memory_storage", return_value=fake_storage),
|
||||
patch.object(ThreadMemoryUpdater, "_get_model", return_value=fake_model),
|
||||
):
|
||||
result = ThreadMemoryUpdater().update_memory(messages, "thread-test")
|
||||
|
||||
assert result is True
|
||||
assert fake_storage.saved is not None
|
||||
assert fake_storage.saved["expected_version"] == 0
|
||||
assert fake_storage.saved["data"]["user"]["topOfMind"]["summary"].startswith("反感“作为 AI")
|
||||
assert fake_storage.saved["data"]["facts"][0]["content"].startswith("偏好直接回答")
|
||||
|
||||
7
frontend/.gitignore
vendored
7
frontend/.gitignore
vendored
@ -47,10 +47,3 @@ test-results
|
||||
|
||||
# idea files
|
||||
.idea
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
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,10 +5,11 @@ 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://localhost:2026";
|
||||
const baseURL = process.env.FRONTEND_E2E_BASE_URL ?? "http://127.0.0.1:3000";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
@ -23,7 +24,7 @@ export default defineConfig({
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: process.env.CI ? "retain-on-failure" : "off",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
|
||||
@ -665,105 +665,89 @@ 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==}
|
||||
@ -918,28 +902,24 @@ 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==}
|
||||
@ -998,42 +978,36 @@ 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==}
|
||||
@ -1083,28 +1057,24 @@ 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==}
|
||||
@ -2118,28 +2088,24 @@ 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==}
|
||||
@ -2541,49 +2507,41 @@ 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==}
|
||||
@ -4201,28 +4159,24 @@ 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==}
|
||||
|
||||
@ -579,7 +579,7 @@ export default function ChatPage() {
|
||||
className={cn(
|
||||
"pointer-events-auto relative w-full max-w-[720px]",
|
||||
showWelcomeStyle && "-translate-y-[calc(50vh-96px)]",
|
||||
brand === "sxwz"&& artifactsOpen ===false && "-translate-x-[172px]"
|
||||
brand === "sxwz" && "-translate-x-[172px]"
|
||||
)}
|
||||
>
|
||||
{!(showWelcomeStyle && thread.isThreadLoading) ? (
|
||||
|
||||
@ -85,10 +85,7 @@ 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"
|
||||
data-testid="thread-memory-panel"
|
||||
>
|
||||
<div className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg">
|
||||
<div className="text-sm font-semibold">
|
||||
<span className="hidden sm:inline">{t.threadMemoryPanel.title}</span>
|
||||
</div>
|
||||
@ -101,7 +98,6 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
||||
void handleLoadMemorySummary();
|
||||
}}
|
||||
disabled={loadingSummary}
|
||||
data-testid="thread-memory-load"
|
||||
>
|
||||
{loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load}
|
||||
</Button>
|
||||
@ -135,7 +131,6 @@ 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>
|
||||
|
||||
@ -1,243 +0,0 @@
|
||||
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