Files
orca/src/relay/fs-handler.test.ts
T
NeilandOrca e33b2006f4 Remove stale max-lines lint disables from files under the limit (#7548)
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.

Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.

Co-authored-by: Orca <help@stably.ai>
2026-07-06 02:12:32 -07:00

777 lines
26 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import { FsHandler } from './fs-handler'
import { MAX_TEXT_FILE_SIZE } from './fs-handler-utils'
import { RelayContext } from './context'
import type { RelayDispatcher } from './dispatcher'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { mkdtempSync, writeFileSync, mkdirSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
const { mockSubscribe } = vi.hoisted(() => ({
mockSubscribe: vi.fn()
}))
vi.mock('@parcel/watcher', () => ({
subscribe: mockSubscribe
}))
function createMockDispatcher() {
const requestHandlers = new Map<
string,
(
params: Record<string, unknown>,
context?: { clientId: number; isStale: () => boolean }
) => Promise<unknown>
>()
const notificationHandlers = new Map<
string,
(
params: Record<string, unknown>,
context?: { clientId: number; isStale: () => boolean }
) => void
>()
const detachListeners = new Set<(clientId: number) => void>()
const notifications: { method: string; params?: Record<string, unknown> }[] = []
return {
onRequest: vi.fn(
(
method: string,
handler: (
params: Record<string, unknown>,
context?: { clientId: number; isStale: () => boolean }
) => Promise<unknown>
) => {
requestHandlers.set(method, handler)
}
),
onNotification: vi.fn(
(
method: string,
handler: (
params: Record<string, unknown>,
context?: { clientId: number; isStale: () => boolean }
) => void
) => {
notificationHandlers.set(method, handler)
}
),
notify: vi.fn((method: string, params?: Record<string, unknown>) => {
notifications.push({ method, params })
}),
onClientDetached: vi.fn((listener: (clientId: number) => void) => {
detachListeners.add(listener)
return () => detachListeners.delete(listener)
}),
_requestHandlers: requestHandlers,
_notificationHandlers: notificationHandlers,
_notifications: notifications,
async callRequest(
method: string,
params: Record<string, unknown> = {},
context?: { clientId?: number; isStale: () => boolean }
) {
const handler = requestHandlers.get(method)
if (!handler) {
throw new Error(`No handler for ${method}`)
}
return handler(params, {
clientId: context?.clientId ?? 1,
isStale: context?.isStale ?? (() => false)
})
},
callNotification(
method: string,
params: Record<string, unknown> = {},
context?: { clientId: number; isStale: () => boolean }
) {
const handler = notificationHandlers.get(method)
if (!handler) {
throw new Error(`No handler for ${method}`)
}
handler(params, context ?? { clientId: 1, isStale: () => false })
},
detachClient(clientId: number) {
for (const listener of detachListeners) {
listener(clientId)
}
}
}
}
function statIdentity(stats: {
dev?: number
ino?: number
nlink?: number
size?: number
mtimeMs?: number
}) {
return `${stats.dev}:${stats.ino}:${stats.nlink ?? 'unknown'}:${stats.size}:${stats.mtimeMs}`
}
describe('FsHandler', () => {
let dispatcher: ReturnType<typeof createMockDispatcher>
let handler: FsHandler
let tmpDir: string
beforeEach(() => {
mockSubscribe.mockReset()
mockSubscribe.mockResolvedValue({ unsubscribe: vi.fn() })
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-fs-'))
dispatcher = createMockDispatcher()
const ctx = new RelayContext()
handler = new FsHandler(dispatcher as unknown as RelayDispatcher, ctx)
})
afterEach(async () => {
handler.dispose()
await fs.rm(tmpDir, { recursive: true, force: true })
})
it('registers all expected handlers', () => {
const methods = Array.from(dispatcher._requestHandlers.keys())
expect(methods).toContain('fs.readDir')
expect(methods).toContain('fs.readFile')
expect(methods).toContain('fs.tempDir')
expect(methods).toContain('fs.writeFile')
expect(methods).toContain('fs.stat')
expect(methods).toContain('fs.deletePath')
expect(methods).toContain('fs.createFile')
expect(methods).toContain('fs.createDir')
expect(methods).toContain('fs.createDirNoClobber')
expect(methods).toContain('fs.rename')
expect(methods).toContain('fs.renameNoClobber')
expect(methods).toContain('fs.copy')
expect(methods).toContain('fs.realpath')
expect(methods).toContain('fs.search')
expect(methods).toContain('fs.listFiles')
expect(methods).toContain('fs.workspaceSpaceScan')
expect(methods).toContain('fs.watch')
const notifMethods = Array.from(dispatcher._notificationHandlers.keys())
expect(notifMethods).toContain('fs.unwatch')
})
it('tempDir returns the relay host temp directory', async () => {
await expect(dispatcher.callRequest('fs.tempDir')).resolves.toBe(tmpdir())
})
it('readDir returns sorted entries with directories first', async () => {
mkdirSync(path.join(tmpDir, 'subdir'))
writeFileSync(path.join(tmpDir, 'file.txt'), 'hello')
writeFileSync(path.join(tmpDir, 'aaa.txt'), 'world')
const result = (await dispatcher.callRequest('fs.readDir', { dirPath: tmpDir })) as {
name: string
isDirectory: boolean
}[]
expect(result[0].name).toBe('subdir')
expect(result[0].isDirectory).toBe(true)
expect(result.find((e) => e.name === 'file.txt')).toBeDefined()
expect(result.find((e) => e.name === 'aaa.txt')).toBeDefined()
})
it('readDir reports symlinked directories as directories', async () => {
const targetDir = path.join(tmpDir, 'external-models')
const linkPath = path.join(tmpDir, 'Model')
mkdirSync(targetDir)
symlinkSync(targetDir, linkPath, process.platform === 'win32' ? 'junction' : 'dir')
const result = (await dispatcher.callRequest('fs.readDir', { dirPath: tmpDir })) as {
name: string
isDirectory: boolean
isSymlink: boolean
}[]
expect(result.find((e) => e.name === 'Model')).toEqual({
name: 'Model',
isDirectory: true,
isSymlink: true
})
})
it('readFile returns text content for text files', async () => {
const filePath = path.join(tmpDir, 'test.txt')
writeFileSync(filePath, 'hello world')
const result = (await dispatcher.callRequest('fs.readFile', { filePath })) as {
content: string
isBinary: boolean
}
expect(result.content).toBe('hello world')
expect(result.isBinary).toBe(false)
})
it('readFile returns text files larger than the old 5MB guard', async () => {
const filePath = path.join(tmpDir, 'large.json')
const content = 'a'.repeat(6 * 1024 * 1024)
writeFileSync(filePath, content)
const result = (await dispatcher.callRequest('fs.readFile', { filePath })) as {
content: string
isBinary: boolean
}
expect(result.content).toBe(content)
expect(result.isBinary).toBe(false)
})
it('readFile returns binary marker for large unknown binary files', async () => {
const filePath = path.join(tmpDir, 'archive.bin')
const content = Buffer.alloc(6 * 1024 * 1024, 0x61)
content[0] = 0x00
writeFileSync(filePath, content)
const result = (await dispatcher.callRequest('fs.readFile', { filePath })) as {
content: string
isBinary: boolean
}
expect(result.content).toBe('')
expect(result.isBinary).toBe(true)
})
it('readFile returns base64 for image files', async () => {
const filePath = path.join(tmpDir, 'test.png')
writeFileSync(filePath, Buffer.from([0x89, 0x50, 0x4e, 0x47]))
const result = (await dispatcher.callRequest('fs.readFile', { filePath })) as {
content: string
isBinary: boolean
isImage: boolean
mimeType: string
}
expect(result.isBinary).toBe(true)
expect(result.isImage).toBe(true)
expect(result.mimeType).toBe('image/png')
expect(result.content).toBeTruthy()
})
it('readFile throws for files exceeding size limit', async () => {
const filePath = path.join(tmpDir, 'huge.txt')
writeFileSync(filePath, Buffer.alloc(11 * 1024 * 1024, 'a'))
await expect(dispatcher.callRequest('fs.readFile', { filePath })).rejects.toThrow(
'File too large'
)
})
it('writeFile creates/overwrites file content', async () => {
const filePath = path.join(tmpDir, 'write-test.txt')
await dispatcher.callRequest('fs.writeFile', { filePath, content: 'new content' })
const content = await fs.readFile(filePath, 'utf-8')
expect(content).toBe('new content')
})
it('readTerminalArtifact reads through a verified artifact handle', async () => {
const filePath = path.join(tmpDir, 'artifact-read.json')
writeFileSync(filePath, '{"ok":true}')
const stats = await fs.stat(filePath)
const result = (await dispatcher.callRequest('fs.readTerminalArtifact', {
filePath,
expectedRealPath: await fs.realpath(filePath),
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})) as { content: string; isBinary: boolean }
expect(result).toEqual({ content: '{"ok":true}', isBinary: false })
})
it('readTerminalArtifact treats SVG artifacts as editable text', async () => {
const filePath = path.join(tmpDir, 'artifact.svg')
writeFileSync(filePath, '<svg><text>ok</text></svg>')
const stats = await fs.stat(filePath)
const result = (await dispatcher.callRequest('fs.readTerminalArtifact', {
filePath,
expectedRealPath: await fs.realpath(filePath),
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})) as { content: string; isBinary: boolean; isImage?: boolean }
expect(result).toEqual({ content: '<svg><text>ok</text></svg>', isBinary: false })
})
it('readTerminalArtifact rejects content beyond the requested byte limit', async () => {
const filePath = path.join(tmpDir, 'artifact-read-too-large.txt')
writeFileSync(filePath, 'abcdef')
await expect(
dispatcher.callRequest('fs.readTerminalArtifact', {
filePath,
expectedRealPath: await fs.realpath(filePath),
maxBytes: 5
})
).rejects.toThrow('file_too_large')
})
it('writeTerminalArtifact writes through a verified artifact handle', async () => {
const filePath = path.join(tmpDir, 'artifact-write.json')
writeFileSync(filePath, '{"ok":true}')
const stats = await fs.stat(filePath)
const result = (await dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: '{"ok":false}',
expectedRealPath: await fs.realpath(filePath),
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})) as { stat: { type: string; size: number } }
await expect(fs.readFile(filePath, 'utf-8')).resolves.toBe('{"ok":false}')
expect(result.stat).toMatchObject({ type: 'file', size: 12 })
})
it.skipIf(process.platform === 'win32')(
'writeTerminalArtifact preserves executable mode across the atomic rename',
async () => {
const filePath = path.join(tmpDir, 'artifact-executable.sh')
writeFileSync(filePath, '#!/bin/sh\necho ok\n')
await fs.chmod(filePath, 0o755)
const stats = await fs.stat(filePath)
await dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: '#!/bin/sh\necho changed\n',
expectedRealPath: await fs.realpath(filePath),
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o755)
}
)
it('writeTerminalArtifact rejects oversized existing content before writing', async () => {
const filePath = path.join(tmpDir, 'artifact-write-too-large.txt')
writeFileSync(filePath, 'abcdef')
await expect(
dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: 'ok',
expectedRealPath: await fs.realpath(filePath),
maxBytes: 5
})
).rejects.toThrow('file_too_large')
await expect(fs.readFile(filePath, 'utf-8')).resolves.toBe('abcdef')
})
it('writeTerminalArtifact clamps client-supplied maxBytes to the text-file cap', async () => {
const filePath = path.join(tmpDir, 'artifact-write-clamp.txt')
writeFileSync(filePath, 'abcdef')
await expect(
dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: 'a'.repeat(MAX_TEXT_FILE_SIZE + 1),
expectedRealPath: await fs.realpath(filePath),
maxBytes: Number.MAX_SAFE_INTEGER
})
).rejects.toThrow('file_too_large')
await expect(fs.readFile(filePath, 'utf-8')).resolves.toBe('abcdef')
})
it('writeTerminalArtifact rejects a retargeted symlink before writing outside temp', async () => {
const filePath = path.join(tmpDir, 'artifact-link.json')
const outsidePath = path.join(tmpDir, 'outside.json')
writeFileSync(filePath, '{"ok":true}')
writeFileSync(outsidePath, '{"secret":true}')
const stats = await fs.stat(filePath)
const expectedRealPath = await fs.realpath(filePath)
await fs.rm(filePath)
symlinkSync(outsidePath, filePath)
await expect(
dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: '{"ok":false}',
expectedRealPath,
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})
).rejects.toThrow('terminal_file_grant_stale')
await expect(fs.readFile(outsidePath, 'utf-8')).resolves.toBe('{"secret":true}')
})
it('writeTerminalArtifact rejects hard-linked files before writing', async () => {
const outsidePath = path.join(tmpDir, 'outside-hardlink.json')
const filePath = path.join(tmpDir, 'artifact-hardlink.json')
writeFileSync(outsidePath, '{"secret":true}')
await fs.link(outsidePath, filePath)
const stats = await fs.stat(filePath)
await expect(
dispatcher.callRequest('fs.writeTerminalArtifact', {
filePath,
content: '{"ok":false}',
expectedRealPath: await fs.realpath(filePath),
expectedStatIdentity: statIdentity(stats),
maxBytes: 512 * 1024
})
).rejects.toThrow('terminal_file_grant_stale')
await expect(fs.readFile(outsidePath, 'utf-8')).resolves.toBe('{"secret":true}')
})
it('stat returns file metadata', async () => {
const filePath = path.join(tmpDir, 'stat-test.txt')
writeFileSync(filePath, 'test')
const result = (await dispatcher.callRequest('fs.stat', { filePath })) as {
size: number
type: string
mtime: number
}
expect(result.type).toBe('file')
expect(result.size).toBe(4)
expect(typeof result.mtime).toBe('number')
})
it('stat returns directory type for directories', async () => {
const result = (await dispatcher.callRequest('fs.stat', { filePath: tmpDir })) as {
type: string
}
expect(result.type).toBe('directory')
})
it('stat returns directory type for symlinked directories', async () => {
const targetDir = path.join(tmpDir, 'external-models')
const linkPath = path.join(tmpDir, 'Model')
mkdirSync(targetDir)
symlinkSync(targetDir, linkPath, process.platform === 'win32' ? 'junction' : 'dir')
const result = (await dispatcher.callRequest('fs.stat', { filePath: linkPath })) as {
type: string
}
expect(result.type).toBe('directory')
})
it('lstat returns symlink type without following links', async () => {
const targetFile = path.join(tmpDir, 'target.txt')
const linkPath = path.join(tmpDir, 'link.txt')
writeFileSync(targetFile, 'target')
symlinkSync(targetFile, linkPath)
const result = (await dispatcher.callRequest('fs.lstat', { filePath: linkPath })) as {
type: string
}
expect(result.type).toBe('symlink')
})
it('workspaceSpaceScan returns bounded top-level size details', async () => {
mkdirSync(path.join(tmpDir, 'node_modules'))
writeFileSync(path.join(tmpDir, 'node_modules', 'pkg.js'), Buffer.alloc(512))
writeFileSync(path.join(tmpDir, 'file.log'), Buffer.alloc(128))
const result = (await dispatcher.callRequest(
'fs.workspaceSpaceScan',
{ rootPath: tmpDir },
{ isStale: () => false }
)) as {
sizeBytes: number
topLevelItems: { name: string; sizeBytes: number }[]
}
expect(result.sizeBytes).toBeGreaterThanOrEqual(640)
expect(result.topLevelItems.map((item) => item.name)).toContain('node_modules')
expect(result.topLevelItems.map((item) => item.name)).toContain('file.log')
})
it('deletePath removes files', async () => {
const filePath = path.join(tmpDir, 'to-delete.txt')
writeFileSync(filePath, 'bye')
await dispatcher.callRequest('fs.deletePath', { targetPath: filePath })
await expect(fs.access(filePath)).rejects.toThrow()
})
it('createFile creates an empty file with parent dirs', async () => {
const filePath = path.join(tmpDir, 'deep', 'nested', 'file.txt')
await dispatcher.callRequest('fs.createFile', { filePath })
const content = await fs.readFile(filePath, 'utf-8')
expect(content).toBe('')
})
it('createDir creates directories recursively', async () => {
const dirPath = path.join(tmpDir, 'a', 'b', 'c')
await dispatcher.callRequest('fs.createDir', { dirPath })
const stats = await fs.stat(dirPath)
expect(stats.isDirectory()).toBe(true)
})
it('createDirNoClobber fails when the directory already exists', async () => {
const dirPath = path.join(tmpDir, 'existing')
mkdirSync(dirPath)
await expect(dispatcher.callRequest('fs.createDirNoClobber', { dirPath })).rejects.toThrow()
})
it('rename moves files', async () => {
const oldPath = path.join(tmpDir, 'old.txt')
const newPath = path.join(tmpDir, 'new.txt')
writeFileSync(oldPath, 'content')
await dispatcher.callRequest('fs.rename', { oldPath, newPath })
await expect(fs.access(oldPath)).rejects.toThrow()
const content = await fs.readFile(newPath, 'utf-8')
expect(content).toBe('content')
})
it('rename preserves raw fs.rename overwrite semantics', async () => {
const oldPath = path.join(tmpDir, 'old.txt')
const newPath = path.join(tmpDir, 'existing.txt')
writeFileSync(oldPath, 'new')
writeFileSync(newPath, 'keep')
await dispatcher.callRequest('fs.rename', { oldPath, newPath })
expect(await fs.readFile(newPath, 'utf-8')).toBe('new')
await expect(fs.access(oldPath)).rejects.toThrow()
})
it('renameNoClobber moves files when destination is available', async () => {
const oldPath = path.join(tmpDir, 'old.txt')
const newPath = path.join(tmpDir, 'new.txt')
writeFileSync(oldPath, 'content')
await dispatcher.callRequest('fs.renameNoClobber', { oldPath, newPath })
await expect(fs.access(oldPath)).rejects.toThrow()
expect(await fs.readFile(newPath, 'utf-8')).toBe('content')
})
it('renameNoClobber does not overwrite an existing destination', async () => {
const oldPath = path.join(tmpDir, 'old.txt')
const newPath = path.join(tmpDir, 'existing.txt')
writeFileSync(oldPath, 'new')
writeFileSync(newPath, 'keep')
await expect(
dispatcher.callRequest('fs.renameNoClobber', { oldPath, newPath })
).rejects.toThrow()
expect(await fs.readFile(newPath, 'utf-8')).toBe('keep')
expect(await fs.readFile(oldPath, 'utf-8')).toBe('new')
})
it('copy duplicates files', async () => {
const src = path.join(tmpDir, 'src.txt')
const dst = path.join(tmpDir, 'dst.txt')
writeFileSync(src, 'original')
await dispatcher.callRequest('fs.copy', { source: src, destination: dst })
const content = await fs.readFile(dst, 'utf-8')
expect(content).toBe('original')
})
it('copy does not overwrite an existing destination', async () => {
const src = path.join(tmpDir, 'src.txt')
const dst = path.join(tmpDir, 'dst.txt')
writeFileSync(src, 'original')
writeFileSync(dst, 'existing')
await expect(
dispatcher.callRequest('fs.copy', { source: src, destination: dst })
).rejects.toThrow('EEXIST')
const content = await fs.readFile(dst, 'utf-8')
expect(content).toBe('existing')
})
it('realpath resolves symlinks', async () => {
const realFile = path.join(tmpDir, 'real.txt')
const linkPath = path.join(tmpDir, 'link.txt')
writeFileSync(realFile, 'real')
symlinkSync(realFile, linkPath)
const result = (await dispatcher.callRequest('fs.realpath', { filePath: linkPath })) as string
// On macOS, /var is a symlink to /private/var, so resolve both to compare
expect(result).toBe(await fs.realpath(realFile))
})
it('does not let stale pending watch remove newer replacement watch', async () => {
const firstUnsubscribe = vi.fn()
const secondUnsubscribe = vi.fn()
let resolveFirst!: () => void
mockSubscribe
.mockReturnValueOnce(
new Promise((resolve) => {
resolveFirst = () => resolve({ unsubscribe: firstUnsubscribe })
})
)
.mockResolvedValueOnce({ unsubscribe: secondUnsubscribe })
const firstWatch = dispatcher.callRequest(
'fs.watch',
{ rootPath: tmpDir },
{ isStale: () => true }
)
while (mockSubscribe.mock.calls.length === 0) {
await Promise.resolve()
}
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => false })
resolveFirst()
await firstWatch
expect(firstUnsubscribe).toHaveBeenCalled()
expect(secondUnsubscribe).not.toHaveBeenCalled()
dispatcher.callNotification('fs.unwatch', { rootPath: tmpDir })
expect(secondUnsubscribe).toHaveBeenCalled()
})
it('unsubscribes an active stale watch before replacing it', async () => {
const firstUnsubscribe = vi.fn()
const secondUnsubscribe = vi.fn()
mockSubscribe
.mockResolvedValueOnce({ unsubscribe: firstUnsubscribe })
.mockResolvedValueOnce({ unsubscribe: secondUnsubscribe })
let stale = false
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => stale })
stale = true
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => false })
expect(firstUnsubscribe).toHaveBeenCalled()
expect(secondUnsubscribe).not.toHaveBeenCalled()
dispatcher.callNotification('fs.unwatch', { rootPath: tmpDir })
expect(secondUnsubscribe).toHaveBeenCalled()
})
it('replaces a stale watch for the same root before enforcing the watch cap', async () => {
const firstUnsubscribe = vi.fn()
const replacementUnsubscribe = vi.fn()
mockSubscribe
.mockResolvedValueOnce({ unsubscribe: firstUnsubscribe })
.mockResolvedValueOnce({ unsubscribe: replacementUnsubscribe })
let stale = false
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => stale })
for (let index = 0; index < 19; index++) {
await dispatcher.callRequest('fs.watch', {
rootPath: path.join(tmpDir, `watched-${index}`)
})
}
stale = true
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => false })
expect(firstUnsubscribe).toHaveBeenCalled()
expect(replacementUnsubscribe).not.toHaveBeenCalled()
})
it('removes stale watches for any root before enforcing the watch cap', async () => {
const staleUnsubscribe = vi.fn()
mockSubscribe
.mockResolvedValueOnce({ unsubscribe: staleUnsubscribe })
.mockResolvedValue({ unsubscribe: vi.fn() })
let stale = false
await dispatcher.callRequest(
'fs.watch',
{ rootPath: path.join(tmpDir, 'stale-root') },
{
isStale: () => stale
}
)
for (let index = 0; index < 19; index += 1) {
await dispatcher.callRequest('fs.watch', {
rootPath: path.join(tmpDir, `watched-${index}`)
})
}
stale = true
await expect(
dispatcher.callRequest('fs.watch', { rootPath: path.join(tmpDir, 'new-root') })
).resolves.toBeUndefined()
expect(staleUnsubscribe).toHaveBeenCalledTimes(1)
})
it('keeps a shared watch alive until every client unwatches it', async () => {
const unsubscribe = vi.fn()
mockSubscribe.mockResolvedValue({ unsubscribe })
await dispatcher.callRequest('fs.watch', { rootPath: tmpDir }, { isStale: () => false })
await dispatcher.callRequest(
'fs.watch',
{ rootPath: tmpDir },
{
clientId: 2,
isStale: () => false
}
)
dispatcher.callNotification(
'fs.unwatch',
{ rootPath: tmpDir },
{
clientId: 1,
isStale: () => false
}
)
expect(unsubscribe).not.toHaveBeenCalled()
dispatcher.callNotification(
'fs.unwatch',
{ rootPath: tmpDir },
{
clientId: 2,
isStale: () => false
}
)
expect(unsubscribe).toHaveBeenCalledTimes(1)
})
it('allows a shared watch attach even when the root watch cap is full', async () => {
mockSubscribe.mockResolvedValue({ unsubscribe: vi.fn() })
for (let index = 0; index < 20; index += 1) {
const dir = path.join(tmpDir, `watched-${index}`)
await fs.mkdir(dir)
await dispatcher.callRequest(
'fs.watch',
{ rootPath: dir },
{
clientId: index + 1,
isStale: () => false
}
)
}
await expect(
dispatcher.callRequest(
'fs.watch',
{ rootPath: path.join(tmpDir, 'watched-0') },
{
clientId: 99,
isStale: () => false
}
)
).resolves.toBeUndefined()
})
it('releases a client watch when the dispatcher detaches that client', async () => {
const unsubscribe = vi.fn()
mockSubscribe.mockResolvedValue({ unsubscribe })
await dispatcher.callRequest(
'fs.watch',
{ rootPath: tmpDir },
{
clientId: 7,
isStale: () => false
}
)
dispatcher.detachClient(7)
expect(unsubscribe).toHaveBeenCalledTimes(1)
})
})