mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
- fix(desktop): Settings Agents/Skills page layout overflow at narrow widths Shift header grid breakpoint md→xl, summary cards sm→3col, add min-w-0/truncate - fix(desktop): TabBar tab style — remove border-r separators, use bg-only active state Drop ::after indicator and inter-tab borders; active tab distinguished by surface bg - feat(desktop): IM Adapters page — horizontal tabs with Feishu first, Telegram second Replace vertical stack with tablist; add ImTabButton with aria-selected + focus-visible - fix(server): bundled IM session cwd resolves to / instead of selected project Root cause: preload.ts chdir(CALLER_DIR) inherits '/' from Tauri-spawned sidecar. Fix: pin CALLER_DIR and PWD to session workDir in conversationService spawn env. Add diagnostic logs at sessionService, ws/handler, and conversationService. - fix(feishu): StreamingCard shows duplicate "thinking" indicators Merge static loading element into streaming_content; renderedText() controls state. - test(adapters): add 5 ImageBlockWatcher edge-case unit tests (reset, relative path, multi-image chunk, malformed data URI) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'bun:test'
|
|
import { ImageBlockWatcher } from '../image-block-watcher.js'
|
|
|
|
describe('ImageBlockWatcher', () => {
|
|
it('extracts a markdown image with http URL', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('Here is  an image.')
|
|
expect(out.length).toBe(1)
|
|
const source = out[0]!.source
|
|
expect(source.kind).toBe('url')
|
|
if (source.kind === 'url') {
|
|
expect(source.url).toBe('https://example.com/foo.png')
|
|
}
|
|
expect(out[0]!.alt).toBe('alt')
|
|
})
|
|
|
|
it('extracts a markdown image with absolute local path', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('')
|
|
expect(out.length).toBe(1)
|
|
const source = out[0]!.source
|
|
expect(source.kind).toBe('path')
|
|
if (source.kind === 'path') {
|
|
expect(source.path).toBe('/tmp/cat.jpg')
|
|
}
|
|
})
|
|
|
|
it('extracts a markdown image with file:// URL as path', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('')
|
|
const source = out[0]!.source
|
|
expect(source.kind).toBe('path')
|
|
if (source.kind === 'path') expect(source.path).toBe('/var/img/x.png')
|
|
})
|
|
|
|
it('extracts a data URI as base64', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('')
|
|
const source = out[0]!.source
|
|
expect(source.kind).toBe('base64')
|
|
if (source.kind === 'base64') {
|
|
expect(source.mime).toBe('image/png')
|
|
expect(source.data).toBe('AAAA')
|
|
}
|
|
})
|
|
|
|
it('deduplicates the same image across multiple feeds', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const a = w.feed('')
|
|
const b = w.feed(' repeated  again')
|
|
expect(a.length).toBe(1)
|
|
expect(b.length).toBe(0)
|
|
})
|
|
|
|
it('handles images split across feed boundaries', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const a = w.feed('a  b')
|
|
expect(a.length).toBe(0)
|
|
expect(b.length).toBe(1)
|
|
const source = b[0]!.source
|
|
expect(source.kind).toBe('path')
|
|
if (source.kind === 'path') expect(source.path).toBe('/tmp/x.png')
|
|
})
|
|
|
|
it('skips non-image markdown links', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('See [docs](https://example.com).')
|
|
expect(out.length).toBe(0)
|
|
})
|
|
|
|
it('drain() returns all accumulated uploads', () => {
|
|
const w = new ImageBlockWatcher()
|
|
w.feed('')
|
|
w.feed(' and ')
|
|
const all = w.drain()
|
|
expect(all.length).toBe(2)
|
|
})
|
|
|
|
it('reset() clears buffer, seen set, and accumulated list', () => {
|
|
const w = new ImageBlockWatcher()
|
|
w.feed('')
|
|
w.reset()
|
|
// After reset, drain() is empty
|
|
expect(w.drain().length).toBe(0)
|
|
// And re-feeding the same image yields a fresh emit (dedup state cleared)
|
|
const out = w.feed('')
|
|
expect(out.length).toBe(1)
|
|
})
|
|
|
|
it('skips relative paths (cannot be resolved safely)', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed(' and ')
|
|
expect(out.length).toBe(1)
|
|
const source = out[0]!.source
|
|
expect(source.kind).toBe('path')
|
|
if (source.kind === 'path') expect(source.path).toBe('/tmp/ok.png')
|
|
})
|
|
|
|
it('extracts multiple images from a single feed chunk in order', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('  ')
|
|
expect(out.length).toBe(3)
|
|
expect(out[0]!.source.kind).toBe('path')
|
|
expect(out[1]!.source.kind).toBe('url')
|
|
expect(out[2]!.source.kind).toBe('base64')
|
|
})
|
|
|
|
it('rejects malformed data URI (not base64)', () => {
|
|
const w = new ImageBlockWatcher()
|
|
const out = w.feed('')
|
|
// Not in `;base64,` form → classify returns null → skipped
|
|
expect(out.length).toBe(0)
|
|
})
|
|
})
|