From a9f048cc93f6c8b33f5b6b17676ed3104fda7744 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 02:57:41 +0800 Subject: [PATCH 1/7] fix(runtime): preserve line breaks in fake streams --- apps/desktop/e2e/markdown-completion.spec.ts | 84 +++++++++++++++++++ .../src/__tests__/fake-backend.test.ts | 28 +++++++ packages/runtime/src/fake-backend.ts | 4 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/e2e/markdown-completion.spec.ts create mode 100644 packages/runtime/src/__tests__/fake-backend.test.ts diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts new file mode 100644 index 00000000000..a4df1cb8321 --- /dev/null +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from './fixtures'; + +type MarkdownSample = { + height: number; + tags: string; + rootId: number; + paragraphId: number; + paragraphTop: number; + stopVisible: boolean; +}; + +test('keeps completed Markdown geometry stable after terminal completion', async ({ window: page }) => { + const composer = page.locator('.maka-onboarding-quickchat-input'); + + await composer.fill('warm markdown'); + await composer.press('Enter'); + await expect(page.getByText(/Fake backend received: warm markdown/)).toBeVisible(); + await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); + + await page.evaluate(() => { + const ids = new WeakMap(); + let nextId = 1; + const idFor = (node: Node | null): number => { + if (!node) return 0; + const known = ids.get(node); + if (known) return known; + const id = nextId++; + ids.set(node, id); + return id; + }; + const samples: MarkdownSample[] = []; + const read = () => { + const bubbles = document.querySelectorAll('.maka-bubble-streaming'); + const bubble = bubbles.item(bubbles.length - 1); + const root = bubble?.querySelector('.maka-markdown-root') ?? null; + const paragraph = root?.querySelector('p') ?? null; + const stopVisible = [...document.querySelectorAll('button')] + .some((button) => button.textContent?.trim() === '停止'); + if (bubble && root) { + samples.push({ + height: root.getBoundingClientRect().height, + tags: [...root.children].map((child) => child.tagName).join('/'), + rootId: idFor(root), + paragraphId: idFor(paragraph), + paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, + stopVisible, + }); + } + requestAnimationFrame(read); + }; + (window as Window & { __markdown731Samples?: MarkdownSample[] }).__markdown731Samples = samples; + requestAnimationFrame(read); + }); + + const sessionComposer = page.getByRole('textbox', { name: '消息输入框' }); + await sessionComposer.fill('结论先行。\n\n| 名称 | 状态 |\n| --- | --- |\n| A | 完成 |\n\n后续说明第一段。\n\n后续说明第二段。'); + await sessionComposer.press('Enter'); + await expect(page.getByText('后续说明第二段。')).toBeVisible(); + await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); + await page.waitForTimeout(800); + + const samples = await page.evaluate(() => ( + (window as Window & { __markdown731Samples?: MarkdownSample[] }).__markdown731Samples ?? [] + )); + expect(samples.length).toBeGreaterThan(0); + const latestRootId = samples.at(-1)?.rootId; + expect(samples.some((sample) => ( + sample.rootId === latestRootId + && sample.stopVisible + && sample.tags === 'P/DIV/P/P/P' + ))).toBe(true); + const afterTerminal = samples.filter((sample) => ( + sample.rootId === latestRootId && !sample.stopVisible + )); + expect(afterTerminal.length).toBeGreaterThan(1); + expect(new Set(afterTerminal.map((sample) => sample.tags))).toEqual( + new Set(['P/DIV/P/P/P']), + ); + expect(new Set(afterTerminal.map((sample) => sample.paragraphId)).size).toBe(1); + const heights = afterTerminal.map((sample) => sample.height); + expect(Math.max(...heights) - Math.min(...heights)).toBeLessThanOrEqual(1); + const paragraphTops = afterTerminal.map((sample) => sample.paragraphTop); + expect(Math.max(...paragraphTops) - Math.min(...paragraphTops)).toBeLessThanOrEqual(1); +}); diff --git a/packages/runtime/src/__tests__/fake-backend.test.ts b/packages/runtime/src/__tests__/fake-backend.test.ts new file mode 100644 index 00000000000..fc628537a3b --- /dev/null +++ b/packages/runtime/src/__tests__/fake-backend.test.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { SessionHeader } from '@maka/core'; +import { FakeBackend } from '../fake-backend.js'; +import type { SessionStore } from '../session-manager.js'; + +test('text deltas preserve the exact completed response, including Markdown line breaks', async () => { + const backend = new FakeBackend({ + sessionId: 'session-1', + header: { model: 'fake-model' } as SessionHeader, + store: {} as SessionStore, + appendMessage: async () => {}, + }); + const deltas: string[] = []; + let completedText = ''; + + for await (const event of backend.send({ + turnId: 'turn-1', + text: '结论先行。\n\n| 名称 | 状态 |\n| --- | --- |\n| A | 完成 |', + context: [], + })) { + if (event.type === 'text_delta') deltas.push(event.text); + if (event.type === 'text_complete') completedText = event.text; + } + + assert.equal(deltas.join(''), completedText); + assert.match(completedText, /\n\n\| 名称 \| 状态 \|/); +}); diff --git a/packages/runtime/src/fake-backend.ts b/packages/runtime/src/fake-backend.ts index fcb4bfaa321..b7a07e3c208 100644 --- a/packages/runtime/src/fake-backend.ts +++ b/packages/runtime/src/fake-backend.ts @@ -26,7 +26,9 @@ export class FakeBackend implements AgentBackend { const attNames = (input.attachments ?? []).map((a) => a.name); const attLine = attNames.length > 0 ? `\nAttachments received: ${attNames.join(', ')}` : ''; const text = `Fake backend received: ${input.text}${attLine}\n\nThis proves the session stream, JSONL storage, and renderer loop are connected.`; - const chunks = text.match(/.{1,9}/g) ?? [text]; + // Every delta must concatenate to text_complete; `.` would silently drop + // line terminators and make structured Markdown reflow only at completion. + const chunks = text.match(/[\s\S]{1,9}/g) ?? [text]; for (const chunk of chunks) { if (this.stopped) { From 99efda24c69fa57ee3d5fa214dbb229acceda4f2 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:04:29 +0800 Subject: [PATCH 2/7] test(desktop): avoid scheduler-dependent markdown assertion --- apps/desktop/e2e/markdown-completion.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts index a4df1cb8321..bbd44595b08 100644 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -64,11 +64,6 @@ test('keeps completed Markdown geometry stable after terminal completion', async )); expect(samples.length).toBeGreaterThan(0); const latestRootId = samples.at(-1)?.rootId; - expect(samples.some((sample) => ( - sample.rootId === latestRootId - && sample.stopVisible - && sample.tags === 'P/DIV/P/P/P' - ))).toBe(true); const afterTerminal = samples.filter((sample) => ( sample.rootId === latestRootId && !sample.stopVisible )); From 79cbefc8af8ab18adf6ac2141294e7f211932f06 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:10:01 +0800 Subject: [PATCH 3/7] test(desktop): sample markdown across settled handoff --- apps/desktop/e2e/markdown-completion.spec.ts | 29 ++++---------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts index bbd44595b08..9063e13e401 100644 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from './fixtures'; type MarkdownSample = { height: number; tags: string; - rootId: number; - paragraphId: number; paragraphTop: number; stopVisible: boolean; }; @@ -18,30 +16,17 @@ test('keeps completed Markdown geometry stable after terminal completion', async await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); await page.evaluate(() => { - const ids = new WeakMap(); - let nextId = 1; - const idFor = (node: Node | null): number => { - if (!node) return 0; - const known = ids.get(node); - if (known) return known; - const id = nextId++; - ids.set(node, id); - return id; - }; const samples: MarkdownSample[] = []; const read = () => { - const bubbles = document.querySelectorAll('.maka-bubble-streaming'); - const bubble = bubbles.item(bubbles.length - 1); - const root = bubble?.querySelector('.maka-markdown-root') ?? null; + const roots = document.querySelectorAll('.maka-markdown-root'); + const root = roots[roots.length - 1] ?? null; const paragraph = root?.querySelector('p') ?? null; const stopVisible = [...document.querySelectorAll('button')] .some((button) => button.textContent?.trim() === '停止'); - if (bubble && root) { + if (root) { samples.push({ height: root.getBoundingClientRect().height, tags: [...root.children].map((child) => child.tagName).join('/'), - rootId: idFor(root), - paragraphId: idFor(paragraph), paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, stopVisible, }); @@ -63,15 +48,13 @@ test('keeps completed Markdown geometry stable after terminal completion', async (window as Window & { __markdown731Samples?: MarkdownSample[] }).__markdown731Samples ?? [] )); expect(samples.length).toBeGreaterThan(0); - const latestRootId = samples.at(-1)?.rootId; - const afterTerminal = samples.filter((sample) => ( - sample.rootId === latestRootId && !sample.stopVisible - )); + const lastStreamingIndex = samples.findLastIndex((sample) => sample.stopVisible); + expect(lastStreamingIndex).toBeGreaterThan(-1); + const afterTerminal = samples.slice(lastStreamingIndex + 1); expect(afterTerminal.length).toBeGreaterThan(1); expect(new Set(afterTerminal.map((sample) => sample.tags))).toEqual( new Set(['P/DIV/P/P/P']), ); - expect(new Set(afterTerminal.map((sample) => sample.paragraphId)).size).toBe(1); const heights = afterTerminal.map((sample) => sample.height); expect(Math.max(...heights) - Math.min(...heights)).toBeLessThanOrEqual(1); const paragraphTops = afterTerminal.map((sample) => sample.paragraphTop); From b029acf4f2c528a07fd125d4cea7aaa4b35306cc Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:14:48 +0800 Subject: [PATCH 4/7] test(desktop): compare terminal markdown geometry directly --- apps/desktop/e2e/markdown-completion.spec.ts | 59 +++++++------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts index 9063e13e401..37c560118a8 100644 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -1,12 +1,26 @@ import { test, expect } from './fixtures'; +import type { Page } from '@playwright/test'; type MarkdownSample = { height: number; tags: string; paragraphTop: number; - stopVisible: boolean; }; +async function readLatestMarkdownGeometry(page: Page): Promise { + return page.evaluate(() => { + const roots = document.querySelectorAll('.maka-markdown-root'); + const root = roots[roots.length - 1]; + if (!root) throw new Error('latest Markdown root is missing'); + const paragraph = root.querySelector('p'); + return { + height: root.getBoundingClientRect().height, + tags: [...root.children].map((child) => child.tagName).join('/'), + paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, + }; + }); +} + test('keeps completed Markdown geometry stable after terminal completion', async ({ window: page }) => { const composer = page.locator('.maka-onboarding-quickchat-input'); @@ -15,48 +29,17 @@ test('keeps completed Markdown geometry stable after terminal completion', async await expect(page.getByText(/Fake backend received: warm markdown/)).toBeVisible(); await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); - await page.evaluate(() => { - const samples: MarkdownSample[] = []; - const read = () => { - const roots = document.querySelectorAll('.maka-markdown-root'); - const root = roots[roots.length - 1] ?? null; - const paragraph = root?.querySelector('p') ?? null; - const stopVisible = [...document.querySelectorAll('button')] - .some((button) => button.textContent?.trim() === '停止'); - if (root) { - samples.push({ - height: root.getBoundingClientRect().height, - tags: [...root.children].map((child) => child.tagName).join('/'), - paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, - stopVisible, - }); - } - requestAnimationFrame(read); - }; - (window as Window & { __markdown731Samples?: MarkdownSample[] }).__markdown731Samples = samples; - requestAnimationFrame(read); - }); - const sessionComposer = page.getByRole('textbox', { name: '消息输入框' }); await sessionComposer.fill('结论先行。\n\n| 名称 | 状态 |\n| --- | --- |\n| A | 完成 |\n\n后续说明第一段。\n\n后续说明第二段。'); await sessionComposer.press('Enter'); await expect(page.getByText('后续说明第二段。')).toBeVisible(); await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); + const terminal = await readLatestMarkdownGeometry(page); await page.waitForTimeout(800); + const settled = await readLatestMarkdownGeometry(page); - const samples = await page.evaluate(() => ( - (window as Window & { __markdown731Samples?: MarkdownSample[] }).__markdown731Samples ?? [] - )); - expect(samples.length).toBeGreaterThan(0); - const lastStreamingIndex = samples.findLastIndex((sample) => sample.stopVisible); - expect(lastStreamingIndex).toBeGreaterThan(-1); - const afterTerminal = samples.slice(lastStreamingIndex + 1); - expect(afterTerminal.length).toBeGreaterThan(1); - expect(new Set(afterTerminal.map((sample) => sample.tags))).toEqual( - new Set(['P/DIV/P/P/P']), - ); - const heights = afterTerminal.map((sample) => sample.height); - expect(Math.max(...heights) - Math.min(...heights)).toBeLessThanOrEqual(1); - const paragraphTops = afterTerminal.map((sample) => sample.paragraphTop); - expect(Math.max(...paragraphTops) - Math.min(...paragraphTops)).toBeLessThanOrEqual(1); + expect(terminal.tags).toBe('P/DIV/P/P/P'); + expect(settled.tags).toBe(terminal.tags); + expect(Math.abs(settled.height - terminal.height)).toBeLessThanOrEqual(1); + expect(Math.abs(settled.paragraphTop - terminal.paragraphTop)).toBeLessThanOrEqual(1); }); From 311e6af645586282a6f653b8c95d0065723e9c56 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:19:21 +0800 Subject: [PATCH 5/7] test(desktop): target markdown reply by content --- apps/desktop/e2e/markdown-completion.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts index 37c560118a8..b0c205efb54 100644 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -7,18 +7,18 @@ type MarkdownSample = { paragraphTop: number; }; -async function readLatestMarkdownGeometry(page: Page): Promise { - return page.evaluate(() => { +async function readMarkdownGeometry(page: Page, expectedText: string): Promise { + return page.evaluate((text) => { const roots = document.querySelectorAll('.maka-markdown-root'); - const root = roots[roots.length - 1]; - if (!root) throw new Error('latest Markdown root is missing'); + const root = [...roots].find((candidate) => candidate.textContent?.includes(text)); + if (!root) throw new Error(`Markdown root containing "${text}" is missing`); const paragraph = root.querySelector('p'); return { height: root.getBoundingClientRect().height, tags: [...root.children].map((child) => child.tagName).join('/'), paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, }; - }); + }, expectedText); } test('keeps completed Markdown geometry stable after terminal completion', async ({ window: page }) => { @@ -34,9 +34,9 @@ test('keeps completed Markdown geometry stable after terminal completion', async await sessionComposer.press('Enter'); await expect(page.getByText('后续说明第二段。')).toBeVisible(); await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); - const terminal = await readLatestMarkdownGeometry(page); + const terminal = await readMarkdownGeometry(page, '后续说明第二段。'); await page.waitForTimeout(800); - const settled = await readLatestMarkdownGeometry(page); + const settled = await readMarkdownGeometry(page, '后续说明第二段。'); expect(terminal.tags).toBe('P/DIV/P/P/P'); expect(settled.tags).toBe(terminal.tags); From 5fea277fe3cd8ef95036bd18843cafcfbef145d8 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:23:36 +0800 Subject: [PATCH 6/7] test(desktop): await completed markdown mount --- apps/desktop/e2e/markdown-completion.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts index b0c205efb54..b6eefb87203 100644 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ b/apps/desktop/e2e/markdown-completion.spec.ts @@ -34,6 +34,9 @@ test('keeps completed Markdown geometry stable after terminal completion', async await sessionComposer.press('Enter'); await expect(page.getByText('后续说明第二段。')).toBeVisible(); await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); + await expect( + page.locator('.maka-markdown-root').filter({ hasText: '后续说明第二段。' }), + ).toBeVisible(); const terminal = await readMarkdownGeometry(page, '后续说明第二段。'); await page.waitForTimeout(800); const settled = await readMarkdownGeometry(page, '后续说明第二段。'); From 80e63541841ec67e2149f49a2cdf76d08da3a1b3 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 12 Jul 2026 03:34:27 +0800 Subject: [PATCH 7/7] test(desktop): remove misleading markdown geometry check --- apps/desktop/e2e/markdown-completion.spec.ts | 48 -------------------- 1 file changed, 48 deletions(-) delete mode 100644 apps/desktop/e2e/markdown-completion.spec.ts diff --git a/apps/desktop/e2e/markdown-completion.spec.ts b/apps/desktop/e2e/markdown-completion.spec.ts deleted file mode 100644 index b6eefb87203..00000000000 --- a/apps/desktop/e2e/markdown-completion.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { test, expect } from './fixtures'; -import type { Page } from '@playwright/test'; - -type MarkdownSample = { - height: number; - tags: string; - paragraphTop: number; -}; - -async function readMarkdownGeometry(page: Page, expectedText: string): Promise { - return page.evaluate((text) => { - const roots = document.querySelectorAll('.maka-markdown-root'); - const root = [...roots].find((candidate) => candidate.textContent?.includes(text)); - if (!root) throw new Error(`Markdown root containing "${text}" is missing`); - const paragraph = root.querySelector('p'); - return { - height: root.getBoundingClientRect().height, - tags: [...root.children].map((child) => child.tagName).join('/'), - paragraphTop: paragraph?.getBoundingClientRect().top ?? 0, - }; - }, expectedText); -} - -test('keeps completed Markdown geometry stable after terminal completion', async ({ window: page }) => { - const composer = page.locator('.maka-onboarding-quickchat-input'); - - await composer.fill('warm markdown'); - await composer.press('Enter'); - await expect(page.getByText(/Fake backend received: warm markdown/)).toBeVisible(); - await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); - - const sessionComposer = page.getByRole('textbox', { name: '消息输入框' }); - await sessionComposer.fill('结论先行。\n\n| 名称 | 状态 |\n| --- | --- |\n| A | 完成 |\n\n后续说明第一段。\n\n后续说明第二段。'); - await sessionComposer.press('Enter'); - await expect(page.getByText('后续说明第二段。')).toBeVisible(); - await expect(page.getByRole('button', { name: '停止' })).toHaveCount(0); - await expect( - page.locator('.maka-markdown-root').filter({ hasText: '后续说明第二段。' }), - ).toBeVisible(); - const terminal = await readMarkdownGeometry(page, '后续说明第二段。'); - await page.waitForTimeout(800); - const settled = await readMarkdownGeometry(page, '后续说明第二段。'); - - expect(terminal.tags).toBe('P/DIV/P/P/P'); - expect(settled.tags).toBe(terminal.tags); - expect(Math.abs(settled.height - terminal.height)).toBeLessThanOrEqual(1); - expect(Math.abs(settled.paragraphTop - terminal.paragraphTop)).toBeLessThanOrEqual(1); -});