fix(windows): make native coverage graph portable
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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> } = {},
|
||||
) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 格式
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']> {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user