learningcircuit--local-deep-research
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
300 行
11 KiB
JavaScript
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
|
|
});
|
|
});
|