Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
f189c43
feat(native-chat): a paired server keeps a store for chat attachments
brennanb2025 Oct 4, 2026
6707980
feat(native-chat): upload attached files into a paired server's chat …
brennanb2025 Oct 4, 2026
9e25d1f
feat(native-chat): attach files to a structured chat on a paired server
brennanb2025 Oct 4, 2026
c9529ac
test(native-chat): type the attachment test doubles without bare casts
brennanb2025 Oct 4, 2026
27dcb7b
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 4, 2026
ffad820
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 4, 2026
7f04bf0
refactor(native-chat): keep the clipboard RPC on its own upload bookk…
brennanb2025 Oct 4, 2026
98187b3
feat(native-chat): claim chat attachments when the host admits a message
brennanb2025 Oct 4, 2026
d10dfaf
feat(native-chat): let structured Claude read the host's chat attachm…
brennanb2025 Oct 4, 2026
467404c
fix(native-chat): skip a dropped folder before staging walks into it
brennanb2025 Oct 4, 2026
ac570f0
fix(native-chat): pin the browser client's chat paste to its server
brennanb2025 Oct 4, 2026
8deee88
refactor(native-chat): let the host's claim be the only attachment check
brennanb2025 Oct 4, 2026
1706734
chore(native-chat): keep the claim hook beside the draft consume and …
brennanb2025 Oct 4, 2026
23a1eaf
test(native-chat): pin the attachment store grant in the Claude launc…
brennanb2025 Oct 4, 2026
e3e74a7
fix(native-chat): create the attachment store at install and claim on…
brennanb2025 Oct 4, 2026
c13bbd8
refactor(native-chat): reuse the newer-Orca notice and drop leftover …
brennanb2025 Oct 4, 2026
7933ea7
fix(native-chat): give a message refused for an expired attachment ba…
brennanb2025 Oct 4, 2026
7606667
fix(native-chat): keep uploads across a prompt, say why a file did no…
brennanb2025 Oct 4, 2026
a6ff388
fix(native-chat): keep a paste's upload across a prompt; pin the Clau…
brennanb2025 Oct 4, 2026
dff514a
test(native-chat): type the grant hand-off captures without casts
brennanb2025 Oct 4, 2026
bf41b08
test(native-chat): pin that the composer files a paste's upload acros…
brennanb2025 Oct 4, 2026
17b6706
fix(native-chat): retry a held rename at commit and keep cut names Wi…
brennanb2025 Oct 4, 2026
5bf9679
fix(native-chat): keep attachments still on their way in the pane's a…
brennanb2025 Oct 4, 2026
af18c94
test(native-chat): a dropped file still uploading stays pending acros…
brennanb2025 Oct 4, 2026
fdc5373
fix(native-chat): reveal a rich-text image in a composer that came ba…
brennanb2025 Oct 4, 2026
2bead96
fix(native-chat): never evict a pane's attachments while a composer s…
brennanb2025 Oct 4, 2026
82c5880
fix(native-chat): never evict the scope just written when every older…
brennanb2025 Oct 4, 2026
690970c
fix(native-chat): keep the attachment store out of the RPC method tab…
brennanb2025 Oct 4, 2026
f8f1e1c
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 4, 2026
ccfa30e
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 4, 2026
ca5bcb5
refactor(native-chat): the submission hook gets its own module
brennanb2025 Oct 4, 2026
3c6bec3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
ba43a87
refactor(native-chat): fit the attachment sweep and paste upload into…
brennanb2025 Oct 6, 2026
491ec81
fix(native-chat): pending chips sit beside main's saved draft, which …
brennanb2025 Oct 6, 2026
c704cee
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
12a65ab
test(native-chat): give the claims test main's provider handle and me…
brennanb2025 Oct 6, 2026
235239b
fix(native-chat): a paste still uploading dies with its tab or workspace
brennanb2025 Oct 6, 2026
f503dd8
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
83aa2e5
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
94f09d7
fix(native-chat): the claim type uses the journal's own SQLite type
brennanb2025 Oct 6, 2026
700ceb2
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
aee5adb
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
5572a75
refactor(native-chat): read the pending-image handlers off the compos…
brennanb2025 Oct 6, 2026
c686bf4
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
3e4d704
test(native-chat): pass the launch-args resolver main now requires in…
brennanb2025 Oct 6, 2026
79bdfd7
fix(native-chat): grant the attachment store beside folders the saved…
brennanb2025 Oct 6, 2026
02cebe3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
837b626
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
5aaa993
test(native-chat): run the attachment store's SQLite tests in the Nod…
brennanb2025 Oct 6, 2026
679a623
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 6, 2026
8e6c9b2
test(native-chat): follow main's attach ownership recheck (#25749) in…
brennanb2025 Oct 6, 2026
25a05c3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
ecd71f7
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
ae64b29
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
846dfc5
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
4d09aa3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
ee9d8d7
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
be41bf0
test(native-chat): a named upload chip still shows when the agent tak…
brennanb2025 Oct 7, 2026
3682e9a
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
58348b6
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
b915afc
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
9c9c264
fix(native-chat): a paired-server image paste failure keeps its error…
brennanb2025 Oct 7, 2026
1b137cf
fix(native-chat): the attachment sweep reads the chat list directly n…
brennanb2025 Oct 7, 2026
d547b1c
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
30461ad
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-chat…
brennanb2025 Oct 7, 2026
e1b4055
Merge branch 'main' of https://github.com/stablyai/orca into HEAD
brennanb2025 Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/ipc/ssh-host-partition-session-export.test.ts',
'src/main/ipc/ssh-host-server-on-connect-wiring.test.ts',
'src/main/ipc/ssh-managed-server-move-conversion.test.ts',
'src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts',
'src/main/native-chat/agent-session-attachments/agent-session-attachment-sweep.test.ts',
'src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts',
'src/main/native-chat/agent-session-wire/agent-session-history-conversation-window.test.ts',
'src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts',
Expand All @@ -46,6 +48,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-attach-reconciliation.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-claude-compact-stop.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-claude-echo-working.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.ts',
Expand Down Expand Up @@ -187,6 +190,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/runtime/structured-agent-session-rollback-compatibility.test.ts',
'src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts',
'src/main/runtime/structured-agent-session-startup-reconcile.test.ts',
'src/main/runtime/structured-claude-attachment-grant.test.ts',
'src/main/runtime/structured-claude-pending-rewind.test.ts',
'src/main/ssh-expired-lease-pane-readoption.test.ts',
'src/main/ssh-reattach-pane-cardinality.test.ts',
Expand Down
38 changes: 36 additions & 2 deletions src/main/claude/claude-structured-launch-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ function resolverFor(
// Manual by default so a test that is not about permissions is not silently about them.
agentDefaultArgs: Record<string, string> = { claude: '' },
hasTranscript: () => Promise<boolean> = async () => true,
resolveLaunchArgs?: () => string[]
resolveLaunchArgs?: () => string[],
attachmentDirectory?: string
) {
return createClaudeStructuredLaunchResolver({
store: { getRecord: () => value, pinLaunchDirectory: vi.fn() },
Expand All @@ -73,7 +74,8 @@ function resolverFor(
resolvePermissionMode: () => claudeStructuredPermissionModeForSettings({ agentDefaultArgs }),
hasTranscript,
resolveLaunchArgs: resolveLaunchArgs ?? (() => value?.launchArgs ?? []),
...(resolveEnv ? { resolveEnv } : {})
...(resolveEnv ? { resolveEnv } : {}),
...(attachmentDirectory ? { attachmentDirectory } : {})
})
}

Expand Down Expand Up @@ -424,6 +426,38 @@ describe('claude structured launch resolution', () => {
}
})

