fix(plugin-sdk): trim negotiated compatibility versions (#1305)

---

Co-authored-by-agent: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Ryanba
2026-03-12 17:22:23 +08:00
committed by GitHub
parent eccf44df51
commit 38b8058ee5
2 changed files with 61 additions and 11 deletions
@@ -449,6 +449,46 @@ describe('for PluginHost', () => {
]))
})
it('should trim whitespace in supported compatibility versions before negotiating', async () => {
const host = new PluginHost({
runtime: 'electron',
transport: { kind: 'in-memory' },
protocolVersion: 'v2',
apiVersion: 'v2',
supportedProtocolVersions: [' v1 '],
supportedApiVersions: [' v1 '],
})
reportPluginCapability(host, {
key: providersCapability,
state: 'ready',
metadata: { source: 'test' },
})
const session = await host.load(testManifest, { cwd: '' })
const compatibilityEvents: Array<{ body?: Record<string, unknown> }> = []
session.channels.host.on(moduleCompatibilityResult, (payload) => {
compatibilityEvents.push(payload as unknown as { body?: Record<string, unknown> })
})
const initialized = await host.init(session.id, {
compatibility: {
supportedProtocolVersions: [' v1 '],
supportedApiVersions: [' v1 '],
},
})
expect(initialized.phase).toBe('ready')
expect(compatibilityEvents).toEqual(expect.arrayContaining([
expect.objectContaining({
body: expect.objectContaining({
protocolVersion: 'v1',
apiVersion: 'v1',
mode: 'downgraded',
}),
}),
]))
})
it('should reject initialization when compatibility has no overlap', async () => {
const host = new PluginHost({
runtime: 'electron',
+21 -11
View File
@@ -350,35 +350,45 @@ function createModuleIdentity(name: string, index: number): ModuleIdentity {
}
// TODO: Maybe support more complex version formats.
function normalizeVersionList(versions: string[]) {
return [...new Set(versions.map(version => version.trim()).filter(Boolean))]
}
function resolveSupportedVersions(preferredVersion: string, supportedVersions?: string[]) {
const list = [preferredVersion, ...(supportedVersions ?? [])]
return [...new Set(list)]
return normalizeVersionList([preferredVersion, ...(supportedVersions ?? [])])
}
function resolveNegotiatedVersion(preferredVersion: string, hostSupportedVersions: string[], peerSupportedVersions?: string[]) {
if (!peerSupportedVersions?.length) {
if (hostSupportedVersions.includes(preferredVersion)) {
const normalizedPreferredVersion = preferredVersion.trim()
const normalizedHostSupportedVersions = normalizeVersionList(hostSupportedVersions)
const normalizedPeerSupportedVersions = peerSupportedVersions && peerSupportedVersions.length > 0
? normalizeVersionList(peerSupportedVersions)
: undefined
if (!normalizedPeerSupportedVersions?.length) {
if (normalizedHostSupportedVersions.includes(normalizedPreferredVersion)) {
return {
acceptedVersion: preferredVersion,
acceptedVersion: normalizedPreferredVersion,
exact: true,
}
}
return {
exact: false,
reason: `Host does not support preferred version "${preferredVersion}".`,
reason: `Host does not support preferred version "${normalizedPreferredVersion}".`,
}
}
if (peerSupportedVersions.includes(preferredVersion) && hostSupportedVersions.includes(preferredVersion)) {
if (normalizedPeerSupportedVersions.includes(normalizedPreferredVersion)
&& normalizedHostSupportedVersions.includes(normalizedPreferredVersion)) {
return {
acceptedVersion: preferredVersion,
acceptedVersion: normalizedPreferredVersion,
exact: true,
}
}
for (const version of hostSupportedVersions) {
if (peerSupportedVersions.includes(version)) {
for (const version of normalizedHostSupportedVersions) {
if (normalizedPeerSupportedVersions.includes(version)) {
return {
acceptedVersion: version,
exact: false,
@@ -388,7 +398,7 @@ function resolveNegotiatedVersion(preferredVersion: string, hostSupportedVersion
return {
exact: false,
reason: `No overlapping supported versions. host=[${hostSupportedVersions.join(', ')}]; peer=[${peerSupportedVersions.join(', ')}].`,
reason: `No overlapping supported versions. host=[${normalizedHostSupportedVersions.join(', ')}]; peer=[${normalizedPeerSupportedVersions.join(', ')}].`,
}
}