chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,152 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { ComposerControls } from '../src/renderer/src/workbuddy/composer/ComposerControls'
|
||||
import { encodePcmWav, downsamplePcm, VOICE_MAX_DURATION_MS } from '../src/renderer/src/workbuddy/composer/VoiceInputControl'
|
||||
import { buildComposerSendPayload, MAX_ATTACHMENT_BYTES, oversizedAttachmentNames, parseResourcePath, readReferenceUris } from '../src/renderer/src/workbuddy/composer/ComposerControls'
|
||||
import { MessageActionToolbar } from '../src/renderer/src/workbuddy/message-actions/MessageActionToolbar'
|
||||
import { buildCopyMessagePayload, buildUserMessageClipboard, getSortedRequestMessages, projectSafeMessage, safeStringify, USER_CONTENT_BLOCKS_MIME } from '../src/renderer/src/workbuddy/message-actions/project-safe-message'
|
||||
import type { ChatMessage } from '../src/shared/types'
|
||||
|
||||
const protectedMessage = {
|
||||
id: 'assistant-sensitive',
|
||||
messageType: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: 'before secret after', _meta: { sensitiveProtection: { version: 2, items: [{ start: 7, end: 13 }] } } },
|
||||
{ type: 'tool-call', tool: { id: 'secret-tool', name: 'readFile', status: 'ok', args: { path: '/private' }, metaData: { sensitiveProtection: { version: 1, items: [{ ciphertext: 'ciphertext-1' }] } }, result: { private: true } } }
|
||||
]
|
||||
}
|
||||
const originalSerialized = JSON.stringify(protectedMessage)
|
||||
const projected = projectSafeMessage(protectedMessage)
|
||||
assert.equal(JSON.stringify(protectedMessage), originalSerialized, 'projection must preserve the caller object')
|
||||
const projectedText = (projected.content?.[0] as { text: string }).text
|
||||
assert.equal(projectedText.startsWith('before ') && projectedText.endsWith(' after'), true)
|
||||
assert.equal(projectedText.includes('secret'), false, 'copy projection must redact protected text')
|
||||
const unchangedUser = { messageType: 'user', content: [{ type: 'text', text: 'user secret', _meta: { sensitiveProtection: { version: 2, items: [{ start: 5, end: 11 }] } } }] }
|
||||
assert.equal(projectSafeMessage(unchangedUser), unchangedUser, 'assistant output hooks leave user messages unchanged')
|
||||
assert.equal((projected.content?.[1] as { tool: { args: { sensitiveContent: string } } }).tool.args.sensitiveContent, 'ciphertext-1')
|
||||
|
||||
const quickQa = projectSafeMessage({ messageType: 'assistant', extra: { quickQa: true }, content: [
|
||||
{ type: 'text', text: 'answer' },
|
||||
{ type: 'tool-call', tool: { id: 'hidden', name: 'search', status: 'ok' } }
|
||||
] })
|
||||
assert.equal(quickQa.content?.length, 1, 'Quick QA copy projection omits tool-call blocks')
|
||||
|
||||
const legacy: ChatMessage = {
|
||||
id: 'legacy-assistant',
|
||||
role: 'assistant',
|
||||
content: 'legacy text',
|
||||
tools: [{ id: 'legacy-tool', name: 'readFile', args: { path: '/tmp/a' }, argsRaw: '{}', output: 'full output', status: 'ok' }],
|
||||
createdAt: 1
|
||||
}
|
||||
const userMessage: ChatMessage = { id: 'user-copy', role: 'user', content: 'plain user text', createdAt: 1 }
|
||||
const userClipboard = buildUserMessageClipboard({ ...userMessage, content: '<plain>&\u200B' })
|
||||
assert.equal(userClipboard.plainText, '<plain>&', 'user text/plain omits the source zero-width marker')
|
||||
assert.equal(userClipboard.html, '<plain>&\u200B')
|
||||
assert.equal(userClipboard.customMimeType, USER_CONTENT_BLOCKS_MIME)
|
||||
assert.deepEqual(JSON.parse(userClipboard.customData), { version: '1.0', blocks: [{ type: 'text', text: '<plain>&\u200B' }] })
|
||||
const richClipboard = buildUserMessageClipboard({
|
||||
...userMessage,
|
||||
content: 'see <img>&\u200B',
|
||||
attachments: [
|
||||
{
|
||||
id: 'image-1', name: 'preview.png', mimeType: 'image/png', size: 3,
|
||||
previewUrl: 'data:image/png;base64,AQID', path: 'C:\\work\\preview.png'
|
||||
},
|
||||
{
|
||||
id: 'file-1', name: 'notes.pdf', mimeType: 'application/pdf', size: 4,
|
||||
path: 'C:\\work\\notes.pdf'
|
||||
}
|
||||
],
|
||||
resources: ['C:\\work\\src.ts', 'C:\\work\\src.ts']
|
||||
})
|
||||
const richPayload = JSON.parse(richClipboard.customData) as {
|
||||
version: string
|
||||
blocks: Array<{ type: string; text?: string; data?: string; mimeType?: string; name?: string; uri?: string; _meta?: Record<string, unknown> }>
|
||||
}
|
||||
assert.equal(richPayload.version, '1.0')
|
||||
assert.deepEqual(richPayload.blocks.map((block) => block.type), ['text', 'image', 'resource_link', 'resource_link', 'resource_link'],
|
||||
'custom MIME should preserve the text, image, file, and both resource blocks')
|
||||
assert.equal(richClipboard.customMimeType, 'application/x-cb-chat-contentblocks')
|
||||
assert.equal(richClipboard.plainText, 'Context: C:\\work\\src.ts\n\nsee <img>&preview.pngnotes.pdf',
|
||||
'plain text should summarize context links once and append attachment phrases')
|
||||
assert.ok(richClipboard.html.includes('see <img>&\u200B'), 'HTML should escape text while retaining source zero-width markers')
|
||||
assert.equal((richClipboard.html.match(/data-contentblock=/g) ?? []).length, 2,
|
||||
'phrase attachment blocks should be carried in the HTML data-contentblock spans')
|
||||
assert.ok(!richClipboard.html.includes('C:\\work\\src.ts'), 'context links are represented in plain text and custom MIME, not HTML')
|
||||
assert.equal(richPayload.blocks[1].data, 'AQID')
|
||||
assert.equal(richPayload.blocks[1].mimeType, 'image/png')
|
||||
assert.equal(richPayload.blocks[1]._meta?.filename, 'preview.png')
|
||||
assert.equal(richPayload.blocks[2].uri, 'C:\\work\\notes.pdf')
|
||||
assert.equal(richPayload.blocks[2]._meta?.filePath, 'C:\\work\\notes.pdf')
|
||||
assert.equal(richPayload.blocks[3]._meta?.displayAsContext, true)
|
||||
const payload = buildCopyMessagePayload('request-1', [legacy])
|
||||
assert.equal(payload.requestId, 'request-1')
|
||||
assert.equal(payload.messageCount, 1)
|
||||
assert.equal(payload.toolCallCount, 1)
|
||||
assert.equal(payload.summary[0].contentBlockCount, 2, 'legacy content and tools both appear in fallback copy')
|
||||
assert.equal((payload.messages[0].content?.[1] as { tool: { result: string } }).tool.result, 'full output')
|
||||
assert.equal(JSON.parse(safeStringify(payload)).summary[0].toolCalls?.[0].id, 'legacy-tool')
|
||||
const circular: { self?: unknown } = {}
|
||||
circular.self = circular
|
||||
assert.equal(JSON.parse(safeStringify(circular)).self, '[Circular]')
|
||||
|
||||
const requestPayload = buildCopyMessagePayload('request-2', [
|
||||
{ id: 'user', role: 'user', content: 'question', createdAt: 1 } as ChatMessage,
|
||||
{ id: 'a1', messageType: 'assistant', content: [{ type: 'text', text: 'one' }] },
|
||||
{ id: 'a2', messageType: 'assistant', content: [{ type: 'text', text: 'two' }] }
|
||||
])
|
||||
assert.equal(requestPayload.messageCount, 2, 'request copy envelope includes assistant messages only')
|
||||
const requestOrder = getSortedRequestMessages([
|
||||
{ id: 'late', role: 'assistant', requestId: 'ordered', content: 'later', createdAt: 20 },
|
||||
{ id: 'other', role: 'assistant', requestId: 'different', content: 'other', createdAt: 5 },
|
||||
{ id: 'early', role: 'assistant', requestId: 'ordered', content: 'earlier', createdAt: 10 }
|
||||
], 'ordered')
|
||||
assert.deepEqual(requestOrder.map((item) => item.id), ['early', 'late'])
|
||||
|
||||
const samples = Float32Array.from([-1, -0.5, 0, 0.5, 1, 0])
|
||||
assert.equal(downsamplePcm(samples, 32_000).length, 3)
|
||||
const wav = encodePcmWav(samples)
|
||||
assert.equal(new TextDecoder().decode(wav.slice(0, 4)), 'RIFF')
|
||||
assert.equal(new DataView(wav.buffer).getUint32(24, true), 16_000)
|
||||
assert.equal(new DataView(wav.buffer).getUint32(40, true), samples.length * 2)
|
||||
assert.equal(VOICE_MAX_DURATION_MS, 295_000)
|
||||
assert.deepEqual(oversizedAttachmentNames([
|
||||
{ name: 'limit.bin', size: MAX_ATTACHMENT_BYTES },
|
||||
{ name: 'over.bin', size: MAX_ATTACHMENT_BYTES + 1 }
|
||||
]), ['over.bin'])
|
||||
assert.deepEqual(buildComposerSendPayload(' prompt ', [], ['C:/work/file.ts'], 'full-access'), {
|
||||
text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], mode: 'full-access'
|
||||
})
|
||||
assert.equal(parseResourcePath('file:///C:/work/file.ts'), 'C:\\work\\file.ts')
|
||||
assert.equal(parseResourcePath('C:\\work\\file.ts'), 'C:\\work\\file.ts')
|
||||
assert.deepEqual(readReferenceUris({ getData: (type) => type === 'application/vnd.code.uri-list' ? 'file:///C:/a.ts\r\nfile:///C:/b.ts' : '' }), ['file:///C:/a.ts', 'file:///C:/b.ts'])
|
||||
assert.deepEqual(readReferenceUris({ getData: (type) => type === 'resourceurls' ? '["file:///C:/a.ts"]' : '' }), ['file:///C:/a.ts'])
|
||||
|
||||
const assistant: ChatMessage = { id: 'toolbar-assistant', role: 'assistant', content: 'done', createdAt: 1, requestId: 'request-3' }
|
||||
const assistantMarkup = renderToStaticMarkup(<MessageActionToolbar message={assistant} canRegenerate isSessionActive isLastAssistant onRegenerate={() => undefined} />)
|
||||
assert.match(assistantMarkup, /复制/)
|
||||
assert.match(assistantMarkup, /重新生成/)
|
||||
assert.doesNotMatch(assistantMarkup, /编辑/)
|
||||
const userMarkup = renderToStaticMarkup(<MessageActionToolbar message={{ id: 'user', role: 'user', content: 'hi', createdAt: 1 }} onEdit={() => undefined} onRevert={() => undefined} onResend={() => undefined} />)
|
||||
assert.match(userMarkup, /编辑/)
|
||||
assert.match(userMarkup, /重新发送/)
|
||||
assert.doesNotMatch(userMarkup, /重新生成/)
|
||||
assert.doesNotMatch(userMarkup, /赞同|不赞同/, 'feedback actions are assistant-only')
|
||||
|
||||
const composerMarkup = renderToStaticMarkup(
|
||||
<ComposerControls value="draft" onChange={() => undefined} onSend={() => undefined} onModeChange={() => undefined} modelSlot={<span>Model slot</span>} workspaceFooter={<span>Workspace slot</span>} sending />
|
||||
)
|
||||
assert.match(composerMarkup, /语音转写服务不可用/, 'without an ASR callback, voice input reports unavailable')
|
||||
assert.match(composerMarkup, /默认沙箱/)
|
||||
assert.match(composerMarkup, /添加/)
|
||||
assert.match(composerMarkup, /Model slot/)
|
||||
assert.match(composerMarkup, /Workspace slot/)
|
||||
assert.equal(composerMarkup.indexOf('Model slot') < composerMarkup.indexOf('语音转写服务不可用'), true)
|
||||
assert.ok(composerMarkup.indexOf('cr-input-toolbar__left') < composerMarkup.indexOf('默认沙箱'))
|
||||
assert.ok(composerMarkup.indexOf('默认沙箱') < composerMarkup.indexOf('cr-input-toolbar__right'), 'permission control belongs in the left toolbar')
|
||||
assert.ok(composerMarkup.indexOf('cr-input-toolbar__right') < composerMarkup.indexOf('wb-composer__workspace-footer'), 'workspace footer remains outside the input toolbar')
|
||||
const activeRunMarkup = renderToStaticMarkup(<ComposerControls value="next draft" onChange={() => undefined} onSend={() => undefined} onStop={() => undefined} />)
|
||||
assert.match(activeRunMarkup, /aria-label="停止"/)
|
||||
assert.doesNotMatch(activeRunMarkup, /<textarea[^>]*disabled/)
|
||||
|
||||
console.log('Composer/message action regression cases passed')
|
||||
Reference in new issue
Block a user