From cbe62cba0a2b0fadbe33bbd4dad1fc710241fe19 Mon Sep 17 00:00:00 2001 From: Delicious233 <101502465+DeliciousBuding@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:49:20 +0800 Subject: [PATCH] =?UTF-8?q?test(chatview):=20=E9=94=81=E5=AE=9A=20Hub=20?= =?UTF-8?q?=E5=9B=B4=E6=A0=8F=E4=BB=A3=E7=A0=81=E6=B8=B2=E6=9F=93=E5=90=88?= =?UTF-8?q?=E5=90=8C(#1971)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Codex --- .../components/AgentGroup.rendering.test.tsx | 28 ++++ .../components/UserMessage.rendering.test.tsx | 16 +++ .../src/chatview/pipeline-integration.test.ts | 42 ++++++ app/shared/src/ui/Markdown.test.tsx | 10 ++ app/web/package.json | 2 +- .../web-stubbed-hub-fenced-code.spec.ts | 125 ++++++++++++++++++ 6 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 app/web/src/__e2e__/web-stubbed-hub-fenced-code.spec.ts diff --git a/app/shared/src/chatview/components/AgentGroup.rendering.test.tsx b/app/shared/src/chatview/components/AgentGroup.rendering.test.tsx index 9900f277a..235b9c953 100644 --- a/app/shared/src/chatview/components/AgentGroup.rendering.test.tsx +++ b/app/shared/src/chatview/components/AgentGroup.rendering.test.tsx @@ -254,3 +254,31 @@ describe('AgentGroup rendering', () => { } }); }); + +describe('AgentGroup fenced code (#1971)', () => { + it('renders agent fenced code bubbles as code blocks', () => { + const item: TranscriptAgentItem = { + id: 'agent-fenced', + agent: 'Hub Agent', + role: 'agent', + time: '', + rows: [], + standaloneRows: [], + runs: [], + parts: [ + { + type: 'bubble', + text: 'Here is a fenced code sample:\n```python\ndef greet():\n return 42\n```\n', + blockId: 'hub-message-m2', + }, + ], + bubbles: ['Here is a fenced code sample:\n```python\ndef greet():\n return 42\n```\n'], + }; + + const { container, getByText } = render(); + + expect(getByText('Here is a fenced code sample:')).toBeInTheDocument(); + expect(container.textContent).toContain('return 42'); + expect(container.querySelectorAll('pre, code, [class*="codeBlockWrapper"]').length).toBeGreaterThan(0); + }); +}); diff --git a/app/shared/src/chatview/components/UserMessage.rendering.test.tsx b/app/shared/src/chatview/components/UserMessage.rendering.test.tsx index 315fe882d..2f7aa8326 100644 --- a/app/shared/src/chatview/components/UserMessage.rendering.test.tsx +++ b/app/shared/src/chatview/components/UserMessage.rendering.test.tsx @@ -185,3 +185,19 @@ describe('UserMessage attachments (#1957)', () => { expect(bubble!.children).toHaveLength(2); }); }); + +describe('UserMessage fenced code (#1971)', () => { + it('renders Hub-delivered fenced code as a code block, not plain text', () => { + const item: TranscriptUserItem = { + type: 'user', + name: 'partner', + text: 'partner fenced probe\n```python\nprint("hello fenced")\n```\n', + }; + + const { container, getByText } = render(); + + expect(getByText('partner fenced probe')).toBeInTheDocument(); + expect(getByText('print("hello fenced")')).toBeInTheDocument(); + expect(container.querySelectorAll('pre, code, [class*="codeBlockWrapper"]').length).toBeGreaterThan(0); + }); +}); diff --git a/app/shared/src/chatview/pipeline-integration.test.ts b/app/shared/src/chatview/pipeline-integration.test.ts index aa6f1a4c3..29e28f7a1 100644 --- a/app/shared/src/chatview/pipeline-integration.test.ts +++ b/app/shared/src/chatview/pipeline-integration.test.ts @@ -1404,3 +1404,45 @@ describe('Cross-pipeline: Edge + Hub blocks mixed in same TranscriptItem[]', () } }) }) + +describe('Pipeline: Hub fenced-code content survives normalization (#1971)', () => { + it('keeps fence structure from snake_case REST messages through blocks and items', () => { + const fenced = 'Here is a fenced code sample:\n```python\nprint("hello")\n```\n' + const blocks = normalizeHubMessagesToTranscript([ + { + id: 'm1', + session_id: 'hub-session-1', + seq_id: 1, + sender_type: 'user', + sender_id: 'partner-1', + content_type: 'text', + content: JSON.stringify({ text: fenced }), + }, + { + id: 'm2', + session_id: 'hub-session-1', + seq_id: 2, + sender_type: 'agent', + sender_id: 'agent-1', + content_type: 'text', + content: JSON.stringify({ text: fenced }), + }, + ]) + + expect(blocks).toHaveLength(2) + for (const block of blocks) { + expect(block.kind).toBe('text') + if (block.kind === 'text') { + expect(block.text).toContain('```python') + expect(block.text).toContain('print("hello")') + } + } + + const items = blocksToTranscriptItems(blocks) + const agentItem = items.find((item) => isTranscriptAgentItem(item)) + expect(agentItem).toBeDefined() + if (agentItem && isTranscriptAgentItem(agentItem)) { + expect(agentItem.bubbles.some((bubble) => bubble.includes('```python'))).toBe(true) + } + }) +}) diff --git a/app/shared/src/ui/Markdown.test.tsx b/app/shared/src/ui/Markdown.test.tsx index 6ec7a5709..0cca6146e 100644 --- a/app/shared/src/ui/Markdown.test.tsx +++ b/app/shared/src/ui/Markdown.test.tsx @@ -338,3 +338,13 @@ describe('markdown table header stickiness (codeg parity)', () => { expect(thRule).toMatch(/z-index:\s*var\(--z-base\);/); }); }); + +describe('fenced code baseline (#1971)', () => { + test('renders fenced code blocks with their exact source', () => { + const container = renderMarkdown('Here is a fenced code sample:\n```python\nprint("hello")\n```\n'); + const codeText = container.textContent ?? ''; + expect(codeText).toContain('print("hello")'); + expect(container.querySelectorAll('pre, code').length).toBeGreaterThan(0); + expect(codeText).not.toContain('```'); + }); +}); diff --git a/app/web/package.json b/app/web/package.json index f02e2ed29..528a61242 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -14,7 +14,7 @@ "test:e2e:smoke": "playwright test --config playwright.config.ts --project=chromium smoke.spec.ts", "test:e2e:real": "playwright test --config playwright.real.config.ts", "test:e2e:chat-flow": "playwright test --config playwright.config.ts --project=chromium chat-flow-contract.spec.ts", - "test:e2e:stubbed-hub": "playwright test --config playwright.config.ts --project=chromium chat-flow-contract.spec.ts web-stubbed-hub-replay-smoke.spec.ts task-contract.spec.ts", + "test:e2e:stubbed-hub": "playwright test --config playwright.config.ts --project=chromium chat-flow-contract.spec.ts web-stubbed-hub-replay-smoke.spec.ts web-stubbed-hub-fenced-code.spec.ts task-contract.spec.ts", "test:e2e:approved-real-stub": "pnpm test:e2e:stubbed-hub", "test:visual:chat-flow": "node scripts/manual-chat-flow-check.mjs", "visual:qa:shell": "node scripts/visual-qa-shell.mjs", diff --git a/app/web/src/__e2e__/web-stubbed-hub-fenced-code.spec.ts b/app/web/src/__e2e__/web-stubbed-hub-fenced-code.spec.ts new file mode 100644 index 000000000..7c403c898 --- /dev/null +++ b/app/web/src/__e2e__/web-stubbed-hub-fenced-code.spec.ts @@ -0,0 +1,125 @@ +import { expect, test, type Route } from '@playwright/test'; + +// #1971 contract: Hub-delivered text messages whose body contains fenced +// code must render as code blocks in the Web transcript, for both human +// (partner) and agent senders, using the real REST shapes (snake_case +// session id, jsonb string content with a `text` field). +const HUB_ORIGIN_HOST = 'hub.test.invalid'; + +const HUMAN_FENCED = 'partner fenced probe\n```python\nprint("hello fenced")\n```\n'; +const AGENT_FENCED = 'Here is a fenced code sample:\n```python\ndef greet():\n return 42\n```\n'; + +function corsHeaders(): Record { + return { + 'access-control-allow-origin': '*', + 'access-control-allow-headers': 'authorization,content-type', + 'access-control-allow-methods': 'GET,POST,PATCH,PUT,DELETE,OPTIONS', + }; +} + +function fulfill(data: unknown) { + return { + status: 200, + contentType: 'application/json', + body: JSON.stringify({ code: 'ok', data }), + headers: corsHeaders(), + }; +} + +test.describe('Web stubbed Hub fenced-code transcript (#1971)', () => { + test('renders fenced code blocks from Hub history for human and agent senders', async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('agenthub.workbench.dataMode', 'approved-real'); + window.sessionStorage.setItem('agenthub_hub_token', 'stubbed-hub-token'); + window.sessionStorage.setItem('agenthub_token_source', 'hub'); + window.sessionStorage.setItem('agenthub_hub_user', JSON.stringify({ + userId: 'user-fenced', + username: 'fenced', + })); + }); + + await page.route('**/*', async (route: Route) => { + const request = route.request(); + const url = new URL(request.url()); + + if (url.host !== HUB_ORIGIN_HOST) { + if (url.host === 'fonts.googleapis.com' || url.host === 'fonts.gstatic.com') { + return route.fulfill({ status: 200, contentType: 'text/css', body: '' }); + } + return route.continue(); + } + + if (request.method() === 'OPTIONS') { + return route.fulfill({ status: 204, headers: corsHeaders() }); + } + + const p = url.pathname; + if (p === '/client/auth/me') { + return route.fulfill(fulfill({ id: 'user-fenced', username: 'fenced', nickname: 'Fenced', avatar_url: '' })); + } + if (p === '/client/sessions') { + return route.fulfill(fulfill([{ + session_id: 'session-fenced', + type: 'private', + name: 'Fenced code probe', + member_count: 2, + unread_count: 0, + }])); + } + if (p === '/client/sessions/session-fenced/messages') { + return route.fulfill(fulfill([ + { + id: 'msg-fenced-1', + session_id: 'session-fenced', + seq_id: 1, + sender_type: 'user', + sender_id: 'partner-1', + content_type: 'text', + content: JSON.stringify({ text: HUMAN_FENCED }), + created_at: '2026-08-25T06:00:00Z', + }, + { + id: 'msg-fenced-2', + session_id: 'session-fenced', + seq_id: 2, + sender_type: 'agent', + sender_id: 'agent-fenced', + content_type: 'text', + content: JSON.stringify({ text: AGENT_FENCED }), + created_at: '2026-08-25T06:01:00Z', + }, + ])); + } + if (p === '/client/sessions/session-fenced/pins') { + return route.fulfill(fulfill([])); + } + if (p === '/client/contacts' || p === '/client/notifications') { + return route.fulfill(fulfill([])); + } + // Any other Hub call fails closed so missing stubs surface loudly. + return route.fulfill({ + status: 404, + contentType: 'application/json', + body: JSON.stringify({ code: 'not_stubbed', path: p }), + headers: corsHeaders(), + }); + }); + + await page.goto('/'); + + await expect(page.getByTestId('agenthub-workbench')).toHaveAttribute('data-page', 'chat'); + // Preamble text of both messages renders. + await expect(page.getByText('partner fenced probe')).toBeVisible(); + await expect(page.getByText('Here is a fenced code sample:')).toBeVisible(); + // The fenced code bodies render inside code-block containers (the + // Markdown CodeBlock wrapper in dev builds carries a readable + // `codeBlockWrapper` CSS-module class; the lazy highlighter or its + //
 fallback both live inside that wrapper).
+    const humanBlock = page.locator('[class*="codeBlockWrapper"]', { hasText: 'print("hello fenced")' });
+    await expect(humanBlock).toBeVisible();
+    const agentBlock = page.locator('[class*="codeBlockWrapper"]', { hasText: 'return 42' });
+    await expect(agentBlock).toBeVisible();
+    // The preamble paragraph and the code block are distinct nodes.
+    await expect(page.getByText('print("hello fenced")')).toBeVisible();
+  });
+});