Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
568 lines
17 KiB
TypeScript
568 lines
17 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { ResolvedRuntimeSettings } from '../runtime-settings-store'
|
|
import type { BrowserToolService } from '../browser/browser-model-tools'
|
|
import { defaultRuntimeCustomizationSettings } from '../../shared/contracts'
|
|
import {
|
|
createAgentRuntime,
|
|
createModelProfileRuntime
|
|
} from './create-runtime'
|
|
import { AgentRuntimeController } from './runtime-controller'
|
|
import type { WorkspaceAccess } from '../workspace'
|
|
|
|
function createBrowserService(): BrowserToolService & {
|
|
dispose: ReturnType<typeof vi.fn>
|
|
} {
|
|
return {
|
|
getOrigin: vi.fn(() => undefined),
|
|
navigate: vi.fn(),
|
|
snapshot: vi.fn(),
|
|
click: vi.fn(),
|
|
type: vi.fn(),
|
|
select: vi.fn(),
|
|
back: vi.fn(),
|
|
screenshot: vi.fn(),
|
|
releaseConversation: vi.fn(async () => undefined),
|
|
dispose: vi.fn(async () => undefined)
|
|
}
|
|
}
|
|
|
|
function settings(
|
|
overrides: Partial<ResolvedRuntimeSettings> = {}
|
|
): ResolvedRuntimeSettings {
|
|
const defaultModelProfileId =
|
|
'00000000-0000-4000-8000-000000000001'
|
|
return {
|
|
provider: 'model',
|
|
modelBaseUrl: 'http://127.0.0.1:11434/v1',
|
|
modelName: 'qwen3',
|
|
modelProtocol: 'openai-chat-completions',
|
|
modelAuthentication: 'none',
|
|
imageGenerationQuality: 'auto',
|
|
modelProfiles: [
|
|
{
|
|
id: defaultModelProfileId,
|
|
name: '默认模型',
|
|
baseUrl: 'http://127.0.0.1:11434/v1',
|
|
modelName: 'qwen3',
|
|
protocol: 'openai-chat-completions',
|
|
authentication: 'none',
|
|
imageGenerationQuality: 'auto'
|
|
}
|
|
],
|
|
defaultModelProfileId,
|
|
opencodeBaseUrl: '',
|
|
opencodeEmbedded: false,
|
|
opencodeBinaryPath: '',
|
|
opencodeConfigPath: '',
|
|
continueBinaryPath: '',
|
|
continueConfigPath: '',
|
|
continueMode: 'chat',
|
|
subagentSmartRoutingEnabled: false,
|
|
knowledgeEmbeddingEnabled: false,
|
|
knowledgeEmbeddingBaseUrl:
|
|
'http://127.0.0.1:11434/v1/embeddings',
|
|
knowledgeEmbeddingModel: 'nomic-embed-text',
|
|
knowledgeRerankEnabled: false,
|
|
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
|
|
knowledgeRerankModel: 'rerank-v3.5',
|
|
runtimeCustomization: defaultRuntimeCustomizationSettings,
|
|
workspacePath: process.cwd(),
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('createAgentRuntime model compatibility', () => {
|
|
it('forwards the existing attachment owner to direct-model output consumers', async () => {
|
|
const backingStore = {
|
|
create: vi.fn(), append: vi.fn(), finish: vi.fn(), read: vi.fn(), release: vi.fn()
|
|
}
|
|
const outputAdopt = vi.fn()
|
|
const outputStore = { backingStore }
|
|
const runtime = createAgentRuntime(process.cwd(), settings(), { outputStore, outputAdopt })
|
|
try {
|
|
expect(runtime).toMatchObject({ options: { outputStore, outputAdopt } })
|
|
expect(backingStore.create).not.toHaveBeenCalled()
|
|
} finally { await runtime.dispose() }
|
|
})
|
|
|
|
it('passes the desktop failure observer to local OpenCode', async () => {
|
|
const observeFailure = vi.fn()
|
|
const runtime = createAgentRuntime(process.cwd(), settings({ provider: 'opencode' }), { observeFailure })
|
|
try {
|
|
expect(runtime).toMatchObject({ options: { observeFailure } })
|
|
} finally {
|
|
await runtime.dispose()
|
|
}
|
|
})
|
|
|
|
it('does not treat the default model profile as the platform DeepSeek source', () => {
|
|
const defaultProfile = {
|
|
id: '00000000-0000-4000-8000-000000000001',
|
|
name: 'Default DeepSeek',
|
|
baseUrl: 'https://api.deepseek.com',
|
|
modelName: 'deepseek-chat',
|
|
protocol: 'openai-chat-completions' as const,
|
|
authentication: 'api-key' as const,
|
|
imageGenerationQuality: 'auto' as const,
|
|
apiKey: 'default-deepseek-key'
|
|
}
|
|
|
|
expect(() =>
|
|
createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'deepseek-harness',
|
|
modelBaseUrl: defaultProfile.baseUrl,
|
|
modelName: defaultProfile.modelName,
|
|
modelProtocol: defaultProfile.protocol,
|
|
modelAuthentication: defaultProfile.authentication,
|
|
apiKey: defaultProfile.apiKey,
|
|
modelProfiles: [defaultProfile]
|
|
}),
|
|
{ deepseekHarnessLauncher: vi.fn() }
|
|
)
|
|
).toThrow(
|
|
'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
|
|
)
|
|
})
|
|
|
|
it('forwards a compatible gateway profile to DeepSeek Harness', async () => {
|
|
const profile = {
|
|
id: '00000000-0000-4000-8000-000000000006',
|
|
name: 'OpenAI-compatible gateway',
|
|
baseUrl: 'https://gateway.example/openai/v1',
|
|
modelName: 'qwen-plus',
|
|
protocol: 'openai-chat-completions' as const,
|
|
authentication: 'api-key' as const,
|
|
supportsImageInput: true,
|
|
imageGenerationQuality: 'auto' as const,
|
|
requestHeaders: { 'x-tenant-id': 'harness-tenant' },
|
|
requestBody: { temperature: 0.2 },
|
|
apiKey: 'gateway-key'
|
|
}
|
|
const deepseekHarnessLauncher = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('stop after launch options'))
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'deepseek-harness',
|
|
modelProfiles: [profile],
|
|
defaultModelProfileId: profile.id,
|
|
deepseekHarnessModelProfile: profile
|
|
}),
|
|
{ deepseekHarnessLauncher }
|
|
)
|
|
|
|
expect(runtime.runtimeId).toBe('deepseek-harness')
|
|
await expect(runtime.getStatus()).resolves.toMatchObject({
|
|
available: true
|
|
})
|
|
expect(deepseekHarnessLauncher).not.toHaveBeenCalled()
|
|
await expect(runtime.testConnection!()).resolves.toMatchObject({
|
|
available: false
|
|
})
|
|
expect(deepseekHarnessLauncher).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
supportsImageInput: true,
|
|
requestHeaders: { 'x-tenant-id': 'harness-tenant' }
|
|
})
|
|
)
|
|
expect(
|
|
deepseekHarnessLauncher.mock.calls[0]?.[0]
|
|
).not.toHaveProperty('requestBody')
|
|
await runtime.dispose()
|
|
})
|
|
|
|
it('creates an available direct runtime for a no-auth model', async () => {
|
|
const runtime = createAgentRuntime(process.cwd(), settings())
|
|
|
|
await expect(runtime.getStatus()).resolves.toMatchObject({
|
|
id: 'model',
|
|
available: true,
|
|
supportsToolExecution: true,
|
|
detail: expect.stringContaining('OpenAI Chat Completions')
|
|
})
|
|
await runtime.dispose()
|
|
})
|
|
|
|
it('forwards the selected profile request customization to direct chats', async () => {
|
|
const profile = {
|
|
id: '00000000-0000-4000-8000-000000000041',
|
|
name: 'Tenant model',
|
|
baseUrl: 'https://gateway.example/v1',
|
|
modelName: 'tenant-model',
|
|
protocol: 'openai-chat-completions' as const,
|
|
authentication: 'none' as const,
|
|
supportsImageInput: false,
|
|
maximumOutputTokens: 48_000,
|
|
imageGenerationQuality: 'auto' as const,
|
|
requestHeaders: {
|
|
'x-tenant-id': 'tenant-a'
|
|
},
|
|
requestBody: {
|
|
temperature: 0.2
|
|
}
|
|
}
|
|
const fetcher = vi.fn<typeof fetch>(async () =>
|
|
new Response(
|
|
[
|
|
`data: ${JSON.stringify({
|
|
choices: [
|
|
{
|
|
delta: { content: 'OK' },
|
|
finish_reason: 'stop'
|
|
}
|
|
]
|
|
})}`,
|
|
'',
|
|
'data: [DONE]',
|
|
'',
|
|
''
|
|
].join('\n'),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'text/event-stream' }
|
|
}
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
modelProfiles: [profile],
|
|
defaultModelProfileId: profile.id
|
|
})
|
|
)
|
|
|
|
try {
|
|
for await (const _event of runtime.run(
|
|
{
|
|
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ee',
|
|
conversationId: 'customized-direct-chat',
|
|
prompt: 'hello'
|
|
},
|
|
new AbortController().signal
|
|
)) {
|
|
void _event
|
|
}
|
|
|
|
const init = fetcher.mock.calls[0]?.[1]
|
|
expect(new Headers(init?.headers).get('x-tenant-id')).toBe(
|
|
'tenant-a'
|
|
)
|
|
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
model: 'tenant-model',
|
|
temperature: 0.2
|
|
})
|
|
} finally {
|
|
await runtime.dispose()
|
|
vi.unstubAllGlobals()
|
|
}
|
|
})
|
|
|
|
it('forwards the selected profile image capability to direct runtimes', async () => {
|
|
const visionSettings = settings({
|
|
supportsImageInput: true
|
|
})
|
|
visionSettings.modelProfiles = visionSettings.modelProfiles.map(
|
|
(profile) => ({
|
|
...profile,
|
|
supportsImageInput: true
|
|
})
|
|
)
|
|
const fetcher = vi.fn(async () =>
|
|
new Response(
|
|
[
|
|
`data: ${JSON.stringify({
|
|
choices: [
|
|
{
|
|
delta: { content: 'OK' },
|
|
finish_reason: 'stop'
|
|
}
|
|
]
|
|
})}`,
|
|
'',
|
|
'data: [DONE]',
|
|
'',
|
|
''
|
|
].join('\n'),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'text/event-stream' }
|
|
}
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetcher)
|
|
const runtime = createAgentRuntime(process.cwd(), visionSettings)
|
|
|
|
try {
|
|
const events = []
|
|
for await (const event of runtime.run(
|
|
{
|
|
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ef',
|
|
conversationId: 'wechat-conversation',
|
|
prompt: '描述图片',
|
|
images: [
|
|
{
|
|
name: '微信图片.png',
|
|
mediaType: 'image/png',
|
|
data: 'aW1hZ2U='
|
|
}
|
|
]
|
|
},
|
|
new AbortController().signal
|
|
)) {
|
|
events.push(event)
|
|
}
|
|
|
|
expect(fetcher).toHaveBeenCalledOnce()
|
|
expect(events).toContainEqual(
|
|
expect.objectContaining({ type: 'done' })
|
|
)
|
|
} finally {
|
|
await runtime.dispose()
|
|
vi.unstubAllGlobals()
|
|
}
|
|
})
|
|
|
|
it('shares injected browser service without runtime-owned disposal', async () => {
|
|
const browserService = createBrowserService()
|
|
const first = createAgentRuntime(process.cwd(), settings(), {
|
|
browserService
|
|
})
|
|
const second = createAgentRuntime(process.cwd(), settings(), {
|
|
browserService
|
|
})
|
|
const controller = new AgentRuntimeController(first)
|
|
|
|
await controller.releaseConversation('conversation-one')
|
|
await controller.replace(second)
|
|
await controller.releaseConversation('conversation-two')
|
|
await controller.dispose()
|
|
|
|
expect(browserService.releaseConversation).toHaveBeenNthCalledWith(
|
|
1,
|
|
'conversation-one'
|
|
)
|
|
expect(browserService.releaseConversation).toHaveBeenNthCalledWith(
|
|
2,
|
|
'conversation-two'
|
|
)
|
|
expect(browserService.dispose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not expose the browser service to OpenCode runtimes', async () => {
|
|
const browserService = createBrowserService()
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'opencode',
|
|
opencodeBaseUrl: 'http://127.0.0.1:4096'
|
|
}),
|
|
{ browserService }
|
|
)
|
|
|
|
await runtime.releaseConversation?.('opencode-conversation')
|
|
await runtime.dispose()
|
|
|
|
expect(browserService.releaseConversation).not.toHaveBeenCalled()
|
|
expect(browserService.dispose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it(
|
|
'treats a blank OpenCode Server as bundled local mode even for legacy false settings',
|
|
async () => {
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'opencode',
|
|
opencodeBaseUrl: '',
|
|
opencodeEmbedded: false
|
|
})
|
|
)
|
|
|
|
await expect(runtime.getStatus()).resolves.not.toMatchObject({
|
|
detail: '未配置 OpenCode Server'
|
|
})
|
|
await runtime.dispose()
|
|
},
|
|
15_000
|
|
)
|
|
|
|
it.each([
|
|
['openai-chat-completions', 'none'],
|
|
['openai-responses', 'api-key']
|
|
] as const)(
|
|
'accepts an OpenCode %s independent profile',
|
|
async (protocol, authentication) => {
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'opencode',
|
|
opencodeModelProfile: {
|
|
id: '00000000-0000-4000-8000-000000000031',
|
|
name: 'OpenAI profile',
|
|
baseUrl: 'https://api.example/v1',
|
|
modelName: 'model',
|
|
protocol,
|
|
authentication,
|
|
imageGenerationQuality: 'auto',
|
|
...(authentication === 'api-key'
|
|
? { apiKey: 'secret' }
|
|
: {})
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(runtime.requiresToolApproval).toBe(false)
|
|
await runtime.dispose()
|
|
}
|
|
)
|
|
|
|
it('marks direct image runtimes and rejects them for Agent Runtimes', async () => {
|
|
const imageSettings = settings({
|
|
modelBaseUrl: 'https://bigtoken.ai/v1',
|
|
modelName: 'gpt-image-2',
|
|
modelProtocol: 'openai-images-generations',
|
|
modelAuthentication: 'api-key',
|
|
imageGenerationQuality: 'high',
|
|
apiKey: 'secret',
|
|
modelProfiles: [
|
|
{
|
|
id: '00000000-0000-4000-8000-000000000001',
|
|
name: '默认图像模型',
|
|
baseUrl: 'https://bigtoken.ai/v1',
|
|
modelName: 'gpt-image-2',
|
|
protocol: 'openai-images-generations',
|
|
authentication: 'api-key',
|
|
imageGenerationQuality: 'high',
|
|
apiKey: 'secret'
|
|
}
|
|
]
|
|
})
|
|
const runtime = createAgentRuntime(process.cwd(), imageSettings)
|
|
await expect(runtime.getStatus()).resolves.toMatchObject({
|
|
capability: 'image-generation'
|
|
})
|
|
await runtime.dispose()
|
|
|
|
expect(() =>
|
|
createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'continue',
|
|
continueModelProfile: {
|
|
id: '00000000-0000-4000-8000-000000000032',
|
|
name: 'Image profile',
|
|
baseUrl: 'https://bigtoken.ai/v1',
|
|
modelName: 'gpt-image-2',
|
|
protocol: 'openai-images-generations',
|
|
authentication: 'api-key',
|
|
imageGenerationQuality: 'high',
|
|
apiKey: 'secret'
|
|
}
|
|
})
|
|
)
|
|
).toThrow('Continue 独立模型连接仅支持')
|
|
expect(() =>
|
|
createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'opencode',
|
|
opencodeModelProfile: {
|
|
id: '00000000-0000-4000-8000-000000000033',
|
|
name: 'Image profile',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
modelName: 'gpt-image-2',
|
|
protocol: 'openai-images-generations',
|
|
authentication: 'api-key',
|
|
imageGenerationQuality: 'auto',
|
|
apiKey: 'secret'
|
|
}
|
|
})
|
|
)
|
|
).toThrow('OpenCode 独立模型连接仅支持')
|
|
})
|
|
|
|
it('accepts a Continue Responses independent profile', async () => {
|
|
const runtime = createAgentRuntime(
|
|
process.cwd(),
|
|
settings({
|
|
provider: 'continue',
|
|
continueModelProfile: {
|
|
id: '00000000-0000-4000-8000-000000000035',
|
|
name: 'Responses profile',
|
|
baseUrl: 'https://api.example/v1',
|
|
modelName: 'gpt-compatible',
|
|
protocol: 'openai-responses',
|
|
authentication: 'api-key',
|
|
imageGenerationQuality: 'auto',
|
|
apiKey: 'secret'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(runtime.requiresToolApproval).toBe(false)
|
|
await runtime.dispose()
|
|
})
|
|
|
|
it('creates a testable runtime for an image model profile', async () => {
|
|
const resolved = settings()
|
|
const runtime = createModelProfileRuntime(
|
|
process.cwd(),
|
|
resolved,
|
|
{
|
|
id: '00000000-0000-4000-8000-000000000034',
|
|
name: 'Image profile',
|
|
baseUrl: 'https://bigtoken.ai/v1',
|
|
modelName: 'gpt-image-2',
|
|
protocol: 'openai-images-generations',
|
|
authentication: 'api-key',
|
|
imageGenerationQuality: 'high',
|
|
apiKey: 'secret'
|
|
}
|
|
)
|
|
|
|
await expect(runtime.getStatus()).resolves.toMatchObject({
|
|
id: 'model',
|
|
capability: 'image-generation',
|
|
available: true
|
|
})
|
|
await runtime.dispose()
|
|
})
|
|
|
|
it('rejects remote process runtimes before invoking any local launcher', () => {
|
|
const launch = vi.fn()
|
|
const launchEnvironmentProvider = vi.fn(() =>
|
|
Object.freeze({ PATH: '/local-tools:/usr/bin' })
|
|
)
|
|
const workspaceAccess = {
|
|
dispose: vi.fn(async () => undefined)
|
|
} as unknown as WorkspaceAccess
|
|
|
|
expect(() =>
|
|
createAgentRuntime(
|
|
'C:\\SafeLocalDefault',
|
|
settings({
|
|
provider: 'deepseek-harness'
|
|
}),
|
|
{
|
|
deepseekHarnessLauncher: launch,
|
|
launchEnvironmentProvider,
|
|
executionSpace: {
|
|
kind: 'ssh',
|
|
hostId: 'host-one',
|
|
remoteRootPath: '/srv/project',
|
|
cacheIdentity: 'remote-one',
|
|
routeIdentity: 'remote-one',
|
|
workspaceAccess
|
|
}
|
|
}
|
|
)
|
|
).toThrow('远程执行空间尚不可用')
|
|
expect(launch).not.toHaveBeenCalled()
|
|
expect(launchEnvironmentProvider).not.toHaveBeenCalled()
|
|
})
|
|
})
|