Files
orca/src/main/plugins/plugin-command-registry.test.ts
T

266 lines
9.4 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { fingerprintPluginConsent } from '../../shared/plugins/plugin-consent-fingerprint'
import { pluginManifestSchema } from '../../shared/plugins/plugin-manifest'
import type { ValidDiscoveredPlugin } from './plugin-discovery'
import { PluginCommandRegistry } from './plugin-command-registry'
function commandPlugin(
id: string,
contributes: {
commands: Record<string, unknown>[]
keybindings?: Record<string, unknown>[]
}
): ValidDiscoveredPlugin {
const manifest = pluginManifestSchema.parse({
manifestVersion: 1,
id,
publisher: 'orca-samples',
name: id,
version: '1.0.0',
engines: { orca: '>=1.0.0' },
pluginApi: 1,
...(contributes.commands.some((command) => command.action === undefined)
? { main: 'worker.js' }
: {}),
contributes,
capabilities: []
})
return {
pluginKey: `orca-samples.${id}`,
rootDir: `/plugins/${id}`,
manifest,
consentFingerprint: fingerprintPluginConsent(manifest, `content-${id}`),
consentContentHash: `content-${id}`,
contentHash: `content-${id}`,
isDev: false
}
}
describe('PluginCommandRegistry', () => {
it('reads binding command IDs once while preserving declaration and binding order', () => {
const commands = Array.from({ length: 256 }, (_, index) => ({
id: `command-${index}`,
title: `Command ${index}`,
action: 'view.tasks'
}))
const keys = Array.from(
{ length: 104 },
(_, index) =>
`Mod+${Math.floor(index / 26) & 1 ? 'Alt+' : ''}${Math.floor(index / 26) & 2 ? 'Shift+' : ''}${String.fromCharCode(65 + (index % 26))}`
)
// Distinct physical chords, with two bindings belonging to the same command.
const uniqueKeys = [...new Set(keys)]
const plugin = commandPlugin('many-commands', {
commands,
keybindings: uniqueKeys.map((key, index) => ({ command: `command-${index % 32}`, key }))
})
let reads = 0
for (const binding of plugin.manifest.contributes.keybindings) {
const command = binding.command
Object.defineProperty(binding, 'command', {
get: () => {
reads++
return command
}
})
}
const registry = new PluginCommandRegistry()
registry.reconcile([plugin], () => false)
const preview = registry.preview(plugin.pluginKey)
expect(preview.map((command) => command.id)).toEqual(commands.map((command) => command.id))
expect(preview[0].keybindings.map((binding) => binding.key)).toEqual(
plugin.manifest.contributes.keybindings
.filter((_, index) => index % 32 === 0)
.map((binding) => binding.key)
)
expect(preview[255].keybindings).toEqual([])
expect(registry.list()).toEqual([])
expect(reads).toBe(uniqueKeys.length)
})
it('records each conflicting owner once instead of every pair', () => {
const plugins = Array.from({ length: 128 }, (_, index) =>
commandPlugin(`plugin-${index}`, {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
)
const registry = new PluginCommandRegistry()
const errors = (registry as unknown as { errors: Map<string, string> }).errors
const writes = vi.spyOn(errors, 'set')
registry.reconcile(plugins, () => true)
expect(registry.list()).toEqual([])
for (const plugin of plugins) {
expect(registry.preview(plugin.pluginKey)).toHaveLength(1)
expect(registry.error(plugin.pluginKey)).toBe(
'plugin keybinding Mod+Alt+T conflicts with another plugin'
)
}
expect(writes).toHaveBeenCalledTimes(plugins.length)
})
it('preserves the last conflicting spelling for repeated owners and chord groups', () => {
const plugin = commandPlugin('repeat', {
commands: [
{ id: 'one', title: 'One', action: 'view.tasks' },
{ id: 'two', title: 'Two', action: 'view.tasks', context: 'worktree' }
]
})
const registry = new PluginCommandRegistry()
registry.reconcile(
[plugin],
() => true,
{
'plugin:orca-samples.repeat/one': ['Mod+Alt+T', 'Mod+Alt+Y'],
'plugin:orca-samples.repeat/two': ['Ctrl+Alt+T', 'Ctrl+Alt+Y']
},
'linux'
)
expect(registry.list()).toEqual([])
expect(registry.error(plugin.pluginKey)).toBe(
'plugin keybinding Ctrl+Alt+Y conflicts with another plugin'
)
})
it('retains pending previews and exposes only approved commands', () => {
const plugin = commandPlugin('aliases', {
commands: [{ id: 'tasks', title: 'Open Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'mod+alt+t' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile([plugin], () => false)
expect(registry.list()).toEqual([])
expect(registry.preview(plugin.pluginKey)).toEqual([
{
pluginKey: plugin.pluginKey,
id: 'tasks',
title: 'Open Tasks',
context: 'global',
handler: { type: 'built-in', action: 'view.tasks' },
keybindings: [{ key: 'Mod+Alt+T', when: 'global' }]
}
])
registry.reconcile([plugin], () => true)
expect(registry.list()).toHaveLength(1)
})
it('projects worker commands and inherited worktree keybinding context', () => {
const plugin = commandPlugin('worker', {
commands: [{ id: 'create', title: 'Create Task', context: 'worktree' }],
keybindings: [{ command: 'create', key: 'Mod+Shift+A' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile([plugin], () => true)
expect(registry.list()).toMatchObject([
{
context: 'worktree',
handler: { type: 'worker' },
keybindings: [{ key: 'Mod+Shift+A', when: 'worktree' }]
}
])
})
it('errors approved plugins whose keybindings overlap', () => {
const global = commandPlugin('global', {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T', when: 'global' }]
})
const worktree = commandPlugin('worktree', {
commands: [{ id: 'tasks', title: 'Tasks', context: 'worktree', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T', when: 'worktree' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile([global, worktree], () => true)
expect(registry.list()).toEqual([])
expect(registry.error(global.pluginKey)).toContain('conflicts')
expect(registry.error(worktree.pluginKey)).toContain('conflicts')
})
it('uses saved effective bindings to recover conflicting plugins', () => {
const first = commandPlugin('first', {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
const second = commandPlugin('second', {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile(
[first, second],
() => true,
{ 'plugin:orca-samples.first/tasks': ['Mod+Shift+T'] },
'linux'
)
expect(registry.list()).toHaveLength(2)
expect(registry.error(first.pluginKey)).toBeNull()
expect(registry.error(second.pluginKey)).toBeNull()
})
it('rejects conflicting saved bindings within one plugin', () => {
const plugin = commandPlugin('aliases', {
commands: [
{ id: 'tasks', title: 'Tasks', action: 'view.tasks' },
{ id: 'sidebar', title: 'Sidebar', action: 'sidebar.left.toggle' }
]
})
const registry = new PluginCommandRegistry()
registry.reconcile(
[plugin],
() => true,
{
'plugin:orca-samples.aliases/tasks': ['Mod+Alt+T'],
'plugin:orca-samples.aliases/sidebar': ['Mod+Alt+T']
},
'linux'
)
expect(registry.list()).toEqual([])
expect(registry.error(plugin.pluginKey)).toContain('conflicts')
})
it('detects cross-platform Mod and physical Ctrl conflicts', () => {
const portable = commandPlugin('portable', {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
const physical = commandPlugin('physical', {
commands: [{ id: 'tasks', title: 'Tasks', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Ctrl+Alt+T' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile([portable, physical], () => true, {}, 'linux')
expect(registry.list()).toEqual([])
expect(registry.error(portable.pluginKey)).toContain('conflicts')
expect(registry.error(physical.pluginKey)).toContain('conflicts')
})
it('allows the same worktree-only chord after one plugin is disabled', () => {
const first = commandPlugin('first', {
commands: [{ id: 'tasks', title: 'Tasks', context: 'worktree', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
const second = commandPlugin('second', {
commands: [{ id: 'tasks', title: 'Tasks', context: 'worktree', action: 'view.tasks' }],
keybindings: [{ command: 'tasks', key: 'Mod+Alt+T' }]
})
const registry = new PluginCommandRegistry()
registry.reconcile([first, second], (plugin) => plugin === first)
expect(registry.list()).toHaveLength(1)
expect(registry.error(first.pluginKey)).toBeNull()
})
})