fix(windows): make native coverage graph portable

This commit is contained in:
Tianyi Cui
2026-08-08 22:19:45 +08:00
parent 3e13581c35
commit 0aeca9c3ce
30 changed files with 107 additions and 60 deletions

View File

@@ -9,7 +9,7 @@
* writes CRLF on Windows, so exact text assertions normalize line endings.
*/
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
@@ -33,7 +33,9 @@ const lf = (text: string): string => text.replace(/\r\n/g, '\n')
/** Case-insensitive path equality on Windows (Get-Location may re-case the drive). */
function samePath(actual: string, expected: string): boolean {
const norm = (value: string) => (process.platform === 'win32' ? value.toLowerCase() : value)
const norm = (value: string) => (
process.platform === 'win32' ? realpathSync.native(value).toLowerCase() : value
)
return norm(actual) === norm(expected)
}
@@ -72,7 +74,11 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () =>
it('falls through an empty configured path to platform resolution', () => {
// SystemRoot points at a non-existent tree so the Windows PowerShell 5.1
// fallback candidate cannot exist either.
expect(resolvePwshPath('', { PATH: 'P:\\Store', SystemRoot: 'S:\\no-windows' }, 'win32')).toBe('pwsh')
expect(resolvePwshPath('', {
PATH: 'P:\\Store',
ProgramFiles: 'P:\\no-program-files',
SystemRoot: 'S:\\no-windows',
}, 'win32')).toBe('pwsh')
})
it('returns pwsh on non-Windows platforms regardless of the environment', () => {

View File

@@ -58,7 +58,7 @@ const STREAM_DOC = [
describe('incremental streaming rendering', () => {
for (const chunkSize of [1, 3, 7, 16]) {
it(`matches a fresh render at every prefix (chunk=${chunkSize})`, () => {
it(`matches a fresh render at every prefix (chunk=${chunkSize})`, { timeout: 20_000 }, () => {
const live = render(<MarkdownText text="" streaming />)
for (let end = chunkSize; end < STREAM_DOC.length + chunkSize; end += chunkSize) {
const prefix = STREAM_DOC.slice(0, Math.min(end, STREAM_DOC.length))

View File

@@ -4122,7 +4122,7 @@ describe('dynamic nested workspace context injection', () => {
await vi.waitFor(() => {
expect(warn).toHaveBeenCalledWith('workspace instruction refresh failed: %o', failure)
})
}, { timeout: 10_000 })
} finally {
await ctx.fiber.dispose()
}

View File

@@ -97,16 +97,23 @@ const GROUP_OTHER_BITS = 0o077
* here — so the check is skipped rather than faked, and the file's protection
* there is whatever the create and replace APIs express.
* @param filename - absolute path of the document.
* @throws when the file exists with group or other permission bits set.
* @throws when the path hierarchy is invalid or the file exists with group or other permission bits set.
*/
async function assertOwnerOnly(filename: string): Promise<void> {
/* v8 ignore next -- native Windows coverage exercises the skip; POSIX covers the check */
if (process.platform === 'win32') return
/* v8 ignore start -- native Windows coverage exercises this path; POSIX covers mode enforcement */
if (process.platform === 'win32') {
// Windows has no POSIX mode bits, but it reports a file-as-parent as
// ordinary ENOENT; canonicalization preserves the invalid-path failure.
await canonicalizeWatchPath(filename)
return
}
/* v8 ignore stop */
let mode: number
try {
mode = (await stat(filename)).mode
} catch (error) {
if (!isENOENT(error)) throw error
await canonicalizeWatchPath(filename)
return
}
const offending = mode & GROUP_OTHER_BITS

View File

@@ -168,7 +168,7 @@ describe('layer ladder', () => {
expect(await stored.credentials.resolve(KEY)).toEqual({ value: 'stored', source: 'file' })
})
it('refuses a document other OS users can read', async () => {
it.skipIf(process.platform === 'win32')('refuses a document other OS users can read', async () => {
const dir = await tempDir()
const path = join(dir, '.credentials.yaml')
await writeFile(path, 'DSH_CRED_TEST: leaked\n', { mode: 0o644 })
@@ -274,7 +274,7 @@ describe('document writes', () => {
const seen = updates(ctx)
await ctx.credentials.set(KEY, 'sk-fresh')
expect(await readFile(path, 'utf8')).toBe('DSH_CRED_TEST: sk-fresh\n')
expect((await stat(path)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(path)).mode & 0o777).toBe(0o600)
expect(await ctx.credentials.resolve(KEY)).toEqual({ value: 'sk-fresh', source: 'file' })
expect(seen).toEqual([KEY])
})

View File

@@ -78,7 +78,7 @@ describe('read-modify-write', () => {
const home = join(dir, 'home')
const ctx = await boot({ path: join(home, '.credentials.yaml'), watch: false })
await ctx.credentials.set(ALPHA, 'one')
expect((await stat(home)).mode & 0o777).toBe(0o700)
if (process.platform !== 'win32') expect((await stat(home)).mode & 0o777).toBe(0o700)
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { join, sep } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
@@ -491,7 +491,8 @@ describe('dsh-agent-spine-demo bundle', () => {
callId: event.data.message.source.callId,
isError: result.isError,
text: result.content.map(block => block.type === 'text' ? block.text : '').join('\n')
.replaceAll(root, '{{cwd}}'),
.replaceAll(root, '{{cwd}}')
.replaceAll(sep, '/'),
}]
}
return []

View File

@@ -59,7 +59,7 @@ function stubAgent(session: Session): Agent {
/** Compose the API over real Session, Agent, Storage, Domain, and Workspace services. */
async function harness(
workspaceRoot = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-workspace-'))),
workspaceRoot = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-workspace-'))),
picker: DirectoryPickerCapability = { kind: 'native', pick: async () => null },
extras: { openPath?: (path: string, signal: AbortSignal) => Promise<void> } = {},
) {

View File

@@ -173,7 +173,8 @@ describe('DeepSeekHarness', () => {
it('resolves a relative launch cwd to an absolute workspace before the handshake', async () => {
// vitest workers forbid chdir, so derive a RELATIVE path from the real
// process cwd to a temp worker dir; resolution is lexical either way.
const dir = await tempDir('sdk-client-relcwd-')
const dir = await mkdtemp(join(process.cwd(), '.dsh-sdk-client-relcwd-'))
cleanups.push(() => rm(dir, { recursive: true, force: true }))
const recordFile = join(dir, 'init.jsonl')
const inner = join(dir, 'worker')
await mkdir(inner)
@@ -332,7 +333,11 @@ describe('HarnessClient', () => {
))
await client.initialize({ cwd: process.cwd(), provider: 'p', model: 'm' })
await client.close()
expect((await stat(sigtermFile)).isFile()).toBe(true)
if (process.platform === 'win32') {
await expect(stat(sigtermFile)).rejects.toMatchObject({ code: 'ENOENT' })
} else {
expect((await stat(sigtermFile)).isFile()).toBe(true)
}
})
it('escalates to SIGKILL when the runtime traps SIGTERM too', async () => {

View File

@@ -91,7 +91,7 @@ function isEEXIST(error: unknown): boolean {
async function assertDirectory(path: string): Promise<boolean> {
try {
const info = await stat(path)
const info = await stat(toNamespacedPath(path))
if (info.isDirectory()) return true
const error = new Error(`path exists but is not a directory: ${path}`) as NodeJS.ErrnoException
error.code = 'ENOTDIR'
@@ -141,7 +141,7 @@ export async function ensureDurableDirectoryWin32(target: string): Promise<void>
async function createLeafDirectoryWin32(parent: string, target: string): Promise<void> {
// Keep the staging component independent of the target basename so a legal
// 255-byte target component does not make mkdtemp's sibling name too long.
const staging = await mkdtemp(join(parent, '.dsh-mkdir-'))
const staging = await mkdtemp(toNamespacedPath(join(parent, '.dsh-mkdir-')))
try {
await publishNewFileWin32(staging, target)
} catch (error) {

View File

@@ -86,7 +86,7 @@ describe('writer lock', () => {
expect(await readFile(lockPath, 'utf8')).toBe('slow-holder\n')
}, 10_000)
it('surfaces a non-contention lock failure as the write error', async () => {
it.skipIf(process.platform === 'win32')('surfaces a non-contention lock failure as the write error', async () => {
const dir = await tempDir()
const path = join(dir, 'settings.yaml')
const ctx = await boot({ path, watch: false })

View File

@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import z from 'schemastery'
import { chmod, lstat, mkdtemp, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'
import { chmod, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
@@ -67,7 +67,7 @@ describe('boot and reads', () => {
await expect(ctx.settings.prepareDocument()).resolves.toBe(path)
expect(await readFile(path, 'utf8')).toBe('')
expect((await stat(path)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(path)).mode & 0o777).toBe(0o600)
expect(scope.get()).toEqual({ theme: 'dark', fontSize: 14 })
})
@@ -128,7 +128,7 @@ describe('boot and reads', () => {
expect(scope.get()).toEqual({ theme: 'dark', fontSize: 14 })
})
it('fails loud at boot when the document exists but is unreadable', async () => {
it.skipIf(process.platform === 'win32')('fails loud at boot when the document exists but is unreadable', async () => {
const dir = await tempDir()
const path = join(dir, 'settings.yaml')
await writeFile(path, 'ui-theme:\n theme: light\n')
@@ -168,7 +168,7 @@ describe('persist', () => {
const written = await readFile(path, 'utf8')
expect(written).toContain('theme: light')
expect((await stat(path)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(path)).mode & 0o777).toBe(0o600)
// Atomic replace leaves no temp artifact behind.
expect((await readdir(dir)).sort()).toEqual(['settings.yaml'])
})
@@ -203,7 +203,7 @@ describe('persist', () => {
expect(await readFile(victim, 'utf8')).toBe('precious')
expect((await lstat(path)).isSymbolicLink()).toBe(false)
expect((await stat(path)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(path)).mode & 0o777).toBe(0o600)
expect(await readFile(path, 'utf8')).toContain('theme: light')
})
@@ -337,16 +337,18 @@ describe('persist', () => {
expect(written).toEqual({ 'ui-theme': { theme: 'light' } })
})
it('rejects and leaves no temp residue when the directory turns unwritable', async () => {
it('rejects and recovers when the document path becomes a directory', async () => {
const dir = await tempDir()
const path = join(dir, 'settings.yaml')
const backup = join(dir, 'settings.committed.yaml')
await writeFile(path, 'ui-theme:\n theme: light\n')
const ctx = await boot({ path, watch: false })
const scope = ctx.settings.register(settingsNamespace('ui-theme'), ThemeSchema)
await chmod(dir, 0o500)
cleanups.push(() => chmod(dir, 0o700))
await rename(path, backup)
await mkdir(path)
await expect(scope.update({ theme: 'dark' })).rejects.toThrow()
await chmod(dir, 0o700)
await rm(path, { recursive: true })
await rename(backup, path)
expect((await readdir(dir)).sort()).toEqual(['settings.yaml'])
expect(scope.get().theme).toBe('light')
// The failed persist must not poison the document write chain.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/skill-local/README.md
README.md: 6d3d97ea34308ccc920d0b42c5a220dd7a9d6d73
README.zh.md: 9948369774bc08a4ddbd4e518b53134ddd7c6882
README.md: dc2e97f89349a85ce548e5f6f1b01408eb32a293
README.zh.md: 0c17d9fb77af3df2071613c2c6b8fc15086581a7

View File

@@ -48,7 +48,7 @@ Existing skill roots are watched with Chokidar. Before opening a native watcher,
A root that does not exist is followed from the nearest existing ancestor one missing path segment at a time. The next segment is probed with `fs.watchFile`; once `.agents`, `skills`, or the configured root appears, observation advances until Chokidar can attach to the real root. Root deletion reverses this process, so deleting and recreating an entire skills directory remains observable. Project-scoped watchers are bounded by `watchMaxProjects`; revisiting an evicted project reattaches observation during discovery.
The first-party filesystem `write` and `edit` tools also synchronously invalidate the provider through `fs/observed` when their target could affect a watched skill entry. This fast path makes the next model step observe its own filesystem mutation without waiting for the host watcher. External IDE, Git, shell, and process changes rely on Chokidar or the missing-path probe. Startup/runtime watcher failures are logged and retried. Discovery still scans readable roots and returns their candidates for direct loading, but marks the observation incomplete so it is not cached or published as an authoritative model catalog. Effect teardown closes every watcher and contains late callbacks.
The first-party filesystem `write` and `edit` tools also synchronously invalidate the provider through `fs/observed` when their target could affect a watched skill entry. This fast path makes the next model step observe its own filesystem mutation without waiting for the host watcher. External IDE, Git, shell, and process changes rely on Chokidar or the missing-path probe. Existing-root watchers remain persistent until effect teardown so Chokidar owns asynchronous native error events; startup/runtime watcher failures are logged and retried. Discovery still scans readable roots and returns their candidates for direct loading, but marks the observation incomplete so it is not cached or published as an authoritative model catalog. Effect teardown closes every watcher and contains late callbacks.
## Skill Format

View File

@@ -48,7 +48,7 @@
不存在的根会从最近的现有祖先开始,每次沿一个缺失路径段跟踪。系统使用 `fs.watchFile` 探测下一段;当 `.agents``skills` 或已配置的根出现后,观察会逐级推进,直至 Chokidar 可以附加到真实根。根删除时,该过程反向执行,因此删除再重建整个 skills 目录仍可被观察到。按项目划分的 watcher 数量受 `watchMaxProjects` 限制;再次访问已被驱逐的项目时,发现阶段会重新附加观察。
如果第一方文件系统 `write``edit` 工具的目标可能影响受监视的 skill 条目,它们还会通过 `fs/observed` 同步使提供方失效。这条快速路径让模型的下一个步骤无需等待宿主 watcher即可观察到自身的文件系统变更。外部 IDE、Git、shell 和进程产生的变更依赖 Chokidar 或缺失路径探测。watcher 启动或运行时失败会被记录并触发重试。发现过程仍会扫描可读根目录并返回其候选项供直接加载但会将观测标记为不完整因此不会缓存也不会作为权威模型目录发布。effect 释放会关闭所有 watcher并收束延迟回调。
如果第一方文件系统 `write``edit` 工具的目标可能影响受监视的 skill 条目,它们还会通过 `fs/observed` 同步使提供方失效。这条快速路径让模型的下一个步骤无需等待宿主 watcher即可观察到自身的文件系统变更。外部 IDE、Git、shell 和进程产生的变更依赖 Chokidar 或缺失路径探测。现有根的 watcher 会保持持久状态直至 effect 释放,使 Chokidar 能够接管异步原生错误事件;watcher 启动或运行时失败会被记录并触发重试。发现过程仍会扫描可读根目录并返回其候选项供直接加载但会将观测标记为不完整因此不会缓存也不会作为权威模型目录发布。effect 释放会关闭所有 watcher并收束延迟回调。
## skill 格式

View File

@@ -487,7 +487,9 @@ class SkillWatchManager {
private async openRootWatcher(state: RootWatchState, mode: Extract<RootWatchMode, { kind: 'root' }>): Promise<WatchHandle> {
const watcher = chokidar.watch(mode.anchor, {
persistent: false,
// Chokidar owns late native fs.watch errors only for persistent watchers;
// this provider's effect explicitly closes every handle at teardown.
persistent: true,
ignoreInitial: true,
depth: 1,
followSymlinks: this.config.followSymlinks,

View File

@@ -132,6 +132,7 @@ describe('skill-local watcher failures', () => {
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['canonical-skill'])
expect(watcherHarness.watchers[0]?.path).toBe(await realpath(root))
expect(watcherHarness.watchers[0]?.options.persistent).toBe(true)
await fiber.dispose()
})

View File

@@ -1,4 +1,4 @@
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
@@ -88,19 +88,23 @@ describe('json backend specifics', () => {
const unit = await backend.kv.open(descriptor)
await unit.putRecord('t', 'k', { v: 'committed' })
await unit.setGlobal({ g: 'committed' })
// Make every publish fail: revoke write permission on the root.
await chmod(root, 0o500)
const path = join(root, 'shape.json')
const backup = join(root, 'shape.committed.json')
// A directory at the publish target rejects atomic replacement on every host.
await rename(path, backup)
await mkdir(path)
await expect(unit.putRecord('t', 'k', { v: 'rejected' })).rejects.toThrow()
await expect(unit.putRecord('t', 'k2', { v: 'also rejected' })).rejects.toThrow()
await expect(unit.deleteRecord('t', 'k')).rejects.toThrow()
await expect(unit.setGlobal({ g: 'rejected' })).rejects.toThrow()
await chmod(root, 0o700)
await rm(path, { recursive: true })
await rename(backup, path)
const snapshot = await unit.loadAll()
expect(snapshot.tables['t']).toEqual({ k: { v: 'committed' } })
expect(snapshot.global).toEqual({ g: 'committed' })
// The next successful publish must not carry rejected writes to disk.
await unit.putRecord('t', 'k3', { v: 'later' })
const text = await readFile(join(root, 'shape.json'), 'utf8')
const text = await readFile(path, 'utf8')
expect(text).not.toContain('rejected')
await backend.close()
})

View File

@@ -90,7 +90,7 @@ afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
await Promise.all(fixtures.splice(0).map(fixture => fixture.close()))
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
}
observedSdkMessages.length = 0
})

View File

@@ -27,6 +27,7 @@ import {
const execFileAsync = promisify(execFile)
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
const codexEntry = join(packageRoot, 'node_modules', '@openai', 'codex', 'bin', 'codex.js')
const codexPackage = JSON.parse(readFileSync(
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
'utf8',
@@ -40,7 +41,7 @@ afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
await Promise.all(fixtures.splice(0).map(fixture => fixture.close()))
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
}
})
@@ -147,7 +148,7 @@ describe('real @openai/codex 0.146.0 product', () => {
{ kind: 'complete', text: sentinel },
])
expect(codexPackage.version).toBe('0.146.0')
const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], {
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('codex-cli 0.146.0')
@@ -178,7 +179,9 @@ describe('real @openai/codex 0.146.0 product', () => {
kind: 'functionCall',
name: 'exec_command',
arguments: {
cmd: 'touch approval-side-effect',
cmd: process.platform === 'win32'
? 'cmd /c type nul > approval-side-effect'
: 'touch approval-side-effect',
sandbox_permissions: 'require_escalated',
justification: 'exercise the unattended approval boundary',
},

View File

@@ -1078,7 +1078,7 @@ describe('SubagentService.listDescendants', () => {
}])
})
it('discovers continuable descendants below ordinary and one-shot intermediates', async () => {
it('discovers continuable descendants below ordinary and one-shot intermediates', { timeout: 20_000 }, async () => {
const { ctx, parent } = await setup([textResponse('one shot')])
// An ordinary fork has no descriptor: omitted itself, subtree still walked.
const fork = ctx.sessions.fork(parent.session, undefined, SessionId('plain-fork'))

View File

@@ -1,4 +1,4 @@
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -17,7 +17,6 @@ function tempHome(): string {
afterEach(() => {
for (const dir of dirs.splice(0)) {
chmodSync(dir, 0o700)
rmSync(dir, { recursive: true, force: true })
}
})
@@ -69,11 +68,10 @@ describe('getOrCreateAnonymousUserId', () => {
expect(id).toBe(winner)
})
it('returns a usable id when the home is unwritable, without persisting', () => {
it('returns a usable id when the home cannot contain files, without persisting', () => {
const home = tempHome()
const blocked = join(home, 'blocked')
mkdirSync(blocked)
chmodSync(blocked, 0o500)
writeFileSync(blocked, 'occupied\n')
const id = getOrCreateAnonymousUserId({ env: { DSH_HOME: blocked } })
expect(id).toMatch(UUID)
expect(existsSync(join(blocked, USER_ID_FILE_NAME))).toBe(false)

View File

@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
@@ -114,9 +114,9 @@ async function boot(): Promise<Context> {
async function linkZod(base: string): Promise<void> {
const { symlink } = await import('node:fs/promises')
const target = join(base, 'node_modules', 'zod')
const source = new URL(import.meta.resolve('zod/package.json')).pathname.replace(/\/package\.json$/, '')
const source = fileURLToPath(new URL('.', import.meta.resolve('zod/package.json')))
await mkdir(join(base, 'node_modules'), { recursive: true })
await symlink(source, target, 'dir')
await symlink(source, target, process.platform === 'win32' ? 'junction' : 'dir')
}
function mountTypertLoader(ctx: Context, config: typertLoader.Config = {}): ReturnType<Context['plugin']> {

View File

@@ -11,6 +11,9 @@ import { BUNDLED_PNPM_VERSION, RepositoryCache, type RepositoryInstall } from '@
const execFileAsync = promisify(execFile)
const roots: string[] = []
/** Normalize Git's platform checkout line endings for source-content assertions. */
const lf = (text: string): string => text.replace(/\r\n/g, '\n')
async function temporaryRoot(name: string): Promise<string> {
const root = await mkdtemp(join(tmpdir(), `cordis-${name}-`))
roots.push(root)
@@ -151,8 +154,8 @@ describe('RepositoryCache', () => {
const installed = await new RepositoryCache(join(root, 'cache')).resolve(specifier)
await expect(readFile(join(installed, 'prepared.txt'), 'utf8')).resolves.toBe('visible|absent\n')
await expect(readFile(join(installed, 'dsh-plugin.mjs'), 'utf8')).resolves.toContain('export function apply')
await expect(readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8'))
.resolves.toBe('repository skill source\n')
expect(lf(await readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8')))
.toBe('repository skill source\n')
await expect(readFile(join(installed, 'package.json'), 'utf8'))
.resolves.toContain('repository-plugin-fixture')
})

View File

@@ -14,7 +14,7 @@ describe('writeFileAtomic', () => {
const target = join(dir, 'nested', 'deep', 'doc.yaml')
await writeFileAtomic(target, 'a: 1\n', { mode: 0o600 })
expect(await readFile(target, 'utf8')).toBe('a: 1\n')
expect((await stat(target)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(target)).mode & 0o777).toBe(0o600)
})
it('replaces existing content and narrows a wider-permission file to the stated mode', async () => {
@@ -23,7 +23,7 @@ describe('writeFileAtomic', () => {
await writeFile(target, 'old', { mode: 0o644 })
await writeFileAtomic(target, 'new', { mode: 0o600 })
expect(await readFile(target, 'utf8')).toBe('new')
expect((await stat(target)).mode & 0o777).toBe(0o600)
if (process.platform !== 'win32') expect((await stat(target)).mode & 0o777).toBe(0o600)
})
it('replaces a symlinked target itself without writing through to the referent', async () => {

View File

@@ -4,7 +4,7 @@
* @module @deepseek-ai/dsh-paths
*/
import { realpath } from 'node:fs/promises'
import { opendir, realpath } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, dirname, join, resolve } from 'node:path'
@@ -32,7 +32,12 @@ export async function canonicalizeWatchPath(path: string): Promise<string> {
const missing: string[] = []
while (true) {
try {
return join(await realpath(current), ...missing.reverse())
const canonical = await realpath(current)
if (missing.length > 0) {
const directory = await opendir(canonical)
await directory.close()
}
return join(canonical, ...missing.reverse())
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const parent = dirname(current)