Files
ycode/scripts/claude-kernel-smoke.mjs

209 lines
8.3 KiB
JavaScript

import assert from 'node:assert/strict'
import { createServer } from 'node:http'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { createSdkMcpServer, query, tool } from '@anthropic-ai/claude-agent-sdk'
import { z } from 'zod'
const executable = resolve(process.env.YCODE_CLAUDE_EXE ?? 'node_modules/@anthropic-ai/claude-agent-sdk-win32-x64/claude.exe')
const temp = await mkdtemp(join(tmpdir(), 'ycode-claude-smoke-'))
const requests = []
function sse(res, event, data) {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
}
function textResponse(res, id, model, text) {
sse(res, 'message_start', {
type: 'message_start',
message: {
id, type: 'message', role: 'assistant', model,
content: [], stop_reason: null, stop_sequence: null,
usage: { input_tokens: 12, output_tokens: 1 }
}
})
sse(res, 'content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } })
sse(res, 'content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } })
sse(res, 'content_block_stop', { type: 'content_block_stop', index: 0 })
sse(res, 'message_delta', {
type: 'message_delta',
delta: { stop_reason: 'end_turn', stop_sequence: null },
usage: { output_tokens: 5 }
})
sse(res, 'message_stop', { type: 'message_stop' })
res.end()
}
const server = createServer((req, res) => {
let raw = ''
req.on('data', chunk => { raw += chunk })
req.on('end', () => {
let body
try { body = JSON.parse(raw || '{}') } catch { body = { raw } }
requests.push({ url: req.url, method: req.method, body })
if (!req.url?.includes('/messages')) {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ data: [] }))
return
}
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
})
const id = `msg_smoke_${requests.length}`
const latestUser = [...(body.messages ?? [])].reverse().find(message => message.role === 'user')
const latestUserContent = JSON.stringify(latestUser?.content ?? [])
if (process.env.DEBUG_CLAUDE_SMOKE === '1') console.error('smoke request', requests.length, latestUserContent.slice(0, 500))
const hasToolResult = latestUserContent.includes('"tool_result"')
const wantsRead = latestUserContent.includes('Read the smoke file.')
if (hasToolResult) {
textResponse(res, id, body.model ?? 'claude-smoke', 'Tool read complete.')
return
}
if (wantsRead && !hasToolResult) {
sse(res, 'message_start', {
type: 'message_start',
message: {
id, type: 'message', role: 'assistant', model: body.model ?? 'claude-smoke',
content: [], stop_reason: null, stop_sequence: null,
usage: { input_tokens: 12, output_tokens: 1 }
}
})
sse(res, 'content_block_start', {
type: 'content_block_start', index: 0,
content_block: { type: 'tool_use', id: 'tool_smoke', name: 'Read', input: {} }
})
sse(res, 'content_block_delta', {
type: 'content_block_delta', index: 0,
delta: { type: 'input_json_delta', partial_json: JSON.stringify({ file_path: join(temp, 'smoke.txt') }) }
})
sse(res, 'content_block_stop', { type: 'content_block_stop', index: 0 })
sse(res, 'message_delta', {
type: 'message_delta',
delta: { stop_reason: 'tool_use', stop_sequence: null },
usage: { output_tokens: 5 }
})
sse(res, 'message_stop', { type: 'message_stop' })
res.end()
return
}
textResponse(res, id, body.model ?? 'claude-smoke', 'Claude mock reply.')
})
})
await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen))
const address = server.address()
if (!address || typeof address === 'string') throw new Error('mock server did not bind')
const sessionId = crypto.randomUUID()
let runtimeSessionId = sessionId
let releaseSecondTurn
const firstTurnComplete = new Promise(resolve => { releaseSecondTurn = resolve })
const input = (async function* () {
yield {
type: 'user',
message: { role: 'user', content: [{ type: 'text', text: 'Read the smoke file.' }] },
parent_tool_use_id: null,
uuid: crypto.randomUUID(),
session_id: runtimeSessionId
}
await firstTurnComplete
yield {
type: 'user',
message: { role: 'user', content: [{ type: 'text', text: 'Reply once more.' }] },
parent_tool_use_id: null,
uuid: crypto.randomUUID(),
session_id: runtimeSessionId
}
})()
await writeFile(join(temp, 'smoke.txt'), 'SMOKE_FILE_CONTENTS', 'utf8')
const messages = []
const results = []
let instance
try {
instance = query({
prompt: input,
options: {
cwd: temp,
model: 'claude-smoke',
pathToClaudeCodeExecutable: executable,
env: {
...process.env,
ANTHROPIC_BASE_URL: `http://127.0.0.1:${address.port}`,
ANTHROPIC_API_KEY: 'ycode-smoke-key',
CLAUDE_CONFIG_DIR: join(temp, 'claude-config'),
CLAUDE_CODE_ENABLE_TODO_TOOLS: '1',
CLAUDE_CODE_ENABLE_TASKS: '0',
...(process.platform === 'win32' ? { CLAUDE_CODE_USE_POWERSHELL_TOOL: '1' } : {})
},
includePartialMessages: true,
permissionMode: 'default',
permissionPrompts: 'host',
settingSources: [],
strictMcpConfig: true,
tools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'PowerShell', 'WebFetch', 'WebSearch',
'TodoWrite', 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode'],
allowedTools: [],
disallowedTools: ['Agent', 'Task', 'Skill', 'Workflow', 'CronCreate', 'CronDelete', 'CronList'],
plugins: [],
agents: {},
skills: [],
mcpServers: {
ycode: createSdkMcpServer({
name: 'ycode',
tools: [tool('ping', 'Test host tool', { value: z.string() }, async ({ value }) => ({
content: [{ type: 'text', text: value }]
}))]
})
},
canUseTool: async (toolName) => toolName === 'Read'
? { behavior: 'allow', updatedInput: { file_path: join(temp, 'smoke.txt') } }
: { behavior: 'deny', message: 'Only Read is enabled in this smoke test.' }
}
})
for await (const message of instance) {
messages.push(message)
if (message.type === 'system' && message.subtype === 'init' && message.session_id) {
runtimeSessionId = message.session_id
}
if (message.type === 'result') {
results.push(message)
if (results.length === 1) releaseSecondTurn()
else break
}
}
} finally {
instance?.close()
await new Promise(resolveClose => server.close(resolveClose))
await rm(temp, { recursive: true, force: true })
}
const result = results.at(-1)
const streamed = messages.filter(message => message.type === 'stream_event')
.map(message => message.event)
.filter(event => event?.type === 'content_block_delta' && event.delta?.type === 'text_delta')
.map(event => event.delta.text)
.join('')
const toolResult = messages.find(message => message.type === 'user' &&
Array.isArray(message.message?.content) &&
message.message.content.some(block => block.type === 'tool_result'))
assert.ok(requests.some(request => request.url?.includes('/messages')), 'Claude Code should call the Anthropic Messages endpoint')
assert.equal(results.length, 2, 'one process should serve two consecutive turns')
assert.equal(new Set(results.map(message => message.session_id)).size, 1,
'both turns should stay in the same Claude session')
assert.equal(streamed, 'Tool read complete.Claude mock reply.')
assert.ok(toolResult, 'the native Read tool result should return to Claude')
assert.equal(result?.subtype, 'success')
assert.equal(result?.result, 'Claude mock reply.')
const init = messages.find(message => message.type === 'system' && message.subtype === 'init')
assert.ok(init?.tools.includes('TodoWrite'), 'TodoWrite should be enabled')
if (process.platform === 'win32') assert.ok(init?.tools.includes('PowerShell'), 'PowerShell should be enabled on Windows')
assert.equal(init?.tools.some(tool => ['Agent', 'Task', 'Skill', 'Workflow', 'CronCreate'].includes(tool)), false)
console.log(JSON.stringify({ result: 'PASS', requests: requests.length, messages: messages.length, turns: results.length, text: result.result }))