From 3e5a0c7f42f01f4c33df6e454f15c3d4bf801325 Mon Sep 17 00:00:00 2001 From: MarsLuay <70299537+MarsLuay@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:27:15 +0000 Subject: [PATCH] refactor: decompose registerAiCommands into command-specific helpers --- src/ai/registerAiCommands.ts | 143 ++++++++++++++++------------ tests/ai-register-commands.test.mjs | 85 +++++++++++++++++ 2 files changed, 165 insertions(+), 63 deletions(-) create mode 100644 tests/ai-register-commands.test.mjs diff --git a/src/ai/registerAiCommands.ts b/src/ai/registerAiCommands.ts index dcd61d8..14847d4 100644 --- a/src/ai/registerAiCommands.ts +++ b/src/ai/registerAiCommands.ts @@ -17,7 +17,7 @@ import { import { AI_COMMAND_IDS, AI_LEGACY_COMMAND_IDS } from './aiCommandIds'; -interface RegisterAiCommandsOptions { +export interface RegisterAiCommandsOptions { plugin: Plugin; getI18n: () => PluginI18nService | null; getAi: () => NpdeAiApi | undefined; @@ -51,69 +51,67 @@ function resolvePathFromClipboard( return resolved; } -export function registerAiCommands(options: RegisterAiCommandsOptions): void { +function registerCapabilitiesCommand(options: RegisterAiCommandsOptions, id: string, name: string): void { const { plugin, getI18n, getAi } = options; + plugin.addCommand({ + id, + name, + callback: async () => { + const ai = requireAi(getAi, getI18n); + if (!ai) return; + const manifest = ai.listCapabilities(); + await copyJsonToClipboard(manifest); + showI18nNotice(getI18n(), 'settings:ai.manifestCopied'); + }, + }); +} - const registerCapabilities = (id: string, name: string) => { - plugin.addCommand({ - id, - name, - callback: async () => { - const ai = requireAi(getAi, getI18n); - if (!ai) return; - const manifest = ai.listCapabilities(); - await copyJsonToClipboard(manifest); - showI18nNotice(getI18n(), 'settings:ai.manifestCopied'); - }, - }); - }; - - const registerDescribe = (id: string, name: string) => { - plugin.addCommand({ - id, - name, - callback: async () => { - const ai = requireAi(getAi, getI18n); - if (!ai) return; - try { - const payload = parseDescribeRequest(await readClipboardJson()); - const path = resolvePathFromClipboard(plugin, getI18n, payload.path); - if (!path) return; - const result = await ai.describe(path); - await copyJsonToClipboard(result); - showI18nNotice(getI18n(), 'settings:ai.describeCopied'); - } catch { - showI18nNotice(getI18n(), 'settings:ai.clipboardInvalid'); - } - }, - }); - }; - - const registerApply = (id: string, name: string) => { - plugin.addCommand({ - id, - name, - callback: async () => { - const ai = requireAi(getAi, getI18n); - if (!ai) return; - try { - const payload = parseApplyRequest(await readClipboardJson()); - const path = resolvePathFromClipboard(plugin, getI18n, payload.path); - if (!path || !payload.ops) return; - const result = await ai.apply(path, payload.ops, { dryRun: payload.dryRun === true }); - await copyJsonToClipboard(result); - showI18nNotice(getI18n(), 'settings:ai.applyCopied'); - } catch { - showI18nNotice(getI18n(), 'settings:ai.clipboardInvalid'); - } - }, - }); - }; +function registerDescribeCommand(options: RegisterAiCommandsOptions, id: string, name: string): void { + const { plugin, getI18n, getAi } = options; + plugin.addCommand({ + id, + name, + callback: async () => { + const ai = requireAi(getAi, getI18n); + if (!ai) return; + try { + const payload = parseDescribeRequest(await readClipboardJson()); + const path = resolvePathFromClipboard(plugin, getI18n, payload.path); + if (!path) return; + const result = await ai.describe(path); + await copyJsonToClipboard(result); + showI18nNotice(getI18n(), 'settings:ai.describeCopied'); + } catch { + showI18nNotice(getI18n(), 'settings:ai.clipboardInvalid'); + } + }, + }); +} - registerCapabilities(AI_COMMAND_IDS.capabilities, 'AI: Copy capabilities (JSON)'); - registerDescribe(AI_COMMAND_IDS.describe, 'AI: Describe document (clipboard JSON in/out)'); - registerApply(AI_COMMAND_IDS.apply, 'AI: Apply operations (clipboard JSON in/out)'); +function registerApplyCommand(options: RegisterAiCommandsOptions, id: string, name: string): void { + const { plugin, getI18n, getAi } = options; + plugin.addCommand({ + id, + name, + callback: async () => { + const ai = requireAi(getAi, getI18n); + if (!ai) return; + try { + const payload = parseApplyRequest(await readClipboardJson()); + const path = resolvePathFromClipboard(plugin, getI18n, payload.path); + if (!path || !payload.ops) return; + const result = await ai.apply(path, payload.ops, { dryRun: payload.dryRun === true }); + await copyJsonToClipboard(result); + showI18nNotice(getI18n(), 'settings:ai.applyCopied'); + } catch { + showI18nNotice(getI18n(), 'settings:ai.clipboardInvalid'); + } + }, + }); +} +function registerValidateCommand(options: RegisterAiCommandsOptions): void { + const { plugin, getI18n, getAi } = options; plugin.addCommand({ id: AI_COMMAND_IDS.validate, name: 'AI: Validate operations (clipboard JSON in/out)', @@ -130,7 +128,10 @@ export function registerAiCommands(options: RegisterAiCommandsOptions): void { } }, }); +} +function registerSaveCommand(options: RegisterAiCommandsOptions): void { + const { plugin, getI18n, getAi } = options; plugin.addCommand({ id: AI_COMMAND_IDS.save, name: 'AI: Save document (clipboard JSON in/out)', @@ -151,7 +152,10 @@ export function registerAiCommands(options: RegisterAiCommandsOptions): void { } }, }); +} +function registerUndoCommand(options: RegisterAiCommandsOptions): void { + const { plugin, getI18n, getAi } = options; plugin.addCommand({ id: AI_COMMAND_IDS.undo, name: 'AI: Undo agent edit (clipboard JSON in/out)', @@ -170,7 +174,10 @@ export function registerAiCommands(options: RegisterAiCommandsOptions): void { } }, }); +} +function registerRedoCommand(options: RegisterAiCommandsOptions): void { + const { plugin, getI18n, getAi } = options; plugin.addCommand({ id: AI_COMMAND_IDS.redo, name: 'AI: Redo agent edit (clipboard JSON in/out)', @@ -189,8 +196,18 @@ export function registerAiCommands(options: RegisterAiCommandsOptions): void { } }, }); +} + +export function registerAiCommands(options: RegisterAiCommandsOptions): void { + registerCapabilitiesCommand(options, AI_COMMAND_IDS.capabilities, 'AI: Copy capabilities (JSON)'); + registerDescribeCommand(options, AI_COMMAND_IDS.describe, 'AI: Describe document (clipboard JSON in/out)'); + registerApplyCommand(options, AI_COMMAND_IDS.apply, 'AI: Apply operations (clipboard JSON in/out)'); + registerValidateCommand(options); + registerSaveCommand(options); + registerUndoCommand(options); + registerRedoCommand(options); - registerCapabilities(AI_LEGACY_COMMAND_IDS.capabilities, 'AI: Copy capability manifest (JSON, legacy id)'); - registerDescribe(AI_LEGACY_COMMAND_IDS.describe, 'AI: Describe document from clipboard JSON (legacy id)'); - registerApply(AI_LEGACY_COMMAND_IDS.apply, 'AI: Apply operations from clipboard JSON (legacy id)'); + registerCapabilitiesCommand(options, AI_LEGACY_COMMAND_IDS.capabilities, 'AI: Copy capability manifest (JSON, legacy id)'); + registerDescribeCommand(options, AI_LEGACY_COMMAND_IDS.describe, 'AI: Describe document from clipboard JSON (legacy id)'); + registerApplyCommand(options, AI_LEGACY_COMMAND_IDS.apply, 'AI: Apply operations from clipboard JSON (legacy id)'); } diff --git a/tests/ai-register-commands.test.mjs b/tests/ai-register-commands.test.mjs new file mode 100644 index 0000000..023e76e --- /dev/null +++ b/tests/ai-register-commands.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { build } from 'esbuild'; +import { mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; + +const projectRoot = path.resolve(import.meta.dirname, '..'); +const require = createRequire(import.meta.url); + +let cachedModule; + +const stubObsidianPlugin = { + name: 'stub-obsidian', + setup(buildContext) { + buildContext.onResolve({ filter: /^obsidian$/ }, () => ({ path: 'obsidian', namespace: 'stub-obsidian' })); + buildContext.onLoad({ filter: /.*/, namespace: 'stub-obsidian' }, () => ({ + contents: 'module.exports = { Notice: class Notice {} };', + loader: 'js', + })); + }, +}; + +async function loadRegisterAiCommandsModule() { + if (cachedModule) return cachedModule; + const outputDirectory = await mkdtemp(path.join(tmpdir(), 'npde-ai-reg-test-')); + const outfile = path.join(outputDirectory, 'registerAiCommands.cjs'); + await build({ + entryPoints: [path.join(projectRoot, 'src/ai/registerAiCommands.ts')], + bundle: true, + format: 'cjs', + logLevel: 'silent', + outfile, + platform: 'node', + target: 'node22', + plugins: [stubObsidianPlugin], + }); + cachedModule = require(outfile); + return cachedModule; +} + +test('registerAiCommands registers all standard and legacy commands', async () => { + const { registerAiCommands } = await loadRegisterAiCommandsModule(); + + const registeredCommands = []; + const mockPlugin = { + addCommand: (cmd) => { + registeredCommands.push(cmd); + }, + app: { + workspace: { + getActiveFile: () => null, + }, + }, + }; + + const mockGetI18n = () => null; + const mockGetAi = () => undefined; + + registerAiCommands({ + plugin: mockPlugin, + getI18n: mockGetI18n, + getAi: mockGetAi, + }); + + assert.equal(registeredCommands.length, 10); + + const commandIds = registeredCommands.map((c) => c.id); + assert.ok(commandIds.includes('npde-ai-capabilities')); + assert.ok(commandIds.includes('npde-ai-describe')); + assert.ok(commandIds.includes('npde-ai-apply')); + assert.ok(commandIds.includes('npde-ai-validate')); + assert.ok(commandIds.includes('npde-ai-save')); + assert.ok(commandIds.includes('npde-ai-undo')); + assert.ok(commandIds.includes('npde-ai-redo')); + + assert.ok(commandIds.includes('npde-ai-list-capabilities')); + assert.ok(commandIds.includes('npde-ai-describe-document')); + assert.ok(commandIds.includes('npde-ai-apply-operations')); + + // Test callback execution when AI is disabled + const capCmd = registeredCommands.find((c) => c.id === 'npde-ai-capabilities'); + await capCmd.callback(); +});