From 03f6cf903b4961f584fc7f19371a92407533745d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 8 Aug 2026 11:39:53 +0800 Subject: [PATCH 01/22] test(ui): delete presentation-only suites --- .../ui/src/__tests__/chat-primitives.test.ts | 20 -- .../__tests__/chat-surface-layout.test.tsx | 45 --- .../src/__tests__/daily-review-panel.test.tsx | 49 ---- .../__tests__/deep-research-progress.test.tsx | 103 ------- .../ui/src/__tests__/model-picker.test.ts | 120 -------- .../plan-reminder-localization.test.ts | 100 ------- .../src/__tests__/processing-block.test.tsx | 228 -------------- .../ui/src/__tests__/quote-ref-chip.test.ts | 26 -- .../src/__tests__/shell-controls-copy.test.ts | 14 - .../__tests__/sidebar-subtraction.test.tsx | 277 ------------------ .../__tests__/turn-running-status.test.tsx | 172 ----------- 11 files changed, 1154 deletions(-) delete mode 100644 packages/ui/src/__tests__/chat-primitives.test.ts delete mode 100644 packages/ui/src/__tests__/chat-surface-layout.test.tsx delete mode 100644 packages/ui/src/__tests__/daily-review-panel.test.tsx delete mode 100644 packages/ui/src/__tests__/deep-research-progress.test.tsx delete mode 100644 packages/ui/src/__tests__/model-picker.test.ts delete mode 100644 packages/ui/src/__tests__/plan-reminder-localization.test.ts delete mode 100644 packages/ui/src/__tests__/processing-block.test.tsx delete mode 100644 packages/ui/src/__tests__/quote-ref-chip.test.ts delete mode 100644 packages/ui/src/__tests__/shell-controls-copy.test.ts delete mode 100644 packages/ui/src/__tests__/sidebar-subtraction.test.tsx delete mode 100644 packages/ui/src/__tests__/turn-running-status.test.tsx diff --git a/packages/ui/src/__tests__/chat-primitives.test.ts b/packages/ui/src/__tests__/chat-primitives.test.ts deleted file mode 100644 index 81ee239015..0000000000 --- a/packages/ui/src/__tests__/chat-primitives.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { Marker } from '../primitives/chat.js'; - -test('Marker keeps its own data-slot/data-variant but forwards the styling data-* hooks', () => { - const el = Marker({ - variant: 'footer-action', - as: 'span', - 'data-slot': 'spoofed', - 'data-variant': 'aborted', - // The literalized `data-[kind=…]:` variants read this off the element, so it - // must flow through unchanged. - 'data-kind': 'model', - } as never); - const props = el.props as Record; - assert.equal(el.type, 'span'); - assert.equal(props['data-slot'], 'marker'); - assert.equal(props['data-variant'], 'footer-action'); - assert.equal(props['data-kind'], 'model'); -}); diff --git a/packages/ui/src/__tests__/chat-surface-layout.test.tsx b/packages/ui/src/__tests__/chat-surface-layout.test.tsx deleted file mode 100644 index 31dcbc9cb6..0000000000 --- a/packages/ui/src/__tests__/chat-surface-layout.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { ChatSurfaceLayout } from '../chat-surface-layout.js'; - -describe('ChatSurfaceLayout', () => { - it('uses one Astryx layout as the chat scroll and composer owner', () => { - const markup = renderToStaticMarkup( - Composer}> -
Transcript
-
, - ); - - assert.match(markup, /class="[^"]*astryx-chat-layout[^"]*"/); - assert.equal(markup.match(/data-chat-scroll-container="true"/g)?.length, 1); - assert.match(markup, /data-transcript="true"/); - assert.match(markup, /data-composer="true"/); - }); - - it('docks the composer at Astryx balanced density', () => { - // The tier is the product decision; the px it resolves to is Astryx's. - // Asserting the tier here leaves the E2E free to measure only what the - // cascade decides — that the gutter is positive and height-invariant — - // instead of pinning a spacing token this package does not own. - const markup = renderToStaticMarkup( - Composer}> -
Transcript
-
, - ); - - assert.match(markup, /data-density="balanced"/); - }); - - it('keeps the composer mounted when the chat surface is hidden', () => { - const markup = renderToStaticMarkup( - , - ); - - assert.match(markup, /hidden=""/); - assert.match(markup, /data-composer="true"/); - assert.match(markup, /Draft survives navigation/); - }); -}); diff --git a/packages/ui/src/__tests__/daily-review-panel.test.tsx b/packages/ui/src/__tests__/daily-review-panel.test.tsx deleted file mode 100644 index fa334572da..0000000000 --- a/packages/ui/src/__tests__/daily-review-panel.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { renderToStaticMarkup } from 'react-dom/server'; - -import { DailyReviewPanel } from '../daily-review-panel.js'; -import { LocaleProvider } from '../locale-context.js'; - -function renderPanel(): string { - return renderToStaticMarkup( - - ({ - day: { fromMs: 0, toMs: 1 }, - totals: { sessionCount: 0, requestCount: 0, totalTokens: 0, costUsd: 0, errorCount: 0 }, - sessions: [], - topModels: [], - topTools: [], - }), - runOnce: async () => ({ archiveId: '1970-01-01-1d' }), - listArchives: async () => [], - getArchive: async () => { - throw new Error('No archive'); - }, - }} - /> - , - ); -} - -describe('Daily Review activity surface', () => { - it('offers one range-based analysis action without the retired modes', () => { - const markup = renderPanel(); - - assert.match(markup, />今日最近 7 天最近 30 天生成分析 { - const markup = renderPanel(); - - assert.doesNotMatch(markup, />报告模型使用工具调用 { - it('renders inspectable progress and an explicit normal-task handoff action', () => { - const markup = renderToStaticMarkup( - undefined} />, - ); - - assert.match(markup, /研究完成 · 原会话保持只读/); - assert.match(markup, /Map project entrypoints/); - assert.match(markup, /DeepResearchProgressPanel/); - assert.match(markup, /Workers: worker-1/); - assert.match(markup, /取舍与风险/); - assert.match(markup, /在新任务中继续实现/); - assert.match(markup, /不会自动发送,也不会改变原研究会话权限/); - }); - - it('does not expose the implementation action before completion', () => { - const run = completedRun(); - const markup = renderToStaticMarkup( - undefined} - />, - ); - - assert.doesNotMatch(markup, /在新任务中继续实现/); - assert.match(markup, /report_writing/); - }); - - it('renders the progress surface from the selected locale catalog', () => { - const markup = renderToStaticMarkup( - , - ); - - assert.match(markup, /Research complete · Original session remains read-only/); - assert.match(markup, /Tradeoffs and risks/); - assert.doesNotMatch(markup, /研究完成|取舍与风险/); - }); -}); - -describe('DeepResearchEmptyHero', () => { - it('exposes each starter label and prompt as one clickable item name', () => { - const copy = getConversationCopy('en').deepResearchEmpty; - const markup = renderToStaticMarkup( - - undefined} /> - , - ); - - const buttons = markup.match(/]*>[\s\S]*?<\/button>/g) ?? []; - for (const suggestion of copy.starters) { - const promptPreview = suggestion.prompt.slice(0, 60); - const button = buttons.find((candidate) => - candidate.includes(suggestion.label) && candidate.includes(promptPreview) - ) ?? ''; - assert.notEqual(button, '', `missing accessible starter for ${suggestion.label}`); - assert.doesNotMatch(button, /aria-label=/, 'visible label and description must own the accessible name'); - } - }); -}); diff --git a/packages/ui/src/__tests__/model-picker.test.ts b/packages/ui/src/__tests__/model-picker.test.ts deleted file mode 100644 index cd7dc5c038..0000000000 --- a/packages/ui/src/__tests__/model-picker.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; -import { - buildModelPickerOptions, - buildModelPickerProviderTypes, -} from '../model-picker-internals.js'; -import type { ModelMenuGroup } from '../chat-model-helpers.js'; - -const groups: ModelMenuGroup[] = [ - { - connectionSlug: 'anthropic-team', - providerType: 'anthropic', - heading: 'Anthropic Team', - choices: [ - { - connectionSlug: 'anthropic-team', - providerType: 'anthropic', - providerLabel: 'Anthropic', - model: 'claude-sonnet-4', - label: 'Claude Sonnet 4', - isDefault: true, - thinkingLevels: [], - }, - ], - }, - { - connectionSlug: 'openai-main', - providerType: 'openai', - heading: 'OpenAI', - choices: [ - { - connectionSlug: 'openai-main', - providerType: 'openai', - providerLabel: 'OpenAI', - model: 'gpt-5', - label: 'GPT-5', - isDefault: true, - thinkingLevels: [], - }, - { - connectionSlug: 'openai-main', - providerType: 'openai', - providerLabel: 'OpenAI', - model: 'o3-mini', - label: 'o3-mini', - isDefault: false, - thinkingLevels: [], - }, - ], - }, -]; - -describe('ModelPicker option shaping', () => { - it('leaves a catalog with no choices empty', () => { - assert.deepEqual(buildModelPickerOptions([]), []); - }); - - it('maps each provider group to public Astryx sections', () => { - assert.deepEqual(buildModelPickerOptions(groups), [ - { - type: 'section', - title: 'Anthropic Team', - options: [ - { - value: 'anthropic-team:claude-sonnet-4', - label: 'Claude Sonnet 4', - }, - ], - }, - { - type: 'section', - title: 'OpenAI', - options: [ - { value: 'openai-main:gpt-5', label: 'GPT-5' }, - { value: 'openai-main:o3-mini', label: 'o3-mini' }, - ], - }, - ]); - }); - - it('models an unknown current value as an ordinary option before the catalog', () => { - assert.deepEqual( - buildModelPickerOptions(groups, { - value: 'legacy:model-that-is-no-longer-listed', - label: 'model-that-is-no-longer-listed', - providerType: 'openai-compatible', - }), - [ - { - value: 'legacy:model-that-is-no-longer-listed', - label: 'model-that-is-no-longer-listed', - }, - { type: 'divider' }, - ...buildModelPickerOptions(groups), - ], - ); - }); - - it('keeps an empty-value choice as an ordinary searchable option', () => { - assert.deepEqual(buildModelPickerOptions([], { value: '', label: '未设置' }), [ - { value: '', label: '未设置' }, - ]); - }); - - it('maps provider marks by public option value without adding private Selector fields', () => { - assert.deepEqual( - [...buildModelPickerProviderTypes(groups, { - value: 'legacy:model', - label: 'Legacy model', - providerType: 'openai-compatible', - })], - [ - ['legacy:model', 'openai-compatible'], - ['anthropic-team:claude-sonnet-4', 'anthropic'], - ['openai-main:gpt-5', 'openai'], - ['openai-main:o3-mini', 'openai'], - ], - ); - }); -}); diff --git a/packages/ui/src/__tests__/plan-reminder-localization.test.ts b/packages/ui/src/__tests__/plan-reminder-localization.test.ts deleted file mode 100644 index 4a32c20c97..0000000000 --- a/packages/ui/src/__tests__/plan-reminder-localization.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { - duplicatePlanReminderTitle, - formatReminderCountdown, - planReminderFormValidation, - planReminderTemplateSeed, -} from '../plan-reminder-helpers.js'; -import { getPlanReminderCopy } from '../plan-reminder-copy.js'; - -describe('plan reminder localization', () => { - it('provides coherent page copy in both locales', () => { - assert.equal(getPlanReminderCopy('zh').page.title, '定时任务'); - assert.equal(getPlanReminderCopy('en').page.title, 'Scheduled tasks'); - }); - - it('formats validation, duplication, and countdowns with the resolved locale', () => { - const input = { - title: '', - parsedRunAt: Date.now() + 60_000, - recurrence: 'none' as const, - cronExpression: '', - delivery: { channel: 'local' as const }, - now: Date.now(), - }; - assert.deepEqual( - planReminderFormValidation(input, 'zh'), - { field: 'title', message: '填写标题后才能保存提醒。' }, - ); - assert.deepEqual( - planReminderFormValidation(input, 'en'), - { field: 'title', message: 'Add a title before saving this reminder.' }, - ); - assert.deepEqual( - planReminderFormValidation({ - ...input, - title: 'Review', - recurrence: 'cron', - cronExpression: 'invalid', - }, 'en'), - { - field: 'cron', - message: 'Cron expressions need five fields, for example 0 9 * * 1-5.', - }, - ); - assert.deepEqual( - planReminderFormValidation({ - ...input, - title: 'Review', - parsedRunAt: Number.NaN, - }, 'zh'), - { field: 'time', message: '选择有效的提醒时间。' }, - ); - assert.deepEqual( - planReminderFormValidation({ - ...input, - title: 'Review', - parsedRunAt: input.now - 1, - }, 'zh'), - { field: 'time', message: '提醒时间必须晚于当前时间。' }, - ); - assert.deepEqual( - planReminderFormValidation({ - ...input, - title: 'Review', - delivery: { channel: 'bot', platform: 'telegram', chatId: '' }, - }, 'en'), - { - field: 'chatId', - message: 'Enter a Chat ID when delivering to a bot chat.', - }, - ); - assert.equal(duplicatePlanReminderTitle('Review', 'en'), 'Review copy'); - assert.equal(formatReminderCountdown(120_000, 'en', 0), 'in 2 minutes'); - assert.equal(formatReminderCountdown(120_000, 'zh', 0), '2 分钟后'); - }); - - it('prefills a localized template into the existing create form seed', () => { - const now = new Date(2026, 6, 25, 10, 0).getTime(); - const template = getPlanReminderCopy('zh').templates.find(({ id }) => id === 'daily-news-brief'); - - assert.ok(template); - const seed = planReminderTemplateSeed(template, now); - assert.match( - seed.runAtLocal, - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/, - 'local date-time values use the ISO minute shape', - ); - const runAt = new Date(seed.runAtLocal); - - assert.equal(seed.editingId, null); - assert.equal(seed.title, '每日新闻摘要'); - assert.match(seed.note, /科技 \/ AI \/ Maka/); - assert.equal(seed.recurrence, 'cron'); - assert.equal(seed.cronExpression, '30 9 * * *'); - assert.equal(runAt.getHours(), 9); - assert.equal(runAt.getMinutes(), 30); - assert.ok(runAt.getTime() > now); - }); -}); diff --git a/packages/ui/src/__tests__/processing-block.test.tsx b/packages/ui/src/__tests__/processing-block.test.tsx deleted file mode 100644 index ae7fc8bcde..0000000000 --- a/packages/ui/src/__tests__/processing-block.test.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { createElement, type ReactNode } from 'react'; -import { renderToStaticMarkup as renderReactToStaticMarkup } from 'react-dom/server'; -import { LocaleProvider } from '../locale-context.js'; -import type { ToolActivityItem, TurnViewModel } from '../materialize.js'; -import { TurnView } from '../chat-turn.js'; - -function renderToStaticMarkup(node: ReactNode): string { - return renderReactToStaticMarkup(createElement(LocaleProvider, { - locale: 'zh', - children: node, - })); -} - -function renderInDeterministicFixture(node: ReactNode): string { - const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'document'); - Object.defineProperty(globalThis, 'document', { - configurable: true, - value: { - documentElement: { - dataset: { makaE2eFixture: 'true' }, - }, - }, - }); - try { - return renderToStaticMarkup(node); - } finally { - if (documentDescriptor) { - Object.defineProperty(globalThis, 'document', documentDescriptor); - } else { - Reflect.deleteProperty(globalThis, 'document'); - } - } -} - -function turnWithTools(tools: ToolActivityItem[]): TurnViewModel { - return { - turnId: 'turn-1', - status: 'completed', - partialOutputRetained: false, - tools, - notes: [], - timeline: [ - { kind: 'thinking', text: 'reasoning', messageId: 'a1' }, - { kind: 'tools', items: tools }, - ], - startedAt: 1, - }; -} - -describe('ProcessingBlock disclosure wiring (#1307)', () => { - it('does not wrap native reasoning and tool disclosures in another processing layer', () => { - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: turnWithTools([ - { toolUseId: 'r1', toolName: 'Read', activityKind: 'read', status: 'completed', args: {} }, - ]), - })); - - assert.doesNotMatch(markup, /data-processing="block"/); - assert.match(markup, /class="[^"]*astryx-chat-tool-calls[^"]*"/); - assert.match(markup, /深度思考/); - }); - - it('keeps an unsettled group in the same collapsed Astryx rows, without a processing wrapper', () => { - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: turnWithTools([ - { toolUseId: 'b1', toolName: 'Bash', activityKind: 'command', status: 'completed', args: {} }, - { toolUseId: 'w1', toolName: 'Write', activityKind: 'edit', status: 'running', args: {}, intent: '写入配置' }, - ]), - })); - assert.doesNotMatch(markup, /data-processing="block"/); - assert.match(markup, /class="[^"]*astryx-chat-tool-calls[^"]*"/); - assert.doesNotMatch(markup, /aria-expanded="true"/); - assert.match(markup, /写入配置/); - }); -}); - -describe('deep-thinking disclosure', () => { - it('shows complete live reasoning immediately in deterministic fixtures', () => { - const markup = renderInDeterministicFixture(createElement(TurnView, { - turn: { - turnId: 'fixture-thinking-turn', - status: 'running', - partialOutputRetained: false, - tools: [], - notes: [], - timeline: [{ - kind: 'thinking', - text: 'deterministic fixture reasoning', - messageId: 'fixture-thinking-1', - live: true, - }], - startedAt: 1, - }, - liveStreaming: {}, - })); - - assert.match(markup, /deterministic fixture reasoning/); - }); - - it('renders through official Astryx ChatReasoning with its native collapsed preview', () => { - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: { - turnId: 'thinking-turn', - status: 'completed', - partialOutputRetained: false, - tools: [], - notes: [], - timeline: [{ kind: 'thinking', text: 'private reasoning', messageId: 'thinking-1' }], - startedAt: 1, - }, - })); - - assert.match(markup, /class="[^"]*astryx-chat-reasoning[^"]*"/); - assert.match(markup, /class="maka-assistant-answer-content"/); - assert.match(markup, /role="button"[^>]*aria-expanded="false"/); - // The reasoning body carries the product wrap class so - // `.maka-chat-reasoning-content` in styles.css can restore pre-wrap — - // Astryx's own atoms declare no white-space, so without this seam the - // inherited `normal` collapses every newline in the thinking text. - assert.match(markup, /class="maka-chat-reasoning-content [^"]*"/); - assert.match(markup, /private reasoning/); - assert.doesNotMatch(markup, /data-slot="reasoning-trigger"/); - assert.doesNotMatch(markup, /复制思考过程/); - assert.doesNotMatch(markup, /data-slot="reasoning-disclosure"/); - }); - - it('draws its chevron from the same Astryx icon registry as the tool rows', () => { - // The ejected lab component used to hand-write its own 12-viewBox chevron, - // which drew a heavier stroke than the tool rows' registry glyph once - // chat-message.css forced both to 10x10. - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: { - ...turnWithTools([ - { toolUseId: 'r1', toolName: 'Read', activityKind: 'read', status: 'completed', args: {} }, - { toolUseId: 'r2', toolName: 'Read', activityKind: 'read', status: 'completed', args: {} }, - ]), - timeline: [ - { kind: 'thinking', text: 'private reasoning', messageId: 'a1' }, - { - kind: 'tools', - items: [ - { toolUseId: 'r1', toolName: 'Read', activityKind: 'read', status: 'completed', args: {} }, - { toolUseId: 'r2', toolName: 'Read', activityKind: 'read', status: 'completed', args: {} }, - ], - }, - ], - }, - })); - - // Compared per row, not over one filtered pool of the whole turn. A pool - // can only ever prove the icons it collected are alike, and the reasoning - // row's old hand-written chevron was not a registry icon at all — it fell - // out of the pool, leaving the tool rows to agree with themselves. Slicing - // the turn at the two row roots keeps the reasoning row inside its own - // assertion, so it fails whichever way the row diverges: no registry icon, - // a different icon name, or a different size. - const registryIcons = (region: string) => - region.match(/]*class="astryx-icon[^"]*"[^>]*>.*?<\/svg><\/span>/g) ?? []; - const toolsAt = markup.indexOf('class="astryx-chat-tool-calls'); - const reasoningAt = markup.indexOf('class="astryx-chat-reasoning'); - assert.ok(reasoningAt >= 0 && toolsAt > reasoningAt, 'expected a reasoning row before the tool row'); - - // The reasoning row's only registry icon is the chevron: its leading - // thinking glyph is still the ejected lab's own inline svg. - const reasoningIcons = registryIcons(markup.slice(reasoningAt, toolsAt)); - assert.equal(reasoningIcons.length, 1, 'the reasoning row must draw exactly one registry icon'); - - // The tool row also carries per-call status icons, so name the chevron by - // its glyph. Both rows resolve the same registry entry, so whichever path - // data that entry holds, the two spans have to come out byte-identical. - const toolChevrons = registryIcons(markup.slice(toolsAt)).filter((icon) => icon.includes('M6 9l6 6 6-6')); - assert.ok(toolChevrons.length > 0, 'expected a chevron on the tool row'); - assert.equal(reasoningIcons[0], toolChevrons[0], 'both rows must render the same chevron markup'); - assert.match(reasoningIcons[0]!, /data-size="xsm"/); - assert.match(reasoningIcons[0]!, /viewBox="0 0 24 24"/); - }); - - it('places assistant actions on ChatMessageMetadata, not a product Marker footer shell', () => { - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: { - turnId: 'footer-turn', - status: 'completed', - partialOutputRetained: false, - tools: [], - notes: [], - timeline: [{ kind: 'text', text: 'answer body', messageId: 'a1', ts: 1 }], - startedAt: 1, - assistant: { id: 'a1', role: 'assistant', text: 'answer body', ts: 1 }, - }, - footerActions: [ - { id: 'copy', label: '复制', enabled: true }, - { id: 'regenerate', label: '重新生成', enabled: true }, - ], - })); - - assert.match(markup, /astryx-chat-message-metadata|chat-message-metadata/); - assert.match(markup, /data-action="copy"/); - assert.match(markup, /data-action="regenerate"/); - assert.doesNotMatch(markup, /data-variant="footer"/); - }); - - it('keeps redaction and truncation product behavior outside the Astryx disclosure', () => { - const secret = 'sk-abcdefghijklmnopqrstuvwxyz123456'; - const markup = renderToStaticMarkup(createElement(TurnView, { - turn: { - turnId: 'safe-thinking-turn', - status: 'completed', - partialOutputRetained: false, - tools: [], - notes: [], - timeline: [{ - kind: 'thinking', - text: `api_key=${secret}`, - truncated: true, - messageId: 'thinking-2', - }], - startedAt: 1, - }, - })); - - assert.match(markup, /深度思考 · 已截断/); - assert.match(markup, /<redacted>/); - assert.doesNotMatch(markup, new RegExp(secret)); - assert.doesNotMatch(markup, /data-truncated="true"/); - }); -}); diff --git a/packages/ui/src/__tests__/quote-ref-chip.test.ts b/packages/ui/src/__tests__/quote-ref-chip.test.ts deleted file mode 100644 index 3daf98fdbd..0000000000 --- a/packages/ui/src/__tests__/quote-ref-chip.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { stripQuoteHeadingMarkers } from '../quote-ref-chip.js'; - -test('strips a leading ATX heading marker so the chip shows the title text', () => { - assert.equal(stripQuoteHeadingMarkers('### Description'), 'Description'); - assert.equal(stripQuoteHeadingMarkers('## Sub section'), 'Sub section'); - assert.equal(stripQuoteHeadingMarkers('# Top'), 'Top'); - assert.equal(stripQuoteHeadingMarkers('###### Deep'), 'Deep'); -}); - -test('strips the marker with one or more spaces/tabs, keeping the rest', () => { - assert.equal(stripQuoteHeadingMarkers('### double space'), 'double space'); - assert.equal(stripQuoteHeadingMarkers('###\ttabbed'), 'tabbed'); -}); - -test('leaves non-heading text untouched', () => { - assert.equal(stripQuoteHeadingMarkers('plain text'), 'plain text'); - assert.equal(stripQuoteHeadingMarkers('.analysis/issue.md ### Description'), '.analysis/issue.md ### Description'); - assert.equal(stripQuoteHeadingMarkers('###Description'), '###Description'); - assert.equal(stripQuoteHeadingMarkers('######x'), '######x'); - assert.equal(stripQuoteHeadingMarkers('####### x'), '####### x'); - assert.equal(stripQuoteHeadingMarkers('C# code'), 'C# code'); - assert.equal(stripQuoteHeadingMarkers('#hashtag'), '#hashtag'); - assert.equal(stripQuoteHeadingMarkers(''), ''); -}); diff --git a/packages/ui/src/__tests__/shell-controls-copy.test.ts b/packages/ui/src/__tests__/shell-controls-copy.test.ts deleted file mode 100644 index 14438577b9..0000000000 --- a/packages/ui/src/__tests__/shell-controls-copy.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; - -import { getShellControlsCopy } from '../shell-controls-copy.js'; - -describe('shared shell controls copy', () => { - it('provides complete navigation and search copy in both locales', () => { - assert.equal(getShellControlsCopy('zh').navigation.settings, '设置'); - assert.equal(getShellControlsCopy('en').navigation.settings, 'Settings'); - assert.equal(getShellControlsCopy('en').navigation.newTask, 'New task'); - assert.equal(getShellControlsCopy('en').search.placeholder, 'Search conversation titles and content…'); - assert.equal(getShellControlsCopy('en').search.results(2), '2 matches'); - }); -}); diff --git a/packages/ui/src/__tests__/sidebar-subtraction.test.tsx b/packages/ui/src/__tests__/sidebar-subtraction.test.tsx deleted file mode 100644 index 75c3875b0d..0000000000 --- a/packages/ui/src/__tests__/sidebar-subtraction.test.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { renderToStaticMarkup } from 'react-dom/server'; -import type { PlanReminder, SessionSummary } from '@maka/core'; -import { LocaleProvider } from '../locale-context.js'; -import { ModuleHubSelector } from '../module-hub-selector.js'; -import { SessionListPanel } from '../session-list-panel.js'; -import { SessionSidebarNav } from '../session-sidebar-nav.js'; - -function renderSidebarNav(): string { - return renderToStaticMarkup( - - {}} - onNew={() => {}} - /> - , - ); -} - -describe('sidebar subtraction', () => { - it('keeps only the hub-level destinations in permanent navigation', () => { - const markup = renderSidebarNav(); - - assert.match(markup, />新任务扩展定时任务技能MCP每日回顾 { - const reminder: PlanReminder = { - id: 'reminder-1', - title: 'Review open work', - note: '', - schedule: { kind: 'once', runAt: 1 }, - delivery: { channel: 'local' }, - status: 'scheduled', - enabled: true, - createdAt: 1, - updatedAt: 1, - runs: [], - runCount: 0, - }; - const markup = renderToStaticMarkup( - - {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - - assert.match(markup, /aria-label="Scheduled tasks, 1 unfinished reminder"/); - assert.doesNotMatch(markup, /maka-nav-count/); - }); - - it('pins permanent destinations in SideNav topContent outside the scroll history', () => { - const session: SessionSummary = { - id: 'session-1', - name: '侧栏置顶导航', - status: 'active', - isFlagged: false, - isArchived: false, - labels: [], - hasUnread: false, - lastMessageAt: 1, - backend: 'ai-sdk', - llmConnectionSlug: 'anthropic-main', - connectionLocked: false, - model: 'claude-sonnet-4-5', - permissionMode: 'ask', - }; - const expanded = renderToStaticMarkup( - - {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - assert.match(expanded, /maka-session-panel-top/); - assert.ok( - expanded.indexOf('maka-session-panel-top') < expanded.indexOf('maka-session-list'), - 'permanent nav top slot must precede scrollable session history', - ); - assert.match(expanded, /maka-session-panel-top[\s\S]*?>新任务 - {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - assert.match(collapsed, /maka-session-panel-top/); - assert.doesNotMatch(collapsed, /maka-session-list/); - assert.doesNotMatch(collapsed, /侧栏置顶导航/); - }); - - it('renders each pair of peer modules as localized view navigation', () => { - const extensions = renderToStaticMarkup( - - {}} /> - , - ); - const automations = renderToStaticMarkup( - - {}} /> - , - ); - - assert.match(extensions, /astryx-tab-list/); - assert.match(extensions, /aria-label="扩展内容:技能"/); - assert.doesNotMatch(extensions, /role="radiogroup"/); - assert.match(extensions, /aria-current="page"/); - assert.match(extensions, />技能MCP计划提醒每日回顾 { - const session: SessionSummary = { - id: 'session-1', - name: '侧栏减法', - status: 'active', - isFlagged: false, - isArchived: false, - labels: [], - hasUnread: false, - lastMessageAt: 1, - backend: 'ai-sdk', - llmConnectionSlug: 'anthropic-main', - connectionLocked: false, - model: 'claude-sonnet-4-5', - permissionMode: 'ask', - }; - const markup = renderToStaticMarkup( - - {}} - onSelectSession={() => {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - - assert.match(markup, /maka-session-heading-section/); - assert.match(markup, /aria-label="会话分组方式"/); - assert.doesNotMatch(markup, />按状态 { - const markup = renderToStaticMarkup( - - {}} - onSelectSession={() => {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - - assert.match(markup, /maka-session-heading-section/); - assert.match(markup, />会话 { - const makeSession = ( - session: Pick, - ): SessionSummary => ({ - ...session, - isArchived: false, - labels: [], - hasUnread: false, - backend: 'ai-sdk', - llmConnectionSlug: 'anthropic-main', - connectionLocked: false, - model: 'claude-sonnet-4-5', - permissionMode: 'ask', - }); - const sessions = [ - makeSession({ - id: 'recent-older', - name: '较早会话', - status: 'done', - isFlagged: false, - lastMessageAt: 100, - }), - makeSession({ - id: 'pinned-older', - name: '较早置顶', - status: 'blocked', - isFlagged: true, - lastMessageAt: 200, - }), - makeSession({ - id: 'recent-newer', - name: '最近会话', - status: 'active', - isFlagged: false, - lastMessageAt: 400, - }), - makeSession({ - id: 'pinned-newer', - name: '最近置顶', - status: 'running', - isFlagged: true, - lastMessageAt: 300, - }), - ]; - const markup = renderToStaticMarkup( - - {}} - onSelectSession={() => {}} - onSelect={() => {}} - onOpenSettings={() => {}} - onNew={() => {}} - /> - , - ); - - assert.match(markup, />置顶最近 { - it('stays up while the turn already has live content on screen', () => { - // The regression this locks: the cue it replaced was gated on the turn - // having produced nothing yet, so it disappeared the moment a tool started - // — leaving a long tool run with no sign the harness was still working. - const markup = render(createElement(TurnView, { - turn: turnWithLiveContent(), - liveStreaming: { runningStatus: true }, - })); - - assert.match(markup, /maka-turn-processing/); - }); - - it('shows a working phrase from the pool', () => { - const markup = render(createElement(TurnView, { - turn: turnWithLiveContent(), - liveStreaming: { runningStatus: true }, - })); - const phrases = getConversationCopy('zh').messages.workingPhrases; - - assert.ok(phrases.some((phrase) => markup.includes(phrase)), 'expected one working phrase'); - }); - - it('keeps the clock out of a static render', () => { - // The elapsed value only exists once an effect has measured it against the - // wall clock, so server markup and the first paint carry the phrase alone. - // Anything else would make a captured render differ run to run. - const markup = render(createElement(TurnView, { - turn: turnWithLiveContent(), - liveStreaming: { runningStatus: true }, - })); - - assert.doesNotMatch(markup, /maka-turn-elapsed/); - }); - - it('runs no animation of its own', () => { - // Both idioms above this row are already spoken for: a running tool card - // spins, and Astryx's `ChatReasoning` shimmers its label while reasoning - // streams. A second spinner or a second sweep put two of them a few pixels - // apart, at different speeds. What moves here is the content itself. - const markup = render(createElement(TurnRunningStatus, { startedAt: 1 })); - - assert.doesNotMatch(markup, /spinner|shimmer/i); - }); - - it('keeps a name on the row now that the spinner is not carrying one', () => { - // Every visible token here moves on the clock and is hidden from the - // accessibility tree; the spinner's label used to be the row's whole - // accessible name. - const markup = render(createElement(TurnRunningStatus, { startedAt: 1 })); - - assert.match(markup, /aria-label="[^"]+"/); - }); - - it('keeps every zh working phrase the same width so the clock never shifts', () => { - // The phrases sit immediately left of a ticking number. Uneven lengths - // would nudge it sideways on every swap. - const widths = new Set(getConversationCopy('zh').messages.workingPhrases.map((p) => p.length)); - - assert.equal(widths.size, 1, `expected one phrase length, got ${[...widths].join(', ')}`); - }); -}); - -describe('time-driven motion gate', () => { - it('reports frozen for a node inside a marked subtree, not just a marked root', () => { - // The CSS contract these attributes drive uses descendant selectors, so a - // host may mark a subtree instead of `` — Storybook marks its shell - // frame. Reading only the root let a one-second timer run inside a tree - // whose motion the stylesheet had already capped to nothing. - const inside = { closest: (selector: string) => (selector.includes('maka-e2e-fixture') ? {} : null) }; - const outside = { closest: () => null }; - const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'document'); - Object.defineProperty(globalThis, 'document', { - configurable: true, - value: { documentElement: { dataset: {} } }, - }); - try { - assert.equal(isTimeDrivenMotionEnabled(inside as unknown as Element), false); - assert.equal(isTimeDrivenMotionEnabled(outside as unknown as Element), true); - assert.equal(isTimeDrivenMotionEnabled(null), true); - } finally { - if (descriptor) Object.defineProperty(globalThis, 'document', descriptor); - else Reflect.deleteProperty(globalThis, 'document'); - } - }); - - it('still reports frozen when the document root carries the attribute', () => { - const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'document'); - Object.defineProperty(globalThis, 'document', { - configurable: true, - value: { documentElement: { dataset: { makaReducedMotion: 'true' } } }, - }); - try { - assert.equal(isTimeDrivenMotionEnabled(null), false); - } finally { - if (descriptor) Object.defineProperty(globalThis, 'document', descriptor); - else Reflect.deleteProperty(globalThis, 'document'); - } - }); -}); - -describe('turn duration formatting', () => { - it('advances in whole seconds and never below one', () => { - // A one-second timer drives this number, so anything finer than a second - // is precision the counter cannot deliver: a tenths digit would jump in - // tenths-of-ten, and a millisecond reading would claim an accuracy the - // interval never had. - const cases: Array<[number, string]> = [ - [0, '0s'], - [450, '0s'], - [999, '0s'], - [1_000, '1s'], - [8_200, '8s'], - [25_400, '25s'], - [59_999, '59s'], - [60_000, '1m 0s'], - [114_900, '1m 54s'], - [3_600_000, '60m 0s'], - ]; - for (const [ms, expected] of cases) assert.equal(formatTurnDuration(ms), expected); - }); - - it('truncates rather than rounds so the clock never runs ahead', () => { - // Rounding would show 26s at 25.6s elapsed — a counter reporting time that - // has not passed yet, and one that could reach `1m 60s`. - assert.equal(formatTurnDuration(25_600), '25s'); - assert.equal(formatTurnDuration(119_800), '1m 59s'); - }); - - it('is deliberately coarser than the tool card shape', () => { - // Tool cards report a span measured after the fact and may resolve below a - // second; this one is read while it advances. Locked so a future de-dup - // does not quietly hand the live clock back its milliseconds. - assert.equal(formatDuration(450), '450 ms'); - assert.equal(formatTurnDuration(450), '0s'); - }); -}); From aa21314d60cb73efea865f1d37225727a72d0584 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 8 Aug 2026 11:39:57 +0800 Subject: [PATCH 02/22] test(desktop): remove obsolete presentation coverage --- .../e2e/default-thinking-level.spec.ts | 56 --- .../__tests__/app-update-activity.test.ts | 26 -- .../__tests__/artifact-list-keyboard.test.ts | 199 --------- .../__tests__/attachment-chat-render.test.ts | 142 ------- .../main/__tests__/attachment-resize.test.ts | 18 - .../src/main/__tests__/branch-banner.test.ts | 89 ---- .../src/main/__tests__/build-info.test.ts | 86 ---- .../computer-use-pip-appearance.test.ts | 381 ------------------ .../__tests__/computer-use-pip-motion.test.ts | 209 ---------- .../custom-pet-companion-model.test.ts | 189 --------- .../custom-pet-library-model.test.ts | 70 ---- .../__tests__/daily-review-actions.test.ts | 18 - .../derive-turn-lineage-badges.test.ts | 71 ---- .../dock-presentation-contract.test.ts | 25 -- .../__tests__/ipc-surface-contract.test.ts | 72 ---- .../main/__tests__/model-wait-state.test.ts | 195 --------- .../__tests__/project-path-display.test.ts | 81 ---- .../provider-connection-status.test.ts | 59 --- .../__tests__/scroll-motion-policy.test.ts | 84 ---- .../__tests__/session-health-notice.test.ts | 167 -------- .../session-inspector-filter.test.ts | 173 -------- .../session-inspector-panel-model.test.ts | 224 ---------- .../src/main/__tests__/session-rail.test.ts | 214 ---------- ...ssion-status-display-normalization.test.ts | 73 ---- .../__tests__/session-workbar-layout.test.ts | 112 ----- .../src/main/__tests__/window-state.test.ts | 74 ---- 26 files changed, 3107 deletions(-) delete mode 100644 apps/desktop/e2e/default-thinking-level.spec.ts delete mode 100644 apps/desktop/src/main/__tests__/app-update-activity.test.ts delete mode 100644 apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts delete mode 100644 apps/desktop/src/main/__tests__/attachment-chat-render.test.ts delete mode 100644 apps/desktop/src/main/__tests__/attachment-resize.test.ts delete mode 100644 apps/desktop/src/main/__tests__/branch-banner.test.ts delete mode 100644 apps/desktop/src/main/__tests__/build-info.test.ts delete mode 100644 apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts delete mode 100644 apps/desktop/src/main/__tests__/computer-use-pip-motion.test.ts delete mode 100644 apps/desktop/src/main/__tests__/custom-pet-companion-model.test.ts delete mode 100644 apps/desktop/src/main/__tests__/custom-pet-library-model.test.ts delete mode 100644 apps/desktop/src/main/__tests__/daily-review-actions.test.ts delete mode 100644 apps/desktop/src/main/__tests__/derive-turn-lineage-badges.test.ts delete mode 100644 apps/desktop/src/main/__tests__/dock-presentation-contract.test.ts delete mode 100644 apps/desktop/src/main/__tests__/ipc-surface-contract.test.ts delete mode 100644 apps/desktop/src/main/__tests__/model-wait-state.test.ts delete mode 100644 apps/desktop/src/main/__tests__/project-path-display.test.ts delete mode 100644 apps/desktop/src/main/__tests__/provider-connection-status.test.ts delete mode 100644 apps/desktop/src/main/__tests__/scroll-motion-policy.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-health-notice.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-inspector-filter.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-rail.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-status-display-normalization.test.ts delete mode 100644 apps/desktop/src/main/__tests__/session-workbar-layout.test.ts delete mode 100644 apps/desktop/src/main/__tests__/window-state.test.ts diff --git a/apps/desktop/e2e/default-thinking-level.spec.ts b/apps/desktop/e2e/default-thinking-level.spec.ts deleted file mode 100644 index bee3445f8c..0000000000 --- a/apps/desktop/e2e/default-thinking-level.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect, test, COMPOSER_INPUT } from './fixtures.js'; - -/** - * Settings → 通用 → 默认思考级别 reaching the next new chat. - * - * This journey is e2e rather than unit because the bug it guards lives in the - * seam between two async sources: the setting arrives from a settings fetch - * that resolves AFTER the composer hook first mounts. Seeding it through a - * useState initializer type-checks, renders, and persists correctly — and the - * setting still never takes effect, because an initializer runs once, before - * the value exists. Only a real round-trip through the settings page and back - * catches that. - */ -test('a configured default thinking level reaches the next new chat, and the composer can still overrule it', async ({ - modelPickerLongWindow: page, -}) => { - const composerChip = () => - page.locator('.maka-composer-left-controls').getByRole('button', { name: /思考级别/ }); - - // Negative control: with nothing configured the chip is the model's own - // default, so a later 关 cannot be mistaken for a value that was always there. - await expect(composerChip()).toContainText('模型默认'); - - await page.getByRole('button', { name: /设置|Settings/ }).click(); - await page - .getByRole('navigation', { name: /设置分组|Settings sections/ }) - .getByRole('button', { name: /通用|General/, exact: true }) - .click(); - - const selector = page.getByRole('combobox', { name: '默认思考级别' }); - await selector.scrollIntoViewIfNeeded(); - await expect(selector).toContainText('跟随模型默认'); - await selector.click(); - await page.getByRole('option', { name: '关', exact: true }).click(); - - await expect - .poll(() => - page.evaluate(() => window.maka.settings.get().then((value) => value.chatDefaults.thinkingLevel)), - ) - .toBe('off'); - - await page.getByRole('button', { name: /返回应用|Back to app/ }).click(); - // Scope to the composer before reading the chip: the settings Selector also - // answers to /思考级别/ and reads 关 once set, so an unscoped lookup passes - // whether or not the chip ever changed. - await expect(page.locator(COMPOSER_INPUT)).toBeVisible(); - await expect(composerChip()).toHaveCount(1); - await expect(composerChip()).toContainText('关'); - - // The per-chat picker must still overrule the configured default — including - // back to 模型默认, which is why an untouched picker and an explicit - // 模型默认 pick cannot share one representation. - await composerChip().click(); - await page.getByRole('menuitem', { name: '模型默认', exact: true }).click(); - await expect(composerChip()).toContainText('模型默认'); -}); diff --git a/apps/desktop/src/main/__tests__/app-update-activity.test.ts b/apps/desktop/src/main/__tests__/app-update-activity.test.ts deleted file mode 100644 index c3b9ca6db0..0000000000 --- a/apps/desktop/src/main/__tests__/app-update-activity.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { hasInterruptibleUpdateWork } from '../app-update-activity.js'; - -describe('App update activity', () => { - test('guards every kind of work that app shutdown interrupts', () => { - const cases = [ - { name: 'idle', session: false, automation: false, shells: 0, expected: false }, - { name: 'session turn', session: true, automation: false, shells: 0, expected: true }, - { name: 'Automation fire', session: false, automation: true, shells: 0, expected: true }, - { name: 'background shell', session: false, automation: false, shells: 1, expected: true }, - ]; - - for (const input of cases) { - assert.equal( - hasInterruptibleUpdateWork({ - sessionActivities: { hasActive: () => input.session }, - automationScheduler: { hasInFlight: () => input.automation }, - shellRuns: { liveCount: () => input.shells }, - }), - input.expected, - input.name, - ); - } - }); -}); diff --git a/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts b/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts deleted file mode 100644 index 91f916f32b..0000000000 --- a/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Tests for the ArtifactPane list keyboard helper (PR108i, @kenji a11y gate #1). - * - * The pure helper has to handle five concerns simultaneously: arrow-key - * selection wrapping, Home/End jumping, Enter/Space activation, Escape - * dismissal, and "no nav key" passthrough. We lock the matrix down so a - * future change can't accidentally start swallowing Esc (which would - * break the global Command Palette) or stop wrapping at the bottom. - */ - -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { - nextArtifactListAction, - type ArtifactListAction, -} from '../../renderer/artifact-list-keyboard.js'; - -function expectAction(actual: ArtifactListAction, expected: ArtifactListAction) { - assert.deepEqual(actual, expected); -} - -const IDS = ['a', 'b', 'c'] as const; - -describe('nextArtifactListAction', () => { - describe('empty list', () => { - it('returns noop regardless of key', () => { - for (const key of ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter', ' ', 'Escape', 'q']) { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: [], key }), - { kind: 'noop' }, - ); - } - }); - }); - - describe('arrow key selection', () => { - it('ArrowDown moves selection to next item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'b' }, - ); - }); - - it('ArrowDown from last wraps to first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'c', visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp moves selection to previous item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp from first wraps to last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'c' }, - ); - }); - - it('ArrowDown with no current selection starts at first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp with no current selection starts at last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'c' }, - ); - }); - }); - - describe('Home / End jumps', () => { - it('Home jumps to first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'Home' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('End jumps to last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'End' }), - { kind: 'select', targetId: 'c' }, - ); - }); - }); - - describe('Enter / Space activation', () => { - it('Enter activates current selection', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'b' }, - ); - }); - - it('Space activates current selection', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: ' ' }), - { kind: 'activate', targetId: 'b' }, - ); - }); - - it('Enter with no selection activates first item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'a' }, - ); - }); - - it('Enter on a stale selection (no longer in list) falls back to first', () => { - // The list churns; the selected id might be deleted between renders. - expectAction( - nextArtifactListAction({ currentSelectedId: 'gone', visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'a' }, - ); - }); - }); - - describe('Escape dismissal (does NOT swallow if list empty)', () => { - it('Escape on non-empty list returns dismiss', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Escape' }), - { kind: 'dismiss' }, - ); - }); - - it('Escape on empty list returns noop (does not steal Esc from Command Palette)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: [], key: 'Escape' }), - { kind: 'noop' }, - ); - }); - }); - - describe('unrelated keys', () => { - it('letter keys return noop', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'q' }), - { kind: 'noop' }, - ); - }); - - it('Tab returns noop (focus moves via browser default, not list helper)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Tab' }), - { kind: 'noop' }, - ); - }); - - it('Shift+Tab returns noop', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Tab' }), - { kind: 'noop' }, - ); - }); - - it('ArrowLeft / ArrowRight return noop (this is a vertical listbox)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowLeft' }), - { kind: 'noop' }, - ); - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowRight' }), - { kind: 'noop' }, - ); - }); - }); - - describe('priority order', () => { - // We lock the order so future edits don't surprise reviewers. - // 1. empty list → noop (regardless of key) - // 2. Escape → dismiss - // 3. Enter / Space → activate - // 4. ArrowDown/Up/Home/End → select - // 5. anything else → noop - it('empty list dominates Escape', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: [], key: 'Escape' }), - { kind: 'noop' }, - ); - }); - - it('Escape dominates ArrowDown when both could apply (Escape is the chord)', () => { - // (Synthetic: keys are single, but verifies the precedence in the switch) - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Escape' }), - { kind: 'dismiss' }, - ); - }); - }); -}); diff --git a/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts b/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts deleted file mode 100644 index ccc99157b6..0000000000 --- a/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createElement, type ReactNode } from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import type { AttachmentRef, SessionSummary, StoredMessage } from '@maka/core'; -import { ChatSurfaceLayout, ChatView, LocaleProvider } from '@maka/ui'; - -function renderWithLocale(child: ReactNode): string { - return renderToStaticMarkup( - createElement(LocaleProvider, { - locale: 'zh', - children: createElement(ChatSurfaceLayout, { composer: null, children: child }), - }), - ); -} - -const activeSession: SessionSummary = { - id: 's1', - name: 'Sent reference check', - isFlagged: false, - isArchived: false, - labels: [], - hasUnread: false, - status: 'active', - backend: 'ai-sdk', - llmConnectionSlug: 'fixture', - connectionLocked: false, - model: 'fixture-model', - permissionMode: 'ask', -}; - -describe('sent reference frontend', () => { - it('keeps sent references outside the authored-text bubble', () => { - const attachments: AttachmentRef[] = [ - { - kind: 'pdf', - name: 'design-spec.pdf', - mimeType: 'application/pdf', - bytes: 512_000, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }, - { - kind: 'image', - name: 'layout.png', - mimeType: 'image/png', - bytes: 4, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-2' }, - }, - ]; - const messages: StoredMessage[] = [{ - type: 'user', - id: 'u1', - turnId: 't1', - ts: 1, - text: '请用 /skill:writer 检查', - attachments, - quotes: [{ text: 'reference', sourceTurnId: 't0' }], - }]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - const bubbleIndex = markup.indexOf('astryx-chat-message-bubble'); - assert.ok(markup.indexOf('astryx-token') < bubbleIndex); - assert.ok(markup.indexOf('maka-user-quotes') < bubbleIndex); - assert.ok(markup.indexOf('maka-user-attachments') < bubbleIndex); - assert.ok(markup.indexOf('astryx-badge') > bubbleIndex); - }); - - it('renders sent Skill invocations as neutral tokens without guessing @ text', () => { - const messages: StoredMessage[] = [ - { - type: 'user', - id: 'u1', - turnId: 't1', - ts: 1, - text: '请用 /skill:writer 看 @notes', - }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - assert.match(markup, /class="astryx-badge neutral[^"]*"[^>]*>[^<]*\/skill:writer<\/span>/); - assert.doesNotMatch(markup, /class="astryx-badge neutral[^"]*"[^>]*>[^<]*@notes<\/span>/); - }); - - it('keeps sent file tokens compact while exposing size accessibly', () => { - const attachment: AttachmentRef = { - kind: 'pdf', - name: 'design-spec.pdf', - mimeType: 'application/pdf', - bytes: 512_000, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }; - const messages: StoredMessage[] = [ - { type: 'user', id: 'u1', turnId: 't1', ts: 1, text: '看下附件', attachments: [attachment] }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - const tokenIndex = markup.indexOf('astryx-token'); - const bubbleIndex = markup.indexOf('astryx-chat-message-bubble'); - assert.notEqual(tokenIndex, -1); - assert.ok(tokenIndex < bubbleIndex); - assert.match(markup, /lucide-file-text/); - assert.match(markup, /class="[^"]*astryx-icon[^"]*"[^>]*data-size="sm"/); - assert.match(markup, /design-spec\.pdf/); - assert.match(markup, /aria-description="500\.0 KB"/); - assert.doesNotMatch(markup, />500\.0 KB { - const attachment: AttachmentRef = { - kind: 'image', - name: 'clipboard.png', - mimeType: 'image/png', - bytes: 4, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }; - const messages: StoredMessage[] = [ - { type: 'user', id: 'u1', turnId: 't1', ts: 1, text: '看这张图', attachments: [attachment] }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - assert.match(markup, /maka-user-attachments/); - assert.match(markup, /astryx-thumbnail/); - assert.doesNotMatch(markup, /maka-user-attachment-thumb(?:-pending|-image)?(?:\s|")/); - }); -}); diff --git a/apps/desktop/src/main/__tests__/attachment-resize.test.ts b/apps/desktop/src/main/__tests__/attachment-resize.test.ts deleted file mode 100644 index e5a695dd4d..0000000000 --- a/apps/desktop/src/main/__tests__/attachment-resize.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { computeResizeDimensions } from '../attachment-resize.js'; - -describe('computeResizeDimensions', () => { - test('scales the longest edge down to the cap, preserving aspect ratio', () => { - assert.deepEqual(computeResizeDimensions(3000, 2000, 2000), { width: 2000, height: 1333 }); - }); - - test('returns null when the image already fits the cap', () => { - assert.equal(computeResizeDimensions(2000, 1000, 2000), null); - assert.equal(computeResizeDimensions(1000, 2000, 2000), null); - }); - - test('returns null for a zero-dimension image (cannot scale)', () => { - assert.equal(computeResizeDimensions(0, 0, 2000), null); - }); -}); diff --git a/apps/desktop/src/main/__tests__/branch-banner.test.ts b/apps/desktop/src/main/__tests__/branch-banner.test.ts deleted file mode 100644 index 21ffcb58e1..0000000000 --- a/apps/desktop/src/main/__tests__/branch-banner.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Tests for branch banner derivation (PR109f). - * - * Locks the contract: - * - banner only renders for sessions with parentSessionId set - * - banner requires the parent session to be visible in the list - * - banner copy uses the parent's display name - * - fromAbortedTurn is caller-supplied; helper never guesses - */ - -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { - deriveBranchBanner, - type BranchBannerSessionInput, -} from '../../renderer/branch-banner.js'; - -function session(partial: Partial & { id: string; name: string }): BranchBannerSessionInput { - return { ...partial }; -} - -describe('deriveBranchBanner', () => { - it('returns undefined for non-branched sessions', () => { - const active = session({ id: 's1', name: '原会话' }); - const result = deriveBranchBanner(active, [active]); - assert.equal(result, undefined); - }); - - it('returns undefined when activeSession is undefined', () => { - const result = deriveBranchBanner(undefined, []); - assert.equal(result, undefined); - }); - - it('returns undefined when parentSessionId references a hidden session', () => { - // Parent archived / filtered out of the visible list — better to - // show no banner than render a banner that clicks into nothing. - const active = session({ id: 's2', name: '分支会话', parentSessionId: 'parent-not-here' }); - const result = deriveBranchBanner(active, [active]); - assert.equal(result, undefined); - }); - - it('returns banner with parent name when parent is visible', () => { - const parent = session({ id: 'p1', name: '父会话' }); - const active = session({ id: 's3', name: '分支会话', parentSessionId: 'p1' }); - const result = deriveBranchBanner(active, [parent, active]); - assert.deepEqual(result, { - parentSessionId: 'p1', - parentSessionName: '父会话', - }); - }); - - it('passes through fromAbortedTurn when caller supplies it', () => { - const parent = session({ id: 'p2', name: '父会话' }); - const active = session({ id: 's4', name: '分支会话', parentSessionId: 'p2' }); - const result = deriveBranchBanner(active, [parent, active], true); - assert.deepEqual(result, { - parentSessionId: 'p2', - parentSessionName: '父会话', - fromAbortedTurn: true, - }); - }); - - it('omits fromAbortedTurn when caller passes false / undefined', () => { - const parent = session({ id: 'p3', name: '父会话' }); - const active = session({ id: 's5', name: '分支会话', parentSessionId: 'p3' }); - const resultFalse = deriveBranchBanner(active, [parent, active], false); - const resultUndef = deriveBranchBanner(active, [parent, active], undefined); - assert.equal(resultFalse?.fromAbortedTurn, undefined); - assert.equal(resultUndef?.fromAbortedTurn, undefined); - }); - - it('does not mutate the input sessions list', () => { - const parent = session({ id: 'p4', name: '父会话' }); - const active = session({ id: 's6', name: '分支会话', parentSessionId: 'p4' }); - const sessions = [parent, active]; - const before = JSON.stringify(sessions); - deriveBranchBanner(active, sessions, true); - assert.equal(JSON.stringify(sessions), before); - }); - - it('uses the parent name verbatim (no truncation, no fallback)', () => { - // If parent rename is involved later, the banner must reflect the - // current name so the user sees the same label as in the sidebar. - const parent = session({ id: 'p5', name: '一个非常长的父会话名称用于测试不截断' }); - const active = session({ id: 's7', name: '分支会话', parentSessionId: 'p5' }); - const result = deriveBranchBanner(active, [parent, active]); - assert.equal(result?.parentSessionName, '一个非常长的父会话名称用于测试不截断'); - }); -}); diff --git a/apps/desktop/src/main/__tests__/build-info.test.ts b/apps/desktop/src/main/__tests__/build-info.test.ts deleted file mode 100644 index 4ce31a088d..0000000000 --- a/apps/desktop/src/main/__tests__/build-info.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * PR-BUILD-HYGIENE-0: cover the dev/packaged + commit-resolution - * branches of `resolveBuildInfo()` so the About-page badge cannot - * silently regress. - */ - -import { strict as assert } from 'node:assert'; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, it } from 'node:test'; - -import { resolveBuildInfo } from '../build-info.js'; - -function makeTempRepo(setup: (gitDir: string) => void): string { - const root = mkdtempSync(join(tmpdir(), 'maka-build-info-')); - const gitDir = join(root, '.git'); - mkdirSync(gitDir, { recursive: true }); - setup(gitDir); - return root; -} - -describe('resolveBuildInfo', () => { - it('returns mode=packaged with no commit when app is packaged', () => { - const info = resolveBuildInfo(true, '/anywhere'); - assert.equal(info.mode, 'packaged'); - assert.equal(info.commit, null); - }); - - it('returns mode=dev with null commit when no .git is found', () => { - const root = mkdtempSync(join(tmpdir(), 'maka-build-info-nogit-')); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.mode, 'dev'); - assert.equal(info.commit, null); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('reads loose ref when HEAD points to a branch with a loose ref file', () => { - const fullSha = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), 'ref: refs/heads/main\n'); - mkdirSync(join(gitDir, 'refs', 'heads'), { recursive: true }); - writeFileSync(join(gitDir, 'refs', 'heads', 'main'), `${fullSha}\n`); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.mode, 'dev'); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('falls back to packed-refs when the loose ref file is missing', () => { - const fullSha = 'feedfacedeadbeef0011223344556677889900aa'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), 'ref: refs/heads/main\n'); - writeFileSync( - join(gitDir, 'packed-refs'), - `# pack-refs with: peeled fully-peeled sorted\n${fullSha} refs/heads/main\n`, - ); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('handles detached HEAD (HEAD contains a sha directly)', () => { - const fullSha = '0123456789abcdef0123456789abcdef01234567'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), `${fullSha}\n`); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); -}); diff --git a/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts b/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts deleted file mode 100644 index dea3145c49..0000000000 --- a/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -/** - * What the Computer Use mirror looks like. - * - * Reported from a screenshot: a stray white L in one corner, two buttons - * crowding the picture, and a bare blue dot standing in for the cursor. The - * values behind those, and the ones recovered from Codex that replace them, - * are asserted here as values — not as strings that happen to appear in the - * file — so that reverting any single one of them fails. - */ - -import { strict as assert } from 'node:assert'; -import { readFile } from 'node:fs/promises'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { describe, it, test } from 'node:test'; -import { CODEX_CURSOR_GLYPH } from '../../renderer/computer-use-overlay/engine/cursor-engine.js'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const DESKTOP = resolve(HERE, '..', '..', '..'); -const PIP_HTML = resolve(DESKTOP, 'src', 'overlay', 'pip.html'); -/** Built by `build:overlay`, which `build:test` runs before `test:dist`. */ -const PIP_BUNDLE = resolve(DESKTOP, 'dist', 'overlay', 'pip.js'); - -/** The page's stylesheet, comments removed. */ -async function styleSheet(): Promise { - const html = await readFile(PIP_HTML, 'utf8'); - const style = /