feat: document contribution runtime APIs
This commit is contained in:
@@ -14,6 +14,8 @@ describe('VerstakPluginAPI contract', () => {
|
||||
expect(typeof api.capabilities.list).toBe('function');
|
||||
expect(typeof api.commands.register).toBe('function');
|
||||
expect(typeof api.commands.execute).toBe('function');
|
||||
expect(typeof api.commands.executeFor).toBe('function');
|
||||
expect(typeof api.contributions.list).toBe('function');
|
||||
expect(typeof api.events.publish).toBe('function');
|
||||
expect(typeof api.events.subscribe).toBe('function');
|
||||
expect(typeof api.files.list).toBe('function');
|
||||
@@ -188,6 +190,32 @@ describe('VerstakPluginAPI contract', () => {
|
||||
await expect(api.commands.execute('cmd.plugin.echo', {})).rejects.toThrow('declared-but-unhandled');
|
||||
});
|
||||
|
||||
test('contributions list and provider command execution', async () => {
|
||||
const api = createMockPluginAPI('consumer.plugin', {
|
||||
contributions: {
|
||||
fileActions: [{
|
||||
pluginId: 'provider.plugin',
|
||||
id: 'provider.file.action',
|
||||
label: 'Provider File Action',
|
||||
handler: 'provider.command',
|
||||
}],
|
||||
},
|
||||
});
|
||||
const providerApi = createMockPluginAPI('provider.plugin');
|
||||
|
||||
await providerApi.commands.register('provider.command', async (args) => args.path);
|
||||
|
||||
await expect(api.contributions.list('fileActions')).resolves.toEqual([
|
||||
expect.objectContaining({ pluginId: 'provider.plugin', id: 'provider.file.action' }),
|
||||
]);
|
||||
await expect(api.commands.executeFor('provider.plugin', 'provider.command', { path: 'Docs/readme.md' })).resolves.toEqual({
|
||||
status: 'handled',
|
||||
pluginId: 'provider.plugin',
|
||||
commandId: 'provider.command',
|
||||
result: 'Docs/readme.md',
|
||||
});
|
||||
});
|
||||
|
||||
test('events publish to subscribers and unsubscribe cleanly', async () => {
|
||||
const api = createMockPluginAPI('event.plugin');
|
||||
const received: unknown[] = [];
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
OpenResourceRequest,
|
||||
OpenResourceResult,
|
||||
PluginSettings,
|
||||
RegisteredContributionPoints,
|
||||
TrashResult,
|
||||
WriteTextOptions,
|
||||
} from './types';
|
||||
@@ -88,6 +89,14 @@ export interface VerstakPluginAPI {
|
||||
commands: {
|
||||
register(commandId: string, handler: PluginCommandHandler): Promise<Unsubscribe>;
|
||||
execute(commandId: string, args?: PluginCommandArgs): Promise<PluginCommandResult>;
|
||||
executeFor(targetPluginId: string, commandId: string, args?: PluginCommandArgs): Promise<PluginCommandResult>;
|
||||
};
|
||||
|
||||
contributions: {
|
||||
list(): Promise<RegisteredContributionPoints>;
|
||||
list<K extends keyof RegisteredContributionPoints>(
|
||||
point: K
|
||||
): Promise<NonNullable<RegisteredContributionPoints[K]>>;
|
||||
};
|
||||
|
||||
events: {
|
||||
|
||||
+39
-7
@@ -1,7 +1,17 @@
|
||||
// Verstak Plugin SDK — Test Utilities
|
||||
|
||||
import type { PluginManifest, PluginState } from './types';
|
||||
import type { VerstakPluginAPI } from './plugin-api';
|
||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||
import type { PluginCommandHandler, VerstakPluginAPI } from './plugin-api';
|
||||
|
||||
const mockCommandHandlers = new Map<string, PluginCommandHandler>();
|
||||
|
||||
function commandKey(pluginId: string, commandId: string): string {
|
||||
return `${pluginId}:${commandId}`;
|
||||
}
|
||||
|
||||
export interface MockPluginAPIOptions {
|
||||
contributions?: RegisteredContributionPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать тестовый manifest для unit-тестов.
|
||||
@@ -40,9 +50,9 @@ export function createTestPluginState(overrides?: Partial<PluginState>): PluginS
|
||||
/**
|
||||
* Создать заглушку VerstakPluginAPI для тестов.
|
||||
*/
|
||||
export function createMockPluginAPI(pluginId = 'test.plugin'): VerstakPluginAPI {
|
||||
export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPluginAPIOptions = {}): VerstakPluginAPI {
|
||||
const settings: Record<string, unknown> = {};
|
||||
const commands = new Map<string, (args: Record<string, unknown>) => unknown>();
|
||||
const commands = new Map<string, PluginCommandHandler>();
|
||||
const eventHandlers = new Map<string, Array<(event: any) => void>>();
|
||||
const files = new Map<string, { type: 'file' | 'folder'; content?: string; modifiedAt: string }>();
|
||||
files.set('', { type: 'folder', modifiedAt: new Date().toISOString() });
|
||||
@@ -107,17 +117,39 @@ export function createMockPluginAPI(pluginId = 'test.plugin'): VerstakPluginAPI
|
||||
list: vi.fn(async () => []),
|
||||
},
|
||||
commands: {
|
||||
register: vi.fn(async (commandId: string, handler: (args: Record<string, unknown>) => unknown) => {
|
||||
register: vi.fn(async (commandId: string, handler: PluginCommandHandler) => {
|
||||
commands.set(commandId, handler);
|
||||
return () => { commands.delete(commandId); };
|
||||
mockCommandHandlers.set(commandKey(pluginId, commandId), handler);
|
||||
return () => {
|
||||
commands.delete(commandId);
|
||||
mockCommandHandlers.delete(commandKey(pluginId, commandId));
|
||||
};
|
||||
}),
|
||||
execute: vi.fn(async (commandId: string, args: Record<string, unknown> = {}) => {
|
||||
const handler = commands.get(commandId);
|
||||
if (!handler) {
|
||||
throw new Error(`declared-but-unhandled: ${commandId}`);
|
||||
}
|
||||
return { status: 'handled' as const, pluginId, commandId, result: await handler(args) };
|
||||
return { status: 'handled' as const, pluginId, commandId, result: await handler(args, { status: 'declared', pluginId, commandId, args }) };
|
||||
}),
|
||||
executeFor: vi.fn(async (targetPluginId: string, commandId: string, args: Record<string, unknown> = {}) => {
|
||||
const handler = mockCommandHandlers.get(commandKey(targetPluginId, commandId));
|
||||
if (!handler) {
|
||||
throw new Error(`declared-but-unhandled: ${targetPluginId}:${commandId}`);
|
||||
}
|
||||
return {
|
||||
status: 'handled' as const,
|
||||
pluginId: targetPluginId,
|
||||
commandId,
|
||||
result: await handler(args, { status: 'declared', pluginId: targetPluginId, commandId, args }),
|
||||
};
|
||||
}),
|
||||
},
|
||||
contributions: {
|
||||
list: vi.fn(async (point?: keyof RegisteredContributionPoints) => {
|
||||
if (!point) return { ...(options.contributions || {}) };
|
||||
return ([...((options.contributions && options.contributions[point]) || [])]) as any;
|
||||
}) as VerstakPluginAPI['contributions']['list'],
|
||||
},
|
||||
events: {
|
||||
publish: vi.fn(async (eventName: string, payload: Record<string, unknown> = {}) => {
|
||||
|
||||
@@ -251,6 +251,25 @@ export interface ContributionWorkspaceItem {
|
||||
component: string;
|
||||
}
|
||||
|
||||
export type RegisteredContribution<T> = T & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export interface RegisteredContributionPoints {
|
||||
views?: RegisteredContribution<ContributionView>[];
|
||||
commands?: RegisteredContribution<ContributionCommand>[];
|
||||
settingsPanels?: RegisteredContribution<ContributionSettingsPanel>[];
|
||||
sidebarItems?: RegisteredContribution<ContributionSidebarItem>[];
|
||||
fileActions?: RegisteredContribution<ContributionAction>[];
|
||||
noteActions?: RegisteredContribution<ContributionAction>[];
|
||||
contextMenuEntries?: RegisteredContribution<ContributionContextMenuEntry>[];
|
||||
searchProviders?: RegisteredContribution<ContributionSearchProvider>[];
|
||||
activityProviders?: RegisteredContribution<ContributionActivityProvider>[];
|
||||
statusBarItems?: RegisteredContribution<ContributionStatusBarItem>[];
|
||||
openProviders?: RegisteredContribution<ContributionOpenProvider>[];
|
||||
workspaceItems?: RegisteredContribution<ContributionWorkspaceItem>[];
|
||||
}
|
||||
|
||||
export interface OpenResourceContext {
|
||||
sourcePluginId?: string;
|
||||
sourceView?: 'files' | 'notes' | string;
|
||||
@@ -380,6 +399,52 @@ export interface NoteSavedEvent extends VerstakEvent {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceCreatedEvent extends VerstakEvent {
|
||||
name: 'workspace.created';
|
||||
payload: {
|
||||
operation: 'create';
|
||||
workspaceRootPath: string;
|
||||
workspaceName: string;
|
||||
title?: string;
|
||||
templateId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceRenamedEvent extends VerstakEvent {
|
||||
name: 'workspace.renamed';
|
||||
payload: {
|
||||
operation: 'rename';
|
||||
workspaceRootPath: string;
|
||||
workspaceName: string;
|
||||
previousWorkspaceRootPath: string;
|
||||
previousWorkspaceName: string;
|
||||
title?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceTrashedEvent extends VerstakEvent {
|
||||
name: 'workspace.trashed';
|
||||
payload: {
|
||||
operation: 'trash';
|
||||
workspaceRootPath: string;
|
||||
workspaceName: string;
|
||||
title?: string;
|
||||
trashId: string;
|
||||
trashPath: string;
|
||||
deletedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceSelectedEvent extends VerstakEvent {
|
||||
name: 'workspace.selected';
|
||||
payload: {
|
||||
operation: 'select';
|
||||
workspaceRootPath: string;
|
||||
workspaceName: string;
|
||||
title?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Lifecycle events
|
||||
export interface PluginEnabledEvent extends VerstakEvent {
|
||||
name: 'plugin.enabled';
|
||||
|
||||
Reference in New Issue
Block a user