mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
test(windows): close final native coverage gaps
This commit is contained in:
@@ -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: 5aafb8b9c7557157135dfa6ecedffed42eb76183
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: 4c71f3eeda86fdc537c22629962b0eccbb994b65
|
||||
2026-08-08-native-windows-pull-request-ci.md: 8b657e3bffa7279560f3f87e5c35e7f462f2cac1
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: 6f23dd1df1ef31a8952425bdc6ed00a720d162dd
|
||||
|
||||
@@ -32,7 +32,9 @@ The final root-probe failure came from applying the extended-length namespace to
|
||||
|
||||
The next complete coverage run reached six independent late failures rather than one shared cascade. React queue-action coverage now resolves its mocked request inside an awaited `act()` before observing the settled render. The unclosed-Markdown workload keeps all 6,400 candidates under an explicit three-second coverage budget, and the asynchronous workspace-projection warning test gives its outer case a twenty-second budget larger than its ten-second poll. Real Claude Code teardown uses asynchronous recursive removal with ten bounded retries after every managed handle reports exit, accommodating Windows' delayed handle release without weakening the quiescence assertions.
|
||||
|
||||
Two product boundaries required foundation repairs. Include's debounced config persistence previously launched an unobserved promise from a timer; a transient Windows `EPERM` while replacing `cordis.yml` could therefore lose the disabled row and escape as an unhandled rejection. The vendored writer now serializes writes, retries only transient access/busy failures with bounded backoff, observes every rejection, and drains the latest write at teardown; the real Loader composition injects one `EPERM` and proves the durable retry. Codex 0.146 advertised `exec_command` to the loopback Responses model on Windows but rejected the returned call in its own router, the same upstream failure class tracked in [openai/codex#31665](https://github.com/openai/codex/issues/31665). Development evidence is pinned to the current stable 0.147.0 release: regenerated upstream schemas preserve the provider-owned handshake, thread/turn, approval, user-input, and elicitation contract, while the real-product suite again proves command rejection without a side effect and whole-tree exit.
|
||||
Two product boundaries required foundation repairs. Include's debounced config persistence previously launched an unobserved promise from a timer; a transient Windows `EPERM` while replacing `cordis.yml` could therefore lose the disabled row and escape as an unhandled rejection. The vendored writer now serializes writes, retries only transient access/busy failures with bounded backoff, observes every rejection, and drains the latest write at teardown; the real Loader composition injects one `EPERM` and proves the durable retry. Codex 0.146 advertised `exec_command` to the loopback Responses model on Windows but rejected the returned call in its own router, the same upstream failure class tracked in [openai/codex#31665](https://github.com/openai/codex/issues/31665). Development evidence is pinned to the current stable 0.147.0 release: regenerated upstream schemas preserve the provider-owned handshake, thread/turn, approval, user-input, and elicitation contract. Because Codex can advertise the legacy `shell_command` instead when unified exec is unavailable on the host, the loopback model now selects an advertised command tool and supplies that tool's argument shape rather than injecting `exec_command` unconditionally. The real-product suite therefore proves unattended rejection without a side effect and whole-tree exit through the product's actual default tool inventory on each host.
|
||||
|
||||
The subsequent exact hosted run isolated seven other fixture contracts. The PowerShell background-output case now waits for process completion before draining and comparing the final delta, while the pi-ai idle-watchdog case retains a bounded one-second close deadline that accommodates the delayed Windows socket notification. The asynchronous workspace projection seeds its in-memory filesystem at the host-resolved root. The Include retry acceptance asserts the injected failure and eventual persistence rather than an incidental total rename count, which may include another valid serialized write. LSP's bare-command fixture supplies a `.cmd` executable through `PATHEXT` on Windows, and URI rendering expectations distinguish the execution world's path convention from the test host's separators. None of these changes skips a supported path or weakens the asserted outcome.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ Status: implemented
|
||||
|
||||
下一次完整覆盖率运行触及的是 6 项相互独立的末端故障,不再是同一问题的连锁结果。React 队列动作覆盖现在会在 awaited `act()` 中解析模拟请求,再观察渲染完成后的状态。未闭合 Markdown 工作负载保留全部 6,400 个候选项,并采用显式的 3 秒覆盖率预算;异步工作区投影告警测试则为外层用例设置 20 秒预算,大于其 10 秒轮询预算。真实 Claude Code 拆卸会在所有受管句柄均报告退出后,采用带 10 次有界重试的异步递归删除,以容纳 Windows 延迟释放句柄的行为,同时不削弱完全停稳断言。
|
||||
|
||||
另有两个产品边界需要基础性修复。Include 的防抖配置持久化此前会从计时器启动一个无人观察的 Promise;Windows 在替换 `cordis.yml` 时若瞬时返回 `EPERM`,既可能丢失已禁用行,也会让 rejection 以未处理形式逸出。现在,vendored writer 会串行化写入,只对瞬时的访问/忙碌错误执行有界退避重试,观察每个 rejection,并在拆卸时排空最新写入;真实 Loader 组合测试会注入一次 `EPERM` 并证明持久化重试。Codex 0.146 在 Windows 上会把 `exec_command` 提供给回环 Responses 模型,却在自身路由器中拒绝模型返回的调用;这与 [openai/codex#31665](https://github.com/openai/codex/issues/31665) 跟踪的上游故障属于同一类。开发证据现锁定当前稳定版 0.147.0:重新生成的上游 schema 保留了提供方拥有的握手、线程/轮次、审批、用户输入和 elicitation 契约;真实产品测试则再次证明命令会在不产生副作用的情况下被拒绝,且整棵进程树退出。
|
||||
另有两个产品边界需要基础性修复。Include 的防抖配置持久化此前会从计时器启动一个无人观察的 Promise;Windows 在替换 `cordis.yml` 时若瞬时返回 `EPERM`,既可能丢失已禁用行,也会让 rejection 以未处理形式逸出。现在,vendored writer 会串行化写入,只对瞬时的访问/忙碌错误执行有界退避重试,观察每个 rejection,并在拆卸时排空最新写入;真实 Loader 组合测试会注入一次 `EPERM` 并证明持久化重试。Codex 0.146 在 Windows 上会把 `exec_command` 提供给回环 Responses 模型,却在自身路由器中拒绝模型返回的调用;这与 [openai/codex#31665](https://github.com/openai/codex/issues/31665) 跟踪的上游故障属于同一类。开发证据现锁定当前稳定版 0.147.0:重新生成的上游 schema 保留了提供方拥有的握手、线程/轮次、审批、用户输入和 elicitation 契约。当宿主无法使用 unified exec 时,Codex 可能改为提供旧版 `shell_command`;因此,回环模型现在会选择实际提供的命令工具,并使用该工具对应的参数形态,而不再无条件注入 `exec_command`。真实产品测试由此会通过各宿主的实际默认工具清单,证明无人值守拒绝不产生副作用,且整棵进程树退出。
|
||||
|
||||
随后的分支头精确托管运行又隔离出另外 7 项 fixture 契约。PowerShell 后台输出场景现在会等待进程完成,再排空并比较最后一段增量;pi-ai 空闲 watchdog 场景则保留 1 秒的有界关闭期限,以容纳 Windows 延迟送达的 socket 通知。异步工作区投影会在宿主解析后的根目录上填充内存文件系统。Include 重试验收现在断言注入的故障与最终持久化结果,而不再断言可能包含另一项合法串行写入的偶然 rename 总次数。LSP 的裸命令 fixture 会在 Windows 上通过 `PATHEXT` 提供 `.cmd` 可执行文件,URI 渲染预期也会区分执行环境的路径约定与测试宿主的分隔符。上述修改既没有跳过受支持路径,也没有削弱结果断言。
|
||||
|
||||
POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。
|
||||
|
||||
|
||||
@@ -314,9 +314,10 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
|
||||
env: { BG_VAR: 'bg-env' },
|
||||
dshEnv: { DSH_BG_VAR: 'bg-dsh-env' },
|
||||
}))
|
||||
const output = await readUntil(proc, '[bg-env][bg-dsh-env]')
|
||||
expect(output).toBe('bg-stdin\n[bg-env][bg-dsh-env]\n')
|
||||
const partialOutput = await readUntil(proc, '[bg-env][bg-dsh-env]')
|
||||
await proc.done
|
||||
const output = partialOutput + lf(proc.readOutput().delta)
|
||||
expect(output).toBe('bg-stdin\n[bg-env][bg-dsh-env]\n')
|
||||
expect(proc.exitCode).toBe(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -4113,11 +4113,12 @@ describe('dynamic nested workspace context injection', () => {
|
||||
await ctx.plugin(RecordingFileSystem)
|
||||
await ctx.plugin(workspaceContext, { maxBytes: 65536 })
|
||||
const fs = ctx.fs as RecordingFileSystem
|
||||
const agent = stubAgent('/')
|
||||
const root = resolve('/')
|
||||
const agent = stubAgent(root)
|
||||
const failure = new Error('projection failed')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
fs.entries.set('/.git', { type: 'directory' })
|
||||
fs.entries.set('/AGENTS.md', { type: 'file', content: 'workspace rule' })
|
||||
fs.entries.set(join(root, '.git'), { type: 'directory' })
|
||||
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'workspace rule' })
|
||||
vi.spyOn(agent.inbox, 'prepend').mockImplementationOnce(() => { throw failure })
|
||||
|
||||
ctx.emit('tools/result', stubToolExecution({
|
||||
|
||||
@@ -22,7 +22,7 @@ import BrowseDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-browse
|
||||
import NativeDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-native'
|
||||
import * as DirectoryPickerAuto from '../src/index.ts'
|
||||
|
||||
const renameControl = vi.hoisted(() => ({ attempts: 0, remainingFailures: 0 }))
|
||||
const renameControl = vi.hoisted(() => ({ attempts: 0, injectedFailures: 0, remainingFailures: 0 }))
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>()
|
||||
@@ -32,6 +32,7 @@ vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
renameControl.attempts++
|
||||
if (renameControl.remainingFailures > 0) {
|
||||
renameControl.remainingFailures--
|
||||
renameControl.injectedFailures++
|
||||
throw Object.assign(new Error(`transient rename failure for ${newPath}`), { code: 'EPERM' })
|
||||
}
|
||||
await actual.rename(oldPath, newPath)
|
||||
@@ -59,6 +60,7 @@ afterEach(async () => {
|
||||
root = undefined
|
||||
fakeBin = undefined
|
||||
renameControl.attempts = 0
|
||||
renameControl.injectedFailures = 0
|
||||
renameControl.remainingFailures = 0
|
||||
})
|
||||
|
||||
@@ -187,6 +189,8 @@ describe('real Loader composition', () => {
|
||||
expect(entryNames(ctx)).not.toContain(NATIVE)
|
||||
// Same self-dispose persistence as above: let the write land before teardown.
|
||||
await expect.poll(async () => await readFile(configPath, 'utf8')).toContain('disabled: true')
|
||||
expect(renameControl.attempts).toBe(2)
|
||||
expect(renameControl.injectedFailures).toBe(1)
|
||||
expect(renameControl.remainingFailures).toBe(0)
|
||||
expect(renameControl.attempts).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -272,7 +272,7 @@ describe('PiAiAdapter provider routing', () => {
|
||||
await Promise.race([
|
||||
server.responseClosed,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
setTimeout(() => { reject(new Error('SDK request did not close after idle timeout')) }, 100)
|
||||
setTimeout(() => { reject(new Error('SDK request did not close after idle timeout')) }, 1_000)
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ describe('lsp-local provider resolution', () => {
|
||||
// A tiny executable script placed on a custom PATH dir: the load-time resolver must find it.
|
||||
const bin = join(root, 'bin')
|
||||
await mkdir(bin)
|
||||
const exe = join(bin, 'fake-lsp')
|
||||
await writeFile(exe, '#!/bin/sh\nexit 0\n')
|
||||
await chmod(exe, 0o755)
|
||||
const exe = join(bin, process.platform === 'win32' ? 'fake-lsp.cmd' : 'fake-lsp')
|
||||
await writeFile(exe, process.platform === 'win32' ? '@exit /b 0\r\n' : '#!/bin/sh\nexit 0\n')
|
||||
if (process.platform !== 'win32') await chmod(exe, 0o755)
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(Lsp)
|
||||
@@ -49,7 +49,7 @@ describe('lsp-local provider resolution', () => {
|
||||
await expect(ctx.plugin(LspLocal, config('onpath', {
|
||||
command: 'fake-lsp',
|
||||
args: [],
|
||||
env: { PATH: bin },
|
||||
env: { PATH: bin, ...process.platform === 'win32' ? { PATHEXT: '.CMD' } : {} },
|
||||
extensionToLanguage: { '.ts': 'typescript' },
|
||||
}))).resolves.toBeDefined()
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('renderUri', () => {
|
||||
it('returns an absolute path for a file: URI outside the workspace', () => {
|
||||
const outside = resolve(WS, '..', 'other', 'lib', 'b.ts')
|
||||
const uri = pathToFileURL(outside).href
|
||||
expect(renderUri(uri, WS_URI)).toBe(outside)
|
||||
expect(renderUri(uri, WS_URI)).toBe(outside.replaceAll('\\', '/'))
|
||||
})
|
||||
|
||||
it('renders the workspace root itself as "."', () => {
|
||||
@@ -83,7 +83,7 @@ describe('renderUri', () => {
|
||||
})
|
||||
|
||||
it('preserves backslashes as ordinary POSIX filename characters', () => {
|
||||
expect(renderUri('file:///home/u/proj/dir%5Cname/a.ts', WS_URI)).toBe('dir\\name/a.ts')
|
||||
expect(renderUri('file:///home/u/proj/dir%5Cname/a.ts', 'file:///home/u/proj')).toBe('dir\\name/a.ts')
|
||||
})
|
||||
|
||||
it('keeps malformed or mismatched URI coordinates verbatim', () => {
|
||||
|
||||
@@ -174,18 +174,32 @@ describe('real @openai/codex 0.147.0 product', () => {
|
||||
}, 60_000)
|
||||
|
||||
it('cancels a real app-server command approval without executing the command', async () => {
|
||||
const { harness, fixture } = await realHarness([
|
||||
const command = process.platform === 'win32'
|
||||
? 'cmd /c type nul > approval-side-effect'
|
||||
: 'touch approval-side-effect'
|
||||
const commandCalls = [
|
||||
{
|
||||
kind: 'functionCall',
|
||||
name: 'exec_command',
|
||||
arguments: {
|
||||
cmd: process.platform === 'win32'
|
||||
? 'cmd /c type nul > approval-side-effect'
|
||||
: 'touch approval-side-effect',
|
||||
cmd: command,
|
||||
sandbox_permissions: 'require_escalated',
|
||||
justification: 'exercise the unattended approval boundary',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'shell_command',
|
||||
arguments: {
|
||||
command,
|
||||
sandbox_permissions: 'require_escalated',
|
||||
justification: 'exercise the unattended approval boundary',
|
||||
},
|
||||
},
|
||||
] as const
|
||||
const { harness, fixture } = await realHarness([
|
||||
{
|
||||
kind: 'advertisedFunctionCall',
|
||||
choices: commandCalls,
|
||||
},
|
||||
])
|
||||
const sideEffect = join(harness.workspace, 'approval-side-effect')
|
||||
const run = await harness.ctx.subagents.start('codex', {
|
||||
@@ -202,9 +216,9 @@ describe('real @openai/codex 0.147.0 product', () => {
|
||||
expect(existsSync(sideEffect)).toBe(false)
|
||||
expect(fixture.requests).toHaveLength(1)
|
||||
const tools = fixture.requests[0]!.body.tools as Array<Record<string, unknown>>
|
||||
expect(tools).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ type: 'function', name: 'exec_command' }),
|
||||
]))
|
||||
expect(commandCalls.some(call => tools.some(tool => (
|
||||
tool.type === 'function' && tool.name === call.name
|
||||
)))).toBe(true)
|
||||
expect(fixture.requests.every(requestEntry =>
|
||||
requestEntry.headers.authorization === 'Bearer dsh-fake-openai-key',
|
||||
)).toBe(true)
|
||||
|
||||
@@ -22,6 +22,13 @@ export type ResponsesBehavior =
|
||||
readonly name: string
|
||||
readonly arguments: Record<string, unknown>
|
||||
}
|
||||
| {
|
||||
readonly kind: 'advertisedFunctionCall'
|
||||
readonly choices: readonly {
|
||||
readonly name: string
|
||||
readonly arguments: Record<string, unknown>
|
||||
}[]
|
||||
}
|
||||
| { readonly kind: 'hold' }
|
||||
|
||||
/** Running package-private Responses fixture. */
|
||||
@@ -218,6 +225,18 @@ function closeServer(server: Server): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function advertisedFunctionNames(body: Record<string, unknown>): Set<string> {
|
||||
if (!Array.isArray(body.tools)) return new Set()
|
||||
return new Set(body.tools.flatMap((tool): string[] => (
|
||||
tool !== null
|
||||
&& typeof tool === 'object'
|
||||
&& (tool as Record<string, unknown>).type === 'function'
|
||||
&& typeof (tool as Record<string, unknown>).name === 'string'
|
||||
? [(tool as Record<string, unknown>).name as string]
|
||||
: []
|
||||
)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a loopback-only Responses SSE fixture.
|
||||
* @param script - one behavior per expected Responses request.
|
||||
@@ -234,11 +253,12 @@ export async function startResponsesFixture(
|
||||
openResponses.add(response)
|
||||
response.on('close', () => { openResponses.delete(response) })
|
||||
void readRequest(request).then((body) => {
|
||||
const parsedBody = JSON.parse(body) as Record<string, unknown>
|
||||
requests.push({
|
||||
method: request.method,
|
||||
path: request.url,
|
||||
headers: request.headers,
|
||||
body: JSON.parse(body) as Record<string, unknown>,
|
||||
body: parsedBody,
|
||||
})
|
||||
started.resolve(undefined)
|
||||
const behavior = behaviors.shift()
|
||||
@@ -247,6 +267,14 @@ export async function startResponsesFixture(
|
||||
response.end(JSON.stringify({ error: { message: 'fixture script exhausted' } }))
|
||||
return
|
||||
}
|
||||
const advertisedCall = behavior.kind === 'advertisedFunctionCall'
|
||||
? behavior.choices.find(choice => advertisedFunctionNames(parsedBody).has(choice.name))
|
||||
: undefined
|
||||
if (behavior.kind === 'advertisedFunctionCall' && advertisedCall === undefined) {
|
||||
response.writeHead(500, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({ error: { message: 'none of the fixture function calls was advertised' } }))
|
||||
return
|
||||
}
|
||||
response.writeHead(200, {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
@@ -254,9 +282,15 @@ export async function startResponsesFixture(
|
||||
'x-request-id': 'req_fixture',
|
||||
})
|
||||
if (behavior.kind === 'hold') return
|
||||
const events = behavior.kind === 'complete'
|
||||
? completeResponsesEvents(behavior.text)
|
||||
: functionCallEvents(behavior.name, behavior.arguments)
|
||||
let events: Record<string, unknown>[]
|
||||
if (behavior.kind === 'complete') {
|
||||
events = completeResponsesEvents(behavior.text)
|
||||
} else {
|
||||
const call = behavior.kind === 'functionCall'
|
||||
? behavior
|
||||
: advertisedCall!
|
||||
events = functionCallEvents(call.name, call.arguments)
|
||||
}
|
||||
for (const event of events) {
|
||||
response.write(`data: ${JSON.stringify(event)}\n\n`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user