fix: refine agent image attachments and restore Korean filenames
This commit is contained in:
@@ -19,6 +19,20 @@ function visit(node) {
|
||||
visit(source)
|
||||
assert.ok(payloadExpression, 'Agent request JSON serialization must exist')
|
||||
|
||||
test('user messages preserve only typed text while attachments remain separate metadata', () => {
|
||||
let initializer
|
||||
function find(node) {
|
||||
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'userMessage') initializer = node.initializer.getText(source)
|
||||
ts.forEachChild(node, find)
|
||||
}
|
||||
find(source)
|
||||
assert.ok(initializer)
|
||||
for (const request of ['이 이미지에서 슬램에 필요한 모듈만 골라봐', '첫 줄\n첨부 파일: 직접 입력한 내용']) {
|
||||
const message = vm.runInNewContext(`(${initializer})`, { request, options: { attachments: [{ name: '스크린샷.png' }] } })
|
||||
assert.equal(message.content, request)
|
||||
}
|
||||
})
|
||||
|
||||
function serialize(attachments, finalize = false, request = '설정 위치 알려줘', imageTokens = []) {
|
||||
return JSON.parse(vm.runInNewContext(payloadExpression, {
|
||||
courseId: 'course-1', weekNumber: 3, request, options: { attachments }, finalize,
|
||||
|
||||
@@ -21,15 +21,38 @@ const widgets = compile(fs.readFileSync(require.resolve('../src/renderer/src/com
|
||||
const panelSource = ts.createSourceFile('WorkspaceQaPanel.tsx', fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/WorkspaceQaPanel.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
||||
const declarations = panelSource.statements.filter(node => ts.isFunctionDeclaration(node) && ['AiMessages', 'AgentMessages', 'TeacherMessages'].includes(node.name?.text))
|
||||
const ProgressiveChatResponse = props => React.createElement('div', { 'data-reveal-start': props.message.revealStartedAt, 'data-collapsed': props.defaultCollapsed }, props.message.content)
|
||||
const ChatImageAttachments = props => React.createElement('div', { 'data-chat-image-attachments': props.variant }, props.images.map(image => React.createElement('img', { key: image.url, src: image.url, alt: image.name })))
|
||||
const messages = compile(declarations.map(node => `export ${node.getText(panelSource)}`).join('\n'), {}, {
|
||||
t: key => key, useLocaleRender() {},
|
||||
ChatResponseSkeleton: widgets.ChatResponseSkeleton,
|
||||
ChatEmptyState: widgets.ChatEmptyState,
|
||||
ProgressiveChatResponse,
|
||||
ChatImageAttachments,
|
||||
MessageCopyButton: () => null,
|
||||
Bot: () => React.createElement('svg'), WandSparkles: () => React.createElement('svg')
|
||||
})
|
||||
|
||||
test('agent image thumbnails are siblings below the user bubble rather than bubble contents', () => {
|
||||
const tree = messages.AgentMessages({
|
||||
dark: true, enabled: true, statusLoading: false, pending: false,
|
||||
messages: [{ id: 'user', role: 'user', content: '이 화면을 확인해줘\n두 번째 줄', imageAttachments: [{ name: '스크린샷.png', url: 'https://files.test/image.png' }] }]
|
||||
})
|
||||
let row
|
||||
function visit(element) {
|
||||
if (!React.isValidElement(element)) return
|
||||
const children = React.Children.toArray(element.props.children)
|
||||
if (children.some(child => React.isValidElement(child) && child.props['data-agent-user-bubble'])) row = children
|
||||
children.forEach(visit)
|
||||
}
|
||||
visit(tree)
|
||||
assert.ok(row)
|
||||
assert.equal(row[0].props.children, '이 화면을 확인해줘\n두 번째 줄')
|
||||
assert.match(row[0].props.className, /whitespace-pre-wrap/)
|
||||
assert.equal(row[1].type, ChatImageAttachments)
|
||||
assert.equal(row[1].props.variant, 'sent')
|
||||
assert.equal(row[1].props.images.length, 1)
|
||||
})
|
||||
|
||||
for (const dark of [false, true]) {
|
||||
for (const mode of ['AiMessages', 'AgentMessages']) {
|
||||
test(`${mode} uses a three-line skeleton instead of a spinner (${dark ? 'dark' : 'light'})`, () => {
|
||||
|
||||
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const vm = require('node:vm')
|
||||
const ts = require('typescript')
|
||||
const React = require('react')
|
||||
const { renderToStaticMarkup } = require('react-dom/server')
|
||||
|
||||
function setup() {
|
||||
const api = {}
|
||||
@@ -18,7 +20,7 @@ function setup() {
|
||||
},
|
||||
URL: { createObjectURL: () => `blob:fixture-${++count}`, revokeObjectURL: url => revoked.push(url) }
|
||||
})
|
||||
return { read: (files, existing = []) => api.readChatAttachments(files, existing, { imagesEnabled: true }), revoked }
|
||||
return { api, read: (files, existing = []) => api.readChatAttachments(files, existing, { imagesEnabled: true }), revoked }
|
||||
}
|
||||
const image = (extra = {}) => ({ name: 'screen.png', size: 100, ...extra })
|
||||
|
||||
@@ -46,3 +48,71 @@ test('a mixed batch failure revokes only newly created image previews', async ()
|
||||
assert.deepEqual(h.revoked, ['blob:fixture-2'])
|
||||
assert.equal(existing[0].previewUrl, 'blob:fixture-1')
|
||||
})
|
||||
|
||||
test('restores cached multipart filenames while preserving valid Unicode and Latin-1 names', () => {
|
||||
const { api } = setup()
|
||||
const name = '스크린샷 2026-10-19 14-30-45.png'
|
||||
assert.equal(api.displayAttachmentName(Buffer.from(name).toString('latin1')), name)
|
||||
for (const value of [name, 'screen.png', 'caf\u00e9.png', '\uD83D\uDCF7.png']) assert.equal(api.displayAttachmentName(value), value)
|
||||
})
|
||||
|
||||
function previewHarness() {
|
||||
const { api } = setup()
|
||||
const exports = {}
|
||||
let preview = null
|
||||
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/components/ChatImageAttachments.tsx'), 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX }
|
||||
}).outputText, {
|
||||
exports,
|
||||
document: { body: {} },
|
||||
require(name) {
|
||||
if (name === 'react') return { ...React, useEffect() {}, useRef: () => ({ current: null }), useState: () => [preview, value => { preview = value }] }
|
||||
if (name === 'react-dom') return { createPortal: child => child }
|
||||
if (name === '../i18n') return { t: key => key, useLocaleRender() {} }
|
||||
if (name === './chatAttachments') return api
|
||||
return require(name)
|
||||
}
|
||||
})
|
||||
const find = (node, predicate) => {
|
||||
if (!React.isValidElement(node)) return null
|
||||
if (predicate(node)) return node
|
||||
return React.Children.toArray(node.props.children).map(child => find(child, predicate)).find(Boolean)
|
||||
}
|
||||
return { render: props => exports.ChatImageAttachments(props), find }
|
||||
}
|
||||
|
||||
for (const dark of [true, false]) {
|
||||
test(`draft combines thumbnail, truncated filename and removal in one bounded item (${dark ? 'dark' : 'light'})`, () => {
|
||||
const h = previewHarness()
|
||||
const name = '스크린샷-긴파일이름-'.repeat(12) + '.png'
|
||||
let removed
|
||||
const props = { images: [{ name, url: 'blob:fixture' }], dark, onRemove: image => { removed = image.url } }
|
||||
const tree = h.render(props)
|
||||
const item = h.find(tree, node => node.props['data-chat-image-draft'])
|
||||
assert.ok(item)
|
||||
assert.match(item.props.className, /w-56.*max-w-full/)
|
||||
assert.ok(h.find(item, node => node.type === 'img'))
|
||||
const label = h.find(item, node => node.type === 'span')
|
||||
assert.equal(label.props.children, name)
|
||||
assert.match(label.props.className, /truncate/)
|
||||
const zoom = h.find(item, node => node.type === 'button' && node.props.title === name)
|
||||
assert.equal(zoom.props.title, name)
|
||||
h.find(item, node => node.type === 'button' && node.props['aria-label'] === `첨부 제거: ${name}`).props.onClick()
|
||||
assert.equal(removed, 'blob:fixture')
|
||||
const busyItem = h.find(h.render({ ...props, removeDisabled: true }), node => node.props['data-chat-image-draft'])
|
||||
assert.equal(h.find(busyItem, node => node.type === 'button' && node.props['aria-label'] === `첨부 제거: ${name}`).props.disabled, true)
|
||||
})
|
||||
}
|
||||
|
||||
test('cached garbled names are restored in the thumbnail label and enlarged dialog', () => {
|
||||
const h = previewHarness()
|
||||
const name = '스크린샷 2026-10-19 14-30-45.png'
|
||||
const props = { images: [{ name: Buffer.from(name).toString('latin1'), url: 'https://files.test/image.png' }], variant: 'sent' }
|
||||
const tree = h.render(props)
|
||||
assert.equal(h.find(tree, node => node.type === 'button').props.title, name)
|
||||
assert.equal(h.find(tree, node => node.props['data-chat-image-draft']), undefined)
|
||||
h.find(tree, node => node.type === 'button').props.onClick()
|
||||
const html = renderToStaticMarkup(h.render(props))
|
||||
assert.match(html, /<dialog[^>]*aria-label="스크린샷 2026-10-19 14-30-45.png"/)
|
||||
assert.ok(html.includes(`>${name}</span>`))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user