Files
goodbuddy/src/main/agent/continue-host-adapter.test.ts
T
mesalogo 0d7ba55a50 fix: preserve tool details and prevent repeated history growth
Show bounded operation summaries for Continue, DeepSeek Harness and direct-model tools, retaining input details across incremental updates. Use one canonical workspace path for DeepSeek Host startup and ACP sessions so equivalent Windows paths reuse a working Runtime.

Compare persisted subagent tool fields by value, release terminal task caches, and avoid rebuilding caches for duplicate remote events. Upgrade to schema 35 to recompact repeated schema-34 tool blocks while preserving events and provenance. Include regression coverage, validation records and temporary-resource test exclusions.

Validation: 4155 tests passed, 66 skipped; typecheck and lint passed. Full desktop real-model validation covered all three tool-summary paths and both Windows path spellings. Storage validation evidence includes local and remote tool workloads and real database upgrade checks.

Release note: Tool records now show paths, commands and search details across runtimes. Fix DeepSeek Windows workspace failures and repeated subagent history growth; schema 35 reclaims redundant history data while preserving execution records, and older clients cannot reopen the upgraded database.
2026-09-14 00:34:51 +08:00

2250 lines
66 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile
} from 'node:fs/promises'
import { existsSync, readFileSync } from 'node:fs'
import { createHash, randomUUID } 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,
inspectContinueNativeConfiguration,
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 Sin(e,t){let n=[];n.push({role:"system",content:t});let r=oot(e);return n.push(...r),n}',
'function Csa(e){return process.platform==="win32"?{shell:"powershell.exe",args:["-NoLogo","-ExecutionPolicy","Bypass","-Command",e]}',
'let{shell:d,args:p}=Csa(e),f=Esa(d,p),g="",y="",A,S=!1,x=18e4;',
'let r=[eS.join(n,".continue",AKt),eS.join(n,".claude",AKt),eS.join(hu.continueHome,AKt)],o=',
'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('json({limit:"100mb"})')
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(
'let r=oot(e).filter(o=>o.role!=="system")'
)
expect(bundle).toContain('"-NoProfile"')
expect(bundle).toContain('[Console]::OutputEncoding')
expect(bundle).toContain(
'f.stdout.setEncoding("utf8"),f.stderr.setEncoding("utf8")'
)
expect(bundle).toContain(
'let r=[eS.join(hu.continueHome,AKt)],o='
)
expect(bundle).toContain('goodbuddyEvents:[]')
expect(bundle).toContain('goodbuddyEventsBytes:0')
expect(bundle).toContain('goodbuddyEventsBytes+=Buffer.byteLength')
expect(bundle).toContain('goodbuddyEventsBytes<=2097152')
expect(bundle).toContain('l.length<=1e5')
expect(bundle).toContain('goodbuddyEventsOverflow:!1')
expect(bundle).toContain('goodbuddyEventsOverflow=!0')
expect(bundle).toContain('goodbuddyEvents:ce')
expect(bundle).toContain('/goodbuddy/question-answer')
expect(bundle).toContain(
'goodbuddyQuestion:Lbe.currentState.pendingQuestion'
)
expect(bundle.indexOf('GOODBUDDY_CONTINUE_HOST_TOKEN')).toBeLessThan(
bundle.indexOf('/goodbuddy/question-answer')
)
expect(bundle).toContain('type:"text",delta:l')
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('rejects a custom MCP loopback capability outside Continue Agent Execute mode', 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,
modelProfile: {
id: '00000000-0000-4000-8000-000000000097',
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
workMode: 'ask',
customMcpCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'request-token'
}
}
)
).rejects.toThrow('仅允许在 Agent Execute 模式')
expect(launchHost).not.toHaveBeenCalled()
})
it('launches the prepared host through the injected launcher', async () => {
let now = Date.now()
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => now)
environmentRestorations.push(() => nowSpy.mockRestore())
const distribution = await createDistribution()
const skillDirectory = join(
distribution.cacheRoot,
'..',
'longdoc-docx'
)
await mkdir(skillDirectory, { recursive: true })
await writeFile(
join(skillDirectory, 'SKILL.md'),
[
'---',
'name: longdoc-docx',
'description: Build a long Word document',
'---',
'',
'# Long document'
].join('\n'),
'utf8'
)
await writeFile(
join(skillDirectory, 'build.py'),
'print("build")\n',
'utf8'
)
let launch:
| {
entryPath: string
args: string[]
env: NodeJS.ProcessEnv
}
| undefined
let killed = false
let generatedConfig = ''
let generatedConfigPath = ''
let isolatedGlobalDirectory = ''
let registeredSkill = ''
let registeredSkillFile = ''
const launchEnvironmentProvider = vi.fn(() =>
Object.freeze({
PATH: 'C:\\GoodBuddy\\tools;C:\\System',
OPENAI_API_KEY: 'must-not-leak',
ELECTRON_RUN_AS_NODE: '1'
})
)
const launchHost: ContinueHostLauncher = (
entryPath,
args,
options
) => {
launch = { entryPath, args, env: options.env }
isolatedGlobalDirectory =
options.env.CONTINUE_GLOBAL_DIR ?? ''
registeredSkill = readFileSync(
join(
isolatedGlobalDirectory,
'skills',
'longdoc-docx',
'SKILL.md'
),
'utf8'
)
registeredSkillFile = readFileSync(
join(
isolatedGlobalDirectory,
'skills',
'longdoc-docx',
'build.py'
),
'utf8'
)
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
if (stateRequests === 2) {
now += 25 * 60 * 60_000
}
return new Response(
JSON.stringify({
session: {
history:
stateRequests <= 2
? []
: [
{
message: {
role: 'assistant',
content: 'HOST_LAUNCH_OK'
}
}
],
usage:
stateRequests <= 2
? {
promptTokens: 20,
completionTokens: 10,
promptTokensDetails: {
cachedTokens: 5,
cacheWriteTokens: 2
}
}
: {
promptTokens: 19,
completionTokens: 9,
promptTokensDetails: {
cachedTokens: 4,
cacheWriteTokens: 1
}
}
},
isProcessing: stateRequests === 2,
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,
launchEnvironmentProvider,
mode: 'chat',
skillPackages: [
{
id: 'longdoc-docx',
directory: skillDirectory
}
],
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',
{
workMode: 'execute',
customMcpCapability: {
endpoint: 'http://127.0.0.1:4567/mcp',
token: 'request-scoped-custom-token'
}
}
)
).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-v7')
expect(launch?.args).toEqual([
'--config',
expect.stringContaining('model-config-'),
'--auto',
'serve',
'--port',
expect.any(String),
'--timeout',
'300'
])
expect(launch?.env.GOODBUDDY_CONTINUE_HOST_TOKEN).toEqual(
expect.any(String)
)
expect(launchEnvironmentProvider).toHaveBeenCalledOnce()
expect(launch?.env.PATH).toBe(
'C:\\GoodBuddy\\tools;C:\\System'
)
expect(launch?.env).not.toHaveProperty('ELECTRON_RUN_AS_NODE')
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'
})
if (process.platform === 'win32') {
expect(launch?.env).toMatchObject({
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
})
}
expect(registeredSkill).toContain('name: longdoc-docx')
expect(registeredSkillFile).toBe('print("build")\n')
expect(existsSync(isolatedGlobalDirectory)).toBe(false)
expect(killed).toBe(true)
expect(JSON.parse(generatedConfig)).toMatchObject({
models: [
{
apiBase: 'https://model.example/v1',
apiKey: '${{ secrets.ANTHROPIC_API_KEY }}',
model: 'private-model'
}
],
mcpServers: [
{
name: 'goodbuddy-custom-mcp',
type: 'streamable-http',
url: 'http://127.0.0.1:4567/mcp',
requestOptions: {
headers: {
Authorization:
'Bearer request-scoped-custom-token'
}
}
}
]
})
expect(generatedConfig).not.toContain('private-key')
expect(launch?.env.ANTHROPIC_API_KEY).toBe('private-key')
expect(existsSync(generatedConfigPath)).toBe(false)
})
it('bounds each host control request without bounding the full 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
}
})
vi.stubGlobal(
'fetch',
vi.fn(
async (
input: string | URL | Request,
init?: RequestInit
): Promise<Response> => {
if (String(input).endsWith('/state')) {
return Response.json({
session: { history: [] },
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
if (String(input).endsWith('/message')) {
return await new Promise<Response>((_resolve, reject) => {
const rejectOnAbort = (): void =>
reject(init?.signal?.reason)
init?.signal?.addEventListener(
'abort',
rejectOnAbort,
{ once: true }
)
if (init?.signal?.aborted) {
rejectOnAbort()
}
})
}
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: '独立模型',
baseUrl: 'https://model.example',
modelName: 'private-model',
protocol: 'anthropic-messages',
authentication: 'api-key',
apiKey: 'private-key'
}
},
{
controlRequestTimeoutMs: 5,
terminateProcessTree: vi.fn(async (child) => {
child.kill()
})
}
)
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny'
)
).rejects.toThrow('Continue 宿主请求超时(/message)')
expect(killed).toBe(true)
})
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_list',
'--allow',
'knowledge_search',
'--allow',
'note_list',
'--allow',
'note_get',
'--allow',
'note_search',
'--allow',
'goodbuddy_config_capabilities',
'--allow',
'goodbuddy_config_get',
'--allow',
'goodbuddy_config_plan',
'--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[] = []
let submittedMessage: unknown
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,
init?: RequestInit
) => {
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
})
}
if (String(input).endsWith('/message')) {
submittedMessage = JSON.parse(String(init?.body)).message
}
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,
supportsImageInput: true,
requestHeaders: {
'x-tenant-id': 'continue-tenant'
},
requestBody: {
temperature: 0.25,
metadata: { source: 'goodbuddy' }
},
...(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'
},
images: [
{
name: 'screenshot.png',
mediaType: 'image/png',
data: 'aW1hZ2U='
}
]
}
)
).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,
capabilities: ['image_input'],
requestOptions: {
headers: {
'x-tenant-id': 'continue-tenant'
},
extraBodyProperties: {
temperature: 0.25,
metadata: { source: 'goodbuddy' }
}
}
}
],
mcpServers: [
{
name: 'goodbuddy-knowledge',
type: 'streamable-http',
url: 'http://127.0.0.1:4567/mcp',
requestOptions: {
headers: {
Authorization: 'Bearer main-only-token'
}
}
}
]
})
expect(submittedMessage).toEqual([
{ type: 'text', text: 'hello' },
{
type: 'imageUrl',
imageUrl: {
url: 'data:image/png;base64,aW1hZ2U='
}
}
])
expect(launchedArgs).toEqual(
expect.arrayContaining([
'--allow',
'knowledge_list',
'--allow',
'knowledge_search',
'--allow',
'note_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 原始错误:路径不存在 ���'
}
]
}
]
},
{
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 原始错误:路径不存在 ���'
}
]
})
expect(killed).toBe(true)
})
it('completes from final history when the patched stream is truncated', async () => {
const distribution = await createDistribution()
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: 'CONTINUE_OK'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEventsOverflow: stateRequests === 2
})
}
return Response.json({})
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: () => ({
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}),
modelProfile: {
id: randomUUID(),
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny'
)
).resolves.toMatchObject({
text: 'CONTINUE_OK',
streamTruncated: true
})
})
it(
'truncates cumulative streamed bytes without stopping the run',
async () => {
const distribution = await createDistribution()
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 < 3
? []
: [
{
message: {
role: 'assistant',
content: '1234567890CONTINUE_OK'
}
}
]
},
isProcessing: stateRequests === 2,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEvents:
stateRequests > 1
? [{ type: 'text', delta: '1234567890' }]
: []
})
}
return Response.json({})
})
)
const forwarded: unknown[] = []
const adapter = new ContinueHostAdapter(
{
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: () => ({
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}),
modelProfile: {
id: randomUUID(),
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
},
{
maximumStreamEventBytes: 60,
terminateProcessTree: vi.fn().mockResolvedValue(undefined)
}
)
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
onEvent: (event) => {
forwarded.push(event)
}
}
)
).resolves.toMatchObject({
text: '1234567890CONTINUE_OK',
streamedText: true,
streamTruncated: true
})
expect(forwarded).toEqual([
{ type: 'text', delta: '1234567890' }
])
}
)
it('stops forwarding later deltas after the patched stream overflows', async () => {
const distribution = await createDistribution()
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 < 4
? []
: [
{
message: {
role: 'assistant',
content: 'prefix missing suffix'
}
}
]
},
isProcessing: stateRequests < 4,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEvents:
stateRequests === 2
? [{ type: 'text', delta: 'prefix ' }]
: stateRequests === 3
? [{ type: 'text', delta: 'suffix' }]
: [],
goodbuddyEventsOverflow: stateRequests === 3
})
}
return Response.json({})
})
)
const forwarded: unknown[] = []
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: () => ({
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}),
modelProfile: {
id: randomUUID(),
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
onEvent: (event) => {
forwarded.push(event)
}
}
)
).resolves.toMatchObject({
text: 'prefix missing suffix',
streamedText: true,
streamTruncated: true
})
expect(forwarded).toEqual([
{ type: 'text', delta: 'prefix ' }
])
})
it('forwards more than 100 unique tool calls', async () => {
const distribution = await createDistribution()
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: 'TOOLS_OK'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEvents:
stateRequests === 2
? Array.from({ length: 101 }, (_, index) => ({
type: 'tool',
callId: `call-${index + 1}`,
name: 'Bash',
state: 'completed'
}))
: []
})
}
return Response.json({})
})
)
const forwarded: unknown[] = []
const adapter = new ContinueHostAdapter(
{
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: () => ({
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}),
modelProfile: {
id: randomUUID(),
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
},
{
maximumStreamEventBytes: 1_000_000,
terminateProcessTree: vi.fn().mockResolvedValue(undefined)
}
)
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
onEvent: (event) => {
forwarded.push(event)
}
}
)
).resolves.toMatchObject({ text: 'TOOLS_OK' })
expect(forwarded).toHaveLength(101)
expect(forwarded.at(-1)).toMatchObject({
type: 'tool',
tool: { callId: 'call-101' }
})
})
it('awaits bounded process cleanup before deleting the run directory', async () => {
const distribution = await createDistribution()
let globalDirectory = ''
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: 'CLEANUP_OK'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null
})
}
return Response.json({})
})
)
let releaseTermination!: () => void
const terminateProcessTree = vi.fn(
() =>
new Promise<void>((resolve) => {
releaseTermination = resolve
})
)
const adapter = new ContinueHostAdapter(
{
binaryPath: distribution.entryPath,
configPath: '',
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: (_entry, _args, options) => {
globalDirectory =
options.env.CONTINUE_GLOBAL_DIR ?? ''
return {
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
}
},
modelProfile: {
id: randomUUID(),
name: 'Local model',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none'
}
},
{
maximumStreamEventBytes: 10_000,
terminateProcessTree
}
)
const run = adapter.run(
'hello',
new AbortController().signal,
async () => 'deny'
)
await vi.waitFor(() =>
expect(terminateProcessTree).toHaveBeenCalledOnce()
)
expect(globalDirectory).toBeTruthy()
expect(existsSync(globalDirectory)).toBe(true)
releaseTermination()
await expect(run).resolves.toEqual({ text: 'CLEANUP_OK' })
expect(existsSync(globalDirectory)).toBe(false)
})
it.each(['completed', 'failed'] as const)('uses auto mode and retains streamed audit metadata on %s', async (toolState) => {
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: toolState
}
]
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyEvents: [
{
type: 'tool',
callId: 'call-1',
name: 'Bash',
state: toolState,
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,
{
workMode: 'execute',
onEvent: (event) => {
streamEvents.push(event)
}
}
)
).resolves.toEqual({
text: 'TOOLS_OK',
streamedText: true,
tools: [
{
callId: 'call-1',
name: 'Bash',
state: toolState,
input:
'{"command":"npm test","token":"secret-token"}',
summary: 'npm test',
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"}',
summary: 'npm test'
}
},
{
type: 'tool',
tool: {
callId: 'call-1',
name: 'Bash',
state: toolState,
output:
'Tests passed\nAuthorization: Bearer secret-token',
input: '{"command":"npm test","token":"secret-token"}',
summary: 'npm test'
}
},
{ type: 'text', delta: 'TOOLS_OK' }
])
expect(launchArgs).toContain('--auto')
expect(launchArgs).not.toContain('--readonly')
expect(authorize).toHaveBeenCalledWith(
expect.objectContaining({ toolName: 'Bash' })
)
expect(permissionBodies).toEqual([
{ requestId: 'permission-1', approved: true }
])
})
it('merges enabled preset Rules and prompts after native configuration metadata', async () => {
const distribution = await createDistribution()
const configPath = join(
distribution.cacheRoot,
'..',
'preset-continue.yaml'
)
await writeFile(
configPath,
JSON.stringify({
name: 'Native',
version: '1.0.0',
schema: 'v1',
models: [{ provider: 'ollama', model: 'qwen3' }],
rules: [{ name: 'Native rule', rule: 'Native content' }],
prompts: [
{ name: 'Native prompt', prompt: 'Native prompt content' }
]
}),
'utf8'
)
let generatedConfig: Record<string, unknown> = {}
const launchHost: ContinueHostLauncher = (_entry, args) => {
const index = args.indexOf('--config')
generatedConfig = JSON.parse(
readFileSync(args[index + 1] ?? '', 'utf8')
)
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: 'PRESET_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
})
await adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
preset: {
id: randomUUID(),
name: 'Preset',
rules: [
{
id: randomUUID(),
name: 'Enabled',
content: 'Enabled content',
enabled: true
},
{
id: randomUUID(),
name: 'Disabled',
content: 'Disabled content',
enabled: false
}
],
prompts: [
{
id: randomUUID(),
name: 'Preset prompt',
description: 'Preset description',
prompt: 'Preset prompt content'
}
]
}
}
)
expect(generatedConfig).toMatchObject({
rules: [
{ name: 'Native rule', rule: 'Native content' },
{ name: 'Enabled', rule: 'Enabled content' }
],
prompts: [
{ name: 'Native prompt', prompt: 'Native prompt content' },
{
name: 'Preset prompt',
description: 'Preset description',
prompt: 'Preset prompt content'
}
]
})
expect(JSON.stringify(generatedConfig)).not.toContain(
'Disabled content'
)
})
it('returns a redacted native inventory without scanning host-inaccessible Skills', async () => {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-continue-inventory-'))
temporaryDirectories.push(root)
const workspace = join(root, 'workspace')
const configPath = join(root, 'continue.jsonc')
await writeFile(
configPath,
JSON.stringify({
rules: [
{
name: 'Native Rule',
rule: 'Only bounded rule content is exposed'
}
],
prompts: [
{
name: 'Native Prompt',
description: 'Safe metadata',
prompt: 'Prompt body'
}
],
mcpServers: [
{
name: 'private-tools',
command: 'secret-command.exe',
url: 'https://secret.example/mcp',
apiKey: 'secret-value'
},
{
name: 'goodbuddy-knowledge',
url: 'http://127.0.0.1/token'
}
]
}),
'utf8'
)
const inventory = await inspectContinueNativeConfiguration({
configPath,
workspace
})
expect(inventory.rules).toEqual([
expect.objectContaining({
name: 'Native Rule',
content: 'Only bounded rule content is exposed'
})
])
expect(inventory.prompts).toEqual([
expect.objectContaining({
name: 'Native Prompt',
prompt: 'Prompt body'
})
])
expect(inventory.mcpServers).toEqual([
expect.objectContaining({
name: 'private-tools',
status: 'unknown'
})
])
expect(inventory).not.toHaveProperty('skills')
expect(JSON.stringify(inventory)).not.toMatch(
/secret-command|secret\.example|secret-value|goodbuddy-knowledge/u
)
expect(inventory.detail).toContain('不提供 Resources')
})
it.each([
{ options: ['Safe', 'Fast'], answer: [['Safe']], submitted: 'Safe', custom: false },
{ options: ['yes', 'no'], answer: [['no']], submitted: 'no', custom: false },
{ options: [], answer: [['Free text']], submitted: 'Free text', custom: true },
{ options: ['yes', 'no'], answer: undefined, submitted: 'User declined to answer this question.', custom: true }
])('bridges QuizService answers $submitted and cleans mappings', async (scenario) => {
const distribution = await createDistribution()
const configPath = join(
distribution.cacheRoot,
'..',
'question-continue.yaml'
)
await writeFile(
configPath,
JSON.stringify({
models: [{ provider: 'ollama', model: 'qwen3' }]
}),
'utf8'
)
const answerBodies: unknown[] = []
let stateRequests = 0
vi.stubGlobal(
'fetch',
vi.fn(async (
input: string | URL | Request,
init?: RequestInit
) => {
const url = String(input)
if (url.endsWith('/goodbuddy/question-answer')) {
answerBodies.push(JSON.parse(String(init?.body)))
return Response.json({ success: true })
}
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: null,
goodbuddyQuestion: {
requestId: 'quiz-123',
timestamp: Date.now(),
question: 'Choose safely',
options: scenario.options,
defaultAnswer: 'Safe'
}
})
}
return Response.json({
session: {
history: [
{
message: {
role: 'assistant',
content: 'QUESTION_OK'
}
}
]
},
isProcessing: false,
messageQueueLength: 0,
pendingPermission: null,
goodbuddyQuestion: null
})
}
return Response.json({})
})
)
const adapter = new ContinueHostAdapter({
binaryPath: distribution.entryPath,
configPath,
workspace: process.cwd(),
cacheRoot: distribution.cacheRoot,
trustedBundleHashes: [distribution.sourceHash],
launchHost: () => ({
exitCode: null,
killed: false,
stderr: null,
once: () => undefined,
kill: () => true
})
})
const events: unknown[] = []
await expect(
adapter.run(
'hello',
new AbortController().signal,
async () => 'deny',
{
onEvent: async (event) => {
events.push(event)
if (event.type === 'question') {
await adapter.respondToQuestion(
event.questionId,
scenario.answer
)
}
}
}
)
).resolves.toEqual({ text: 'QUESTION_OK' })
expect(events).toContainEqual(
expect.objectContaining({
type: 'question',
questionId: 'quiz-123',
questions: [
expect.objectContaining({
question: 'Choose safely',
options: scenario.options.map(label => ({ label, description: '' }))
})
]
})
)
expect(answerBodies).toEqual([
{
requestId: 'quiz-123',
answer: scenario.submitted,
isCustomAnswer: scenario.custom
}
])
await expect(
adapter.respondToQuestion('quiz-123', [['Safe']])
).rejects.toThrow('已失效或不存在')
})
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 requestBodies: unknown[] = []
const tenantHeaders: Array<string | string[] | undefined> = []
const server = createServer(async (request, response) => {
requestPaths.push(request.url ?? '')
tenantHeaders.push(request.headers['x-tenant-id'])
let body = ''
for await (const chunk of request) {
body += chunk
}
if (body) {
requestBodies.push(JSON.parse(body))
}
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',
requestHeaders: {
'x-tenant-id': 'continue-native'
},
requestBody: {
temperature: 0.25,
metadata: { source: 'goodbuddy' }
}
}
})
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)
expect(tenantHeaders).toContain('continue-native')
expect(requestBodies).toContainEqual(
expect.objectContaining({
temperature: 0.25,
metadata: { source: 'goodbuddy' }
})
)
if (protocol === 'openai-chat-completions') {
const chatRequest = requestBodies.find(
(body): body is { messages: Array<{ role?: unknown }> } =>
Boolean(
body &&
typeof body === 'object' &&
'messages' in body &&
Array.isArray(body.messages)
)
)
expect(chatRequest).toBeDefined()
expect(chatRequest?.messages[0]?.role).toBe('system')
expect(
chatRequest?.messages.filter(
(message) => message.role === 'system'
)
).toHaveLength(1)
}
} 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
)
})