Wait for model delivery acknowledgements before completing remote prompts and preserve completed outcomes when idle Runtime cleanup fails. Explicit messages after an unknown outcome now open a fresh binding with history instead of reusing the failed session; recovery remains attach-only. Run obsolete payload cleanup through the current Agent after activation, retaining history and unresolved process ownership. Live legacy daemons and shared Runtime versions remain deferred. Includes focused regressions, local OpenCode simulation, and isolated Linux validation evidence. Typecheck and lint passed; the full test run timed out. Release note: Remote conversations can continue after an uncertain result, and completed answers no longer fail solely because Runtime cleanup fails. Updates remove verified inactive Agent payloads and owned orphan processes while retaining active or uncertain installations and conversation history.
1000 lines
32 KiB
TypeScript
1000 lines
32 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdtempSync,
|
|
rmSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import {
|
|
request as httpRequest,
|
|
type RequestOptions
|
|
} from 'node:http'
|
|
import { tmpdir } from 'node:os'
|
|
import net from 'node:net'
|
|
import { Duplex } from 'node:stream'
|
|
import { join, resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
REMOTE_MODEL_GATEWAY_LIMITS,
|
|
type RemoteModelGatewayRequest,
|
|
type RemoteModelGatewayResponse
|
|
} from '../shared/remote-model-gateway-contracts'
|
|
import {
|
|
createOpenCodeModelBridgeProviderConfig,
|
|
MODEL_BRIDGE_SDK_AUTH_SENTINEL,
|
|
ModelBridgeLoopbackProxy,
|
|
runOpenCodeModelBridgeHelper,
|
|
type ModelBridgeHelperSpawn
|
|
} from './model-bridge-helper'
|
|
import { createOpenCodeLaunchProfile } from './opencode-runtime-profile'
|
|
import { createRuntimeBundleTestFixture } from './runtime-bundle-test-fixture'
|
|
import {
|
|
createUnixModelBridgeExchange,
|
|
MODEL_BRIDGE_BROKER_SOCKET_NAME,
|
|
ModelBridgeBrokerServer,
|
|
type ModelBridgeExchange
|
|
} from './model-bridge-broker'
|
|
|
|
const temporaryPaths: string[] = []
|
|
const modelBridgeRouteToken = 'A'.repeat(43)
|
|
const validResponse: RemoteModelGatewayResponse = {
|
|
status: 201,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-request-id': 'provider-request'
|
|
},
|
|
bodyBase64: Buffer.from('{"ok":true}', 'utf8').toString(
|
|
'base64'
|
|
)
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
for (const path of temporaryPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('model bridge loopback helper', () => {
|
|
it.each(['ask', 'execute'] as const)(
|
|
'preserves the %s launch profile offline resources in the final OpenCode spawn',
|
|
async (workMode) => {
|
|
const fixture = await createRuntimeBundleTestFixture()
|
|
temporaryPaths.push(fixture.root)
|
|
const socketPath = join(fixture.root, 'bridge', 'model.sock')
|
|
const profile = createOpenCodeLaunchProfile({
|
|
...fixture,
|
|
workspaceDirectory: fixture.root,
|
|
workMode,
|
|
modelBridge: {
|
|
agentExecutablePath: join(fixture.root, 'agent', 'goodbuddy-agent'),
|
|
bridgeDirectory: join(fixture.root, 'bridge'),
|
|
socketPath,
|
|
policy: {
|
|
protocol: 'anthropic-messages',
|
|
model: 'private-model',
|
|
modelProfileDigest: `sha256:${'9'.repeat(64)}`,
|
|
supportsImageInput: false
|
|
}
|
|
}
|
|
})
|
|
const spawn = vi.fn<ModelBridgeHelperSpawn>(() => {
|
|
const child = new EventEmitter()
|
|
queueMicrotask(() => child.emit('close', 0, null))
|
|
return child
|
|
})
|
|
const resources = {
|
|
HOME: fixture.root,
|
|
PATH: join(fixture.root, 'tools'),
|
|
TMPDIR: join(fixture.root, 'tmp'),
|
|
XDG_CACHE_HOME: join(fixture.root, 'cache'),
|
|
XDG_CONFIG_HOME: join(fixture.root, 'config'),
|
|
XDG_DATA_HOME: join(fixture.root, 'data'),
|
|
XDG_STATE_HOME: join(fixture.root, 'state')
|
|
}
|
|
await expect(runOpenCodeModelBridgeHelper({
|
|
socketPath,
|
|
protocol: 'anthropic-messages',
|
|
model: 'private-model',
|
|
supportsImageInput: false,
|
|
workMode,
|
|
opencodeEntrypoint: join(fixture.bundleDirectory, 'bin', 'opencode'),
|
|
environment: {
|
|
...profile.env,
|
|
...resources,
|
|
OPENCODE_CONFIG_CONTENT: '{"model":"must-not-be-used"}',
|
|
OPENCODE_CONFIG: '/unrelated/config.json',
|
|
ANTHROPIC_API_KEY: 'must-not-be-forwarded',
|
|
NODE_OPTIONS: '--inspect'
|
|
},
|
|
spawn
|
|
})).resolves.toBe(0)
|
|
expect(spawn).toHaveBeenCalledOnce()
|
|
const [executable, args, options] = spawn.mock.calls[0]!
|
|
expect(executable).toBe(join(fixture.bundleDirectory, 'bin', 'opencode'))
|
|
expect(args).toEqual(['acp'])
|
|
expect(options).toMatchObject({ shell: false, stdio: 'inherit' })
|
|
expect(options.env).toMatchObject({
|
|
OPENCODE_ENABLE_QUESTION_TOOL: 'true',
|
|
...resources,
|
|
LANG: profile.env.LANG,
|
|
LC_ALL: profile.env.LC_ALL,
|
|
OPENCODE_CONFIG_DIR: join(fixture.bundleDirectory, 'config', 'opencode'),
|
|
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: '1',
|
|
OPENCODE_DISABLE_EMBEDDED_WEB_UI: '1',
|
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: '1',
|
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: '1',
|
|
OPENCODE_DISABLE_LSP_DOWNLOAD: '1',
|
|
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
OPENCODE_DISABLE_PROJECT_CONFIG: '1',
|
|
OPENCODE_DISABLE_SHARE: '1'
|
|
})
|
|
expect(options.env.OPENCODE_CONFIG).toBeUndefined()
|
|
expect(options.env.ANTHROPIC_API_KEY).toBeUndefined()
|
|
expect(options.env.NODE_OPTIONS).toBeUndefined()
|
|
const config = JSON.parse(options.env.OPENCODE_CONFIG_CONTENT!)
|
|
expect(config.model).toBe('goodbuddy-anthropic/private-model')
|
|
expect(config.snapshot).toBe(false)
|
|
expect(config.permission).toBe(workMode === 'ask' ? 'ask' : 'allow')
|
|
expect(config.plugin).toHaveLength(1)
|
|
}
|
|
)
|
|
|
|
it('forwards only the bounded request contract and returns the response', async () => {
|
|
const exchange = vi.fn<ModelBridgeExchange>(
|
|
async () => validResponse
|
|
)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
headers: {
|
|
accept: 'application/json',
|
|
'content-type': 'application/json',
|
|
'x-api-key': MODEL_BRIDGE_SDK_AUTH_SENTINEL
|
|
},
|
|
body: '{"model":"private-model","messages":[]}'
|
|
})
|
|
|
|
expect(response).toEqual({
|
|
status: 201,
|
|
headers: expect.objectContaining({
|
|
'content-type': 'application/json',
|
|
'x-request-id': 'provider-request'
|
|
}),
|
|
body: '{"ok":true}'
|
|
})
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
const [request, context] = exchange.mock.calls[0]!
|
|
expect(request).toEqual({
|
|
method: 'POST',
|
|
path: '/v1/messages',
|
|
headers: {
|
|
accept: 'application/json',
|
|
'content-type': 'application/json'
|
|
},
|
|
bodyBase64: Buffer.from(
|
|
'{"model":"private-model","messages":[]}',
|
|
'utf8'
|
|
).toString('base64')
|
|
})
|
|
expect(context.requestId).toMatch(/^model-/u)
|
|
expect(context.signal.aborted).toBe(false)
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('acknowledges delivery only after the HTTP response flushes', async () => {
|
|
const acknowledgeDelivery = vi.fn(async () => undefined)
|
|
const failDelivery = vi.fn(async () => undefined)
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange: async () => ({
|
|
response: validResponse,
|
|
acknowledgeDelivery,
|
|
failDelivery
|
|
})
|
|
})
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
expect(response.body).toBe('{"ok":true}')
|
|
expect(acknowledgeDelivery).toHaveBeenCalledOnce()
|
|
expect(failDelivery).not.toHaveBeenCalled()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('flushes and acknowledges a multi-frame-sized HTTP response', async () => {
|
|
const body = Buffer.alloc(600 * 1024, 0x61)
|
|
const acknowledgeDelivery = vi.fn(async () => undefined)
|
|
const failDelivery = vi.fn(async () => undefined)
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange: async () => ({
|
|
response: {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/octet-stream' },
|
|
bodyBase64: body.toString('base64')
|
|
},
|
|
acknowledgeDelivery,
|
|
failDelivery
|
|
})
|
|
})
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
expect(Buffer.byteLength(response.body)).toBe(body.byteLength)
|
|
expect(acknowledgeDelivery).toHaveBeenCalledOnce()
|
|
expect(failDelivery).not.toHaveBeenCalled()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
name: 'non-POST method',
|
|
request: { method: 'GET', path: '/v1/messages' },
|
|
status: 405
|
|
},
|
|
{
|
|
name: 'unknown path',
|
|
request: { path: '/images/generations' },
|
|
status: 404
|
|
},
|
|
{
|
|
name: 'path query',
|
|
request: { path: '/v1/messages?token=secret' },
|
|
status: 404
|
|
},
|
|
{
|
|
name: 'credential header',
|
|
request: {
|
|
path: '/v1/messages',
|
|
headers: { authorization: 'Bearer remote-secret' }
|
|
},
|
|
status: 400
|
|
}
|
|
])('rejects $name without dispatch', async ({ request, status }) => {
|
|
const exchange = vi.fn(async () => validResponse)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, request)
|
|
expect(response.status).toBe(status)
|
|
expect(response.body).not.toContain('remote-secret')
|
|
expect(exchange).not.toHaveBeenCalled()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('rejects a declared oversized body before dispatch', async () => {
|
|
const exchange = vi.fn(async () => validResponse)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
headers: {
|
|
'content-length': String(
|
|
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes + 1
|
|
)
|
|
},
|
|
body: ''
|
|
})
|
|
expect(response.status).toBe(413)
|
|
expect(exchange).not.toHaveBeenCalled()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('rejects loopback requests without the per-helper route capability', async () => {
|
|
const exchange = vi.fn(async () => validResponse)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
authenticated: false,
|
|
path: '/v1/messages',
|
|
headers: {
|
|
'x-api-key': MODEL_BRIDGE_SDK_AUTH_SENTINEL
|
|
},
|
|
body: '{}'
|
|
})
|
|
|
|
expect(response.status).toBe(404)
|
|
expect(exchange).not.toHaveBeenCalled()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('drops harmless SDK metadata headers but rejects chunked framing', async () => {
|
|
const exchange = vi.fn<ModelBridgeExchange>(
|
|
async () => validResponse
|
|
)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const accepted = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-stainless-runtime': 'node'
|
|
},
|
|
body: '{}'
|
|
})
|
|
expect(accepted.status).toBe(201)
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
expect(exchange.mock.calls[0]![0].headers).toEqual({
|
|
'content-type': 'application/json'
|
|
})
|
|
|
|
const rejected = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'transfer-encoding': 'chunked'
|
|
},
|
|
body: '{}'
|
|
})
|
|
expect(rejected.status).toBe(400)
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('accepts only the fixed SDK compatibility marker and strips it', async () => {
|
|
const exchange = vi.fn<ModelBridgeExchange>(
|
|
async () => validResponse
|
|
)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const accepted = await sendHttp(origin, {
|
|
path: '/v1/responses',
|
|
headers: {
|
|
authorization: `Bearer ${MODEL_BRIDGE_SDK_AUTH_SENTINEL}`
|
|
},
|
|
body: '{}'
|
|
})
|
|
expect(accepted.status).toBe(201)
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
expect(exchange.mock.calls[0]![0].headers).toEqual({})
|
|
|
|
const acceptedChat = await sendHttp(origin, {
|
|
path: '/v1/chat/completions',
|
|
headers: {
|
|
authorization: `Bearer ${MODEL_BRIDGE_SDK_AUTH_SENTINEL}`
|
|
},
|
|
body: '{}'
|
|
})
|
|
expect(acceptedChat.status).toBe(201)
|
|
expect(exchange).toHaveBeenCalledTimes(2)
|
|
expect(exchange.mock.calls[1]![0].headers).toEqual({})
|
|
|
|
for (const authorization of [
|
|
'Bearer remote-secret',
|
|
`Basic ${MODEL_BRIDGE_SDK_AUTH_SENTINEL}`
|
|
]) {
|
|
const rejected = await sendHttp(origin, {
|
|
path: '/v1/responses',
|
|
headers: { authorization },
|
|
body: '{}'
|
|
})
|
|
expect(rejected.status).toBe(400)
|
|
}
|
|
expect(exchange).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('times out once, aborts dispatch, and never returns raw errors', async () => {
|
|
let dispatchSignal: AbortSignal | undefined
|
|
const exchange = vi.fn(
|
|
async (
|
|
_request: RemoteModelGatewayRequest,
|
|
context: { signal: AbortSignal }
|
|
) => {
|
|
dispatchSignal = context.signal
|
|
await new Promise(() => undefined)
|
|
return validResponse
|
|
}
|
|
)
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange,
|
|
requestTimeoutMs: 20
|
|
})
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
expect(response.status).toBe(504)
|
|
expect(response.body).toBe(
|
|
'{"error":{"code":"bridge-timeout"}}'
|
|
)
|
|
expect(dispatchSignal?.aborted).toBe(true)
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('maps dispatch errors without leaking credentials', async () => {
|
|
const privateValue = 'provider-key-do-not-leak'
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange: async () => {
|
|
throw new Error(`Upstream used ${privateValue}`)
|
|
}
|
|
})
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
expect(response.status).toBe(502)
|
|
expect(response.body).toBe(
|
|
'{"error":{"code":"bridge-failed"}}'
|
|
)
|
|
expect(response.body).not.toContain(privateValue)
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('queues concurrent OpenCode requests instead of returning bridge-busy', async () => {
|
|
let markStarted: (() => void) | undefined
|
|
let releaseFirst: (() => void) | undefined
|
|
const started = new Promise<void>((resolveStarted) => {
|
|
markStarted = resolveStarted
|
|
})
|
|
const firstGate = new Promise<void>((resolve) => {
|
|
releaseFirst = resolve
|
|
})
|
|
let callIndex = 0
|
|
const exchange = vi.fn(async () => {
|
|
const index = callIndex++
|
|
if (index === 0) {
|
|
markStarted?.()
|
|
await firstGate
|
|
}
|
|
return validResponse
|
|
})
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange
|
|
})
|
|
const origin = await proxy.listen()
|
|
const first = sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
await started
|
|
const second = sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
})
|
|
try {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 20))
|
|
expect(exchange).toHaveBeenCalledOnce()
|
|
releaseFirst?.()
|
|
await expect(first).resolves.toMatchObject({ status: 201 })
|
|
await expect(second).resolves.toMatchObject({ status: 201 })
|
|
expect(exchange).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
await proxy.close()
|
|
}
|
|
})
|
|
|
|
it('aborts an in-flight dispatch when the proxy closes', async () => {
|
|
let dispatchSignal: AbortSignal | undefined
|
|
let markStarted: (() => void) | undefined
|
|
const started = new Promise<void>((resolveStarted) => {
|
|
markStarted = resolveStarted
|
|
})
|
|
const proxy = new ModelBridgeLoopbackProxy({
|
|
exchange: async (_request, context) => {
|
|
dispatchSignal = context.signal
|
|
markStarted?.()
|
|
await new Promise(() => undefined)
|
|
return validResponse
|
|
}
|
|
})
|
|
const origin = await proxy.listen()
|
|
const pending = sendHttp(origin, {
|
|
path: '/v1/messages',
|
|
body: '{}'
|
|
}).catch(() => undefined)
|
|
await started
|
|
|
|
await proxy.close()
|
|
await pending
|
|
expect(dispatchSignal?.aborted).toBe(true)
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
protocol: 'anthropic-messages' as const,
|
|
providerId: 'goodbuddy-anthropic',
|
|
npm: '@ai-sdk/anthropic',
|
|
requiresSdkAuthSentinel: true
|
|
},
|
|
{
|
|
protocol: 'openai-chat-completions' as const,
|
|
providerId: 'goodbuddy-openai-chat',
|
|
npm: '@ai-sdk/openai-compatible',
|
|
requiresSdkAuthSentinel: true
|
|
},
|
|
{
|
|
protocol: 'openai-responses' as const,
|
|
providerId: 'goodbuddy-openai-responses',
|
|
npm: '@ai-sdk/openai',
|
|
requiresSdkAuthSentinel: true
|
|
}
|
|
])(
|
|
'creates secret-free $protocol OpenCode metadata',
|
|
({ protocol, providerId, npm, requiresSdkAuthSentinel }) => {
|
|
const config = createOpenCodeModelBridgeProviderConfig({
|
|
protocol,
|
|
model: 'private-model',
|
|
name: 'Private model',
|
|
loopbackOrigin:
|
|
`http://127.0.0.1:12345/${modelBridgeRouteToken}`,
|
|
supportsImageInput: true,
|
|
workMode: 'ask'
|
|
})
|
|
|
|
expect(config).toMatchObject({
|
|
model: `${providerId}/private-model`,
|
|
permission: 'ask',
|
|
agent: {
|
|
title: {
|
|
disable: true
|
|
},
|
|
build: {
|
|
permission: 'ask'
|
|
}
|
|
},
|
|
provider: {
|
|
[providerId]: {
|
|
npm,
|
|
options: {
|
|
baseURL:
|
|
`http://127.0.0.1:12345/${modelBridgeRouteToken}/v1`
|
|
},
|
|
models: {
|
|
'private-model': {
|
|
attachment: true,
|
|
provider: { npm }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
const options = config.provider[providerId]!.options
|
|
expect(options.apiKey).toBe(
|
|
requiresSdkAuthSentinel
|
|
? MODEL_BRIDGE_SDK_AUTH_SENTINEL
|
|
: undefined
|
|
)
|
|
expect(JSON.stringify(config)).not.toContain('provider.example')
|
|
}
|
|
)
|
|
|
|
it('explicitly allows Execute permissions instead of inheriting directory prompts', () => {
|
|
const config = createOpenCodeModelBridgeProviderConfig({
|
|
protocol: 'openai-responses',
|
|
model: 'private-model',
|
|
loopbackOrigin:
|
|
`http://127.0.0.1:12345/${modelBridgeRouteToken}`,
|
|
workMode: 'execute'
|
|
})
|
|
|
|
expect(config.permission).toBe('allow')
|
|
expect(config.agent.build.permission).toBe('allow')
|
|
})
|
|
})
|
|
|
|
describe('model bridge Unix broker', () => {
|
|
const runOnUnix = process.platform === 'win32' ? it.skip : it
|
|
|
|
runOnUnix.each(['delivered', 'failed', 'forced', 'timeout'] as const)(
|
|
'settles a flushed HTTP response before completion close (%s)', async outcome => {
|
|
let releaseAck!: () => void
|
|
const ackGate = new Promise<void>(resolve => { releaseAck = resolve })
|
|
let enteredAck!: () => void
|
|
const ackEntered = new Promise<void>(resolve => { enteredAck = resolve })
|
|
const failDelivery = vi.fn()
|
|
const broker = new ModelBridgeBrokerServer({
|
|
scratchDirectory: privateTemporaryDirectory(),
|
|
dispatch: async () => ({
|
|
response: validResponse,
|
|
acknowledgeDelivery: async () => {
|
|
enteredAck()
|
|
await ackGate
|
|
if (outcome === 'failed') throw new Error('delivery failed')
|
|
},
|
|
failDelivery
|
|
})
|
|
})
|
|
await broker.listen()
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange: createUnixModelBridgeExchange({ socketPath: broker.socketPath }) })
|
|
const origin = await proxy.listen()
|
|
try {
|
|
const response = await sendHttp(origin, { path: '/v1/messages', body: '{}' })
|
|
expect(response.body).toBe('{"ok":true}')
|
|
await ackEntered
|
|
let closed = false
|
|
const close = broker.close({ waitForDelivery: outcome !== 'forced' }).then(() => { closed = true })
|
|
// An event-loop turn proves completion close has not torn down the ACK socket.
|
|
await new Promise<void>(resolve => setImmediate(resolve))
|
|
if (outcome === 'forced') {
|
|
await close
|
|
expect(failDelivery).toHaveBeenCalledOnce()
|
|
} else if (outcome === 'timeout') {
|
|
await expect(close).rejects.toThrow()
|
|
expect(failDelivery).toHaveBeenCalledOnce()
|
|
} else {
|
|
expect(closed).toBe(false)
|
|
expect(failDelivery).not.toHaveBeenCalled()
|
|
releaseAck()
|
|
await close
|
|
expect(failDelivery).toHaveBeenCalledTimes(outcome === 'failed' ? 1 : 0)
|
|
}
|
|
expect(existsSync(broker.socketPath)).toBe(false)
|
|
} finally {
|
|
releaseAck()
|
|
await proxy.close()
|
|
await broker.close()
|
|
}
|
|
}
|
|
)
|
|
|
|
runOnUnix('routes shared Sessions concurrently and rejects queued work after its operation ends', async () => {
|
|
let unblock!: () => void
|
|
const blocked = new Promise<void>(resolve => { unblock = resolve })
|
|
const dispatchOne = vi.fn(async () => { await blocked; return validResponse })
|
|
const dispatchTwo = vi.fn(async () => validResponse)
|
|
const brokers = [dispatchOne, dispatchTwo].map(dispatch => new ModelBridgeBrokerServer({
|
|
scratchDirectory: privateTemporaryDirectory(), dispatch
|
|
}))
|
|
const fallback = vi.fn(async () => validResponse)
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange: fallback, sharedSessions: true })
|
|
await Promise.all(brokers.map(broker => broker.listen()))
|
|
const origin = await proxy.listen()
|
|
const route = (sessionId: string, operationId: string, index: number) => fetch(`${origin}/session`, {
|
|
method: 'POST', body: JSON.stringify({ sessionId, operationId, socketPath: brokers[index]!.socketPath, workMode: 'execute' })
|
|
})
|
|
const prompt = (sessionId: string, operationId: string) => fetch(`${origin}/v1/messages`, {
|
|
method: 'POST', headers: { 'content-type': 'application/json', 'x-goodbuddy-session': sessionId, 'x-goodbuddy-operation': operationId },
|
|
body: '{"model":"fixture"}'
|
|
})
|
|
try {
|
|
expect((await route('one', 'operation-1', 0)).ok).toBe(true)
|
|
expect((await route('two', 'operation-2', 1)).ok).toBe(true)
|
|
const first = prompt('one', 'operation-1')
|
|
await vi.waitFor(() => expect(dispatchOne).toHaveBeenCalledOnce())
|
|
const queued = prompt('one', 'operation-1')
|
|
expect((await prompt('two', 'operation-2')).status).toBe(201)
|
|
await route('one', 'next-operation', 0)
|
|
unblock()
|
|
expect((await first).status).toBe(201)
|
|
expect((await queued).status).toBe(409)
|
|
expect((await prompt('one', 'operation-1')).status).toBe(409)
|
|
expect((await prompt('one', 'next-operation')).status).toBe(201)
|
|
expect(dispatchOne).toHaveBeenCalledTimes(2)
|
|
expect(fallback).not.toHaveBeenCalled()
|
|
for (const dispatch of [dispatchOne, dispatchTwo]) {
|
|
expect(dispatch.mock.calls[0]?.[0]).not.toHaveProperty('headers.x-goodbuddy-session')
|
|
}
|
|
} finally {
|
|
unblock()
|
|
await proxy.close()
|
|
await Promise.all(brokers.map(broker => broker.close()))
|
|
}
|
|
})
|
|
|
|
it('allows a delayed connection with the default unlimited request timeout', async () => {
|
|
const socket = new Duplex({
|
|
read() {},
|
|
write(chunk: Buffer, _encoding, callback) {
|
|
const packet = JSON.parse(chunk.subarray(4).toString()) as {
|
|
type: string
|
|
requestId: string
|
|
}
|
|
const response = Buffer.from(JSON.stringify(
|
|
packet.type === 'model-request'
|
|
? { version: 1, type: 'model-response', requestId: packet.requestId, ok: true, response: validResponse }
|
|
: { version: 1, type: 'delivery-accepted', requestId: packet.requestId }
|
|
))
|
|
const frame = Buffer.alloc(4 + response.length)
|
|
frame.writeUInt32BE(response.length)
|
|
response.copy(frame, 4)
|
|
this.push(frame)
|
|
callback()
|
|
}
|
|
})
|
|
vi.spyOn(net.Socket.prototype, 'connect').mockReturnValueOnce(socket as net.Socket)
|
|
const connect = setTimeout(() => socket.emit('connect'), 30)
|
|
try {
|
|
const exchange = createUnixModelBridgeExchange({
|
|
socketPath: resolve(tmpdir(), 'delayed-model-bridge.sock'),
|
|
connectTimeoutMs: 500
|
|
})
|
|
const result = await exchange({
|
|
method: 'POST', path: '/v1/messages', headers: {}, bodyBase64: ''
|
|
}, { requestId: 'delayed-connect', signal: new AbortController().signal })
|
|
expect('response' in result).toBe(true)
|
|
if ('response' in result) {
|
|
expect(result.response).toEqual(validResponse)
|
|
await result.acknowledgeDelivery()
|
|
}
|
|
} finally {
|
|
clearTimeout(connect)
|
|
socket.destroy()
|
|
}
|
|
})
|
|
|
|
runOnUnix('rejects a socket path beyond the Unix kernel limit', () => {
|
|
const scratch = resolve(
|
|
privateTemporaryDirectory(),
|
|
'a'.repeat(150)
|
|
)
|
|
expect(
|
|
() =>
|
|
new ModelBridgeBrokerServer({
|
|
scratchDirectory: scratch,
|
|
dispatch: async () => validResponse
|
|
})
|
|
).toThrow('endpoint path is too long')
|
|
})
|
|
|
|
runOnUnix(
|
|
'uses a private owned socket, exchanges one request, and cleans up',
|
|
async () => {
|
|
const scratch = privateTemporaryDirectory()
|
|
const dispatch = vi.fn(async () => validResponse)
|
|
const broker = new ModelBridgeBrokerServer({
|
|
scratchDirectory: scratch,
|
|
dispatch
|
|
})
|
|
await broker.listen()
|
|
const socketPath = resolve(
|
|
scratch,
|
|
MODEL_BRIDGE_BROKER_SOCKET_NAME
|
|
)
|
|
expect(lstatSync(socketPath).mode & 0o777).toBe(0o600)
|
|
|
|
const exchange = createUnixModelBridgeExchange({
|
|
socketPath
|
|
})
|
|
const request: RemoteModelGatewayRequest = {
|
|
method: 'POST',
|
|
path: '/v1/responses',
|
|
headers: { 'content-type': 'application/json' },
|
|
bodyBase64: Buffer.from('{}').toString('base64')
|
|
}
|
|
const exchanged = await exchange(request, {
|
|
requestId: 'request-a',
|
|
signal: new AbortController().signal
|
|
})
|
|
expect(
|
|
'response' in exchanged ? exchanged.response : exchanged
|
|
).toEqual(validResponse)
|
|
if ('response' in exchanged) {
|
|
await exchanged.acknowledgeDelivery()
|
|
}
|
|
expect(dispatch).toHaveBeenCalledWith(request, {
|
|
requestId: 'request-a',
|
|
signal: expect.any(AbortSignal)
|
|
})
|
|
|
|
await broker.close()
|
|
expect(existsSync(socketPath)).toBe(false)
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'drains a response larger than the Unix socket receive buffer',
|
|
async () => {
|
|
const scratch = privateTemporaryDirectory()
|
|
const responseBody = Buffer.alloc(256 * 1024, 0x61)
|
|
const broker = new ModelBridgeBrokerServer({
|
|
scratchDirectory: scratch,
|
|
dispatch: async () => ({
|
|
...validResponse,
|
|
bodyBase64: responseBody.toString('base64')
|
|
})
|
|
})
|
|
await broker.listen()
|
|
try {
|
|
const exchange = createUnixModelBridgeExchange({
|
|
socketPath: broker.socketPath,
|
|
requestTimeoutMs: 2_000
|
|
})
|
|
const exchanged = await exchange(
|
|
{
|
|
method: 'POST',
|
|
path: '/v1/responses',
|
|
headers: { 'content-type': 'application/json' },
|
|
bodyBase64: Buffer.from('{}').toString('base64')
|
|
},
|
|
{
|
|
requestId: 'request-large-response',
|
|
signal: new AbortController().signal
|
|
}
|
|
)
|
|
expect('response' in exchanged).toBe(true)
|
|
if ('response' in exchanged) {
|
|
expect(
|
|
Buffer.from(
|
|
exchanged.response.bodyBase64,
|
|
'base64'
|
|
).byteLength
|
|
).toBe(responseBody.byteLength)
|
|
await exchanged.acknowledgeDelivery()
|
|
}
|
|
} finally {
|
|
await broker.close()
|
|
}
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'refuses to replace or remove a non-socket endpoint path',
|
|
async () => {
|
|
const scratch = privateTemporaryDirectory()
|
|
const socketPath = join(
|
|
scratch,
|
|
MODEL_BRIDGE_BROKER_SOCKET_NAME
|
|
)
|
|
writeFileSync(socketPath, 'user-owned')
|
|
chmodSync(socketPath, 0o600)
|
|
const broker = new ModelBridgeBrokerServer({
|
|
scratchDirectory: scratch,
|
|
dispatch: async () => validResponse
|
|
})
|
|
|
|
await expect(broker.listen()).rejects.toThrow(
|
|
'not an owned Unix socket'
|
|
)
|
|
await broker.close()
|
|
expect(existsSync(socketPath)).toBe(true)
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'bounds dispatch time and forwards only a generic failure code',
|
|
async () => {
|
|
const scratch = privateTemporaryDirectory()
|
|
const privateValue = 'provider-secret-in-error'
|
|
let dispatchSignal: AbortSignal | undefined
|
|
const broker = new ModelBridgeBrokerServer({
|
|
scratchDirectory: scratch,
|
|
requestTimeoutMs: 20,
|
|
dispatch: async (_request, context) => {
|
|
dispatchSignal = context.signal
|
|
await new Promise(() => undefined)
|
|
throw new Error(privateValue)
|
|
}
|
|
})
|
|
await broker.listen()
|
|
try {
|
|
const exchange = createUnixModelBridgeExchange({
|
|
socketPath: broker.socketPath,
|
|
requestTimeoutMs: 1_000
|
|
})
|
|
await expect(
|
|
exchange(
|
|
{
|
|
method: 'POST',
|
|
path: '/v1/messages',
|
|
headers: {},
|
|
bodyBase64: ''
|
|
},
|
|
{
|
|
requestId: 'request-timeout',
|
|
signal: new AbortController().signal
|
|
}
|
|
)
|
|
).rejects.toMatchObject({
|
|
code: 'request-timeout',
|
|
message: 'Model bridge request timed out'
|
|
})
|
|
expect(dispatchSignal?.aborted).toBe(true)
|
|
} finally {
|
|
await broker.close()
|
|
}
|
|
}
|
|
)
|
|
})
|
|
|
|
function privateTemporaryDirectory(): string {
|
|
const path = mkdtempSync(join(tmpdir(), 'goodbuddy-model-bridge-'))
|
|
temporaryPaths.push(path)
|
|
if (process.platform !== 'win32') {
|
|
chmodSync(path, 0o700)
|
|
}
|
|
return resolve(path)
|
|
}
|
|
|
|
async function sendHttp(
|
|
origin: string,
|
|
options: {
|
|
method?: string
|
|
path?: string
|
|
authenticated?: boolean
|
|
headers?: Record<string, string>
|
|
body?: string
|
|
}
|
|
): Promise<{
|
|
status: number
|
|
headers: Record<string, string | string[] | undefined>
|
|
body: string
|
|
}> {
|
|
const url = new URL(origin)
|
|
const body = options.body ?? ''
|
|
const requestPath = options.path ?? '/v1/messages'
|
|
const basePath =
|
|
url.pathname === '/'
|
|
? ''
|
|
: url.pathname.replace(/\/$/u, '')
|
|
const requestOptions: RequestOptions = {
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
method: options.method ?? 'POST',
|
|
path:
|
|
options.authenticated === false
|
|
? requestPath
|
|
: `${basePath}${requestPath}`,
|
|
headers: {
|
|
...(options.headers ?? {}),
|
|
...(!hasHeader(options.headers, 'content-length') &&
|
|
!hasHeader(options.headers, 'transfer-encoding')
|
|
? { 'content-length': Buffer.byteLength(body) }
|
|
: {})
|
|
}
|
|
}
|
|
return await new Promise((resolveResponse, reject) => {
|
|
const request = httpRequest(requestOptions, (response) => {
|
|
const chunks: Buffer[] = []
|
|
response.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
response.once('end', () => {
|
|
resolveResponse({
|
|
status: response.statusCode ?? 0,
|
|
headers: response.headers,
|
|
body: Buffer.concat(chunks).toString('utf8')
|
|
})
|
|
})
|
|
})
|
|
request.once('error', reject)
|
|
request.end(body)
|
|
})
|
|
}
|
|
|
|
function hasHeader(
|
|
headers: Record<string, string> | undefined,
|
|
expected: string
|
|
): boolean {
|
|
return Object.keys(headers ?? {}).some(
|
|
(name) => name.toLowerCase() === expected
|
|
)
|
|
}
|