fix(gui): harden multimodal image attachments

This commit is contained in:
Yichen Jiang
2026-07-23 19:38:37 +08:00
parent eea595fcb4
commit 580e05b794
61 changed files with 1700 additions and 214 deletions

View File

@@ -23,9 +23,11 @@ describe('connection lifecycle', () => {
it('announces connected after describe + both streams open, then pumps frames to sinks', async () => {
const api = new FakeApiClient()
const muxSeen: string[] = []
const descriptions: string[] = []
let connected = 0
const controller = new ConnectionController(api, {
onMuxEnvelope: envelope => muxSeen.push(envelope.payload.type),
onDescription: description => descriptions.push(description.version),
onConnected: () => { connected++ },
}, FAST)
controller.start()
@@ -34,6 +36,7 @@ describe('connection lifecycle', () => {
api.pushMux(subscribedFrame())
await vi.waitFor(() => { expect(muxSeen).toEqual(['session/subscribed']) })
expect(api.callsOf('host.describe')).toHaveLength(1)
expect(descriptions).toEqual(['0-fake'])
} finally {
controller.stop()
}
@@ -83,6 +86,35 @@ describe('connection lifecycle', () => {
}
})
it('treats a host.describe business error as generation failure', async () => {
const api = new FakeApiClient()
let describeCalls = 0
api.onDescribe = () => {
describeCalls += 1
if (describeCalls === 1) {
return Promise.resolve({
rpcId: 'bad-describe' as never,
result: {
ok: false as const,
error: { code: 'internal' as const, message: 'not ready', details: {} },
},
})
}
return Promise.resolve(ok({ version: '0', cwd: '/f', attachedSessions: 0 }))
}
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(describeCalls).toBe(2) })
await vi.waitFor(() => { expect(connected).toBe(1) })
} finally {
controller.stop()
warnSpy.mockRestore()
}
})
it('converges stream/error frames into reconnect instead of dispatching them', async () => {
const api = new FakeApiClient()
const muxSeen: string[] = []

View File

@@ -207,6 +207,31 @@ describe('createFixtureApi', () => {
})
})
it('accounts for every base64 padding form and reports a missing fixture attachment', async () => {
const api = createFixtureApi()
const created = await api.sessions.create(req({}))
if (!created.result.ok) throw new Error('create failed')
const sessionId = created.result.value.sessionId
const prompted = await api.sessions.prompt(req({
sessionId,
mode: 'queue' as const,
content: ['YQ==', 'YWI=', 'YWJj'].map(data => ({
type: 'image' as const,
mediaType: 'image/png' as const,
data,
})),
}))
expect(prompted.result.ok).toBe(true)
const missing = await api.sessions.attachment(req({
sessionId,
attachmentId: 'fixture:missing' as never,
}))
expect(missing.result).toMatchObject({
ok: false, error: { details: { reason: 'ATTACHMENT_NOT_FOUND' } },
})
await api.sessions.cancel(req({ sessionId }))
})
it('gamma interval flip emits host/session-status and a running log-less session subscribes at lastSeq -1', async () => {
vi.useFakeTimers()
try {