chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+246
View File
@@ -0,0 +1,246 @@
/**
* 最小闭环冒烟测试(不依赖 Electron、不依赖真实 API Key)。
*
* 用一个本地假上游(OpenAI 兼容 SSE)驱动真实的 gateway + agent loop,
* 验证:SSE 流式解析 → 流式工具调用组装 → 工具执行 → 上下文回填 → 终态。
*
* 运行:npm run smoke
*/
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Gateway } from '../src/main/gateway'
import { runTurn } from '../src/main/agent/loop'
import { executeTool } from '../src/main/agent/tools'
import type { ChatEvent, ChatMessage } from '../src/shared/types'
let failures = 0
function check(label: string, condition: boolean, detail = ''): void {
if (condition) {
console.log(` ✓ ${label}`)
} else {
failures++
console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`)
}
}
function sseChunk(payload: unknown): string {
return `data: ${JSON.stringify(payload)}\n\n`
}
/** 假上游:第 1 次请求返回工具调用,第 2 次返回最终文本 */
function startFakeUpstream(): Promise<{ server: Server; port: number; requests: unknown[] }> {
const requests: unknown[] = []
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
let raw = ''
req.on('data', (c) => (raw += c))
req.on('end', () => {
const body = JSON.parse(raw || '{}')
requests.push(body)
const round = requests.length
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
})
if (round === 1) {
// 分片下发工具调用,模拟真实上游的增量行为
res.write(
sseChunk({
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_abc',
type: 'function',
function: { name: 'list_dir', arguments: '{"pa' }
}
]
},
finish_reason: null
}
]
})
)
res.write(
sseChunk({
choices: [
{
index: 0,
delta: {
tool_calls: [{ index: 0, function: { arguments: 'th":"."}' } }]
},
finish_reason: null
}
]
})
)
res.write(sseChunk({ choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }] }))
} else {
for (const piece of ['工作目录里有 ', 'a.txt', ' 和 ', 'b.txt', '。']) {
res.write(sseChunk({ choices: [{ index: 0, delta: { content: piece } }] }))
}
res.write(sseChunk({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] }))
}
res.write('data: [DONE]\n\n')
res.end()
})
})
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => {
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
resolve({ server, port, requests })
})
})
}
async function main(): Promise<void> {
// ---------- 准备临时工作目录 ----------
const workspaceRoot = await mkdtemp(join(tmpdir(), 'ycode-smoke-'))
await writeFile(join(workspaceRoot, 'a.txt'), 'hello a', 'utf8')
await writeFile(join(workspaceRoot, 'b.txt'), 'hello b', 'utf8')
const { server, port, requests } = await startFakeUpstream()
try {
const gateway = new Gateway({
baseUrl: `http://127.0.0.1:${port}/v1`,
model: 'fake-model',
apiKey: 'test-key',
temperature: 0.7,
maxToolRounds: 4
})
const events: ChatEvent[] = []
const history: ChatMessage[] = []
console.log('\n[1] Agent 闭环:工具调用 → 执行 → 回填 → 终态')
const result = await runTurn({
runId: 'run-smoke',
history,
userText: '列出工作目录里有哪些文件',
signal: new AbortController().signal,
emit: (event) => events.push(event),
gateway,
model: 'fake-model',
maxToolRounds: 4,
workspaceRoot
})
check('上游被调用 2 次(工具轮 + 收尾轮)', requests.length === 2, `实际 ${requests.length}`)
const textDeltas = events.filter((e) => e.type === 'text')
check('收到流式文本增量 ≥ 5 段', textDeltas.length >= 5, `实际 ${textDeltas.length}`)
check(
'最终文本拼接正确',
result.content === '工作目录里有 a.txt 和 b.txt。',
`实际「${result.content}」`
)
check('产生 1 条工具调用记录', result.tools.length === 1, `实际 ${result.tools.length}`)
const tool = result.tools[0]
check('工具名为 list_dir', tool?.name === 'list_dir', `实际 ${tool?.name}`)
check(
'流式分片的 arguments 被正确拼成 JSON',
JSON.stringify(tool?.args) === JSON.stringify({ path: '.' }),
`实际 ${JSON.stringify(tool?.args)}`
)
check('工具执行成功', tool?.status === 'ok', `实际 ${tool?.status}`)
check('工具结果含 a.txt', tool?.summary?.includes('a.txt') === true)
const doneEvent = events.find((e) => e.type === 'done')
check('发出 done 终态事件', doneEvent !== undefined)
// 第 2 次请求应包含回填的 assistant(tool_calls) 与 tool 结果
const second = requests[1] as { messages: Array<Record<string, unknown>> }
check(
'第 2 次请求回填了 assistant.tool_calls',
Array.isArray(second?.messages) &&
second.messages.some((m) => m.role === 'assistant' && Array.isArray(m.tool_calls))
)
check(
'第 2 次请求回填了 tool 角色结果',
Array.isArray(second?.messages) &&
second.messages.some(
(m) => m.role === 'tool' && String(m.content ?? '').includes('a.txt')
)
)
console.log('\n[2] 工具安全边界:工作区逃逸必须被拒绝')
const escape = await executeTool('read_file', { path: '../../../../etc/passwd' }, { workspaceRoot })
check('越界读取被拒绝', escape.ok === false, `实际 ok=${escape.ok}`)
check('拒绝理由可读', escape.content.includes('越界'))
const unknown = await executeTool('not_a_tool', {}, { workspaceRoot })
check('未知工具被拒绝', unknown.ok === false)
const normal = await executeTool('read_file', { path: 'a.txt' }, { workspaceRoot })
check('工作区内读取正常', normal.ok === true && normal.content.includes('hello a'))
console.log('\n[3] 取消与错误传播')
const abortController = new AbortController()
abortController.abort()
const cancelled = await runTurn({
runId: 'run-cancel',
history: [],
userText: 'x',
signal: abortController.signal,
emit: () => {},
gateway,
model: 'fake-model',
maxToolRounds: 4,
workspaceRoot
}).then(
() => 'resolved',
(err: Error) => err.name
)
check('已取消的信号会中断回合', cancelled === 'AbortError', `实际 ${cancelled}`)
const badGateway = new Gateway({
baseUrl: `http://127.0.0.1:${port}/v1`,
model: 'fake-model',
apiKey: '',
temperature: 0.7,
maxToolRounds: 2
})
const missingKey = await runTurn({
runId: 'run-nokey',
history: [],
userText: 'x',
signal: new AbortController().signal,
emit: () => {},
gateway: badGateway,
model: 'fake-model',
maxToolRounds: 2,
workspaceRoot
}).then(
() => 'resolved',
(err: Error) => err.message
)
check('缺少 Key 时给出明确错误', String(missingKey).includes('API Key'), `实际 ${missingKey}`)
} finally {
server.close()
await rm(workspaceRoot, { recursive: true, force: true })
}
console.log(failures === 0 ? '\n全部通过 ✓\n' : `\n${failures} 项失败 ✗\n`)
process.exit(failures === 0 ? 0 : 1)
}
main().catch((err) => {
console.error('冒烟测试异常终止:', err)
process.exit(1)
})