test(windows): make native coverage fixtures portable
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user