/** F01 acceptance reproduction extended through the real reducer and MessageBubble. * No Electron, model, SDK, or external service; DOM behavior, not visual parity. */ import assert from 'node:assert/strict' import { createRequire } from 'node:module' import type { ChatMessage, ChatEvent, MessagePart } from '../src/shared/types' const { JSDOM } = createRequire(process.cwd() + '/package.json')(process.env.YCODE_TEST_DOM_MODULE || 'jsdom') const dom = new JSDOM('
', { pretendToBeVisual: true, url: 'http://localhost/' }) for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'MouseEvent', 'KeyboardEvent', 'Event']) Object.defineProperty(globalThis, key, { value: dom.window[key], configurable: true }) Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true }) Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) const { act } = require('react') as typeof import('react') const { createRoot } = require('react-dom/client') as typeof import('react-dom/client') const MessageBubble = require('../src/renderer/src/components/MessageBubble').default as typeof import('../src/renderer/src/components/MessageBubble').default const { applyAssistantEvent } = require('../src/shared/message-parts') as typeof import('../src/shared/message-parts') const { completedFoldPartIdentity } = require('../src/renderer/src/workbuddy/conversation/completed-fold') as typeof import('../src/renderer/src/workbuddy/conversation/completed-fold') const root = createRoot(document.getElementById('root')!) const plan = '.cb-plan-header-left' const task = '.cb-task-result-item-title' const outer = '.cr-collapse--completed > .cr-collapse__header' const nodes = (selector: string) => [...document.querySelectorAll(selector)] const get = (selector: string, index = 0) => { const node = nodes(selector)[index]; assert.ok(node, `${selector}[${index}]`); return node } const states = () => ({ plan: nodes(plan).map(node => node.getAttribute('aria-expanded')), task: nodes(task).map(node => node.getAttribute('aria-expanded')) }) async function render(message: ChatMessage) { await act(async () => root.render()) } async function click(selector: string, index = 0) { await act(async () => get(selector, index).dispatchEvent(new MouseEvent('click', { bubbles: true }))) } type EventInput = ChatEvent extends infer E ? E extends ChatEvent ? Omit : never : never const event = (value: EventInput) => ({ runId: 'native-identity', ...value }) as ChatEvent const planPart = (id: string): Extract => ({ type: 'plan', plan: { id, title: `Plan ${id}`, steps: [] } }) const taskPart = (id: string): Extract => ({ type: 'task', task: { id, title: `Task ${id}`, status: 'success', summary: `Result ${id}` } }) async function main() { // All native business IDs, including non-disclosure image/artifact parts, are stable. const stableParts: MessagePart[] = [planPart('same'), taskPart('same'), { type: 'artifact', artifact: { id: 'same', name: 'File', path: '/file' } }, { type: 'image', id: 'same', url: '/image' }, { type: 'reasoning', id: 'same', text: 'Thought' }, { type: 'tool', toolId: 'same' }] const identities = stableParts.map(part => completedFoldPartIdentity(part, 1)) assert.equal(new Set(identities).size, stableParts.length, 'part types cannot share identity') stableParts.forEach((part, i) => assert.equal(completedFoldPartIdentity(part, 99), identities[i], `${part.type} ignores position`)) assert.notEqual(completedFoldPartIdentity(planPart('1'), 1), completedFoldPartIdentity({ type: 'plan', plan: {} }, 1), 'business IDs cannot collide with fallback indices') assert.equal(completedFoldPartIdentity({ type: 'future', id: 'native' }, 1), completedFoldPartIdentity({ type: 'future', id: 'native' }, 2), 'unknown top-level IDs remain supported') let message: ChatMessage = { id: 'native-one', role: 'assistant', createdAt: 1, content: '', status: 'streaming', parts: [] } // Exact acceptance event sequence: two text chunks separated by reasoning. const streamingEvents: EventInput[] = [ { type: 'text', delta: 'Draft one' }, { type: 'part', part: { type: 'reasoning', id: 'r', text: 'Thought', finished: true } }, { type: 'text', delta: 'Draft two' }, { type: 'part', part: planPart('p') }, { type: 'part', part: taskPart('x') } ] for (const value of streamingEvents) message = applyAssistantEvent(message, event(value)) assert.deepEqual(message.parts?.map(part => part.type), ['text', 'reasoning', 'text', 'plan', 'task']) await render(message) assert.deepEqual(states(), { plan: ['true'], task: ['true'] }) await click(plan); await click(task) const collapsed = { plan: ['false'], task: ['false'] } assert.deepEqual(states(), collapsed) message = applyAssistantEvent(message, event({ type: 'done', content: 'Corrected final prose', tools: [], turnDurationMs: 1000 })) assert.deepEqual(message.parts?.map(part => part.type), ['text', 'reasoning', 'plan', 'task'], 'real done reducer coalesces text') await render(message) assert.deepEqual(states(), collapsed, 'terminal correction preserves choices') for (let repeat = 0; repeat < 2; repeat++) { await click(outer); assert.deepEqual(states(), collapsed, 'outer expansion preserves choices') await click(outer); assert.deepEqual(states(), collapsed, 'outer collapse preserves choices') } // Insert new same-kind cards before originals, then reorder. New cards must not inherit state. const prefix = message.parts!.filter(part => part.type !== 'plan' && part.type !== 'task') message = { ...message, parts: [...prefix, planPart('p2'), taskPart('x2'), planPart('p'), taskPart('x')] } await render(message) assert.deepEqual(states(), { plan: ['true', 'false'], task: ['true', 'false'] }, 'insertions retain original identity only') await click(plan, 1) // explicitly open original; keep task closed await click(plan, 0) // close new plan; leave new task open message = { ...message, parts: [taskPart('x'), planPart('p'), ...prefix, taskPart('x2'), planPart('p2')] } await render(message) assert.deepEqual(states(), { plan: ['true', 'false'], task: ['false', 'true'] }, 'reorder retains independent choices') await click(task, 1) await click(outer); await click(outer) assert.deepEqual(states(), { plan: ['true', 'false'], task: ['false', 'false'] }, 'reordered choices survive outer reparenting') await render({ ...message, id: 'native-two' }) assert.deepEqual(states(), { plan: ['true', 'true'], task: ['true', 'true'] }, 'same part IDs in another message are isolated') assert.equal(get(outer).getAttribute('aria-expanded'), 'false') // Legacy persisted payloads may predate required ID fields. Keep their positional fallback, // without promising identity across arbitrary insertions when no business identity exists. const legacyParts = [planPart(''), taskPart('')].map(part => { const copy = structuredClone(part) as any delete copy[copy.type].id return copy }) as MessagePart[] const legacy = { ...message, id: 'legacy', parts: [...prefix, ...legacyParts] } await render(legacy) await click(plan); await click(task) await render(structuredClone(legacy)) await click(outer); await click(outer) assert.deepEqual(states(), collapsed, 'no-ID old history retains choice at the same position') assert.notEqual(completedFoldPartIdentity(legacyParts[0], 1), completedFoldPartIdentity(legacyParts[0], 2), 'no-ID fallback remains positional') await render({ ...legacy, id: 'legacy-other' }) assert.deepEqual(states(), { plan: ['true'], task: ['true'] }, 'legacy fallback remains message-isolated') await act(async () => root.unmount()); dom.window.close() console.log('completed-fold-native-identity: real reducer done correction, native IDs, insertion/reorder, repeated outer toggles, message isolation and legacy fallback passed') } main().then(() => process.exit(0)).catch(error => { console.error(error); dom.window.close(); process.exit(1) })