it("lets the agent read the host's chat attachment store, outside the workspace", async () => {
const launch = await resolverFor(
record(),
undefined,
false,
{ claude: '' },
async () => true,
undefined,
'/state/agent-session-attachments'
)({ identity: IDENTITY })

expect(launch.options.additionalDirectories).toEqual(['/state/agent-session-attachments'])
expect(launch.cwd).toBe('/repos/workspace-1')
})

it('grants the attachment store beside folders the saved Arguments add', async () => {
const launch = await resolverFor(
record(),
undefined,
false,
{ claude: '' },
async () => true,
() => ['--add-dir', '/extra'],
'/state/agent-session-attachments'
)({ identity: IDENTITY })

expect(launch.options.additionalDirectories).toEqual([
'/extra',
'/state/agent-session-attachments'
])
})

it('builds on the supplied inherited env instead of Orca process env', async () => {
const launch = await createClaudeStructuredLaunchResolver({
resolveLaunchArgs: () => [],
Expand Down
21 changes: 11 additions & 10 deletions src/main/claude/claude-structured-launch-resolution.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getClaudeProfileRouter } from '../claude-accounts/claude-profile-installed-router'
import { createHash } from 'node:crypto'
import type {
Options as ClaudeAgentSdkOptions,
PermissionMode
Expand Down Expand Up @@ -44,6 +43,9 @@ import type { ClaudeThinkingDisplaySupport } from './claude-thinking-display-sup
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
import { resolveAgentSessionLaunchDirectory } from '../runtime/agent-session-launch-directory'
import { CLAUDE_STRUCTURED_AGENT } from './claude-structured-agent-definition'
import { claudeSessionIdForOrcaSession } from './claude-structured-session-id'

export { claudeSessionIdForOrcaSession }

export const CLAUDE_DEFAULT_SETTING_SOURCES = ['user', 'project', 'local'] as const
export const CLAUDE_SESSION_STATE_EVENTS_ENV = 'CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS'
Expand Down Expand Up @@ -135,6 +137,9 @@ export type ClaudeStructuredLaunchResolverDeps = {
authSwitchSettleTimeoutMs?: number
/** Account state for the managed-account gate; null when it cannot be read, which refuses. */
readManagedAccountGate?: () => ClaudeManagedAccountGateSettings | null
/** The host's chat attachment store: files a client attached live there, outside the workspace,
* and the agent reads them without asking. */
attachmentDirectory?: string
/** Whether this CLI takes the thinking-display flag. Absent ⇒ the flag is never passed. */
thinkingDisplay?: Pick<ClaudeThinkingDisplaySupport, 'argsFor'>
/** Whether Claude wrote a transcript for this id; defaults to the transcript resolver. */
Expand Down Expand Up @@ -198,14 +203,6 @@ export async function assertClaudeAuthSwitchSettled(
}
}

export function claudeSessionIdForOrcaSession(sessionId: string): string {
const bytes = createHash('sha256').update(`orca-claude:${sessionId}`).digest().subarray(0, 16)
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
const hex = bytes.toString('hex')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}

export function createClaudeStructuredLaunchResolver(
deps: ClaudeStructuredLaunchResolverDeps
): (input: { identity: AgentSessionJournalIdentity }) => Promise<ClaudeStructuredLaunch> {
Expand Down Expand Up @@ -265,7 +262,11 @@ export function createClaudeStructuredLaunchResolver(
env: claudeProbeEnv(sources)
})
const configured = claudeStructuredLaunchArgs(await deps.resolveLaunchArgs())
const { additionalDirectories } = configured
// The user's own --add-dir folders, plus the host's chat attachment store.
const additionalDirectories = [
...configured.additionalDirectories,
...(deps.attachmentDirectory ? [deps.attachmentDirectory] : [])
]
const permission = claudeStructuredPermissionOptions(
(await deps.resolvePermissionMode?.()) ?? 'default'
)
Expand Down
10 changes: 10 additions & 0 deletions src/main/claude/claude-structured-session-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createHash } from 'node:crypto'

/** A stable UUID (v4 layout) for the Claude conversation behind an Orca chat. */
export function claudeSessionIdForOrcaSession(sessionId: string): string {
const bytes = createHash('sha256').update(`orca-claude:${sessionId}`).digest().subarray(0, 16)
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
const hex = bytes.toString('hex')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}
36 changes: 36 additions & 0 deletions src/main/ipc/agent-session-attachment-upload-ipc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { app, ipcMain } from 'electron'
import type {
AgentSessionAttachmentPathUploadResult,
AgentSessionAttachmentUploadTarget
} from '../../shared/agent-session-attachments'
import { resolveEnvironment } from '../../shared/runtime-environment-store'
import { uploadExternalPathsToAgentSessionAttachments } from './agent-session-attachment-upload'
import { abortWhenRendererGone } from './renderer-lifetime-abort'

export function registerAgentSessionAttachmentUploadHandlers(): void {
ipcMain.handle(
'fs:uploadPathsToAgentSessionAttachments',
async (
event,
args: AgentSessionAttachmentUploadTarget & { paths: string[] }
): Promise<AgentSessionAttachmentPathUploadResult> => {
const userDataPath = app.getPath('userData')
// Why: a reload or close must stop the transfer; the server sweeps the part file it leaves.
const lifetime = abortWhenRendererGone(event.sender)
try {
return await uploadExternalPathsToAgentSessionAttachments(
{
...args,
// The manual-disconnect check keys on the resolved id, not whatever selector came in.
environmentId: resolveEnvironment(userDataPath, args.environmentId).id,
userDataPath,
signal: lifetime.signal
},
args.paths
)
} finally {
lifetime.dispose()
}
}
)
}
166 changes: 166 additions & 0 deletions src/main/ipc/agent-session-attachment-upload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store'

type CallOptions = { expectedEnvironmentRuntimeId?: string; signal?: AbortSignal }
type Call = {
method: string
params: Record<string, unknown>
revision?: number
options?: CallOptions
}

const calls: Call[] = []
let serverStore: AgentSessionAttachmentStore
let failMethod: string | null = null

// The paired server, played by a real store: every call answers as the RPC handler would.
vi.mock('./runtime-environment-transport-routing', () => ({
callRuntimeEnvironment: async (
_userDataPath: string,
_environmentId: string,
method: string,
params: Record<string, unknown>,
_timeoutMs?: number,
revision?: number,
_envelope?: unknown,
options?: CallOptions
) => {
calls.push({ method, params, revision, options })
if (method === failMethod) {
return { ok: false, error: { code: 'internal', message: `${method} failed` } }
}
const callerKey = 'client-a'
const uploadId = String(params.uploadId)
try {
const result =
method === 'agentSessionAttachment.uploadStart'
? await serverStore.startUpload({
callerKey,
sessionId: String(params.sessionId),
name: String(params.name),
byteLength: Number(params.byteLength)
})
: method === 'agentSessionAttachment.uploadAppend'
? await serverStore.appendChunk({
callerKey,
uploadId,
offset: Number(params.offset),
contentBase64: String(params.contentBase64)
})
: method === 'agentSessionAttachment.uploadCommit'
? await serverStore.commitUpload({ callerKey, uploadId })
: await serverStore.abortUpload({ callerKey, uploadId })
return { ok: true, result }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { ok: false, error: { code: 'internal', message } }
}
}
}))

const { uploadBufferToAgentSessionAttachments, uploadExternalPathsToAgentSessionAttachments } =
await import('./agent-session-attachment-upload')

let workDir: string
const context = () => ({
environmentId: 'env-1',
sessionId: 'session-1',
expectedEnvironmentPairingRevision: 7,
expectedEnvironmentRuntimeId: 'runtime-a',
userDataPath: join(workDir, 'client-user-data')
})

beforeEach(async () => {
workDir = await mkdtemp(join(tmpdir(), 'orca-attachment-upload-'))
serverStore = new AgentSessionAttachmentStore(
join(workDir, 'server', 'agent-session-attachments'),
{ hasSession: () => true }
)
calls.length = 0
failMethod = null
})

afterEach(async () => {
serverStore.clearInFlightForTests()
await rm(workDir, { recursive: true, force: true })
})

describe('uploadExternalPathsToAgentSessionAttachments', () => {
it('uploads dropped files in order and returns only server paths', async () => {
const big = Buffer.alloc(1024 * 1024, 7)
await writeFile(join(workDir, 'big.bin'), big)
await writeFile(join(workDir, 'notes.md'), '# hi')

const result = await uploadExternalPathsToAgentSessionAttachments(context(), [
join(workDir, 'big.bin'),
join(workDir, 'notes.md')
])

expect(result.skipped).toEqual([])
expect(result.failed).toEqual([])
expect(result.uploaded.map(({ sourcePath }) => sourcePath)).toEqual([
join(workDir, 'big.bin'),
join(workDir, 'notes.md')
])
for (const { path } of result.uploaded) {
expect(path.startsWith(serverStore.rootDir)).toBe(true)
}
expect(await readFile(result.uploaded[0].path)).toEqual(big)
expect(await readFile(result.uploaded[1].path, 'utf8')).toBe('# hi')
})

it('pins every call to the pairing and every call to the server process', async () => {
await writeFile(join(workDir, 'a.txt'), 'a')
await uploadExternalPathsToAgentSessionAttachments(context(), [join(workDir, 'a.txt')])
expect(calls.map((call) => call.method)).toEqual([
'agentSessionAttachment.uploadStart',
'agentSessionAttachment.uploadAppend',
'agentSessionAttachment.uploadCommit'
])
for (const call of calls) {
expect(call.revision).toBe(7)
expect(call.options?.expectedEnvironmentRuntimeId).toBe('runtime-a')
}
})

it('skips a folder as unsupported without walking into it', async () => {
await mkdir(join(workDir, 'folder', 'nested'), { recursive: true })
await writeFile(join(workDir, 'folder', 'inside.txt'), 'x')
// A link inside would make the stager skip the folder as a symlink: never reached.
await symlink(join(workDir, 'folder', 'inside.txt'), join(workDir, 'folder', 'nested', 'link'))
const result = await uploadExternalPathsToAgentSessionAttachments(context(), [
join(workDir, 'folder')
])
expect(result.skipped).toEqual([{ sourcePath: join(workDir, 'folder'), reason: 'unsupported' }])
expect(calls).toEqual([])
})
Comment on lines +129 to +139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Skip the tests that create symlinks on Windows. Both tests call symlink. Windows CI environments without elevation or Developer Mode cannot create symlinks, so these tests can fail there.

  • src/main/ipc/agent-session-attachment-upload.test.ts#L130-L140: change the test to it.skipIf(process.platform === 'win32')(...).
  • src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts#L195-L202: change the test to it.skipIf(process.platform === 'win32')(...).

Based on learnings: tests that create symlinks "should be guarded with it.skipIf(process.platform === 'win32')".

📍 Affects 2 files
  • src/main/ipc/agent-session-attachment-upload.test.ts#L130-L140 (this comment)
  • src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts#L195-L202

Source: Learnings


it('aborts a failed upload on the server and reports the file as failed', async () => {
await writeFile(join(workDir, 'a.txt'), 'a')
failMethod = 'agentSessionAttachment.uploadCommit'
const result = await uploadExternalPathsToAgentSessionAttachments(context(), [
join(workDir, 'a.txt')
])
expect(result.uploaded).toEqual([])
expect(result.failed).toEqual([
{ sourcePath: join(workDir, 'a.txt'), reason: 'agentSessionAttachment.uploadCommit failed' }
])
expect(calls.at(-1)?.method).toBe('agentSessionAttachment.uploadAbort')
})
})

describe('uploadBufferToAgentSessionAttachments', () => {
it('stores a pasted image under the chat', async () => {
const png = Buffer.alloc(900 * 1024, 3)
const stored = await uploadBufferToAgentSessionAttachments(context(), 'orca-paste-1.png', png)
expect(stored.name).toBe('orca-paste-1.png')
expect(await readFile(stored.path)).toEqual(png)
// 900 KiB in 384 KiB slices.
expect(
calls.filter((call) => call.method === 'agentSessionAttachment.uploadAppend')
).toHaveLength(3)
})
})
Loading
Loading