Files
goodbuddy/src/main/agent/opencode-selection-isolation.test.ts
T
mesalogo bb8c04af6c
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
fix: use bundled OpenCode path in isolation tests
Linux release CI could not start OpenCode because the isolation test assumed a platform-specific npm executable name. Reuse the production runtime path resolver, which matches the installed bin/opencode.exe on all platforms.

Validation: nine focused tests, typecheck, lint, and bilingual release-note verification passed. Product behavior, versions, and release notes remain unchanged; main-branch CI must pass before tagging.
2026-09-23 20:09:05 +08:00

201 lines
10 KiB
TypeScript

// @vitest-environment node
import { createServer } from 'node:http'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import spawn from 'cross-spawn'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { runtimeSettingsInputSchema } from '../../shared/contracts'
import { RuntimeSettingsStore } from '../runtime-settings-store'
import { applyRuntimeSelection } from './runtime-selection'
import { OpenCodeRuntime } from './opencode-runtime'
import { resolveBundledRuntimePaths } from './bundled-runtimes'
const cipher = {
isAvailable: () => false,
encrypt: (): never => { throw new Error('No credentials in this test') },
decrypt: (): never => { throw new Error('No credentials in this test') }
}
const newId = '00000000-0000-4000-8000-000000000123'
const denial = "OpenCode's free tier can only be used from within OpenCode"
describe('OpenCode isolated configuration paths', () => {
let fixtureRoot: string
let bundledConfigPath: string
beforeAll(async () => {
fixtureRoot = await mkdtemp(join(tmpdir(), 'goodbuddy-opencode-config-isolation-'))
const { prepareBundledOpenCodeConfig } = createRequire(import.meta.url)(
'../../../build/opencode-config.cjs'
)
bundledConfigPath = prepareBundledOpenCodeConfig(fixtureRoot)
}, 60_000)
afterAll(async () => {
if (fixtureRoot) await rm(fixtureRoot, { recursive: true, force: true })
})
it.each(['default', 'profile', 'platform', 'external'] as const)(
'resolves persisted %s source independently of the global default', async (source) => {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-source-matrix-'))
try {
const path = join(root, 'runtime-settings.json')
const store = new RuntimeSettingsStore(path, cipher, {})
const fresh = await store.getPublicSettings()
await store.update(runtimeSettingsInputSchema.parse({
...Object.fromEntries(Object.entries(fresh).filter(([key]) => key in runtimeSettingsInputSchema.shape)),
embeddingConnections: undefined,
apiKey: { action: 'keep' },
modelProfiles: [...fresh.modelProfiles, {
...fresh.modelProfiles[0], id: newId, name: 'Local probe',
modelName: 'probe-model', baseUrl: 'http://127.0.0.1:12345/v1'
}].map((profile) => ({
id: profile.id, name: profile.name, baseUrl: profile.baseUrl,
modelName: profile.modelName, protocol: profile.protocol,
authentication: profile.authentication, imageGenerationQuality: profile.imageGenerationQuality,
apiKey: { action: 'keep' }
})),
opencodeModelSource: source === 'profile'
? { kind: 'profile', profileId: newId }
: { kind: source === 'external' ? 'default' : source },
opencodeBaseUrl: source === 'external' ? 'http://127.0.0.1:12346' : ''
}))
const reloaded = new RuntimeSettingsStore(path, cipher, {})
const selected = applyRuntimeSelection(await reloaded.getResolvedSettings(), { provider: 'opencode' })
expect(selected.settings.opencodeEmbedded).toBe(source !== 'external')
expect(selected.settings.opencodeModelProfile?.modelName).toBe(
source === 'default' ? 'qwen3' : source === 'profile' ? 'probe-model' : undefined
)
expect((await reloaded.getPublicSettings()).opencodeModelSource.kind).toBe(
source === 'external' ? 'platform' : source
)
if (source === 'platform') {
expect(applyRuntimeSelection(await reloaded.getResolvedSettings(), {
provider: 'opencode', profileId: newId
}).settings.opencodeModelProfile?.id).toBe(newId)
}
} finally {
await rm(root, { recursive: true, force: true })
}
}
)
it.each(['profile', 'native-file', 'native-empty'] as const)(
'runs bundled OpenCode with isolated %s configuration and loopback-only model calls', async (source) => {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-opencode-isolation-'))
const requests: Array<{ path: string; model: string }> = []
const server = createServer(async (request, response) => {
let body = ''
for await (const chunk of request) body += String(chunk)
requests.push({ path: request.url ?? '', model: JSON.parse(body).model })
response.writeHead(403, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: { message: denial, type: 'forbidden' } }))
})
await new Promise<void>((done) => server.listen(0, '127.0.0.1', done))
const address = server.address()
if (!address || typeof address === 'string') throw new Error('No loopback address')
const baseURL = `http://127.0.0.1:${address.port}/v1`
const configPath = join(root, 'native.json')
await writeFile(configPath, JSON.stringify({
model: 'local-native/probe-model',
provider: { 'local-native': {
npm: '@ai-sdk/openai-compatible',
options: { baseURL }, models: { 'probe-model': {} }
} }
}))
let launchEnvironment: NodeJS.ProcessEnv = {}
let serverUrl = ''
const binaryPath = resolveBundledRuntimePaths({
appPath: process.cwd(), resourcesPath: process.cwd(), packaged: false
}).opencode
const runtime = new OpenCodeRuntime({
embedded: true, binaryPath, defaultWorkspace: root,
configPath: source === 'native-empty' ? '' : configPath,
sharedCacheRoot: join(root, 'runtime-cache'),
bundledConfigPath,
...(source === 'profile' ? { modelProfile: {
id: newId, name: 'Local mock', baseUrl: baseURL, modelName: 'probe-model',
protocol: 'openai-chat-completions' as const, authentication: 'none' as const
} } : {})
}, {
startupTimeoutMs: 20_000,
validateBinary: async () => ({ path: binaryPath, detail: 'Isolated bundled binary' }),
spawn: ((command, args, options) => {
// Allow only OS plumbing and the configuration generated by this runtime.
const env: NodeJS.ProcessEnv = {}
for (const [key, value] of Object.entries(options?.env ?? {})) {
if (/^(PATH|SYSTEMROOT|WINDIR|COMSPEC|PATHEXT)$/i.test(key) ||
/^OPENCODE_(CONFIG|CONFIG_DIR|CONFIG_CONTENT|SERVER_USERNAME|SERVER_PASSWORD|DISABLE_[A-Z_]+|EXPERIMENTAL_DISABLE_FILEWATCHER)$/.test(key) ||
key.startsWith('XDG_')) env[key] = value
}
Object.assign(env, { HOME: root, USERPROFILE: root, APPDATA: join(root, 'appdata'),
LOCALAPPDATA: join(root, 'localappdata'), TEMP: root, TMP: root,
OPENCODE_DISABLE_DEFAULT_PLUGINS: '1' })
launchEnvironment = env
serverUrl = `http://127.0.0.1:${args!.find((arg) => arg.startsWith('--port='))!.split('=')[1]}`
return spawn(command, args!, { ...options, env })
}) as typeof spawn
})
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 25_000)
try {
const connection = await runtime.testConnection()
expect(connection, connection.detail).toMatchObject({ available: true })
const config = JSON.parse(launchEnvironment.OPENCODE_CONFIG_CONTENT!)
expect(config.model).toBe(source === 'profile' ? 'goodbuddy-openai-chat/probe-model' : undefined)
expect(launchEnvironment.OPENCODE_CONFIG).toBe(source === 'native-file' ? configPath : undefined)
expect(launchEnvironment.XDG_CONFIG_HOME).toBe(join(root, 'runtime-cache', 'config'))
if (source === 'native-empty') {
// Inspect the bundled catalog without issuing any inference request.
const requestOptions = {
headers: { authorization: `Basic ${Buffer.from(
`${launchEnvironment.OPENCODE_SERVER_USERNAME}:${launchEnvironment.OPENCODE_SERVER_PASSWORD}`
).toString('base64')}` }, signal: controller.signal
}
const health = await (await fetch(`${serverUrl}/global/health`, requestOptions)).json() as { version: string }
const installed = JSON.parse(await readFile(join(process.cwd(), 'node_modules', 'opencode-ai', 'package.json'), 'utf8'))
expect(health.version).toBe(installed.version)
const response = await fetch(`${serverUrl}/config/providers`, requestOptions)
expect(response.status).toBe(200)
const catalog = await response.json() as {
providers: Array<{ id: string; models: Record<string, { cost: { input: number } }> }>;
default: Record<string, string>
}
const native = catalog.providers.find((provider) => provider.id === 'opencode')
expect(native).toBeDefined()
expect(Object.values(native!.models).every((model) => model.cost.input === 0)).toBe(true)
expect(native!.models[catalog.default.opencode!]).toBeDefined()
process.stdout.write('Isolated native catalog: ' + JSON.stringify({
version: health.version,
providers: catalog.providers.map((provider) => provider.id),
default: catalog.default, freeModels: Object.keys(native!.models)
}) + '\n')
expect(requests).toEqual([])
} else {
let failure = ''
try {
for await (const event of runtime.run({ requestId: '403-isolation',
conversationId: '403-isolation', prompt: 'Reply OK', workMode: 'ask'
}, controller.signal)) void event
} catch (error) {
failure = error instanceof Error ? error.message : String(error)
}
expect(requests.length).toBeGreaterThan(0)
expect(requests.every((request) => request.path === '/v1/chat/completions' &&
request.model === 'probe-model')).toBe(true)
expect(failure).toContain(denial)
expect(failure).toContain('statusCode: 403')
process.stdout.write('Isolated 403 result: ' + JSON.stringify({ source, requests: requests.length, failure }) + '\n')
}
expect(JSON.parse(await readFile(configPath, 'utf8')).model).toBe('local-native/probe-model')
} finally {
clearTimeout(timeout)
controller.abort()
await runtime.dispose()
server.closeAllConnections()
await new Promise<void>((done) => server.close(() => done()))
await rm(root, { recursive: true, force: true })
}
}, 40_000
)
})