Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/shared/src/chatview/components/AgentGroup.rendering.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<AgentGroup item={item} chatMode="dm" />);

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);
});
});
16 changes: 16 additions & 0 deletions app/shared/src/chatview/components/UserMessage.rendering.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<UserMessage item={item} chatMode="dm" />);

expect(getByText('partner fenced probe')).toBeInTheDocument();
expect(getByText('print("hello fenced")')).toBeInTheDocument();
expect(container.querySelectorAll('pre, code, [class*="codeBlockWrapper"]').length).toBeGreaterThan(0);
});
});
42 changes: 42 additions & 0 deletions app/shared/src/chatview/pipeline-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
})
10 changes: 10 additions & 0 deletions app/shared/src/ui/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('```');
});
});
2 changes: 1 addition & 1 deletion app/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
125 changes: 125 additions & 0 deletions app/web/src/__e2e__/web-stubbed-hub-fenced-code.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
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
// <pre><code> 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();
});
});
Loading