Add sync API to plugin SDK

This commit is contained in:
2026-06-27 12:36:31 +08:00
parent 0cbab61826
commit cdc0b373d8
10 changed files with 148 additions and 8 deletions
+38 -1
View File
@@ -1,4 +1,5 @@
import { describe, expect, test } from 'vitest';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import manifestSchema from '../schemas/manifest.json';
import type { OpenResourceRequest, PluginManifest } from './types';
import { createMockPluginAPI } from './test-utils';
@@ -24,6 +25,9 @@ describe('VerstakPluginAPI contract', () => {
expect(typeof api.files.trash).toBe('function');
expect(typeof api.workbench.openResource).toBe('function');
expect(typeof api.workbench.editResource).toBe('function');
expect(typeof api.sync.status).toBe('function');
expect(typeof api.sync.configure).toBe('function');
expect(typeof api.sync.now).toBe('function');
});
test('manifest schema accepts files permissions used by platform-test', () => {
@@ -35,13 +39,46 @@ describe('VerstakPluginAPI contract', () => {
expect(permissionEnum).toContain('workbench.open');
});
test('official plugin manifests comply with SDK apiVersion and permission schema', () => {
const pluginsDir = new URL('../../verstak-official-plugins/plugins/', import.meta.url);
if (!existsSync(pluginsDir)) {
return;
}
const apiVersionPattern = new RegExp((manifestSchema as any).properties.apiVersion.pattern);
const permissionEnum = ((manifestSchema as any).properties.permissions.items.enum || []) as string[];
const problems: string[] = [];
for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const manifestPath = new URL(`${entry.name}/plugin.json`, pluginsDir);
if (!existsSync(manifestPath)) {
continue;
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as PluginManifest;
if (!apiVersionPattern.test(manifest.apiVersion)) {
problems.push(`${manifest.id}: apiVersion ${manifest.apiVersion} does not match SDK schema`);
}
for (const permission of manifest.permissions) {
if (!permissionEnum.includes(permission)) {
problems.push(`${manifest.id}: permission ${permission} is not in SDK schema`);
}
}
}
expect(problems).toEqual([]);
});
test('manifest types accept open provider contributions', () => {
const manifest: PluginManifest = {
schemaVersion: 1,
id: 'verstak.default-editor',
name: 'Default Editor',
version: '0.1.0',
apiVersion: '1',
apiVersion: '0.1.0',
provides: ['editor.text', 'editor.text.markdown'],
permissions: ['ui.register', 'files.read', 'files.write', 'workbench.open'],
contributes: {
+32
View File
@@ -46,6 +46,29 @@ export interface PluginEvent<TPayload = Record<string, unknown>> {
timestamp: string;
}
export interface SyncStatus {
configured: boolean;
serverUrl: string;
deviceId: string;
deviceName: string;
connected: boolean;
revoked: boolean;
tokenStored: boolean;
unpushedOps: number;
lastSyncAt: string;
syncInterval: number;
lastError: string;
statusLabel: string;
}
export interface SyncNowResult {
pushed: number;
pulled: number;
serverSequence: number;
conflicts?: unknown[];
applyErrors?: string[];
}
export interface VerstakPluginAPI {
readonly pluginId: string;
@@ -95,6 +118,15 @@ export interface VerstakPluginAPI {
editResource(request: OpenResourceRequest): Promise<OpenResourceResult>;
};
sync: {
status(): Promise<SyncStatus>;
configure(serverUrl: string, username: string, password: string): Promise<void>;
disconnect(): Promise<void>;
testConnection(serverUrl: string, username: string, password: string): Promise<void>;
setInterval(minutes: number): Promise<void>;
now(): Promise<SyncNowResult>;
};
dispose?: () => void;
}
+22 -1
View File
@@ -12,7 +12,7 @@ export function createTestManifest(overrides?: Partial<PluginManifest>): PluginM
id: 'test.plugin',
name: 'Test Plugin',
version: '0.1.0',
apiVersion: '1',
apiVersion: '0.1.0',
description: 'A test plugin for platform verification',
source: 'local',
provides: ['test.capability'],
@@ -220,6 +220,27 @@ export function createMockPluginAPI(pluginId = 'test.plugin'): VerstakPluginAPI
request: { ...request, mode: 'edit' as const },
})),
},
sync: {
status: vi.fn(async () => ({
configured: false,
serverUrl: '',
deviceId: '',
deviceName: '',
connected: false,
revoked: false,
tokenStored: false,
unpushedOps: 0,
lastSyncAt: '',
syncInterval: 0,
lastError: '',
statusLabel: 'disabled',
})),
configure: vi.fn(async () => {}),
disconnect: vi.fn(async () => {}),
testConnection: vi.fn(async () => {}),
setInterval: vi.fn(async () => {}),
now: vi.fn(async () => ({ pushed: 0, pulled: 0, serverSequence: 0 })),
},
dispose: vi.fn(),
};
}