- fs-tree: symlink 目标 stat 从串行改为 32 并发工作池(readdir 流关闭后统一 探测),symlink 密集/网络目标目录不再累加延迟;顺序与分类语义不变 - html-route: 解码不再重建反斜杠 UNC,统一重建为平台中性的 //server/share/... 前斜杠形态(win32/POSIX 均正确 resolve),从而删除 windowsPathStyle 平台信号 与 client isWindowsStylePath 助手;同时解决无 cwd 时正斜杠 UNC 的往返盲点 - 测试/文档同步;移除未使用的 isPosix
114 lines
4.7 KiB
TypeScript
114 lines
4.7 KiB
TypeScript
/**
|
|
* fs-tree symlink listing: the explorer must show a symlink as what it
|
|
* points at (a symlink to a directory expands like a directory) and flag
|
|
* links whose target is missing. The host listing keeps the probe cheap:
|
|
* only entries that are actually symlinks are stat'ed.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { listDirectory } from '../src/fs-tree.ts'
|
|
|
|
/**
|
|
* Symlink creation needs extra privileges on Windows; skip the suite there
|
|
* rather than fail (mirror of the smoke spec's platform-tolerant fixtures).
|
|
*/
|
|
const canSymlink = (() => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-sidebar-symlink-probe-'))
|
|
try {
|
|
symlinkSync('target', join(dir, 'link'))
|
|
return true
|
|
} catch {
|
|
return false
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})()
|
|
|
|
/** A scratch level with real entries plus three symlinks (dir/file/dangling). */
|
|
function makeFixture(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-sidebar-symlink-'))
|
|
mkdirSync(join(dir, 'real-dir'))
|
|
writeFileSync(join(dir, 'real-file.txt'), 'content')
|
|
symlinkSync(join(dir, 'real-dir'), join(dir, 'link-to-dir'))
|
|
symlinkSync(join(dir, 'real-file.txt'), join(dir, 'link-to-file'))
|
|
symlinkSync(join(dir, 'missing-target'), join(dir, 'broken-link'))
|
|
return dir
|
|
}
|
|
|
|
describe.skipIf(!canSymlink)('fs-tree symlink listing', () => {
|
|
it('reports a symlink to a directory as an expandable directory', async () => {
|
|
const dir = makeFixture()
|
|
try {
|
|
const listing = await listDirectory(dir)
|
|
const row = listing.entries.find(entry => entry.name === 'link-to-dir')
|
|
expect(row).toMatchObject({ isDir: true, isSymlink: true, broken: false })
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('reports a symlink to a file as a file with a link badge', async () => {
|
|
const dir = makeFixture()
|
|
try {
|
|
const listing = await listDirectory(dir)
|
|
const row = listing.entries.find(entry => entry.name === 'link-to-file')
|
|
expect(row).toMatchObject({ isDir: false, isSymlink: true, broken: false })
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('flags a dangling symlink as broken', async () => {
|
|
const dir = makeFixture()
|
|
try {
|
|
const listing = await listDirectory(dir)
|
|
const row = listing.entries.find(entry => entry.name === 'broken-link')
|
|
expect(row).toMatchObject({ isDir: false, isSymlink: true, broken: true })
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('leaves regular entries unmarked and sorts symlinked dirs with the directories', async () => {
|
|
const dir = makeFixture()
|
|
try {
|
|
const listing = await listDirectory(dir)
|
|
const realDir = listing.entries.find(entry => entry.name === 'real-dir')
|
|
const realFile = listing.entries.find(entry => entry.name === 'real-file.txt')
|
|
expect(realDir).toMatchObject({ isDir: true, isSymlink: false, broken: false })
|
|
expect(realFile).toMatchObject({ isDir: false, isSymlink: false, broken: false })
|
|
// Directory-first ordering counts a symlinked directory as a directory.
|
|
const dirs = listing.entries.filter(entry => entry.isDir).map(entry => entry.name)
|
|
expect(dirs).toEqual(['link-to-dir', 'real-dir'])
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('classifies every row correctly across a symlink-heavy level (bounded probe)', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-sidebar-symlink-many-'))
|
|
mkdirSync(join(dir, 'real-dir'))
|
|
writeFileSync(join(dir, 'real-file.txt'), 'content')
|
|
try {
|
|
// More links than the probe concurrency cap exercises the worker pool;
|
|
// every directory link must still classify as a directory and every
|
|
// file link as a file (order is already checked by the sort assertion).
|
|
for (let index = 0; index < 48; index += 1) {
|
|
symlinkSync(join(dir, 'real-dir'), join(dir, `dir-link-${index}`))
|
|
symlinkSync(join(dir, 'real-file.txt'), join(dir, `file-link-${index}`))
|
|
}
|
|
const listing = await listDirectory(dir)
|
|
for (let index = 0; index < 48; index += 1) {
|
|
expect(listing.entries.find(entry => entry.name === `dir-link-${index}`))
|
|
.toMatchObject({ isDir: true, isSymlink: true, broken: false })
|
|
expect(listing.entries.find(entry => entry.name === `file-link-${index}`))
|
|
.toMatchObject({ isDir: false, isSymlink: true, broken: false })
|
|
}
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|