test(windows): make native coverage fixtures portable

This commit is contained in:
Tianyi Cui
2026-08-08 20:36:31 +08:00
parent 85e9f3b7cb
commit f96be1db13
6 changed files with 47 additions and 16 deletions

View File

@@ -62,6 +62,17 @@ async function expectFlushError(promise: Promise<unknown>, message: RegExp): Pro
throw new Error('expected flush to reject')
}
async function expectFlushCode(promise: Promise<unknown>, codes: readonly string[]): Promise<void> {
try {
await promise
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(codes).toContain((error as NodeJS.ErrnoException).code)
return
}
throw new Error('expected flush to reject')
}
async function freshRoot(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'dsh-jsonl-'))
dirs.push(dir)
@@ -1343,7 +1354,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
s = inner.sessions.create(SessionId('exists-fault'), { meta: { cwd } })
appendClosedTurn(s)
}, { inject: ['sessions'] }))
await expect(ctx2.sessions.flush(s)).rejects.toThrow(/EEXIST|ENOTDIR/)
await expectFlushCode(ctx2.sessions.flush(s), ['EEXIST', 'ENOTDIR'])
await ctx2.fiber.dispose()
})

View File

@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import * as acp from '../src/index.ts'
import { acpStopReason, acpContentText, DEFAULT_DISPOSE_EOF_GRACE_MS, DEFAULT_DISPOSE_GRACE_MS, disposeAcpChild, startAcpRun, toAcpPrompt, type AcpRunSpec } from '../src/run.ts'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
@@ -108,14 +109,19 @@ describe('child env layering (through the subprocess seam)', () => {
// The spec.env layer merges after the seam's scrub, so the child's own
// explicitly-forwarded key survives while ambient credentials do not.
const running = spawnSubprocess({
argv: ['bash', '-c', 'echo "[${ACP_TEST_AMBIENT_SECRET_TOKEN:-absent}|$DEEPSEEK_API_KEY]"'],
argv: [
process.execPath,
'--input-type=module',
'--eval',
'process.stdout.write(JSON.stringify([process.env.ACP_TEST_AMBIENT_SECRET_TOKEN ?? "absent", process.env.DEEPSEEK_API_KEY]))',
],
cwd: process.cwd(),
stdio: { stdin: 'ignore', stdout: { maxBytes: 1000 }, stderr: { maxBytes: 1000 } },
graceMs: 1000,
env: { DEEPSEEK_API_KEY: 'explicit' },
})
await running.done
expect(running.collected.stdout!.readFrom(0).text.trim()).toBe('[absent|explicit]')
expect(running.collected.stdout!.readFrom(0).text).toBe('["absent","explicit"]')
} finally {
delete process.env.ACP_TEST_AMBIENT_SECRET_TOKEN
}
@@ -139,42 +145,50 @@ describe('child env layering (through the subprocess seam)', () => {
})
describe('disposeAcpChild (the backend-owned teardown ladder over seam verbs)', () => {
const bash = (command: string, stdin: 'pipe' | 'ignore' = 'pipe') => spawnSubprocess({
argv: ['bash', '-c', command],
const node = (source: string, stdin: 'pipe' | 'ignore' = 'pipe') => spawnSubprocess({
argv: [process.execPath, '--input-type=module', '--eval', source],
cwd: process.cwd(),
stdio: { stdin, stdout: { maxBytes: 1000 }, stderr: { maxBytes: 1000 } },
graceMs: 200,
})
const expectHostTermination = (outcome: SubprocessOutcome, posixSignal: NodeJS.Signals): void => {
if (process.platform === 'win32') {
expect(outcome.signal).toBeNull()
expect(outcome.exitCode).not.toBe(0)
} else {
expect(outcome.signal).toBe(posixSignal)
}
}
it('tier 1: a cooperative child exits on stdin EOF without any signal', async () => {
const child = bash('read -r line; exit 0')
const child = node('process.stdin.resume(); process.stdin.on("end", () => process.exit(0))')
await disposeAcpChild(child, 5_000)
const outcome = await child.done
expect(outcome.exitCode).toBe(0)
expect(outcome.signal).toBeNull()
})
it('tier 2: an EOF-deaf child dies by the terminate escalation (SIGTERM)', async () => {
const child = bash('sleep 60')
it('tier 2: an EOF-deaf child reaches the host terminate outcome', async () => {
const child = node('setInterval(() => {}, 60_000)')
await disposeAcpChild(child, 100)
const outcome = await child.done
expect(outcome.signal).toBe('SIGTERM')
expectHostTermination(outcome, 'SIGTERM')
})
it('tier 3: a TERM-trapping child dies by the escalation SIGKILL', async () => {
const child = bash("trap '' TERM; echo armed; sleep 60", 'ignore')
it('tier 3: a TERM-trapping child reaches the host force-termination outcome', async () => {
const child = node('process.on("SIGTERM", () => {}); process.stdout.write("armed\\n"); setInterval(() => {}, 60_000)', 'ignore')
// Wait for the trap to arm so SIGTERM cannot race the default handler.
while (!child.collected.stdout!.readFrom(0).text.includes('armed')) {
await new Promise(resolve => setTimeout(resolve, 10))
}
await disposeAcpChild(child, 50)
const outcome = await child.done
expect(outcome.signal).toBe('SIGKILL')
expectHostTermination(outcome, 'SIGKILL')
})
it('observes a spawn-level rejection and returns without a process to reap', async () => {
const child = spawnSubprocess({
argv: ['bash', '-c', 'true'],
argv: [process.execPath, '--input-type=module', '--eval', ''],
cwd: '/nonexistent-dir-dsh-acp-ladder-test',
stdio: { stdin: 'ignore', stdout: { maxBytes: 1000 }, stderr: { maxBytes: 1000 } },
graceMs: 200,