Skip to content

Commit b318da7

Browse files
committed
fix(lifecycle): gate dynamic registration; handle register errors
Swallow client.register() rejections to avoid unhandledRejection when clients return -32601 to client/registerCapability. Add tests in tests/lifecycle/ConfigurationManager.test.ts. Fixes #72
1 parent 1d73cbf commit b318da7

File tree

2 files changed

+94
-5
lines changed

2 files changed

+94
-5
lines changed

src/lifecycle/ConfigurationManager.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,15 @@ export class ConfigurationManager {
118118
setup (capabilities: ClientCapabilities): void {
119119
const connection = ClientConnection.getConnection()
120120

121-
this.hasConfigurationCapability = capabilities.workspace?.configuration != null
122-
123-
if (this.hasConfigurationCapability) {
124-
// Register for configuration changes
125-
void connection.client.register(DidChangeConfigurationNotification.type)
121+
this.hasConfigurationCapability = capabilities.workspace?.configuration === true
122+
123+
const canDynamicRegister = capabilities.workspace?.didChangeConfiguration?.dynamicRegistration === true
124+
if (canDynamicRegister) {
125+
// Register for configuration changes if client supports dynamic registration
126+
const p = connection.client.register(DidChangeConfigurationNotification.type)
127+
if (p != null && typeof (p as any).catch === 'function') {
128+
void (p as Promise<any>).catch(() => {})
129+
}
126130
}
127131

128132
connection.onDidChangeConfiguration(params => { void this.handleConfigurationChanged(params) })
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2025 The MathWorks, Inc.
2+
import assert from 'assert'
3+
import sinon from 'sinon'
4+
5+
import ClientConnection from '../../src/ClientConnection'
6+
import ConfigurationManager from '../../src/lifecycle/ConfigurationManager'
7+
import { DidChangeConfigurationNotification } from 'vscode-languageserver'
8+
9+
function makeMockConnection() {
10+
const register = sinon.stub()
11+
const getConfiguration = sinon.stub().resolves({})
12+
const onDidChangeConfiguration = sinon.stub()
13+
const mock: any = {
14+
client: { register },
15+
workspace: { getConfiguration },
16+
onDidChangeConfiguration,
17+
console: { error: sinon.stub(), warn: sinon.stub(), info: sinon.stub(), log: sinon.stub() },
18+
}
19+
return { mock, stubs: { register, getConfiguration, onDidChangeConfiguration } }
20+
}
21+
22+
describe('ConfigurationManager.setup', () => {
23+
afterEach(() => {
24+
sinon.restore()
25+
ClientConnection._clearConnection()
26+
})
27+
28+
it('does not register didChangeConfiguration when dynamicRegistration is false/undefined', () => {
29+
const { mock, stubs } = makeMockConnection()
30+
ClientConnection._setConnection(mock)
31+
32+
const capabilities: any = { workspace: { configuration: true } }
33+
ConfigurationManager.setup(capabilities)
34+
35+
assert.equal(stubs.register.called, false, 'client.register should not be called')
36+
})
37+
38+
it('registers didChangeConfiguration when dynamicRegistration is true', () => {
39+
const { mock, stubs } = makeMockConnection()
40+
ClientConnection._setConnection(mock)
41+
42+
const capabilities: any = { workspace: { configuration: true, didChangeConfiguration: { dynamicRegistration: true } } }
43+
ConfigurationManager.setup(capabilities)
44+
45+
assert.equal(stubs.register.calledOnce, true, 'client.register should be called once')
46+
const arg = stubs.register.getCall(0).args[0]
47+
assert.equal(arg, DidChangeConfigurationNotification.type, 'should register DidChangeConfigurationNotification')
48+
})
49+
50+
it('swallows registration errors (no unhandledRejection)', async () => {
51+
const { mock, stubs } = makeMockConnection()
52+
// Cause register to reject
53+
stubs.register.rejects(new Error('Unhandled method client/registerCapability'))
54+
ClientConnection._setConnection(mock)
55+
56+
let unhandled = 0
57+
const handler = () => {
58+
unhandled++
59+
}
60+
process.on('unhandledRejection', handler)
61+
62+
try {
63+
const capabilities: any = { workspace: { configuration: true, didChangeConfiguration: { dynamicRegistration: true } } }
64+
ConfigurationManager.setup(capabilities)
65+
// allow microtasks to run
66+
await new Promise((r) => setTimeout(r, 0))
67+
assert.equal(unhandled, 0, 'should not emit unhandledRejection')
68+
} finally {
69+
process.off('unhandledRejection', handler)
70+
}
71+
})
72+
73+
it('requests configuration when workspace.configuration is true', async () => {
74+
const { mock, stubs } = makeMockConnection()
75+
stubs.getConfiguration.resolves({ installPath: '/opt/matlab', telemetry: false })
76+
ClientConnection._setConnection(mock)
77+
78+
const capabilities: any = { workspace: { configuration: true } }
79+
ConfigurationManager.setup(capabilities)
80+
81+
const cfg = await ConfigurationManager.getConfiguration()
82+
assert.ok(stubs.getConfiguration.calledOnce, 'workspace.getConfiguration should be called')
83+
assert.equal(typeof cfg, 'object')
84+
})
85+
})

0 commit comments

Comments
 (0)