Files
goodbuddy/src/main/agent/continue-host-adapter.test.ts
T

1155 lines
34 KiB
TypeScript

import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile
} from 'node:fs/promises'
import { existsSync, readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
ContinueHostAdapter,
type ContinueHostLauncher
} from './continue-host-adapter'
const temporaryDirectories: string[] = []
const environmentRestorations: Array<() => void> = []
const inheritedProviderCredentials = {
ANTHROPIC_API_KEY: 'inherited-anthropic',
OPENAI_API_KEY: 'inherited-openai',
GOOGLE_GENERATIVE_AI_API_KEY: 'inherited-google',
GEMINI_API_KEY: 'inherited-gemini',
AWS_ACCESS_KEY_ID: 'inherited-aws-access',
AWS_SECRET_ACCESS_KEY: 'inherited-aws-secret',
AWS_SESSION_TOKEN: 'inherited-aws-session',
AWS_PROFILE: 'inherited-aws-profile',
OPENROUTER_API_KEY: 'inherited-openrouter'
} as const
function inheritProviderCredentials(): void {
const previousEnvironment = Object.fromEntries(
Object.keys(inheritedProviderCredentials).map((name) => [
name,
process.env[name]
])
)
Object.assign(process.env, inheritedProviderCredentials)
environmentRestorations.push(() => {
for (const [name, value] of Object.entries(previousEnvironment)) {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
})
}
async function createDistribution(version = '1.5.47'): Promise<{
cacheRoot: string
entryPath: string
sourceHash: string
}> {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-continue-host-'))
temporaryDirectories.push(root)
const distribution = join(root, 'package', 'dist')
const cacheRoot = join(root, 'cache')
await mkdir(distribution, { recursive: true })
await writeFile(
join(root, 'package', 'package.json'),
JSON.stringify({ version }),
'utf8'
)
await writeFile(join(distribution, 'cn.js'), 'import "./index.js"\n', 'utf8')
await writeFile(join(distribution, 'xhr-sync-worker.js'), '', 'utf8')
const sourceBundle = [
'toolPermissionOverrides:s,headless:!0});let[a,u,l,c]',
'i={allow:o.allow,ask:o.ask,exclude:o.exclude,isHeadless:e.headless}',
'E6t.initialize({isHeadless:e.headless},r,n)',
'function ZZo(e){let t=[];if(e.exclude)for(let n of e.exclude){let r=n;t.push({tool:r,permission:"exclude"})}if(e.ask)for(let n of e.ask){let r=n;t.push({tool:r,permission:"ask"})}if(e.allow)for(let n of e.allow){let r=n;t.push({tool:r,permission:"allow"})}return t}',
'let j=(0,atn.default)();j.use(atn.default.json()),j.get("/state"',
'listen(i,async()=>{console.log(Ht.green(`Server started on http://localhost:${i}`))',
'async function SCt(e){return n5e||',
'shouldUseResponsesEndpoint(t){return this.config.useResponsesApi===!1?!1:this.apiBase==="https://api.openai.com/v1/"&&A0e(t)}',
'function uAe(e,t){let n={provider:e.provider,model:e.model,apiKey:e.apiKey,apiBase:e.apiBase,requestOptions:e.requestOptions,env:e.env};return CGn(n)??null}',
'function Csa(e){return process.platform==="win32"?{shell:"powershell.exe",args:["-NoLogo","-ExecutionPolicy","Bypass","-Command",e]}',
'a={onContent:u=>{},onContentComplete:u=>{},onToolStart:(u,l)=>{},onToolResult:(u,l,c)=>{},onToolError:(u,l)=>{},onToolPermissionRequest:',
'pendingPermission:null},B=',
'j.get("/state",(we,Te)=>{M.lastActivity=Date.now(),B();let ue=e7e(M.session,M.isProcessing,rS.getQueueLength(),M.pendingPermission);Te.json(ue)})',
'n?.onToolStart?.(i.name,i.arguments);',
'n?.onToolError?.(l,i.name)',
't?.onToolStart?.(c.name,c.arguments);',
't?.onToolResult?.(String(y.content),c.name,"canceled")',
't?.onToolResult?.(f,c.name,"done")',
't?.onToolError?.(g,c.name)',
't?.onToolError?.(p,c.name)'
].join(';')
await writeFile(join(distribution, 'index.js'), sourceBundle, 'utf8')
return {
cacheRoot,
entryPath: join(distribution, 'cn.js'),
sourceHash: createHash('sha256')
.update(sourceBundle)
.digest('hex')
}
}
afterEach(async () => {
vi.unstubAllGlobals()
for (const restoreEnvironment of environmentRestorations.splice(0)) {
restoreEnvironment()
}
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
)
})
describe('ContinueHostAdapter', () => {
it('creates a versioned authenticated loopback host copy', async () => {
const distribution = await createDistribution()
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash]
})
const prepared = await adapter.getPreparedHost()
const bundle = await readFile(
join(prepared.entryPath, '..', 'index.js'),
'utf8'
)
const bootstrap = await readFile(
join(prepared.entryPath, '..', 'utility-bootstrap.mjs'),
'utf8'
)
expect(prepared.version).toBe('1.5.47')
expect(bundle).toContain('interactivePermissions:!0')
expect(bundle).toContain(
'isHeadless:e.interactivePermissions?!1:e.headless'
)
expect(bundle).toContain('GOODBUDDY_CONTINUE_HOST_TOKEN')
expect(bundle).toContain('listen(i,"127.0.0.1"')
expect(bundle).toContain(
'GOODBUDDY_DISABLE_CONTINUE_UPDATES'
)
expect(bundle).toContain(
'this.config.useResponsesApi===!0?!0'
)
expect(bundle).toContain(
'useResponsesApi:e.useResponsesApi'
)
expect(bundle).toContain('"-NoProfile"')
expect(bundle).toContain('[Console]::OutputEncoding')
expect(bundle).toContain('goodbuddyEvents:[]')
expect(bundle).toContain('goodbuddyEvents:ce')
expect(bundle).toContain('type:"text",delta:u')
expect(bundle).toContain('onToolStart?.(c.name,c.arguments,c.id)')
expect(bundle).toContain(
'function ZZo(e){let t=[];if(e.allow)'
)
expect(bundle).not.toContain(
'toolPermissionOverrides:s,headless:!0});let'
)
expect(bootstrap).toContain(
'process.argv = process.argv.slice(2)'
)
})
it('rejects unsupported Continue versions without patching them', async () => {
const distribution = await createDistribution('1.6.0')
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash]
})
await expect(adapter.getPreparedHost()).rejects.toThrow(
'仅支持 1.5.47'
)
})
it('rejects an untrusted bundle even when markers and version match', async () => {
const distribution = await createDistribution()
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: ['0'.repeat(64)]
})
await expect(adapter.getPreparedHost()).rejects.toThrow(
'兼容性校验'
)
})
it('removes capability config when host preparation fails after generation', async () => {
const distribution = await createDistribution()
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [],
modelProfile: {
id: '00000000-0000-4000-8000-000000000099',
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
await expect(
adapter.run(
'search',
new AbortController().signal,
async () => 'deny',
{
workMode: 'ask',
knowledgeCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'main-only-token'
}
}
)
).rejects.toThrow('未通过宿主兼容性校验')
await expect(readdir(distribution.cacheRoot)).resolves.not.toEqual(
expect.arrayContaining([
expect.stringMatching(/^model-config-/u)
])
)
})
it('removes capability config when cancellation reaches the pre-spawn check', async () => {
const distribution = await createDistribution()
const launchHost = vi.fn<ContinueHostLauncher>()
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
modelProfile: {
id: '00000000-0000-4000-8000-000000000098',
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
const controller = new AbortController()
const pending = adapter.run(
'search',
controller.signal,
async () => 'deny',
{
workMode: 'ask',
knowledgeCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'main-only-token'
}
}
)
setTimeout(() => controller.abort(new Error('cancelled')), 0)
await expect(pending).rejects.toThrow('cancelled')
expect(launchHost).not.toHaveBeenCalled()
await expect(readdir(distribution.cacheRoot)).resolves.not.toEqual(
expect.arrayContaining([
expect.stringMatching(/^model-config-/u)
])
)
})
it('blocks runs without an explicit model profile or config file', async () => {
const launchHost = vi.fn()
const adapter = new ContinueHostAdapter({
binaryPath: 'C:\\unused\\cn.js',
configPath: '',
workspace: process.cwd(),
cacheRoot: 'C:\\unused\\cache',
launchHost: launchHost as unknown as ContinueHostLauncher
})
await expect(
adapter.run('hello', new AbortController().signal, async () => 'deny')
).rejects.toThrow('尚未配置模型连接')
expect(launchHost).not.toHaveBeenCalled()
})
it('launches the prepared host through the injected launcher', async () => {
const distribution = await createDistribution()
let launch:
| {
entryPath: string
args: string[]
env: NodeJS.ProcessEnv
}
| undefined
let killed = false
let generatedConfig = ''
let generatedConfigPath = ''
const launchHost: ContinueHostLauncher = (
entryPath,
args,
options
) => {
launch = { entryPath, args, env: options.env }
const configIndex = args.indexOf('--config')
if (configIndex >= 0) {
generatedConfigPath = args[configIndex + 1] ?? ''
generatedConfig = readFileSync(generatedConfigPath, 'utf8')
}
return {
exitCode: null,
get killed() {
return killed
},
stderr: null,
once: () => undefined,
kill: () => {
killed = true
return true
}
}
}
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input)
if (url.endsWith('/state')) {
stateRequests += 1
return new Response(
JSON.stringify({
session: {
history:
stateRequests === 1
? []
: [
{
message: {
role: 'assistant',
content: 'HOST_LAUNCH_OK'
}
}
],
usage:
stateRequests === 1
? {
promptTokens: 20,
completionTokens: 10,
promptTokensDetails: {
cachedTokens: 5,
cacheWriteTokens: 2
}
}
: {
promptTokens: 19,
completionTokens: 9,
promptTokensDetails: {
cachedTokens: 4,
cacheWriteTokens: 1
}
}
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
)
}
return new Response('{}')
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
mode: 'chat',
modelProfile: {
id: '00000000-0000-4000-8000-000000000011',
name: '独立模型',
baseUrl: 'https://model.example',
modelName: 'private-model',
protocol: 'anthropic-messages',
authentication: 'api-key',
apiKey: 'private-key'
}
})
await expect(
adapter.run('hello', new AbortController().signal, async () => 'deny')
).resolves.toEqual({
text: 'HOST_LAUNCH_OK',
usage: {
provider: 'anthropic',
model: 'private-model',
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0
}
})
expect(launch?.entryPath).toContain('host-v6')
expect(launch?.args).toEqual([
'--config',
expect.stringContaining('model-config-'),
'--readonly',
'serve',
'--port',
expect.any(String),
'--timeout',
'300'
])
expect(launch?.env.GOODBUDDY_CONTINUE_HOST_TOKEN).toEqual(
expect.any(String)
)
expect(launch?.env).toMatchObject({
CONTINUE_CLI_AUTO_UPDATED: '1',
CONTINUE_CLI_ENABLE_TELEMETRY: '0',
CONTINUE_METRICS_ENABLED: '0',
CONTINUE_GLOBAL_DIR: expect.stringContaining('isolated-global'),
DO_NOT_TRACK: '1',
GOODBUDDY_DISABLE_CONTINUE_UPDATES: '1',
OTEL_EXPORTER_OTLP_ENDPOINT: '',
OTEL_EXPORTER_OTLP_HEADERS: '',
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: '',
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '',
OTEL_LOGS_EXPORTER: 'none',
OTEL_LOG_USER_PROMPTS: '0',
OTEL_METRICS_EXPORTER: 'none',
OTEL_SDK_DISABLED: 'true',
OTEL_TRACES_EXPORTER: 'none'
})
expect(killed).toBe(true)
expect(JSON.parse(generatedConfig)).toMatchObject({
models: [
{
apiBase: 'https://model.example/v1',
apiKey: '${{ secrets.ANTHROPIC_API_KEY }}',
model: 'private-model'
}
]
})
expect(generatedConfig).not.toContain('private-key')
expect(launch?.env.ANTHROPIC_API_KEY).toBe('private-key')
expect(existsSync(generatedConfigPath)).toBe(false)
})
it('injects scoped knowledge into a temporary copy of a JSONC config', async () => {
const distribution = await createDistribution()
const configPath = join(
distribution.cacheRoot,
'..',
'continue.jsonc'
)
const originalConfig = [
'{',
' // User-managed Continue configuration',
' "name": "Private Continue",',
' "version": "1.0.0",',
' "schema": "v1",',
' "models": [{ "provider": "ollama", "model": "qwen3" }],',
' "mcpServers": [{ "name": "user-tools", "command": "tool.exe" }],',
'}'
].join('\n')
await writeFile(configPath, originalConfig, 'utf8')
let generatedConfig = ''
let generatedConfigPath = ''
let killed = false
const launchHost: ContinueHostLauncher = (
_entryPath,
args
) => {
const configIndex = args.indexOf('--config')
generatedConfigPath = args[configIndex + 1] ?? ''
generatedConfig = readFileSync(generatedConfigPath, 'utf8')
expect(args).toEqual([
'--config',
expect.stringContaining('knowledge-config-'),
'--allow',
'knowledge_search',
'--exclude',
'*',
'serve',
'--port',
expect.any(String),
'--timeout',
'300'
])
return {
exitCode: null,
get killed() {
return killed
},
stderr: null,
once: () => undefined,
kill: () => {
killed = true
return true
}
}
}
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
if (String(input).endsWith('/state')) {
stateRequests += 1
return Response.json({
session: {
history:
stateRequests === 1
? []
: [
{
message: {
role: 'assistant',
content: 'CONFIG_KNOWLEDGE_OK'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
return Response.json({})
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath,
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
mode: 'agent'
})
await expect(
adapter.run(
'search',
new AbortController().signal,
async () => 'deny',
{
workMode: 'ask',
knowledgeCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'main-only-token'
}
}
)
).resolves.toEqual({ text: 'CONFIG_KNOWLEDGE_OK' })
expect(JSON.parse(generatedConfig)).toMatchObject({
name: 'Private Continue',
models: [{ provider: 'ollama', model: 'qwen3' }],
mcpServers: [
{
name: 'goodbuddy-knowledge',
type: 'streamable-http',
url: 'http://127.0.0.1:4567/mcp',
requestOptions: {
headers: {
Authorization: 'Bearer main-only-token'
}
}
}
]
})
expect(generatedConfig).not.toContain('user-tools')
await expect(readFile(configPath, 'utf8')).resolves.toBe(
originalConfig
)
expect(killed).toBe(true)
expect(existsSync(generatedConfigPath)).toBe(false)
})
it.each([
{
label: 'Chat Completions without authentication',
protocol: 'openai-chat-completions' as const,
authentication: 'none' as const,
useResponsesApi: false
},
{
label: 'Responses with an API key',
protocol: 'openai-responses' as const,
authentication: 'api-key' as const,
useResponsesApi: true
}
])(
'generates an explicit OpenAI config for $label',
async ({
protocol,
authentication,
useResponsesApi
}) => {
inheritProviderCredentials()
const distribution = await createDistribution()
let generatedConfig = ''
let launchedEnvironment: NodeJS.ProcessEnv | undefined
let launchedArgs: string[] = []
const launchHost: ContinueHostLauncher = (
_entryPath,
args,
options
) => {
launchedArgs = args
const configIndex = args.indexOf('--config')
generatedConfig = readFileSync(
args[configIndex + 1] ?? '',
'utf8'
)
launchedEnvironment = options.env
return {
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}
}
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
if (String(input).endsWith('/state')) {
stateRequests += 1
return Response.json({
session: {
history:
stateRequests === 1
? []
: [
{
message: {
role: 'assistant',
content: 'OLLAMA_OK'
}
}
],
usage:
stateRequests === 1
? {
promptTokens: 100,
completionTokens: 20,
promptTokensDetails: {
cachedTokens: 10,
cacheWriteTokens: 3
}
}
: {
promptTokens: 131,
completionTokens: 29,
promptTokensDetails: {
cachedTokens: 23,
cacheWriteTokens: 7
}
}
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
return Response.json({})
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
modelProfile: {
id: '00000000-0000-4000-8000-000000000012',
name: 'Ollama',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol,
authentication,
...(authentication === 'api-key'
? { apiKey: 'private-key' }
: {})
}
})
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
workMode: 'ask',
knowledgeCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'main-only-token'
}
}
)
).resolves.toEqual({
text: 'OLLAMA_OK',
usage: {
provider: 'openai',
model: 'qwen3',
inputTokens: 31,
outputTokens: 9,
cacheReadTokens: 13,
cacheWriteTokens: 4
}
})
expect(JSON.parse(generatedConfig)).toMatchObject({
models: [
{
provider: 'openai',
apiBase: 'http://127.0.0.1:11434/v1',
model: 'qwen3',
useResponsesApi
}
],
mcpServers: [
{
name: 'goodbuddy-knowledge',
type: 'streamable-http',
url: 'http://127.0.0.1:4567/mcp',
requestOptions: {
headers: {
Authorization: 'Bearer main-only-token'
}
}
}
]
})
expect(launchedArgs).toEqual(
expect.arrayContaining([
'--allow',
'knowledge_search',
'--exclude',
'*'
])
)
expect(launchedArgs).not.toContain('--readonly')
if (authentication === 'api-key') {
expect(JSON.parse(generatedConfig)).toMatchObject({
models: [
{
apiKey: '${{ secrets.OPENAI_API_KEY }}'
}
]
})
expect(launchedEnvironment?.OPENAI_API_KEY).toBe('private-key')
} else {
expect(generatedConfig).not.toContain('apiKey')
expect(launchedEnvironment).not.toHaveProperty(
'OPENAI_API_KEY'
)
}
for (const name of Object.keys(inheritedProviderCredentials)) {
const selectedCredential =
authentication === 'api-key' ? 'OPENAI_API_KEY' : undefined
if (name !== selectedCredential) {
expect(launchedEnvironment).not.toHaveProperty(name)
}
}
}
)
it('turns a strict upstream error envelope into a failed run', async () => {
const distribution = await createDistribution()
let killed = false
const launchHost: ContinueHostLauncher = () => ({
exitCode: null,
get killed() {
return killed
},
stderr: null,
once: () => undefined,
kill: () => {
killed = true
return true
}
})
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
if (String(input).endsWith('/state')) {
stateRequests += 1
return Response.json({
session: {
history:
stateRequests === 1
? []
: [
{
message: {
role: 'assistant',
content: 'Partial response'
},
toolCallStates: [
{
toolCallId: 'call-1',
toolCall: {
function: { name: 'Bash' }
},
status: 'errored',
output: [
{
content:
'PowerShell parser failed Authorization: Bearer secret-token'
}
]
}
]
},
{
message: {
role: 'system',
content:
'Error: {"error":{"message":"Request not allowed"}}'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
return Response.json({})
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
modelProfile: {
id: '00000000-0000-4000-8000-000000000013',
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'local-model',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
const run = adapter.run(
'hello',
new AbortController().signal,
async () => 'deny'
)
await expect(
run
).rejects.toMatchObject({
message: 'Continue 模型请求失败:Request not allowed',
tools: [
{
callId: 'call-1',
name: 'Bash',
state: 'failed',
error:
'PowerShell parser failed Authorization: Bearer secret-token'
}
]
})
expect(killed).toBe(true)
})
it('returns audit metadata for auto-approved agent tools', async () => {
const distribution = await createDistribution()
let launchArgs: string[] = []
const permissionBodies: unknown[] = []
const streamEvents: unknown[] = []
const launchHost: ContinueHostLauncher = (
_entryPath,
args
) => {
launchArgs = args
return {
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}
}
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(
async (
input: string | URL | Request,
init?: RequestInit
) => {
const url = String(input)
if (url.endsWith('/permission')) {
permissionBodies.push(JSON.parse(String(init?.body)))
return Response.json({})
}
if (url.endsWith('/state')) {
stateRequests += 1
if (stateRequests === 1) {
return Response.json({
session: { history: [] },
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
if (stateRequests === 2) {
return Response.json({
session: { history: [] },
isProcessing: true,
messageQueueLength: 0,
pendingPermission: {
toolName: 'Bash',
toolArgs: { command: 'npm test' },
requestId: 'permission-1'
},
goodbuddyEvents: [
{ type: 'text', delta: '先检查命令。' },
{
type: 'tool',
callId: 'call-1',
name: 'Bash',
state: 'running',
input:
'{"command":"npm test","token":"secret-token"}'
}
]
})
}
return Response.json({
session: {
history: [
{
message: {
role: 'assistant',
content: 'TOOLS_OK'
},
toolCallStates: [
{
toolCallId: 'call-1',
toolCall: {
function: { name: 'Bash' }
},
status: 'done'
}
]
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEvents: [
{
type: 'tool',
callId: 'call-1',
name: 'Bash',
state: 'completed',
output:
'Tests passed\nAuthorization: Bearer secret-token'
},
{ type: 'text', delta: 'TOOLS_OK' }
]
})
}
return Response.json({})
}
)
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: 'C:\\safe\\continue.yaml',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost,
mode: 'agent'
})
const authorize = vi.fn(async () => 'once' as const)
await expect(
adapter.run(
'hello',
new AbortController().signal,
authorize,
{
onEvent: (event) => {
streamEvents.push(event)
}
}
)
).resolves.toEqual({
text: 'TOOLS_OK',
streamedText: true,
tools: [
{
callId: 'call-1',
name: 'Bash',
state: 'completed',
input:
'{"command":"npm test","token":"secret-token"}',
output:
'Tests passed\nAuthorization: Bearer secret-token'
}
]
})
expect(streamEvents).toEqual([
{ type: 'text', delta: '先检查命令。' },
{
type: 'tool',
tool: {
callId: 'call-1',
name: 'Bash',
state: 'running',
input:
'{"command":"npm test","token":"secret-token"}'
}
},
{
type: 'tool',
tool: {
callId: 'call-1',
name: 'Bash',
state: 'completed',
output:
'Tests passed\nAuthorization: Bearer secret-token'
}
},
{ type: 'text', delta: 'TOOLS_OK' }
])
expect(launchArgs).not.toContain('--readonly')
expect(authorize).toHaveBeenCalledWith(
expect.objectContaining({ toolName: 'Bash' })
)
expect(permissionBodies).toEqual([
{ requestId: 'permission-1', approved: true }
])
})
it.each([
{
label: 'Chat Completions',
protocol: 'openai-chat-completions' as const,
expectedPath: '/v1/chat/completions',
unexpectedPath: '/v1/responses'
},
{
label: 'Responses',
protocol: 'openai-responses' as const,
expectedPath: '/v1/responses',
unexpectedPath: '/v1/chat/completions'
}
])(
'routes a custom-base $label profile to its explicit endpoint in Continue 1.5.47',
async ({
protocol,
expectedPath,
unexpectedPath
}) => {
const root = await mkdtemp(
join(tmpdir(), 'goodbuddy-continue-responses-')
)
temporaryDirectories.push(root)
const requestPaths: string[] = []
const server = createServer((request, response) => {
requestPaths.push(request.url ?? '')
request.resume()
response.writeHead(400, {
'content-type': 'application/json'
})
response.end(
JSON.stringify({
error: {
message: 'Intentional local routing probe'
}
})
)
})
await new Promise<void>((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => resolveListen())
})
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Failed to bind local routing probe')
}
const adapter = new ContinueHostAdapter({
binaryPath: join(
process.cwd(),
'node_modules',
'@continuedev',
'cli',
'dist',
'cn.js'
),
configPath: '',
workspace: root,
cacheRoot: join(root, 'cache'),
modelProfile: {
id: '00000000-0000-4000-8000-000000000014',
name: 'Local endpoint probe',
baseUrl: `http://127.0.0.1:${address.port}/v1`,
modelName: 'probe-model',
protocol,
authentication: 'none'
}
})
const controller = new AbortController()
const timeout = setTimeout(
() => controller.abort(new Error('Routing probe timed out')),
20_000
)
try {
await adapter
.run('Reply with OK', controller.signal, async () => 'deny')
.catch(() => undefined)
expect(requestPaths).toContain(expectedPath)
expect(requestPaths).not.toContain(unexpectedPath)
} finally {
clearTimeout(timeout)
adapter.dispose()
await new Promise((resolveWait) =>
setTimeout(resolveWait, 500)
)
await new Promise<void>((resolveClose, reject) => {
server.close((error) =>
error ? reject(error) : resolveClose()
)
})
}
},
30_000
)
})