mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* Open large pasted text and attached text in structured chats * Read pending pasted text before its host route resolves * Pass structured attachment eligibility from the owning chat * Preserve composer and Markdown state when inspecting text attachments * fix: preserve attachment identity and cancel deferred reads * test: consolidate phone attachment rendering contracts * Scope attachment previews to visible chats and share route indexes * test: consolidate duplicate attachment preview coverage * perf: share remaining workspace catalog derivations * feat(chat): open text attachments from structured goal objectives * fix(chat): reuse preview owner and bind app hooks * refactor(chat): trim duplicate tests and unused capability initialization * test(chat): share routing scope mount fixture * Fix routing performance attribution and folder row imports * Share attachment capability through its domain module * Align attachment fixtures and warm routing contracts with current gates * Preserve relay connection timeout default for ordinary requests * Share image classification ownership and repair integration fixtures * Repair real attachment integration fixture boundaries * Expose e2e compiler diagnostics and prune stale read allowance * Type the recall fixture row ref as a div * Keep text attachment capability in its existing capability owner * Keep image MIME bindings in the media type owner * Own private bridge response completion at the request boundary * Complete bridge fixture return and retention types * Localize native chat text attachment preview controls * Complete attachment preview localization at presentation boundaries * Preserve legacy preview locale identity and complete localization controls * Fix typed mobile host witness lookup * Commit attachment page edits and reset mobile page scroll * test(chat): remove duplicate preview localization coverage * fix(mobile): apply page input font and footer safe area * fix(chat): commit edited page before admitted navigation * fix(chat): retain field readiness on pending commit * fix: preserve attachment field focus until control activation * fix: restore attachment preview styles and canonical close * Bind mobile input font role to the central theme * Type font source fixture cases as tuples * Attribute inherited owner filter work and qualify folder fixtures * Keep owner filter fixture input types source-derived * test: retain attachment capability assertion import * test: focus attachment coverage on important regressions * Replace text attachment paging with bounded scrolling and trim tests * Preserve literal phone attachment whitespace and trim unused test meter * Keep scroll view refs and measurements out of render * Keep preview collection independent of native clipboard initialization * Give the phone scroll fixture coherent measured DOM geometry * Commit attachment scroll extent before restoring its anchor * Preserve text viewport geometry across bounded reads * Unify bounded text scrolling around the viewport * Preserve the live text anchor across viewport resizing * Measure committed text viewport bounds from the host * Preserve live attachment position across native client extent changes * Use the actual first source block in the resize regression fixture * Reuse subscription handle type * Keep attachment effects keyed to their existing triggers * Keep attachment rows within the committed track * Allow the full attachment renderer contract to settle * Open text attachment previews in a large modal * Preserve attachment modal header layout with primitive-owned styles
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { createElement } from 'react'
|
|
import { EventEmitter } from 'node:events'
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, expect, it, vi } from 'vitest'
|
|
import { NativeChatTextAttachmentPreview } from '@/components/native-chat/NativeChatTextAttachmentPreview'
|
|
import { AgentSessionAttachmentStore } from '../../src/main/native-chat/agent-session-attachments/agent-session-attachment-store'
|
|
import { createFilesystemHandlerContext } from '../../src/main/ipc/filesystem/filesystem-handler-context'
|
|
import { registerFilesystemDownloadHandlers } from '../../src/main/ipc/filesystem/filesystem-download-handlers'
|
|
import type { Store } from '../../src/main/persistence/loading-store/store'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
handlers: new Map<string, (...args: unknown[]) => Promise<unknown>>(),
|
|
dialog: vi.fn(),
|
|
write: vi.fn(),
|
|
rpc: vi.fn()
|
|
}))
|
|
vi.mock('electron', () => ({
|
|
BrowserWindow: { fromWebContents: () => null },
|
|
dialog: { showSaveDialog: mocks.dialog },
|
|
ipcMain: {
|
|
handle: (name: string, handler: (...args: unknown[]) => Promise<unknown>) =>
|
|
mocks.handlers.set(name, handler)
|
|
}
|
|
}))
|
|
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc }))
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn() } }))
|
|
afterEach(cleanup)
|
|
|
|
it('saves zero-byte and invalid UTF8 committed files through the real native sink and copies empty text', async () => {
|
|
const folder = await mkdtemp(join(tmpdir(), 'export35636-'))
|
|
const sourceStore = new AgentSessionAttachmentStore(join(folder, 'agent-session-attachments'), {
|
|
hasSession: () => true
|
|
})
|
|
// SAFETY: only download handlers are registered; they do not access persistence.
|
|
const store: Store = Object.create(null)
|
|
const cancellations = { begin: () => null, finish: () => {}, cancel: () => {} }
|
|
const context = createFilesystemHandlerContext(store, undefined, cancellations, cancellations)
|
|
registerFilesystemDownloadHandlers(context)
|
|
const sender = Object.assign(new EventEmitter(), { id: 35636, isDestroyed: () => false })
|
|
const invoke = (name: string, args: unknown) => {
|
|
const handler = mocks.handlers.get(`fs:${name}`)
|
|
if (!handler) {
|
|
throw new Error(`Missing ${name}`)
|
|
}
|
|
return handler({ sender }, args)
|
|
}
|
|
const append = vi.fn((args: unknown) => invoke('appendDownloadedFileChunk', args))
|
|
const finish = vi.fn((args: unknown) => invoke('finishDownloadedFile', args))
|
|
const cancel = vi.fn((args: unknown) => invoke('cancelDownloadedFile', args))
|
|
Object.defineProperty(window, 'api', {
|
|
configurable: true,
|
|
value: {
|
|
ui: { writeClipboardText: mocks.write },
|
|
fs: {
|
|
startDownloadedFile: (args: unknown) => invoke('startDownloadedFile', args),
|
|
appendDownloadedFileChunk: append,
|
|
finishDownloadedFile: finish,
|
|
cancelDownloadedFile: cancel
|
|
}
|
|
}
|
|
})
|
|
mocks.rpc.mockImplementation(async (_target, method, request) =>
|
|
method === 'status.get'
|
|
? { runtimeId: '35636', capabilities: ['native-chat.text-attachment-read.v1'] }
|
|
: sourceStore.readTextChunk(request)
|
|
)
|
|
try {
|
|
for (const bytes of [Buffer.alloc(0), Buffer.from([0xff, 0, 0x80])]) {
|
|
const name = bytes.length ? 'invalid.txt' : 'empty.txt'
|
|
const { uploadId } = await sourceStore.startUpload({
|
|
callerKey: '35636',
|
|
sessionId: 's',
|
|
name,
|
|
byteLength: bytes.length
|
|
})
|
|
if (bytes.length) {
|
|
await sourceStore.appendChunk({
|
|
callerKey: '35636',
|
|
uploadId,
|
|
offset: 0,
|
|
contentBase64: bytes.toString('base64')
|
|
})
|
|
}
|
|
const { path } = await sourceStore.commitUpload({ callerKey: '35636', uploadId })
|
|
const destination = join(folder, name)
|
|
mocks.dialog.mockResolvedValue({ canceled: false, filePath: destination })
|
|
append.mockClear()
|
|
finish.mockClear()
|
|
cancel.mockClear()
|
|
const view = render(
|
|
createElement(NativeChatTextAttachmentPreview, {
|
|
path,
|
|
target: { kind: 'local' },
|
|
onClose: () => {}
|
|
})
|
|
)
|
|
await waitFor(() =>
|
|
expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(
|
|
false
|
|
)
|
|
)
|
|
if (!bytes.length) {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
|
|
await waitFor(() => expect(mocks.write).toHaveBeenCalledWith(''))
|
|
await waitFor(() =>
|
|
expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(
|
|
false
|
|
)
|
|
)
|
|
}
|
|
fireEvent.click(screen.getByRole('button', { name: 'Save file' }))
|
|
await waitFor(() => expect(finish).toHaveBeenCalledOnce())
|
|
await waitFor(() => expect(context.downloadSessions.size).toBe(0))
|
|
expect(await readFile(destination)).toEqual(bytes)
|
|
expect(append).toHaveBeenCalledTimes(bytes.length ? 1 : 0)
|
|
expect(cancel).not.toHaveBeenCalled()
|
|
view.unmount()
|
|
}
|
|
} finally {
|
|
for (const id of context.downloadSessions.keys()) {
|
|
await context.closeDownloadSession(id, true)
|
|
}
|
|
sourceStore.clearInFlightForTests()
|
|
await rm(folder, { recursive: true, force: true })
|
|
}
|
|
})
|