feat(agent-presets): add Codex and Claude Code subagent tools
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
rmSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { Context } from 'cordis'
|
||||
@@ -87,6 +87,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||
]) mkdirSync(directory)
|
||||
|
||||
const env = {
|
||||
PATH: `${dirname(claudeBin)}${delimiter}${process.env.PATH ?? ''}`,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
|
||||
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import {
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import type {
|
||||
@@ -19,7 +21,7 @@ import { Context } from 'cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import * as claudeCode from '../src/index.ts'
|
||||
import {
|
||||
@@ -98,9 +100,11 @@ afterEach(async () => {
|
||||
interface RealHarness {
|
||||
readonly ctx: Context
|
||||
readonly handles: SubprocessHandle[]
|
||||
readonly spawnSpecs: SubprocessSpawnSpec[]
|
||||
readonly parent: Agent
|
||||
readonly workspace: string
|
||||
readonly env: Record<string, string>
|
||||
readonly executable: string
|
||||
}
|
||||
|
||||
async function realHarness(behavior: MessagesBehavior): Promise<{
|
||||
@@ -112,9 +116,14 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
|
||||
const workspace = join(root, 'workspace')
|
||||
const claudeConfig = join(root, 'claude-config')
|
||||
const xdgConfig = join(root, 'xdg')
|
||||
const nativeBin = join(root, 'native-bin')
|
||||
mkdirSync(workspace)
|
||||
mkdirSync(claudeConfig)
|
||||
mkdirSync(xdgConfig)
|
||||
mkdirSync(nativeBin)
|
||||
const executable = join(nativeBin, process.platform === 'win32' ? 'claude.exe' : 'claude')
|
||||
if (process.platform === 'win32') copyFileSync(claudeBin, executable)
|
||||
else symlinkSync(claudeBin, executable)
|
||||
writeFileSync(
|
||||
join(claudeConfig, 'settings.json'),
|
||||
`${JSON.stringify({ model: settingsModel }, null, 2)}\n`,
|
||||
@@ -122,6 +131,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
|
||||
const fixture = await startMessagesFixture(behavior)
|
||||
fixtures.push(fixture)
|
||||
const env = {
|
||||
PATH: `${nativeBin}${delimiter}${process.env.PATH ?? ''}`,
|
||||
ANTHROPIC_API_KEY: fakeKey,
|
||||
ANTHROPIC_BASE_URL: fixture.baseUrl,
|
||||
CLAUDE_CONFIG_DIR: claudeConfig,
|
||||
@@ -141,8 +151,10 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
const handles: SubprocessHandle[] = []
|
||||
const spawnSpecs: SubprocessSpawnSpec[] = []
|
||||
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
|
||||
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
|
||||
spawnSpecs.push(spec)
|
||||
const handle = spawn(spec)
|
||||
handles.push(handle)
|
||||
return handle
|
||||
@@ -153,7 +165,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
|
||||
session: { header: { cwd: workspace } },
|
||||
} as unknown as Agent
|
||||
return {
|
||||
harness: { ctx, handles, parent, workspace, env },
|
||||
harness: { ctx, handles, spawnSpecs, parent, workspace, env, executable },
|
||||
fixture,
|
||||
}
|
||||
}
|
||||
@@ -195,7 +207,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
|
||||
expect(sdkPackage.version).toBe('0.3.220')
|
||||
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
|
||||
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
|
||||
const version = await execFileAsync(claudeBin, ['--version'], {
|
||||
const version = await execFileAsync(harness.executable, ['--version'], {
|
||||
env: { ...process.env, ...harness.env },
|
||||
})
|
||||
expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
|
||||
@@ -212,6 +224,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
|
||||
message.type === 'system' && message.subtype === 'init',
|
||||
)
|
||||
expect(initMessage?.claude_code_version).toBe('2.1.220')
|
||||
expect(harness.spawnSpecs[0]?.argv[0]).toBe(harness.executable)
|
||||
|
||||
expect(fixture.requests).toHaveLength(1)
|
||||
const recorded = fixture.requests[0]!
|
||||
|
||||
@@ -248,6 +248,7 @@ function fakeRun(
|
||||
const options: FakeRun['options'] = []
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
env: { ANTHROPIC_API_KEY: 'fake-key' },
|
||||
disposeGraceMs: 5,
|
||||
spawn: (spawnSpec) => {
|
||||
@@ -331,6 +332,8 @@ describe('task admission and package contracts', () => {
|
||||
const child = fakeChild()
|
||||
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
|
||||
.mockImplementation(() => child.handle)
|
||||
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable')
|
||||
.mockResolvedValue('/native/claude')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
await ctx.plugin(claudeCode, {
|
||||
env: {
|
||||
@@ -352,6 +355,11 @@ describe('task admission and package contracts', () => {
|
||||
)
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
resolveExecutable.mockRejectedValueOnce(new Error('claude missing from PATH'))
|
||||
await expect(ctx.subagents.start('claude-code', request()))
|
||||
.rejects.toThrow('claude missing from PATH')
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
const run = await ctx.subagents.start('claude-code', request())
|
||||
child.settle({ exitCode: 9, signal: null })
|
||||
child.stdout.end()
|
||||
@@ -362,6 +370,13 @@ describe('task admission and package contracts', () => {
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
'subagent-claude-code: child run failed (error):',
|
||||
))
|
||||
expect(resolveExecutable).toHaveBeenCalledWith(
|
||||
'claude',
|
||||
expect.objectContaining({ ANTHROPIC_API_KEY: 'provider-fake-key' }),
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
expect(queryMock.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable)
|
||||
.toBe('/native/claude')
|
||||
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cwd: process.cwd(),
|
||||
graceMs: 29,
|
||||
@@ -441,6 +456,19 @@ describe('official spawn projection', () => {
|
||||
)).toThrow('SDK spawn request omitted its workspace')
|
||||
})
|
||||
|
||||
it.each(['cmd', 'bat'])('routes a Windows .%s shim through cmd.exe', (extension) => {
|
||||
const command = String.raw`C:\Program Files\Claude\claude.${extension}`
|
||||
const spec = claudeSpawnSpec(sdkSpawnOptions({
|
||||
command,
|
||||
args: ['--output-format', 'stream-json'],
|
||||
}), 7, 'win32')
|
||||
|
||||
expect(spec.argv).toEqual([
|
||||
'cmd.exe', '/d', '/s', '/c', command,
|
||||
'--output-format', 'stream-json',
|
||||
])
|
||||
})
|
||||
|
||||
it('projects streams, exit facts, listeners, and idempotent tree termination', async () => {
|
||||
const child = fakeChild({ exitOnTerminate: false })
|
||||
const process = new ManagedClaudeCodeProcess(child.handle)
|
||||
@@ -508,6 +536,7 @@ describe('query options and result mapping', () => {
|
||||
const captured: SubprocessHandle[] = []
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
env: {
|
||||
HOST_VISIBLE: 'overridden',
|
||||
ANTHROPIC_API_KEY: 'explicit-fake-key',
|
||||
@@ -523,6 +552,7 @@ describe('query options and result mapping', () => {
|
||||
expect(options).toMatchObject({
|
||||
abortController: controller,
|
||||
cwd: '/workspace',
|
||||
pathToClaudeCodeExecutable: '/native/claude',
|
||||
persistSession: false,
|
||||
disallowedTools: ['AskUserQuestion'],
|
||||
})
|
||||
@@ -670,6 +700,7 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
let index = 0
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
env: {},
|
||||
disposeGraceMs: 5,
|
||||
spawn: () => children[index++]!.handle,
|
||||
@@ -720,6 +751,7 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
request(undefined, parentAbort.signal),
|
||||
{
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
env: {},
|
||||
disposeGraceMs: 5,
|
||||
spawn: () => child.handle,
|
||||
|
||||
Reference in New Issue
Block a user