dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,146 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
createManagedPathMatcher, fileManagerLocations, FileInventory, FileManagerError,
|
||||
FileRevisionConflictError, FileSearchManager, FileSizeManager, isManagedPath, normalizeAbsolutePath,
|
||||
} from '../../control-plane/modules/file-manager/index.mjs'
|
||||
|
||||
test('normalizes absolute paths and rejects unsafe path representations', () => {
|
||||
assert.equal(normalizeAbsolutePath('/workspace/a/../b'), '/workspace/b')
|
||||
assert.throws(() => normalizeAbsolutePath('workspace'), /absolute/)
|
||||
assert.throws(() => normalizeAbsolutePath('/workspace/a\nb'), /control/)
|
||||
assert.throws(() => normalizeAbsolutePath(`/${'a'.repeat(4097)}`), /too long/)
|
||||
})
|
||||
|
||||
test('identifies only exact platform-managed roots and descendants', () => {
|
||||
assert.equal(isManagedPath('/data/platform/state'), true)
|
||||
assert.equal(isManagedPath('/data/platform/state/updater/status.json'), true)
|
||||
assert.equal(isManagedPath('/data/platform/stateful'), false)
|
||||
assert.equal(isManagedPath('/data/dsh'), false)
|
||||
const custom = createManagedPathMatcher('/srv/platform')
|
||||
assert.equal(custom('/srv/platform/store/objects'), true)
|
||||
assert.equal(custom('/data/platform/store/objects'), false)
|
||||
})
|
||||
|
||||
test('derives the default directory and unique shortcuts from existing environment paths', () => {
|
||||
assert.deepEqual(fileManagerLocations({
|
||||
defaultWorkspace: '/work/current', dshHome: '/srv/dsh', platformData: '/srv/platform',
|
||||
}), {
|
||||
defaultPath: '/work/current', shortcuts: ['/work/current', '/srv/dsh', '/srv/platform', '/'],
|
||||
})
|
||||
assert.deepEqual(fileManagerLocations({ defaultWorkspace: '/data/dsh' }).shortcuts, ['/data/dsh', '/data/platform', '/'])
|
||||
assert.throws(() => fileManagerLocations({ defaultWorkspace: 'relative' }), /absolute/)
|
||||
})
|
||||
|
||||
test('lists directories with stable metadata, directory-first sorting, and revision cursors', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-list-'))
|
||||
await mkdir(join(root, 'folder'))
|
||||
await writeFile(join(root, 'z.txt'), 'z')
|
||||
await writeFile(join(root, '.hidden'), 'hidden')
|
||||
await symlink('missing', join(root, 'dangling'))
|
||||
const inventory = new FileInventory()
|
||||
const first = await inventory.list(root, { limit: 2 })
|
||||
assert.equal(first.entries[0].name, 'folder')
|
||||
assert.equal(first.entries.some(entry => entry.name === '.hidden'), true)
|
||||
assert.equal(first.total, 4)
|
||||
assert.equal(typeof first.nextCursor, 'string')
|
||||
const second = await inventory.list(root, { limit: 2, cursor: first.nextCursor })
|
||||
assert.equal(second.entries.length, 2)
|
||||
const dangling = [...first.entries, ...second.entries].find(entry => entry.name === 'dangling')
|
||||
assert.deepEqual([dangling.type, dangling.targetExists], ['symlink', false])
|
||||
await writeFile(join(root, 'new.txt'), 'new')
|
||||
await assert.rejects(inventory.list(root, { cursor: first.nextCursor }), FileRevisionConflictError)
|
||||
})
|
||||
|
||||
test('name pagination resolves detailed metadata only for the requested page', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-page-'))
|
||||
for (let index = 0; index < 20; index += 1) await writeFile(join(root, `${String(index).padStart(2, '0')}.txt`), 'x')
|
||||
let resolutions = 0
|
||||
const inventory = new FileInventory({
|
||||
identity: { resolve: async () => { resolutions += 1; return { user: 'node', group: 'node' } } },
|
||||
})
|
||||
const page = await inventory.list(root, { limit: 5 })
|
||||
assert.equal(page.entries.length, 5)
|
||||
assert.equal(page.total, 20)
|
||||
assert.equal(resolutions, 5)
|
||||
const thirdPage = await inventory.list(root, { limit: 5, offset: 10 })
|
||||
assert.deepEqual(thirdPage.entries.map(entry => entry.name), ['10.txt', '11.txt', '12.txt', '13.txt', '14.txt'])
|
||||
assert.equal(resolutions, 10)
|
||||
})
|
||||
|
||||
test('reads only bounded UTF-8 regular files and detects binary content', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-content-'))
|
||||
const text = join(root, '中文.txt')
|
||||
await writeFile(text, 'hello\r\n世界\r\n')
|
||||
const inventory = new FileInventory()
|
||||
const result = await inventory.content(text)
|
||||
assert.equal(result.content, 'hello\r\n世界\r\n')
|
||||
assert.equal(result.newline, 'crlf')
|
||||
assert.match(result.revision, /^sha256:[a-f0-9]{64}$/)
|
||||
await writeFile(join(root, 'binary'), Buffer.from([1, 0, 2]))
|
||||
await assert.rejects(inventory.content(join(root, 'binary')), error => error.statusCode === 415)
|
||||
await assert.rejects(inventory.content(root), error => error.statusCode === 415)
|
||||
})
|
||||
|
||||
test('reports container user and group and calculates directory size without following links', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-size-'))
|
||||
await mkdir(join(root, 'nested'))
|
||||
await writeFile(join(root, 'one.txt'), '1234')
|
||||
await writeFile(join(root, 'nested', 'two.txt'), '567890')
|
||||
await symlink('one.txt', join(root, 'link'))
|
||||
const identity = { resolve: async () => ({ user: 'node', group: 'node' }) }
|
||||
const inventory = new FileInventory({ identity })
|
||||
const listing = await inventory.list(root)
|
||||
assert.equal(listing.entries.every(entry => entry.user === 'node' && entry.group === 'node'), true)
|
||||
const size = new FileSizeManager({ inventory })
|
||||
const directory = await inventory.stat(root)
|
||||
const task = size.start({ path: root, revision: directory.revision })
|
||||
await size.tasks.get(task.taskId).completion
|
||||
const result = size.get(task.taskId)
|
||||
assert.equal(result.status, 'success')
|
||||
assert.equal(result.bytes, 10)
|
||||
assert.equal(result.entries, 4)
|
||||
const stale = size.start({ path: root, revision: listing.revision })
|
||||
await size.tasks.get(stale.taskId).completion
|
||||
assert.equal(size.get(stale.taskId).status, 'failed')
|
||||
})
|
||||
|
||||
test('bounded search does not follow symlinked directories and can be cancelled', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-search-'))
|
||||
await mkdir(join(root, 'nested'))
|
||||
await writeFile(join(root, 'nested', 'Needle.txt'), 'found')
|
||||
await symlink(root, join(root, 'nested', 'loop'))
|
||||
const states = []
|
||||
const manager = new FileSearchManager({ onState: state => states.push(state) })
|
||||
const task = manager.start({ path: root, query: 'needle' })
|
||||
await manager.tasks.get(task.taskId).completion
|
||||
const result = manager.get(task.taskId)
|
||||
assert.equal(result.status, 'success')
|
||||
assert.deepEqual(result.results.map(entry => entry.name), ['Needle.txt'])
|
||||
assert.equal(result.scanned, 3)
|
||||
assert.equal(states.at(-1).status, 'success')
|
||||
|
||||
const cancelled = manager.start({ path: root, query: 'x' })
|
||||
manager.cancel(cancelled.taskId)
|
||||
await manager.tasks.get(cancelled.taskId).completion
|
||||
assert.equal(manager.get(cancelled.taskId).status, 'cancelled')
|
||||
})
|
||||
|
||||
test('rejects invalid list, search, and task parameters', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-file-invalid-'))
|
||||
const inventory = new FileInventory()
|
||||
await assert.rejects(inventory.list(root, { limit: 1001 }), FileManagerError)
|
||||
await assert.rejects(inventory.list(root, { sort: 'owner' }), /sort/)
|
||||
await assert.rejects(inventory.list(root, { offset: -1 }), /offset/)
|
||||
await assert.rejects(inventory.list(root, { offset: 1.5 }), /offset/)
|
||||
await writeFile(join(root, 'one'), '1')
|
||||
await writeFile(join(root, 'two'), '2')
|
||||
const first = await inventory.list(root, { limit: 1 })
|
||||
await assert.rejects(inventory.list(root, { cursor: first.nextCursor, offset: 0 }), /cannot be combined/)
|
||||
const manager = new FileSearchManager()
|
||||
assert.throws(() => manager.start({ path: root, query: '' }), /query/)
|
||||
assert.throws(() => manager.get('missing'), error => error.statusCode === 404)
|
||||
})
|
||||
Reference in new issue
Block a user