Files
goodbuddy/src/main/agent/runtime-e2e.manual.test.ts
T
mesalogo cbbe30896e feat: expand multi-runtime workflows for 0.10.0
GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
2026-08-17 12:57:12 +08:00

1252 lines
35 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { z } from 'zod'
import {
defaultContextCompressionSettings,
modelProtocolSchema
} from '../../shared/contracts'
import { ContinueAgentRuntime } from './continue-runtime'
import { ModelAgentRuntime } from './model-runtime'
import { OpenCodeRuntime } from './opencode-runtime'
import { AgentRuntimeController } from './runtime-controller'
import type { RuntimeEvent } from './runtime'
import type {
ModelToolCallContext,
ModelToolDefinition,
ModelToolProviderLike,
ModelToolResult
} from './model-tool-provider'
import { GoodBuddyConfigService } from '../goodbuddy-config-service'
import { ApplicationSettingsStore } from '../application-settings-store'
import {
BrowserProfileService,
MemoryBrowserProfileStore
} from '../capabilities/browser-profile-service'
import {
CapabilityService,
type CapabilityCipher,
type ResolvedMcpServer
} from '../capabilities/capability-service'
import {
goodbuddyConfigToolByName,
goodbuddyConfigTools
} from '../../shared/goodbuddy-config-tools'
import { KnowledgeMcpGateway } from './knowledge-mcp-gateway'
const enabled = process.env.GOODBUDDY_RUN_RUNTIME_E2E === '1'
const apiKey =
process.env.GOODBUDDY_E2E_API_KEY ??
process.env.ANTHROPIC_API_KEY ??
''
const configuredBaseUrl =
process.env.GOODBUDDY_E2E_BASE_URL ??
process.env.ANTHROPIC_BASE_URL ??
'https://api.anthropic.com'
const configuredUrl = new URL(configuredBaseUrl)
configuredUrl.search = ''
configuredUrl.hash = ''
const baseUrl = configuredUrl.toString().replace(/\/$/u, '')
const modelName =
process.env.GOODBUDDY_E2E_MODEL ?? 'claude-sonnet-5'
const protocol = modelProtocolSchema
.exclude(['openai-images-generations'])
.parse(
process.env.GOODBUDDY_E2E_PROTOCOL ?? 'anthropic-messages'
)
const portableRoot = process.env.GOODBUDDY_E2E_PACKAGED_ROOT
? resolve(process.env.GOODBUDDY_E2E_PACKAGED_ROOT)
: join(
process.cwd(),
'dist',
'harness-package-probe',
'win-unpacked'
)
async function collectText(
events: AsyncGenerator<RuntimeEvent, void, void>
): Promise<string> {
let output = ''
for await (const event of events) {
if (event.type === 'text') {
output += event.delta
}
}
return output
}
async function collectEvents(
events: AsyncGenerator<RuntimeEvent, void, void>
): Promise<RuntimeEvent[]> {
const collected: RuntimeEvent[] = []
for await (const event of events) {
collected.push(event)
}
return collected
}
function customMcpServer(
assignment: 'opencode' | 'continue'
): ResolvedMcpServer {
return {
id:
assignment === 'opencode'
? '00000000-0000-4000-8000-0000000000e1'
: '00000000-0000-4000-8000-0000000000e2',
name: 'Live Blueprint MCP',
description: 'Deterministic local Runtime E2E fixture',
enabled: true,
allowDynamicTools: false,
assignments: [assignment],
secretConfigured: false,
transport: 'stdio',
command: process.execPath,
args: [
resolve('tests', 'fixtures', 'web-3d-game-mcp.mjs')
]
}
}
function textResult(value: unknown): ModelToolResult {
const text = JSON.stringify(value)
return {
parts: [{ type: 'text', text }],
contextBytes: Buffer.byteLength(text)
}
}
class RealModelConfigToolProvider implements ModelToolProviderLike {
readonly calls: string[] = []
private planId?: string
constructor(
private readonly service: GoodBuddyConfigService,
private readonly workspacePath: string,
private readonly requestId: string
) {}
async listTools(
context: ModelToolCallContext
): Promise<ModelToolDefinition[]> {
return goodbuddyConfigTools
.filter(
(tool) =>
context.workMode === 'execute' || tool.access === 'read'
)
.map((tool) => {
const schema = z.toJSONSchema(tool.inputSchema, {
target: 'draft-7'
}) as Record<string, unknown>
Reflect.deleteProperty(schema, '$schema')
return {
name: tool.name,
displayName: tool.title,
description: tool.description,
inputSchema: schema,
source: 'builtin'
}
})
}
getApproval() {
return {
scopeKey: 'real-model-config-test',
title: 'Unexpected config write',
description: 'Real config discovery test must not apply changes',
allowPermanent: false
}
}
async callTool(
name: string,
argumentsValue: Record<string, unknown>,
signal: AbortSignal
): Promise<ModelToolResult> {
signal.throwIfAborted()
this.calls.push(name)
const tool = goodbuddyConfigToolByName.get(
name as Parameters<typeof goodbuddyConfigToolByName.get>[0]
)
if (!tool) {
throw new Error(`Unexpected tool: ${name}`)
}
switch (name) {
case 'goodbuddy_config_capabilities':
return textResult({
capabilities: this.service.getCapabilities(argumentsValue)
})
case 'goodbuddy_config_get':
return textResult({
config: await this.service.getSnapshot(argumentsValue)
})
case 'goodbuddy_config_plan': {
const plan = await this.service.plan(
this.requestId,
this.workspacePath,
argumentsValue
)
this.planId = plan.planId
return textResult({ plan })
}
default:
throw new Error('Apply is forbidden in the real discovery test')
}
}
async releaseConversation(): Promise<void> {}
async dispose(): Promise<void> {}
getPlannedId(): string | undefined {
return this.planId
}
}
class RealLongAgentToolProvider implements ModelToolProviderLike {
readonly completedSteps: number[] = []
async listTools(): Promise<ModelToolDefinition[]> {
if (this.completedSteps.length >= 3) {
return []
}
const expectedStep = this.completedSteps.length + 1
return [
{
name: 'record_progress',
displayName: 'Record progress',
description:
expectedStep <= 3
? `Record required progress step ${expectedStep}. Call exactly once with step ${expectedStep} before continuing.`
: 'All required progress is recorded. Do not call this tool again.',
inputSchema: {
type: 'object',
properties: {
step: {
type: 'integer',
const: expectedStep
}
},
required: ['step'],
additionalProperties: false
},
source: 'builtin'
}
]
}
getApproval() {
return {
scopeKey: 'real-long-agent-test',
title: 'Record test progress',
description: 'Record deterministic E2E progress',
allowPermanent: false
}
}
async callTool(
name: string,
argumentsValue: Record<string, unknown>,
signal: AbortSignal
): Promise<ModelToolResult> {
signal.throwIfAborted()
const expectedStep = this.completedSteps.length + 1
if (
name !== 'record_progress' ||
argumentsValue.step !== expectedStep ||
expectedStep > 3
) {
throw new Error(
`Unexpected progress call: ${name} ${JSON.stringify(argumentsValue)}`
)
}
this.completedSteps.push(expectedStep)
const text = [
`STEP_${expectedStep}_RECORDED`,
`evidence-${expectedStep} `.repeat(4_000)
].join('\n')
return {
parts: [{ type: 'text', text }],
contextBytes: Buffer.byteLength(text)
}
}
async releaseConversation(): Promise<void> {}
async dispose(): Promise<void> {}
}
describe.runIf(enabled)('runtime end-to-end', () => {
let workspace = ''
beforeAll(async () => {
if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY is required for Runtime E2E')
}
workspace = await mkdtemp(join(tmpdir(), 'goodbuddy-runtime-e2e-'))
})
afterAll(async () => {
if (workspace) {
await new Promise((resolve) => setTimeout(resolve, 500))
await rm(workspace, { recursive: true, force: true })
}
})
it(
'streams a complete response through the direct model runtime',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key'
})
try {
const output = await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Return exactly this text and nothing else: MODEL_E2E_OK'
},
new AbortController().signal
)
)
expect(output).toContain('MODEL_E2E_OK')
} finally {
await runtime.dispose()
}
},
120_000
)
it(
'continues real direct-model history with local message IDs',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
maxOutputTokens: 128
})
try {
const output = await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Return exactly this text and nothing else: LOCAL_HISTORY_ID_E2E_OK',
history: [
{
role: 'user',
content:
'The required verification text is LOCAL_HISTORY_ID_E2E_OK.'
},
{
role: 'assistant',
content:
'I will return that verification text when asked.'
}
],
historyMessageIds: [
crypto.randomUUID(),
crypto.randomUUID()
]
},
new AbortController().signal
)
)
expect(output).toContain('LOCAL_HISTORY_ID_E2E_OK')
} finally {
await runtime.dispose()
}
},
120_000
)
it(
'counts a real image in provider-reported input usage',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
supportsImageInput: true,
maxOutputTokens: 128,
contextCompression: {
settings: {
...defaultContextCompressionSettings,
enabled: true
},
contextWindowTokens: 32_000
}
})
const baselineEvents: RuntimeEvent[] = []
const imageEvents: RuntimeEvent[] = []
const prompt =
'Return exactly this text and nothing else: IMAGE_USAGE_E2E_OK'
try {
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt
},
new AbortController().signal
)) {
baselineEvents.push(event)
}
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt,
images: [
{
name: 'goodbuddy-icon.png',
mediaType: 'image/png',
data: await readFile(
join(process.cwd(), 'build', 'icon.png'),
'base64'
)
}
]
},
new AbortController().signal
)) {
imageEvents.push(event)
}
} finally {
await runtime.dispose()
}
const baselineUsage = baselineEvents.find(
(
event
): event is Extract<RuntimeEvent, { type: 'model-usage' }> =>
event.type === 'model-usage'
)
const imageUsage = imageEvents.find(
(
event
): event is Extract<RuntimeEvent, { type: 'model-usage' }> =>
event.type === 'model-usage'
)
expect(baselineUsage).toBeDefined()
expect(imageUsage).toBeDefined()
expect(imageUsage!.inputTokens).toBeGreaterThan(
baselineUsage!.inputTokens
)
expect(
imageEvents.filter(
(event) => event.type === 'context-metrics'
)
).toEqual([
expect.objectContaining({
source: 'provider',
contextTokens:
imageUsage!.inputTokens +
imageUsage!.outputTokens +
(protocol === 'anthropic-messages'
? imageUsage!.cacheReadTokens +
imageUsage!.cacheWriteTokens
: 0)
})
])
},
180_000
)
it(
'compresses real direct-model history and preserves earlier and recent facts',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
contextCompression: {
settings: {
...defaultContextCompressionSettings,
enabled: true
},
contextWindowTokens: 32_000
}
})
const events: RuntimeEvent[] = []
try {
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Reply with exactly one line beginning CONTEXT_COMPRESSION_E2E_OK, followed by the project codename and deploy region found in the prior conversation.',
history: [
{
role: 'user',
content: [
'The project codename is ORBIT-739.',
'Background notes:',
'alpha '.repeat(8_000)
].join('\n')
},
{
role: 'assistant',
content: [
'I will remember the project codename.',
'Acknowledgement notes:',
'gamma '.repeat(6_500)
].join('\n')
},
{
role: 'user',
content: [
'The deploy region is AP-SOUTH-7.',
'Recent notes:',
'beta '.repeat(5_000)
].join('\n')
},
{
role: 'assistant',
content:
'I will also remember the deploy region.'
}
]
},
new AbortController().signal
)) {
events.push(event)
}
} finally {
await runtime.dispose()
}
const output = events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
expect(events).toContainEqual(
expect.objectContaining({
type: 'context-compression',
state: 'started'
})
)
expect(events).toContainEqual(
expect.objectContaining({
type: 'context-compression',
state: 'completed',
estimatedAfterTokens: expect.any(Number)
})
)
expect(events).toContainEqual(
expect.objectContaining({
type: 'model-usage',
callId: expect.stringMatching(/^context-summary:/u)
})
)
expect(output).toContain('CONTEXT_COMPRESSION_E2E_OK')
expect(output).toContain('ORBIT-739')
expect(output).toContain('AP-SOUTH-7')
},
120_000
)
it(
'compresses context after a real completed response reaches the threshold',
async () => {
const expectedOutput = [
'POST_RESPONSE_COMPRESSION_E2E_OK_',
'SAFE'.repeat(16)
].join('')
const prompt = `Return exactly this text and nothing else: ${expectedOutput}`
const history = [
{
role: 'user' as const,
content: `baseline\n${'alpha '.repeat(8_500)}`
},
{
role: 'assistant' as const,
content: 'ack'
}
]
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
maxOutputTokens: 128,
contextCompression: {
settings: {
...defaultContextCompressionSettings,
enabled: true,
triggerTokens: 8_000,
recentRawTokens: 4_000
},
contextWindowTokens: 32_000
}
})
const events: RuntimeEvent[] = []
try {
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt,
history
},
new AbortController().signal
)) {
events.push(event)
}
} finally {
await runtime.dispose()
}
const output = events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
const lastTextIndex = events.reduce(
(lastIndex, event, index) =>
event.type === 'text' ? index : lastIndex,
-1
)
const postResponseCompressionIndex = events.findIndex(
(event) =>
event.type === 'context-compression' &&
event.scope === 'conversation' &&
event.state === 'started'
)
expect(output).toContain(expectedOutput)
expect(lastTextIndex).toBeGreaterThanOrEqual(0)
expect(postResponseCompressionIndex).toBeGreaterThan(
lastTextIndex
)
expect(
events
.filter((event) => event.type === 'context-metrics')
.at(-1)
).toMatchObject({
type: 'context-metrics',
source: 'provider'
})
expect(events).not.toContainEqual(
expect.objectContaining({
type: 'context-metrics',
source: 'estimated'
})
)
expect(events.at(-1)).toMatchObject({ type: 'done' })
},
180_000
)
it(
'compacts a real multi-round Agent run and continues to completion',
async () => {
const toolProvider = new RealLongAgentToolProvider()
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
toolProvider,
contextCompression: {
settings: {
...defaultContextCompressionSettings,
enabled: true,
triggerTokens: 8_000,
recentRawTokens: 4_000
},
contextWindowTokens: 32_000
}
})
const events: RuntimeEvent[] = []
try {
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Call record_progress sequentially for steps 1, 2, and 3. Wait for each result before calling the next step. After all three results, do not call tools again and reply with LONG_AGENT_COMPRESSION_E2E_OK.'
},
new AbortController().signal,
async () => 'once'
)) {
events.push(event)
}
} finally {
await runtime.dispose()
}
const output = events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
expect(toolProvider.completedSteps).toEqual([1, 2, 3])
expect(events).toContainEqual(
expect.objectContaining({
type: 'context-compression',
scope: 'agent-run',
state: 'completed'
})
)
expect(output).toContain('LONG_AGENT_COMPRESSION_E2E_OK')
expect(
events
.filter((event) => event.type === 'context-metrics')
.at(-1)
).toMatchObject({ source: 'provider' })
expect(events).not.toContainEqual(
expect.objectContaining({
type: 'context-metrics',
source: 'estimated'
})
)
expect(events.at(-1)).toMatchObject({ type: 'done' })
},
240_000
)
it(
'discovers and plans GoodBuddy configuration through a real model',
async () => {
const testRoot = await mkdtemp(
join(tmpdir(), 'goodbuddy-config-model-e2e-')
)
const builtinSkillsRoot = join(testRoot, 'builtin-skills')
const importedSkillsRoot = join(testRoot, 'imported-skills')
await mkdir(builtinSkillsRoot, { recursive: true })
const cipher: CapabilityCipher = {
isAvailable: () => true,
encrypt: (value) => Buffer.from(value),
decrypt: (value) => value.toString()
}
const configService = new GoodBuddyConfigService(
new ApplicationSettingsStore(join(testRoot, 'application.json')),
new CapabilityService(
join(testRoot, 'capabilities.json'),
builtinSkillsRoot,
importedSkillsRoot,
cipher,
{
browserProfiles: new BrowserProfileService(
new MemoryBrowserProfileStore()
)
}
)
)
const requestId = crypto.randomUUID()
const toolProvider = new RealModelConfigToolProvider(
configService,
workspace,
requestId
)
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
defaultWorkspace: workspace,
toolProvider
})
try {
const output = await collectText(
runtime.run(
{
requestId,
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Use GoodBuddy configuration tools. First discover capabilities and examples, then read the sanitized current configuration, then create (but do not apply) a plan that sets checkUpdatesOnStartup to false. Finish with CONFIG_PLAN_OK and the plan risk. Never call apply.'
},
new AbortController().signal,
async (event) =>
event.toolName === 'goodbuddy_config_apply'
? 'deny'
: 'once'
)
)
expect(toolProvider.calls).toEqual(
expect.arrayContaining([
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan'
])
)
expect(toolProvider.calls).not.toContain('goodbuddy_config_apply')
expect(toolProvider.getPlannedId()).toBeDefined()
expect(output).toContain('CONFIG_PLAN_OK')
} finally {
await runtime.dispose()
await rm(testRoot, { recursive: true, force: true })
}
},
120_000
)
it(
'cancels an in-flight direct model task',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
contextCompression: {
settings: {
...defaultContextCompressionSettings,
enabled: true
},
contextWindowTokens: 32_000
}
})
const abortController = new AbortController()
const events: RuntimeEvent[] = []
try {
const result = (async () => {
for await (const event of runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Write a detailed technical essay of at least 3000 words.'
},
abortController.signal
)) {
events.push(event)
}
})()
setTimeout(() => abortController.abort(), 50)
await expect(result).rejects.toMatchObject({
name: 'AbortError'
})
expect(events).not.toContainEqual(
expect.objectContaining({
type: 'context-metrics'
})
)
} finally {
await runtime.dispose()
}
},
120_000
)
it(
'completes an Execute file task through bundled OpenCode',
async () => {
const runtime = new AgentRuntimeController(
new OpenCodeRuntime({
embedded: true,
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'opencode',
'opencode.exe'
),
configPath: '',
defaultWorkspace: workspace,
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey,
protocol,
authentication: 'api-key'
}
})
)
const approvals: string[] = []
try {
await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Create opencode-output.txt in the current workspace with exactly OPENCODE_E2E_OK. Use the file tools and finish only after verifying the file.'
},
new AbortController().signal,
async (request) => {
approvals.push(request.scopeKey)
return 'once'
}
)
)
expect(approvals).not.toContain('runtime:whole-run')
expect(approvals).toEqual([])
await expect(
readFile(join(workspace, 'opencode-output.txt'), 'utf8')
).resolves.toMatch(/^OPENCODE_E2E_OK\r?\n?$/u)
} finally {
await runtime.dispose()
}
},
180_000
)
it(
'compacts and continues a real bundled OpenCode session',
async () => {
const runtime = new OpenCodeRuntime({
embedded: true,
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'opencode',
'opencode.exe'
),
configPath: '',
defaultWorkspace: workspace,
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey,
protocol,
authentication: 'api-key'
}
})
const conversationId = crypto.randomUUID()
const signal = new AbortController().signal
try {
await expect(
collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId,
workMode: 'ask',
prompt:
'Remember that the verification codename is NATIVE-COMPACT-739. Reply with exactly OPENCODE_COMPACT_READY.'
},
signal
)
)
).resolves.toContain('OPENCODE_COMPACT_READY')
await expect(
runtime.compactConversation(
{
requestId: crypto.randomUUID(),
conversationId,
runtimeSelection: { provider: 'opencode' },
history: [
{
role: 'user',
content:
'The verification codename is NATIVE-COMPACT-739.'
},
{
role: 'assistant',
content: 'OPENCODE_COMPACT_READY'
}
],
historyMessageIds: [
crypto.randomUUID(),
crypto.randomUUID()
]
},
signal
)
).resolves.toMatchObject({
result: {
provider: 'opencode',
strategy: 'native',
compacted: true
}
})
await expect(
collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId,
workMode: 'ask',
prompt:
'Return exactly the verification codename from before and nothing else.'
},
signal
)
)
).resolves.toContain('NATIVE-COMPACT-739')
} finally {
await runtime.dispose()
}
},
180_000
)
it(
'completes an Execute file task through bundled Continue',
async () => {
const runtime = new AgentRuntimeController(
new ContinueAgentRuntime({
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'continue',
'dist',
'cn.js'
),
configPath: '',
defaultWorkspace: workspace,
hostCacheRoot: join(workspace, '.continue-host'),
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey,
protocol,
authentication: 'api-key'
}
})
)
const approvals: string[] = []
try {
await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Create continue-output.txt in the current workspace with exactly CONTINUE_E2E_OK. Use tools and finish only after verifying the file.'
},
new AbortController().signal,
async (request) => {
approvals.push(request.scopeKey)
return 'once'
}
)
)
expect(approvals).toEqual([])
await expect(
readFile(join(workspace, 'continue-output.txt'), 'utf8')
).resolves.toMatch(/^CONTINUE_E2E_OK\r?\n?$/u)
} finally {
await runtime.dispose()
}
},
180_000
)
it(
'calls a Main-brokered custom MCP through bundled OpenCode',
async () => {
const gateway = new KnowledgeMcpGateway({} as never)
await gateway.start()
const runtime = new AgentRuntimeController(
new OpenCodeRuntime({
embedded: true,
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'opencode',
'opencode.exe'
),
configPath: '',
defaultWorkspace: workspace,
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey,
protocol,
authentication: 'api-key'
},
knowledgeGateway: gateway,
mcpServers: [customMcpServer('opencode')]
})
)
try {
const events = await collectEvents(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Use the assigned custom MCP tool to create a neon-ruins game blueprint with seed opencode-live and targetCount 5. Then reply with OPENCODE_MCP_E2E_OK and the blueprint title.'
},
new AbortController().signal,
async (request) =>
[
request.scopeKey,
request.title,
request.description,
request.toolName ?? ''
].some((value) =>
value.includes('create_game_blueprint')
)
? 'once'
: 'deny'
)
)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'tool',
name: expect.stringContaining(
'create_game_blueprint'
),
state: 'completed'
})
])
)
expect(
events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
).toContain('OPENCODE_MCP_E2E_OK')
expect(
events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
).toContain('Prism Relay')
} finally {
await runtime.dispose()
await gateway.dispose()
}
},
180_000
)
it(
'calls a Main-brokered custom MCP through bundled Continue',
async () => {
const gateway = new KnowledgeMcpGateway({} as never)
await gateway.start()
const runtime = new AgentRuntimeController(
new ContinueAgentRuntime({
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'continue',
'dist',
'cn.js'
),
configPath: '',
defaultWorkspace: workspace,
hostCacheRoot: join(workspace, '.continue-mcp-host'),
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey,
protocol,
authentication: 'api-key'
},
knowledgeGateway: gateway,
mcpServers: [customMcpServer('continue')]
})
)
try {
const events = await collectEvents(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Use the assigned custom MCP tool to create a neon-ruins game blueprint with seed continue-live and targetCount 5. Then reply with CONTINUE_MCP_E2E_OK and the blueprint title.'
},
new AbortController().signal,
async (request) =>
[
request.scopeKey,
request.title,
request.description,
request.toolName ?? ''
].some((value) =>
value.includes('create_game_blueprint')
)
? 'once'
: 'deny'
)
)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'tool',
name: expect.stringContaining(
'create_game_blueprint'
),
state: 'completed'
})
])
)
const output = events
.flatMap((event) =>
event.type === 'text' ? [event.delta] : []
)
.join('')
expect(output).toContain('CONTINUE_MCP_E2E_OK')
expect(output).toContain('Prism Relay')
} finally {
await runtime.dispose()
await gateway.dispose()
}
},
180_000
)
})