mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
The local PTY readiness poll held its inferred_idle fallback for exactly one pollIntervalMs after a prompt marker, so a bash foreground handoff that lands on the silence boundary only wins the exact stdin_read attribution when the kernel publishes it inside that single poll. On a slow or loaded host it does not, and the attribution flips. handoffGraceMs replaces the hardcoded one-poll window as a validated, deployment-owned config field defaulting to 500ms, rejected at load when it cannot contain one readiness poll. Real-shell tests that interrupt a send now assert the session is usable again rather than which readiness tier observed the handoff, because no fixed grace removes the race.
122 lines
5.2 KiB
TypeScript
122 lines
5.2 KiB
TypeScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
import Include from '@cordisjs/plugin-include'
|
|
import { CallId } from '@deepseek-ai/dsh-llm'
|
|
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
|
import PtyService from '@deepseek-ai/dsh-pty'
|
|
import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
|
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
|
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
|
|
import * as PtyLocal from '@deepseek-ai/dsh-pty-local'
|
|
import * as ToolPty from '@deepseek-ai/dsh-tool-pty'
|
|
|
|
let root: string | undefined
|
|
let context: Context | undefined
|
|
|
|
afterEach(async () => {
|
|
await context?.fiber.dispose()
|
|
context = undefined
|
|
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
|
root = undefined
|
|
})
|
|
|
|
class PassthroughSandbox extends SandboxProvider {
|
|
confine(argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv {
|
|
return { argv: [...argv], enforcement: 'full', denialSignatures: [], runnerFailureSignatures: [] }
|
|
}
|
|
}
|
|
|
|
function agent(ctx: Context): Agent {
|
|
const scope = ctx.plugin(() => {})
|
|
const id = SessionId('pty-loader-agent')
|
|
const value: Agent = {
|
|
id, options: {}, session: new Session(id), status: 'idle', ctx: scope.ctx,
|
|
followup: () => AgentMessageId('stub'), queue: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
|
}
|
|
ctx.agents.register(value)
|
|
return value
|
|
}
|
|
|
|
function resultText(result: { content: { type: string; text?: string }[] }): string {
|
|
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
|
}
|
|
|
|
const suite = process.platform === 'linux' || process.platform === 'darwin' ? describe : describe.skip
|
|
|
|
suite('terminal real Loader composition through cordis.yml', () => {
|
|
it('boots cordis.yml and preserves shell state across real tool calls', async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'dsh-pty-loader-'))
|
|
const configPath = join(root, 'cordis.yml')
|
|
await writeFile(configPath, [
|
|
"- name: '@deepseek-ai/dsh-agent'",
|
|
"- name: '@deepseek-ai/dsh-system-prompt'",
|
|
"- name: '@deepseek-ai/dsh-tools'",
|
|
"- name: '@deepseek-ai/dsh-pty'",
|
|
"- name: '@deepseek-ai/dsh-test-sandbox'",
|
|
"- name: '@deepseek-ai/dsh-sandbox-policy'",
|
|
' config:',
|
|
' mode: danger-full-access',
|
|
` workspaceRoot: ${JSON.stringify(root)}`,
|
|
"- name: '@deepseek-ai/dsh-pty-local'",
|
|
' config:',
|
|
' pollIntervalMs: 10',
|
|
' exactProbeAfterMs: 20',
|
|
' idleSilenceMs: 250',
|
|
' handoffGraceMs: 250',
|
|
' timeoutMs: 2000',
|
|
' disposeGraceMs: 500',
|
|
"- name: '@deepseek-ai/dsh-tool-pty'",
|
|
'',
|
|
].join('\n'))
|
|
|
|
context = new Context()
|
|
context.baseUrl = pathToFileURL(root).href + '/'
|
|
await context.plugin(Loader)
|
|
context.loader.builtins.include = Include
|
|
const modules = new Map<string, unknown>([
|
|
['@deepseek-ai/dsh-agent', AgentRegistry],
|
|
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
|
|
['@deepseek-ai/dsh-tools', ToolRegistry],
|
|
['@deepseek-ai/dsh-pty', PtyService],
|
|
['@deepseek-ai/dsh-test-sandbox', PassthroughSandbox],
|
|
['@deepseek-ai/dsh-sandbox-policy', SandboxPolicyService],
|
|
['@deepseek-ai/dsh-pty-local', PtyLocal],
|
|
['@deepseek-ai/dsh-tool-pty', ToolPty],
|
|
])
|
|
context.loader.internal = {
|
|
version: 'v2',
|
|
async import(specifier: string) {
|
|
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
|
|
return modules.get(specifier)
|
|
},
|
|
} as unknown as NonNullable<typeof context.loader.internal>
|
|
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
|
|
await context.loader.await()
|
|
|
|
const owner = agent(context)
|
|
const signal = new AbortController().signal
|
|
const spawn = await context.tools.execute({
|
|
signal, callId: CallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner,
|
|
})
|
|
expect(resultText(spawn)).toContain('started terminal session pty-1 (main)')
|
|
|
|
await context.tools.execute({
|
|
signal, callId: CallId('state'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner,
|
|
})
|
|
const read = await context.tools.execute({
|
|
signal, callId: CallId('read'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner,
|
|
})
|
|
expect(resultText(read)).toContain('cwd=/ keep=loader')
|
|
expect(context.pty.list(owner)).toHaveLength(1)
|
|
}, 15_000)
|
|
})
|