mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
* plugins: commands[].replaces lets a worker command take over a built-in action A worker command with replaces: <action> (the declarative action list, engines >=1.4.248, needs main) runs in place of that built-in from its shortcut, plugin aliases and the sidebar menus; a failed or silent (2 s) worker falls back to Orca's own. The install screen names each replacement, the plugin page has a per-action switch (settings.pluginReplacementsOff), and when two plugins replace one action the one installed first wins and the other's log says so. Update review counts replaces as new access. * fix(plugins): narrow replaced action ids before looking up their titles * plugins: harden command replaces after review - Consent includes the replaced actions, so adding replaces to a dev folder needs a new approval; CLI approval and update review name each takeover. - Shortcuts main intercepts (Tasks, Workspace Board, history), search with a selection and floating maximize now go through the replacement too. - A worker that only starts after the 2 s fallback no longer runs the command as well: main refuses to start it past the deadline. - A workspace-only command does not replace an action without a workspace. - An update keeps the plugin's first install time, so it keeps its takeover. - Drop the commands.run promise until Orca offers it. * plugins: commands.run always runs the built-in, never a replacement A5b's commands.run dispatches with the 'plugin-host' source; the app dispatcher now builds handlers that run Orca's own action for it, so a plugin that replaces an action and calls commands.run cannot loop. Docs and schema describe wrapping the built-in with commands.run; regenerated.
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
|
|
import type { PluginLockfile } from '../../shared/plugins/installation/plugin-install-lockfile'
|
|
import { markCommandReplacementOwners } from './plugin-command-replacement-owners'
|
|
|
|
function entry(
|
|
pluginKey: string,
|
|
status: PluginHostListEntry['status'] = 'idle'
|
|
): PluginHostListEntry {
|
|
return {
|
|
pluginKey,
|
|
consentFingerprint: null,
|
|
name: pluginKey,
|
|
version: '1.0.0',
|
|
publisher: 'acme',
|
|
status,
|
|
needsReconsent: false,
|
|
isDev: false,
|
|
official: false,
|
|
bundled: false,
|
|
capabilities: [],
|
|
panels: [],
|
|
commands: [
|
|
{
|
|
id: 'tasks',
|
|
title: 'Tasks',
|
|
context: 'global',
|
|
handler: { type: 'worker' },
|
|
keybindings: [],
|
|
replaces: 'view.tasks'
|
|
}
|
|
],
|
|
hasWorker: true,
|
|
restarts: 0
|
|
}
|
|
}
|
|
|
|
function lock(times: Record<string, number>): Pick<PluginLockfile, 'plugins'> {
|
|
const plugins = Object.fromEntries(
|
|
Object.entries(times).map(([key, installedAt]) => [key, { installedAt }])
|
|
)
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the owners read only installedAt.
|
|
return { plugins } as unknown as Pick<PluginLockfile, 'plugins'>
|
|
}
|
|
|
|
describe('who replaces a built-in action', () => {
|
|
it('lets the plugin installed first replace it, marks and logs the other once', () => {
|
|
const log = vi.fn()
|
|
const entries = [entry('acme.later'), entry('acme.first'), entry('acme.dev')]
|
|
const marked = markCommandReplacementOwners(
|
|
entries,
|
|
lock({ 'acme.first': 1, 'acme.later': 2 }),
|
|
log
|
|
)
|
|
expect(marked.map((each) => each.commands[0]?.replacedBy)).toEqual([
|
|
'acme.first',
|
|
undefined,
|
|
'acme.first'
|
|
])
|
|
expect(log).toHaveBeenCalledWith(
|
|
'acme.later',
|
|
'command tasks does not replace view.tasks: acme.first was installed first and replaces it'
|
|
)
|
|
markCommandReplacementOwners(entries, lock({ 'acme.first': 1, 'acme.later': 2 }), log)
|
|
expect(log).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('ignores plugins that are off', () => {
|
|
const marked = markCommandReplacementOwners(
|
|
[entry('acme.first', 'disabled'), entry('acme.later')],
|
|
lock({ 'acme.first': 1, 'acme.later': 2 }),
|
|
vi.fn()
|
|
)
|
|
expect(marked[1]?.commands[0]?.replacedBy).toBeUndefined()
|
|
})
|
|
})
|