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
@@ -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 .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
2026-08-08-native-windows-pull-request-ci.md: a798aa906c2e22bceb021d1a95c081e9fa534d7c
2026-08-08-native-windows-pull-request-ci.zh.md: 58fed00e1f324a8d61af179421c7bc5703c25678
2026-08-08-native-windows-pull-request-ci.md: 0769a8dc582f48c1baddbb177e2e9921b809e391
2026-08-08-native-windows-pull-request-ci.zh.md: 7431d917adf2bfd8cc0cf31e9c249b4f28e6caf0
@@ -26,6 +26,10 @@ After the branch incorporated a newer `master`, the next native coverage run fou
The next exact-head run exposed one remaining observational built-bin failure: its lifecycle fixtures used `process.kill()` or `subprocess.kill()` to send `SIGTERM`, which unconditionally terminates a Windows target instead of delivering the registered process event for graceful disposal. POSIX acceptance still sends the real signal. On Windows the fixture requests that same registered event from inside the child, directly for a self-terminating probe and through a marker for parent-controlled lifecycle cases, so the assembled shutdown and disposal path remains covered without asserting an operating-system facility that does not exist. That acceptance then exposed the underlying early-shutdown race: a signal could dispose the root after boot returned while fallback HMR watchers were mounting, and the resulting inactive-service error escaped as a boot failure. Post-boot setup now admits work only while the authoritative root fiber is active and contains a concurrent setup error only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud.
Running the complete instrumented graph instead of the earlier reduced inventory exposed the remaining cross-platform fixture contracts. Windows path identity now accounts for 8.3 aliases, native separators, Git checkout line endings, cross-drive relative paths, and file URLs before constructing loader symlinks. The JSONL durable-directory helper applies the extended-length namespace to probes and staging creation, real product tests invoke portable executable entries and tolerate bounded Windows handle release, and stress tests retain their workloads with explicit coverage budgets. A credential document or watch path whose deepest existing ancestor is a file now fails `ENOTDIR` on every host, while `skill-local` uses effect-owned persistent Chokidar handles so asynchronous libuv errors are contained instead of escaping the test process.
POSIX mode bits, chmod-based unreadability, and chmod-based writer-lock refusal do not exist as equivalent Windows facilities. Those acceptance cases remain enforced on POSIX and are skipped on Windows; content, atomic replacement, symlink safety, rollback and recovery through platform-independent filesystem conflicts, and native Windows long-path behavior remain covered. No supported product source is excluded from Windows coverage to accommodate these differences.
Wine-only infrastructure is absent from the supported workflow: there is no apt-cache producer, compatibility script, hoisted snapshot install, Windows Node download, or local `check:windows-wine` command. The [archived Wine experiment](../../archived/process/2026-07-27-wine-windows-gates-experiment.md) remains historical evidence for its measured latency and fidelity trade-offs, not a current execution path.
## Alternatives considered
@@ -26,6 +26,10 @@ Status: implemented
下一次分支头精确运行暴露出观测项中剩余的一项 built-bin 故障:其生命周期 fixture 通过 `process.kill()``subprocess.kill()` 发送 `SIGTERM`;在 Windows 上,这种调用会无条件终止目标进程,而不会交付为优雅释放所注册的进程事件。POSIX 验收仍发送真实信号。在 Windows 上,fixture 改为从子进程内部请求同一个已注册事件:自终止探测直接请求,由父进程控制的生命周期场景则通过标记请求;因此,完整组装后的关闭与释放路径仍得到覆盖,也无需断言操作系统提供了本不存在的信号机制。该项验收随即暴露出底层的提前关闭竞态:boot 返回后,回退 HMR watcher 仍在挂载,此时信号可能对根 fiber 执行 dispose(资源释放),由此产生的服务未激活错误会逸出并被报告为 boot 失败。boot 后 setup 现在只会在权威根 fiber 仍处于活跃状态时接纳工作;只有当本次调用所记录的信号已取得关闭流程所有权时,才会隔离并发 setup 错误,无关的 HMR 故障仍会响亮失败。
运行完整的覆盖率插桩图而非此前缩减的清单后,剩余的跨平台 fixture 契约也显现出来。Windows 路径标识现在会在比较或构造 loader 符号链接前处理 8.3 别名、原生分隔符、Git 检出换行、跨盘符相对路径与文件 URL。JSONL 持久目录辅助函数会对探测与临时目录创建应用扩展长度命名空间;真实产品测试会调用可移植的可执行入口,并以有界重试容纳 Windows 句柄释放;压力测试则保留原工作负载并获得显式的覆盖率预算。如果凭据文档或监听路径最深的现有祖先是文件,所有宿主现在都会返回 `ENOTDIR``skill-local` 同时改用由 effect 拥有的持久 Chokidar 句柄,使异步 libuv 错误得到收束,不再逸出测试进程。
POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。
受支持的工作流不含 Wine 专属基础设施:不存在 apt 缓存生产者、兼容性脚本、对仓库快照执行的 hoisted 安装、Windows Node 下载或本地 `check:windows-wine` 命令。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。
## 曾考虑的替代方案
@@ -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 -2
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
+1 -1
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
+1 -1
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 格式
+3 -1
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,
@@ -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)
+3 -3
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']> {
@@ -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 () => {
+7 -2
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)
+3 -1
View File
@@ -1,7 +1,7 @@
/** Regression coverage for source declarations owned by the client test aggregate. */
import { existsSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
@@ -14,6 +14,7 @@ function clientCssDeclarations(): string[] {
.filter(entry => entry.isDirectory())
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
.filter(existsSync)
.map(file => file.replaceAll(sep, '/'))
.sort()
}
@@ -26,6 +27,7 @@ describe('client TypeScript aggregate', () => {
}
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root)
const loaded = parsed.fileNames
.map(file => file.replaceAll(sep, '/'))
.filter(file => file.endsWith('/src/css-modules.d.ts'))
.sort()
expect(loaded).toEqual(clientCssDeclarations())