fix: resolve deadlock in config handler during plugin initialization (#1304)
* fix: resolve deadlock in config handler during plugin initialization The config handler and createBuiltinAgents were calling fetchAvailableModels with client, which triggers client.provider.list() API call to OpenCode server. This caused a deadlock because: - Plugin initialization waits for server response - Server waits for plugin init to complete before handling requests Now using cache-only mode by passing undefined instead of client. If cache is unavailable, the fallback chain will use the first model. Fixes #1301 * test: add regression tests for deadlock prevention in fetchAvailableModels Add tests to ensure fetchAvailableModels is called with undefined client during plugin initialization. This prevents regression on issue #1301. - config-handler.test.ts: verify config handler does not pass client - utils.test.ts: verify createBuiltinAgents does not pass client * test: restore spies in utils.test.ts to prevent test pollution Add mockRestore() calls for all spies created in test cases to ensure proper cleanup between tests and prevent state leakage. * test: restore fetchAvailableModels spy --------- Co-authored-by: robin <robin@watcha.com> Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
@@ -396,3 +396,46 @@ describe("Prometheus direct override priority over category", () => {
|
||||
expect(agents.prometheus.temperature).toBe(0.1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
||||
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => {
|
||||
// #given - This test ensures we don't regress on issue #1301
|
||||
// Passing client to fetchAvailableModels during config handler causes deadlock:
|
||||
// - Plugin init waits for server response (client.provider.list())
|
||||
// - Server waits for plugin init to complete before handling requests
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set<string>())
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
sisyphus_agent: {
|
||||
planner_enabled: true,
|
||||
},
|
||||
}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
agent: {},
|
||||
}
|
||||
const mockClient = {
|
||||
provider: { list: () => Promise.resolve({ data: { connected: [] } }) },
|
||||
model: { list: () => Promise.resolve({ data: [] }) },
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp", client: mockClient },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then - fetchAvailableModels must be called with undefined as first argument (no client)
|
||||
// This prevents the deadlock described in issue #1301
|
||||
expect(fetchSpy).toHaveBeenCalled()
|
||||
const firstCallArgs = fetchSpy.mock.calls[0]
|
||||
expect(firstCallArgs[0]).toBeUndefined()
|
||||
|
||||
fetchSpy.mockRestore?.()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user