项目文件夹

文件
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

300 行
11 KiB
JavaScript

/**
* Tests for the expandable observation detail in chat.js live steps.
*
* Observation progress events carry the (bounded) full tool output in
* `data.content` alongside the one-line `data.message`. The live step
* handler must append the detail beneath the message — mirroring what
* `_compose_chat_step_content` persists to chat_progress_steps on the
* backend, so the live accordion and a post-reload session render
* identically (symmetry invariant). Non-observation phases must NOT
* compose: `metadata["content"]` is reused there for large payloads
* (full synthesized answers) that don't belong in step rows.
*
* `handleProgressUpdate` is private to the chat IIFE, so we drive it the
* way production does: load a session whose messages response reports an
* in-progress research, capture the progress callback that chat.js hands
* to window.socket.subscribeToResearch, and invoke it with progress
* events.
*/
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
const SESSION_ID = 'sess-1';
const RESEARCH_ID = 'research-1';
function buildChatDom() {
// Static literal (no interpolation) so no-unsanitized/property is
// satisfied; SESSION_ID must match this meta content.
document.head.innerHTML = `
<meta name="chat-session-id" content="sess-1">
`;
document.body.innerHTML = `
<div id="chat-welcome"></div>
<textarea id="chat-input"></textarea>
<button id="send-btn"></button>
<div id="chat-title"></div>
<div id="chat-messages" role="log"></div>
<template id="thinking-template">
<div class="ldr-chat-message ldr-chat-message-assistant ldr-chat-message-thinking" role="status">
<div class="ldr-chat-message-avatar"><i class="fas fa-robot"></i></div>
<div class="ldr-chat-message-content">
<div class="ldr-chat-thinking-text" hidden></div>
<div class="ldr-chat-thinking-dots"><span></span><span></span><span></span></div>
</div>
</div>
</template>
<template id="message-template">
<div class="ldr-chat-message">
<div class="ldr-chat-message-avatar"><i class="fas fa-user"></i></div>
<div class="ldr-chat-message-content">
<div class="ldr-chat-message-text"></div>
<div class="ldr-chat-message-meta"><span class="ldr-chat-message-time"></span></div>
</div>
</div>
</template>
`;
}
const json = (payload) => ({
ok: true,
status: 200,
json: async () => payload,
});
const flush = async (n = 10) => {
for (let i = 0; i < n; i++) await Promise.resolve();
};
/** The step elements inside the live accordion. */
function liveSteps() {
return document.querySelectorAll(
'.ldr-chat-steps-group[data-live="true"] .ldr-chat-message-step'
);
}
describe('chat.js — live step observation detail (data.content)', () => {
let progressCb;
beforeEach(async () => {
vi.resetModules();
progressCb = null;
buildChatDom();
window.api = { getCsrfToken: () => '' };
window.ui = { renderMarkdown: (t) => t };
window.socket = {
subscribeToResearch: vi.fn((id, cb) => {
progressCb = cb;
}),
unsubscribeFromResearch: vi.fn(),
getSocketInstance: vi.fn(() => ({
on: vi.fn(),
off: vi.fn(),
})),
};
// Session restore: one prior user message + an in-progress
// research, so loadSession() subscribes to progress events.
globalThis.fetch = vi.fn(async (url) => {
const u = String(url);
if (u.includes(`/api/chat/sessions/${SESSION_ID}/messages`)) {
return json({
success: true,
messages: [
{
id: 'm1',
role: 'user',
message_type: 'chat',
content: 'hello',
created_at: '2026-07-11T00:00:00Z',
},
],
has_more: false,
in_progress_research_id: RESEARCH_ID,
});
}
if (u.includes(`/api/chat/sessions/${SESSION_ID}`)) {
return json({
success: true,
session: { id: SESSION_ID, title: 'Test session' },
});
}
return json({ success: true, sessions: [] });
});
await import('@js/components/chat.js');
await flush();
expect(window.socket.subscribeToResearch).toHaveBeenCalledWith(
RESEARCH_ID,
expect.any(Function)
);
expect(progressCb).toBeTypeOf('function');
});
afterEach(() => {
delete globalThis.fetch;
});
it('appends data.content beneath the message for observation steps', () => {
const message = '📄 From the web (SearXNG): [1] Some Title (http://a.com) Snip';
const detail = '[1] Some Title (http://a.com)\nThe full snippet body with much more text.';
progressCb({
research_id: RESEARCH_ID,
phase: 'observation',
message,
content: detail,
progress: 42,
});
const steps = liveSteps();
expect(steps.length).toBe(1);
const text = steps[0].querySelector('.ldr-chat-message-text').textContent;
expect(text).toBe(message + '\n\n' + detail);
});
it('keeps the bare message when an observation has no content', () => {
const message = '📄 From PubMed: no results';
progressCb({
research_id: RESEARCH_ID,
phase: 'observation',
message,
progress: 42,
});
const steps = liveSteps();
expect(steps.length).toBe(1);
expect(
steps[0].querySelector('.ldr-chat-message-text').textContent
).toBe(message);
});
it('does NOT compose content for non-observation step phases', () => {
const message = 'Generating final answer…';
progressCb({
research_id: RESEARCH_ID,
phase: 'output_generation',
message,
content: 'the entire synthesized answer — must not land in the step',
progress: 90,
});
const steps = liveSteps();
expect(steps.length).toBe(1);
expect(
steps[0].querySelector('.ldr-chat-message-text').textContent
).toBe(message);
});
it('keeps the thinking-bubble preview on the short message only', () => {
progressCb({
research_id: RESEARCH_ID,
phase: 'observation',
message: '📄 From arXiv: [2] Paper',
content: '[2] Paper (http://arxiv.org/x)\nAbstract text',
progress: 42,
});
const preview = document.querySelector('.ldr-chat-thinking-text');
expect(preview).not.toBeNull();
expect(preview.textContent).toBe('📄 From arXiv: [2] Paper');
expect(preview.textContent).not.toContain('Abstract text');
});
});
describe('chat.js — reloaded step icons use the persisted phase', () => {
/**
* Persisted step rows now carry composed observation detail — raw web
* text that can contain words like "failed". The reload icon must come
* from the row's `phase` field, not the content heuristic, so a
* successful result row never renders with the error icon.
*/
function stepIcons() {
return [
...document.querySelectorAll(
'.ldr-chat-steps-group .ldr-chat-message-step .ldr-chat-message-avatar i'
),
].map((i) => i.className);
}
beforeEach(async () => {
vi.resetModules();
buildChatDom();
window.api = { getCsrfToken: () => '' };
window.ui = { renderMarkdown: (t) => t };
window.socket = {
subscribeToResearch: vi.fn(),
unsubscribeFromResearch: vi.fn(),
getSocketInstance: vi.fn(() => ({ on: vi.fn(), off: vi.fn() })),
};
// Completed session: no in-progress research, two persisted steps —
// one observation whose detail contains failure-y web text, one
// legacy row without a phase (pre-phase-column fallback).
globalThis.fetch = vi.fn(async (url) => {
const u = String(url);
if (u.includes(`/api/chat/sessions/${SESSION_ID}/messages`)) {
return json({
success: true,
messages: [
{
id: 'm1',
role: 'user',
message_type: 'chat',
content: 'hello',
created_at: '2026-07-11T00:00:00Z',
},
{
id: 'step-1',
role: 'assistant',
message_type: 'step',
phase: 'observation',
content:
'📄 From PubMed: [1] Titration study\n\n'
+ '[1] Titration study (http://p.com)\n'
+ 'The first titration attempt failed; the '
+ 'second failed titration was repeated.',
created_at: '2026-07-11T00:00:01Z',
},
{
id: 'step-2',
role: 'assistant',
message_type: 'step',
content: 'Starting research: "hello"',
created_at: '2026-07-11T00:00:02Z',
},
],
has_more: false,
in_progress_research_id: null,
});
}
if (u.includes(`/api/chat/sessions/${SESSION_ID}`)) {
return json({
success: true,
session: { id: SESSION_ID, title: 'Test session' },
});
}
return json({ success: true, sessions: [] });
});
await import('@js/components/chat.js');
await flush();
});
afterEach(() => {
delete globalThis.fetch;
});
it('uses the phase icon for observation rows despite "failed" in the detail', () => {
const icons = stepIcons();
expect(icons.length).toBe(2);
expect(icons[0]).toContain('fa-eye'); // observation, NOT fa-circle-xmark
expect(icons[0]).not.toContain('fa-circle-xmark');
});
it('falls back to the content heuristic for rows without a phase', () => {
const icons = stepIcons();
expect(icons[1]).toContain('fa-play'); // "starting research" heuristic
});
});