fix: stabilize Runtime workflows for 0.12.3

OpenCode status checks could cold-start throwaway servers, unrelated
conversations could block each other, and model, Continue, or remote paths
could replay or duplicate work under failure and backlog conditions. Settings
could also lose edits or overwrite unrelated model fields.

This update makes status checks passive, keeps per-conversation ordering while
allowing concurrency, preserves native long-running work with bounded display
capture and backpressure, narrows retries to known pre-dispatch failures, and
aligns remote Ask permissions without reducing Execute. It also keeps
Embedding updates narrow, improves settings and browser guidance, removes the
unused legacy Main gateway state, and records the validated Desktop 0.12.3 and
Agent 0.11.17 release metadata.

Release note: 改善 OpenCode 切换、多会话长任务、模型重试、远程 Ask/Execute 与设置保存可靠性,并完成 Desktop 0.12.3 / Agent 0.11.17 发布准备。
This commit is contained in:
mesalogo
2026-09-04 08:05:29 +08:00
parent 374968d50b
commit 4490e5fd15
60 changed files with 1639 additions and 3839 deletions
+137 -10
View File
@@ -86,6 +86,7 @@ function dependencies(
deps: Partial<OpenCodeRuntimeDependencies>
spawnMock: ReturnType<typeof vi.fn>
detectBinary: ReturnType<typeof vi.fn>
validateBinary: ReturnType<typeof vi.fn>
createClient: ReturnType<typeof vi.fn>
checkServerHealth: ReturnType<typeof vi.fn>
} {
@@ -94,12 +95,17 @@ function dependencies(
path: 'opencode',
detail: 'OpenCode CLI 已就绪'
})
const validateBinary = vi.fn().mockResolvedValue({
path: 'opencode',
detail: 'OpenCode CLI 已就绪'
})
const createClient = vi.fn(() => fakeClient())
const checkServerHealth = vi.fn().mockResolvedValue(true)
return {
deps: {
spawn: spawnMock as unknown as typeof spawn,
detectBinary,
validateBinary,
createClient: createClient as unknown as typeof createOpencodeClient,
checkServerHealth,
platform: 'linux',
@@ -108,6 +114,7 @@ function dependencies(
},
spawnMock,
detectBinary,
validateBinary,
createClient,
checkServerHealth
}
@@ -290,6 +297,126 @@ async function collectRun(
}
describe('OpenCodeRuntime embedded launcher', () => {
it('checks embedded availability without starting OpenCode', async () => {
const child = fakeChild()
const {
deps,
spawnMock,
detectBinary,
validateBinary,
createClient,
checkServerHealth
} = dependencies(child)
const runtime = new OpenCodeRuntime(options(), deps)
await expect(runtime.getStatus()).resolves.toMatchObject({
available: true,
detail: 'OpenCode 已配置,将在首次使用时启动'
})
expect(detectBinary).toHaveBeenCalledWith(
'opencode',
'',
undefined
)
expect(validateBinary).toHaveBeenCalledWith(
'opencode',
'opencode'
)
expect(spawnMock).not.toHaveBeenCalled()
expect(createClient).not.toHaveBeenCalled()
expect(checkServerHealth).not.toHaveBeenCalled()
await runtime.dispose()
})
it('reports an unavailable embedded binary without starting OpenCode', async () => {
const child = fakeChild()
const {
deps,
spawnMock,
detectBinary,
validateBinary,
createClient,
checkServerHealth
} = dependencies(child)
detectBinary.mockResolvedValueOnce({
path: '/missing/opencode',
detail: 'OpenCode CLI 已就绪'
})
validateBinary.mockResolvedValueOnce({
detail: 'OpenCode CLI 未找到'
})
const runtime = new OpenCodeRuntime(
options({
binaryPath: '/missing/opencode',
bundledBinaryPath: '/bundled/opencode'
}),
deps
)
await expect(runtime.getStatus()).resolves.toEqual({
id: 'opencode',
label: 'OpenCode',
available: false,
supportsToolExecution: true,
detail: 'OpenCode CLI 未找到'
})
expect(spawnMock).not.toHaveBeenCalled()
expect(detectBinary).toHaveBeenCalledWith(
'opencode',
'/missing/opencode',
'/bundled/opencode'
)
expect(validateBinary).toHaveBeenCalledWith(
'opencode',
'/missing/opencode'
)
expect(createClient).not.toHaveBeenCalled()
expect(checkServerHealth).not.toHaveBeenCalled()
await runtime.dispose()
})
it('reports a missing embedded model credential without starting OpenCode', async () => {
const child = fakeChild()
const {
deps,
spawnMock,
detectBinary,
validateBinary,
createClient,
checkServerHealth
} = dependencies(child)
const runtime = new OpenCodeRuntime(
options({
modelProfile: {
id: '00000000-0000-4000-8000-000000000011',
name: 'Independent model',
baseUrl: 'https://model.example',
modelName: 'private-model',
protocol: 'anthropic-messages',
authentication: 'api-key'
}
}),
deps
)
await expect(runtime.getStatus()).resolves.toEqual({
id: 'opencode',
label: 'OpenCode',
available: false,
supportsToolExecution: true,
detail: 'OpenCode 独立模型连接尚未配置 API Key'
})
expect(detectBinary).not.toHaveBeenCalled()
expect(validateBinary).not.toHaveBeenCalled()
expect(spawnMock).not.toHaveBeenCalled()
expect(createClient).not.toHaveBeenCalled()
expect(checkServerHealth).not.toHaveBeenCalled()
await runtime.dispose()
})
it('uses the detected binary and passes an absolute config path only through env', async () => {
const serverChild = fakeChild(314)
const killerChild = fakeChild(315)
@@ -329,7 +456,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
}
)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true,
detail:
'由 GoodBuddy 以当前用户权限管理本机 OpenCode 进程'
@@ -476,7 +603,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
deps
)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
const spawnOptions = spawnMock.mock.calls[0]?.[2] as
@@ -553,7 +680,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
deps
)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
const spawnOptions = spawnMock.mock.calls[0]?.[2] as
@@ -626,7 +753,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
)
try {
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
const environment = (
@@ -857,7 +984,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
deps
)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
const spawnOptions = spawnMock.mock.calls[0]?.[2] as
@@ -931,7 +1058,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
'https://telemetry.invalid'
try {
const runtime = new OpenCodeRuntime(options(), deps)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
@@ -996,7 +1123,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
.mockResolvedValueOnce(true)
const runtime = new OpenCodeRuntime(options(), deps)
await expect(runtime.getStatus()).resolves.toMatchObject({
await expect(runtime.testConnection()).resolves.toMatchObject({
available: true
})
expect(checkServerHealth).toHaveBeenCalledTimes(2)
@@ -1023,7 +1150,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
})
const runtime = new OpenCodeRuntime(options(), deps)
let settled = false
const statusPromise = runtime.getStatus().finally(() => {
const statusPromise = runtime.testConnection().finally(() => {
settled = true
})
@@ -1053,7 +1180,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
stderrOf(child).write(secret)
const runtime = new OpenCodeRuntime(options(), deps)
const status = await runtime.getStatus()
const status = await runtime.testConnection()
expect(status).toMatchObject({
available: false,
@@ -1088,7 +1215,7 @@ describe('OpenCodeRuntime embedded launcher', () => {
})
const runtime = new OpenCodeRuntime(options(), deps)
const status = await runtime.getStatus()
const status = await runtime.testConnection()
expect(status.detail).toBe('OpenCode Server 启动前退出(code 9)')
expect(status.detail).not.toContain(secret)