Files
orca/src/main/ipc/filesystem-import.test.ts
T
mmarabelandNeil 68f0b2e835 feat(runtime): stream file uploads instead of buffering whole files (#16106)
* feat(runtime): stream file uploads instead of buffering whole files

Staging read each dropped file whole with readFile(), base64-encoded it
(a 4/3 expansion), and passed the string through IPC to the renderer,
which re-chunked it. Peak memory was ~2.3x the file size before a byte
moved, so a 25 MB per-file cap existed to protect the heap.

Staging now records identity only. The byte pump moves into main, where
the file handle and the runtime socket both live: 384 KiB slices (512 KiB
once base64-encoded, matching the chunk size the renderer used) appended
through the existing files.writeBase64Chunk RPC. Peak memory is one slice
regardless of file size, so the ceilings become user-safety limits on an
unattended transfer — 2 GB per file, 8 GB per drop — and over-limit errors
name both the size and the limit.

Because staging and streaming are separate calls, the staged entry carries
size, inode, device and mtime, and the streamer re-checks all four against
the pre-open lstat and against the handle it actually reads. A source
replaced or rewritten at the same size between the two calls is refused
rather than uploaded under the original name. The post-read check compares
mtime as well as size, so an in-place rewrite mid-transfer aborts before
commitUpload renames anything into place.

O_NOFOLLOW, realpath containment and stat identity are preserved, and the
pairing revision plus the runtime id ride every chunk, so a re-pair or a
replacement runtime aborts instead of appending the rest of the file to a
different host.

No wire change: files.writeBase64Chunk and its params are untouched, so
old and new hosts behave identically. The SSH import path is separate and
unchanged. The web client has no local filesystem to stream from and says
so instead of failing obscurely.

* fix(runtime): close the empty-upload and per-drop budget holes

Two gaps the first pass left open.

A zero-byte source returned before the post-transfer identity check, so a
file that gained content during the empty write's round trip committed as
an empty file at the user's chosen name. The empty chunk now falls through
to the same final check the slice loop uses.

Each staged source also started its own byte counter, so the 8 GB ceiling
capped one source rather than the drop: five 2 GB files staged cleanly at
10 GB total. The IPC handler now carries one budget across sourcePaths and
adds only what each source actually staged. The per-file ceiling is still
re-enforced where the bytes move; the drop total holds at staging because
identity enforcement means each file streams exactly the bytes measured.

* docs(runtime): name the invariants the upload helpers carry

* fix(runtime): name the source in errors and stop uploads with their window

Three problems an independent review turned up.

A dropped file's relative path is '', so the over-limit error read "'' is
3 GB, over the 2 GB per-file remote import limit" — the message this change
exists to fix, naming nothing. Errors now fall back to the file's own name;
the staged entry keeps '' so the destination path is unaffected. The
streamer had the same shape, falling back to the hidden .orca-upload-<nonce>
temp destination, a path the user never chose.

The byte loop used to live in the renderer and died with it. Moving it into
main meant closing or reloading the window left the rest of a multi-GB
transfer running, with the renderer's temp cleanup never reaching its
finally. An AbortSignal now rides the caller's lifetime and every chunk, is
re-checked per slice, and main sweeps the abandoned temp path itself when
the renderer is no longer there to do it.

Upload failures also reached the import result wrapped in Electron's
"Error invoking remote method '...'" prefix, because the throw crossed IPC
instead of happening in-renderer; extractIpcErrorMessage unwraps it.

An existing staging test asserted the empty-name message, so it encoded the
bug rather than catching it; it now asserts the file name.

* test(runtime): cover the containment check and the per-chunk host guards

The "escapes the dropped root" test only reached the lstat symlink guard,
so assertEntryInsideRoot had no coverage at all. The shape that actually
needs it is a regular file under a symlinked intermediate directory: lstat
sees a plain file, and realpath containment is the only thing that refuses
it. Disabling the guard now fails this test and nothing else.

Nothing asserted that the SSH target, connection generation and execution
host reach the writeBase64Chunk params either — the renderer tests stop at
the IPC boundary, so the streamer's half of that contract was untested.

* fix(runtime): survive a straggling append when sweeping an aborted upload

Aborting rejects the in-flight chunk locally, but the host may still apply
that append, and appends open with flag 'a' — which recreates the file the
sweep just deleted. The delete and the straggler also race: they are
separate calls on a queue that is not ordered between them.

Slices are strictly sequential, so at most one append can be outstanding.
A second pass after it has had time to land is therefore sufficient, not
merely a heuristic. The sweep moves out of filesystem-mutations.ts into its
own module so the behaviour is testable directly.

Found by an independent review pass, which also pointed out that the
"escapes the dropped root" test only reached the lstat symlink guard.

* fix(runtime): abort uploads only when the document commits, and honour manual disconnect per chunk

did-start-navigation fires before will-navigate blocks an external link or a
stray file drop, and the renderer survives those (verified against Electron 43
with a hidden window). Aborting there killed a healthy upload with a misleading
'window went away' error. did-navigate fires only once a new document has
replaced the caller.

The renderer's per-chunk calls used to go through the IPC handler that refuses
a manually disconnected environment; the loop in main made no such check, so a
disconnect mid-upload kept pushing the rest of the file. The handler now
resolves the selector to an environment id and the streamer checks it per slice.

Adds slice-boundary coverage against the real chunk schema and host write
flags, staging-to-stream on a real filesystem, and handler-level lifetime tests.

---------

Co-authored-by: Neil <neil@stably.ai>
2026-09-14 14:08:31 -07:00

811 lines
24 KiB
TypeScript

import path from 'node:path'
import { constants } from 'node:fs'
import { Readable, Writable } from 'node:stream'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const handlers = new Map<string, (_event: unknown, args: unknown) => Promise<unknown>>()
const {
handleMock,
lstatMock,
mkdirMock,
realpathMock,
copyFileMock,
openMock,
readFileMock,
readdirMock,
rmMock,
unlinkMock
} = vi.hoisted(() => ({
handleMock: vi.fn(),
lstatMock: vi.fn(),
mkdirMock: vi.fn(),
realpathMock: vi.fn(),
copyFileMock: vi.fn(),
openMock: vi.fn(),
readFileMock: vi.fn(),
readdirMock: vi.fn(),
rmMock: vi.fn(),
unlinkMock: vi.fn()
}))
vi.mock('electron', () => ({
ipcMain: { handle: handleMock }
}))
vi.mock('fs/promises', () => ({
lstat: lstatMock,
mkdir: mkdirMock,
open: openMock,
rename: vi.fn(),
writeFile: vi.fn(),
realpath: realpathMock,
copyFile: copyFileMock,
readFile: readFileMock,
readdir: readdirMock,
rm: rmMock,
unlink: unlinkMock
}))
import { registerFilesystemMutationHandlers } from './filesystem-mutations'
const REPO_PATH = path.resolve('/workspace/repo')
const WORKSPACE_DIR = path.resolve('/workspace')
const store = {
getRepos: () => [
{ id: 'repo-1', path: REPO_PATH, displayName: 'repo', badgeColor: '#000', addedAt: 0 }
],
getSettings: () => ({ workspaceDir: WORKSPACE_DIR })
}
function enoent(): Error {
return Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
describe('fs:importExternalPaths', () => {
const destDir = path.resolve('/workspace/repo/src')
function mockSourceFile(filePath: string): void {
const resolvedPath = path.resolve(filePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
size: 12,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false
}
}
throw enoent()
})
}
function mockSourceDir(dirPath: string, entries: { name: string; isDir: boolean }[]): void {
const resolvedDir = path.resolve(dirPath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedDir) {
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
}
const entry = entries.find((e) => path.join(resolvedDir, e.name) === p)
if (entry) {
return {
size: entry.isDir ? 0 : 12,
ino: entry.isDir ? 2 : 3,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => !entry.isDir,
isDirectory: () => entry.isDir,
isSymbolicLink: () => false
}
}
throw enoent()
})
readdirMock.mockImplementation(async () => {
return entries.map((e) => ({
name: e.name,
isDirectory: () => e.isDir,
isSymbolicLink: () => false,
isFile: () => !e.isDir
}))
})
}
function mockSymlinkSource(filePath: string): void {
const resolvedPath = path.resolve(filePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return { isFile: () => false, isDirectory: () => false, isSymbolicLink: () => true }
}
throw enoent()
})
}
function mockLocalCopyOpenSuccess(content = Buffer.from('file-content')): void {
openMock.mockImplementation(async (_p: string, flags: unknown) => {
if (flags === 'wx') {
const written: Buffer[] = []
return {
createWriteStream: () =>
new Writable({
write(chunk, _encoding, callback) {
written.push(Buffer.from(chunk))
callback()
}
}),
close: vi.fn().mockResolvedValue(undefined),
written
}
}
return {
stat: vi.fn().mockResolvedValue({
size: content.byteLength,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
createReadStream: () => Readable.from([content]),
close: vi.fn().mockResolvedValue(undefined)
}
})
}
beforeEach(() => {
handlers.clear()
handleMock.mockReset()
lstatMock.mockReset()
mkdirMock.mockReset()
realpathMock.mockReset()
copyFileMock.mockReset()
openMock.mockReset()
readFileMock.mockReset()
readdirMock.mockReset()
rmMock.mockReset()
unlinkMock.mockReset()
handleMock.mockImplementation((channel: string, handler: never) => {
handlers.set(channel, handler)
})
realpathMock.mockImplementation(async (p: string) => p)
lstatMock.mockRejectedValue(enoent())
mkdirMock.mockResolvedValue(undefined)
copyFileMock.mockResolvedValue(undefined)
mockLocalCopyOpenSuccess()
readFileMock.mockResolvedValue(Buffer.from('file-content'))
readdirMock.mockResolvedValue([])
rmMock.mockResolvedValue(undefined)
unlinkMock.mockResolvedValue(undefined)
registerFilesystemMutationHandlers(store as never)
})
it('imports a single file', async () => {
const sourcePath = '/tmp/dropped/logo.png'
mockSourceFile(sourcePath)
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: unknown[] }
expect(result.results).toHaveLength(1)
expect(result.results[0]).toMatchObject({
status: 'imported',
kind: 'file',
renamed: false,
destPath: path.join(destDir, 'logo.png')
})
expect(openMock).toHaveBeenCalledWith(
path.resolve(sourcePath),
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
)
expect(openMock).toHaveBeenCalledWith(path.join(destDir, 'logo.png'), 'wx')
expect(copyFileMock).not.toHaveBeenCalled()
})
it('fails local import instead of clobbering when the chosen destination appears late', async () => {
const sourcePath = '/tmp/dropped/logo.png'
mockSourceFile(sourcePath)
openMock.mockImplementation(async (_p: string, flags: unknown) => {
if (flags === 'wx') {
throw Object.assign(new Error('EEXIST'), { code: 'EEXIST' })
}
return {
stat: vi.fn().mockResolvedValue({
size: 12,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
createReadStream: () => Readable.from([Buffer.from('file-content')]),
close: vi.fn().mockResolvedValue(undefined)
}
})
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; reason?: string }[] }
expect(result.results[0]).toMatchObject({ status: 'failed', reason: 'EEXIST' })
expect(openMock).toHaveBeenCalledWith(
path.resolve(sourcePath),
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
)
expect(openMock).toHaveBeenCalledWith(path.join(destDir, 'logo.png'), 'wx')
})
it('imports multiple files in one batch', async () => {
const sources = ['/tmp/dropped/a.txt', '/tmp/dropped/b.txt']
lstatMock.mockImplementation(async (p: string) => {
const resolved = [path.resolve(sources[0]), path.resolve(sources[1])]
if (resolved.includes(p)) {
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
}
throw enoent()
})
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: sources,
destDir
})) as { results: { status: string }[] }
expect(result.results).toHaveLength(2)
expect(result.results[0]).toMatchObject({ status: 'imported' })
expect(result.results[1]).toMatchObject({ status: 'imported' })
})
it('imports a directory recursively', async () => {
const sourcePath = '/tmp/dropped/assets'
mockSourceDir(sourcePath, [
{ name: 'icon.png', isDir: false },
{ name: 'fonts', isDir: true }
])
readdirMock
.mockResolvedValueOnce([
{
name: 'icon.png',
isDirectory: () => false,
isSymbolicLink: () => false,
isFile: () => true
},
{ name: 'fonts', isDirectory: () => true, isSymbolicLink: () => false, isFile: () => false }
])
.mockResolvedValue([])
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; kind?: string }[] }
expect(result.results).toHaveLength(1)
expect(result.results[0]).toMatchObject({
status: 'imported',
kind: 'directory',
renamed: false
})
expect(mkdirMock).toHaveBeenCalled()
})
it('deconflicts top-level filename collisions', async () => {
const sourcePath = '/tmp/dropped/logo.png'
const existingDest = path.join(destDir, 'logo.png')
lstatMock.mockImplementation(async (p: string) => {
if (p === path.resolve(sourcePath)) {
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
}
if (p === existingDest) {
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
}
throw enoent()
})
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; destPath?: string; renamed?: boolean }[] }
expect(result.results[0]).toMatchObject({
status: 'imported',
destPath: path.join(destDir, 'logo copy.png'),
renamed: true
})
})
it('deconflicts top-level directory collisions', async () => {
const sourcePath = '/tmp/dropped/assets'
const existingDest = path.join(destDir, 'assets')
lstatMock.mockImplementation(async (p: string) => {
if (p === path.resolve(sourcePath)) {
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
}
if (p === existingDest) {
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
}
throw enoent()
})
readdirMock.mockResolvedValue([])
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; destPath?: string; renamed?: boolean }[] }
expect(result.results[0]).toMatchObject({
status: 'imported',
destPath: path.join(destDir, 'assets copy'),
renamed: true
})
})
it('rejects top-level symlink sources before canonicalization', async () => {
const sourcePath = '/tmp/dropped/link.txt'
mockSymlinkSource(sourcePath)
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; reason?: string }[] }
expect(result.results[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
expect(copyFileMock).not.toHaveBeenCalled()
})
it('skips a dropped directory with nested symlinks without leaving partial output', async () => {
const sourcePath = '/tmp/dropped/mixeddir'
lstatMock.mockImplementation(async (p: string) => {
if (p === path.resolve(sourcePath)) {
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
}
throw enoent()
})
readdirMock.mockResolvedValue([
{
name: 'normal.txt',
isDirectory: () => false,
isSymbolicLink: () => false,
isFile: () => true
},
{
name: 'bad-link',
isDirectory: () => false,
isSymbolicLink: () => true,
isFile: () => false
}
])
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; reason?: string }[] }
expect(result.results[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
expect(copyFileMock).not.toHaveBeenCalled()
})
it('fails and removes output if a local directory entry becomes a symlink after pre-scan', async () => {
const sourcePath = '/tmp/dropped/mixeddir'
const resolvedSource = path.resolve(sourcePath)
const childPath = path.join(resolvedSource, 'normal.txt')
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedSource) {
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
}
if (p === childPath) {
return { isFile: () => false, isDirectory: () => false, isSymbolicLink: () => true }
}
throw enoent()
})
readdirMock.mockResolvedValue([
{
name: 'normal.txt',
isDirectory: () => false,
isSymbolicLink: () => false,
isFile: () => true
}
])
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir
})) as { results: { status: string; reason?: string }[] }
expect(result.results[0]).toMatchObject({
status: 'failed',
reason: "Symlink not allowed in 'normal.txt'"
})
expect(openMock).not.toHaveBeenCalledWith(childPath, expect.anything())
expect(rmMock).toHaveBeenCalledWith(path.join(destDir, 'mixeddir'), {
recursive: true,
force: true
})
})
it('rejects unauthorized destinations', async () => {
const sourcePath = '/tmp/dropped/file.txt'
mockSourceFile(sourcePath)
realpathMock.mockImplementation(async (p: string) => {
if (p === path.resolve('/outside/evil')) {
return path.resolve('/outside/evil')
}
return p
})
await expect(
handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: [sourcePath],
destDir: '/outside/evil'
})
).rejects.toThrow('Access denied')
})
it('returns per-item results including rename metadata', async () => {
const sources = ['/tmp/dropped/a.txt', '/tmp/dropped/missing.txt']
lstatMock.mockImplementation(async (p: string) => {
if (p === path.resolve(sources[0])) {
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
}
throw enoent()
})
const result = (await handlers.get('fs:importExternalPaths')!(null, {
sourcePaths: sources,
destDir
})) as {
results: {
sourcePath: string
status: string
reason?: string
renamed?: boolean
}[]
}
expect(result.results).toHaveLength(2)
expect(result.results[0]).toMatchObject({
sourcePath: sources[0],
status: 'imported',
renamed: false
})
expect(result.results[1]).toMatchObject({
sourcePath: sources[1],
status: 'skipped',
reason: 'missing'
})
})
it('stages external files for runtime upload without copying into the local worktree', async () => {
const sourcePath = '/tmp/dropped/logo.png'
const resolvedPath = path.resolve(sourcePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
size: 4,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false
}
}
throw enoent()
})
const closeMock = vi.fn().mockResolvedValue(undefined)
const readFileHandleMock = vi.fn().mockResolvedValue(Buffer.from('png'))
openMock.mockResolvedValue({
stat: vi.fn().mockResolvedValue({
size: 4,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
readFile: readFileHandleMock,
close: closeMock
})
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: unknown[] }
expect(result.sources).toEqual([
{
sourcePath,
status: 'staged',
name: 'logo.png',
kind: 'file',
entries: [
{
relativePath: '',
kind: 'file',
byteLength: 4,
inode: 1,
deviceId: 1,
modifiedAtMs: 1700000000000
}
]
}
])
expect(copyFileMock).not.toHaveBeenCalled()
// Why: bodies stream at upload time, so staging must never read the file.
expect(readFileHandleMock).not.toHaveBeenCalled()
expect(closeMock).toHaveBeenCalled()
})
it('stages runtime upload directories whose child names start with dotdot characters', async () => {
const sourcePath = '/tmp/dropped/project'
const resolvedPath = path.resolve(sourcePath)
const childDir = path.join(resolvedPath, '..assets')
const childFile = path.join(childDir, 'icon.txt')
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath || p === childDir) {
return {
size: 0,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => false,
isDirectory: () => true,
isSymbolicLink: () => false
}
}
if (p === childFile) {
return {
size: 4,
ino: 2,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false
}
}
throw enoent()
})
readdirMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return [
{
name: '..assets',
isDirectory: () => true,
isSymbolicLink: () => false,
isFile: () => false
}
]
}
if (p === childDir) {
return [
{
name: 'icon.txt',
isDirectory: () => false,
isSymbolicLink: () => false,
isFile: () => true
}
]
}
return []
})
openMock.mockResolvedValue({
stat: vi.fn().mockResolvedValue({
size: 4,
ino: 2,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
readFile: vi.fn().mockResolvedValue(Buffer.from('icon')),
close: vi.fn().mockResolvedValue(undefined)
})
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: unknown[] }
expect(result.sources).toEqual([
{
sourcePath,
status: 'staged',
name: 'project',
kind: 'directory',
entries: [
{ relativePath: '', kind: 'directory' },
{ relativePath: '..assets', kind: 'directory' },
{
relativePath: '..assets/icon.txt',
kind: 'file',
byteLength: 4,
inode: 2,
deviceId: 1,
modifiedAtMs: 1700000000000
}
]
}
])
})
it('skips runtime upload directories with nested symlinks during the staging traversal', async () => {
const sourcePath = '/tmp/dropped/project'
const resolvedPath = path.resolve(sourcePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
size: 0,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => false,
isDirectory: () => true,
isSymbolicLink: () => false
}
}
throw enoent()
})
readdirMock.mockResolvedValue([
{
name: 'link.txt',
isDirectory: () => false,
isSymbolicLink: () => true,
isFile: () => false
}
])
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: { status: string; reason?: string }[] }
expect(result.sources[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
expect(readdirMock).toHaveBeenCalledOnce()
expect(openMock).not.toHaveBeenCalled()
})
it('checks runtime upload directory byte budget before opening a file that exceeds the total cap', async () => {
const sourcePath = '/tmp/dropped/project'
const resolvedPath = path.resolve(sourcePath)
const filePaths = ['one.bin', 'two.bin', 'three.bin', 'four.bin', 'overflow.bin'].map((name) =>
path.join(resolvedPath, name)
)
const mib = 1024 * 1024
// Four files exactly fill the 8 GB total ceiling; the fifth pushes past it.
const regularSize = 2 * 1024 * mib
const overflowSize = Number(mib)
const readFileMock = vi.fn().mockResolvedValue(Buffer.from('chunk'))
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
size: 0,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => false,
isDirectory: () => true,
isSymbolicLink: () => false
}
}
const fileIndex = filePaths.indexOf(p)
if (fileIndex !== -1) {
const size = fileIndex === filePaths.length - 1 ? overflowSize : regularSize
return {
size,
ino: fileIndex + 2,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false
}
}
throw enoent()
})
readdirMock.mockResolvedValue(
filePaths.map((filePath) => ({
name: path.basename(filePath),
isDirectory: () => false,
isSymbolicLink: () => false,
isFile: () => true
}))
)
openMock.mockImplementation(async (p: string) => {
const fileIndex = filePaths.indexOf(p)
if (fileIndex !== -1 && fileIndex < filePaths.length - 1) {
return {
stat: vi.fn().mockResolvedValue({
size: regularSize,
ino: fileIndex + 2,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
readFile: readFileMock,
close: vi.fn().mockResolvedValue(undefined)
}
}
throw new Error(`unexpected open: ${p}`)
})
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: { status: string; reason?: string }[] }
expect(result.sources[0]).toMatchObject({ status: 'failed' })
expect(result.sources[0]?.reason).toContain('total remote import limit')
expect(readFileMock).not.toHaveBeenCalled()
expect(openMock).not.toHaveBeenCalledWith(filePaths.at(-1), expect.anything())
})
it('fails runtime upload staging when a file changes between lstat and open', async () => {
const sourcePath = '/tmp/dropped/logo.png'
const resolvedPath = path.resolve(sourcePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
size: 4,
ino: 1,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false
}
}
throw enoent()
})
const readFileHandleMock = vi.fn().mockResolvedValue(Buffer.from('png'))
openMock.mockResolvedValue({
stat: vi.fn().mockResolvedValue({
size: 4,
ino: 2,
dev: 1,
mtimeMs: 1700000000000,
isFile: () => true
}),
readFile: readFileHandleMock,
close: vi.fn().mockResolvedValue(undefined)
})
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: { status: string; reason?: string }[] }
expect(result.sources[0]).toMatchObject({
status: 'failed',
reason: "File changed during upload staging: 'logo.png'"
})
expect(readFileHandleMock).not.toHaveBeenCalled()
})
it('fails runtime upload staging when a checked directory resolves outside the upload root', async () => {
const sourcePath = '/tmp/dropped/assets'
const resolvedPath = path.resolve(sourcePath)
lstatMock.mockImplementation(async (p: string) => {
if (p === resolvedPath) {
return {
isFile: () => false,
isDirectory: () => true,
isSymbolicLink: () => false
}
}
throw enoent()
})
readdirMock.mockResolvedValue([])
realpathMock
.mockResolvedValueOnce(resolvedPath)
.mockResolvedValueOnce(path.resolve('/private/assets'))
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
sourcePaths: [sourcePath]
})) as { sources: { status: string; reason?: string }[] }
expect(result.sources[0]).toMatchObject({
status: 'failed',
reason: "Path escaped upload root during staging: ''"
})
})
})