757 lines
40 KiB
JavaScript
757 lines
40 KiB
JavaScript
/**
|
||
* Wails Mock Bridge — эмулирует window['go']['api']['App'] для тестового окружения.
|
||
*
|
||
* Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом.
|
||
* Состояние мутабельно — тесты могут менять его между сценариями.
|
||
*/
|
||
(function () {
|
||
if (window.__wailsMockReady) return;
|
||
|
||
// ── Mutable state ──────────────────────────────────────────────────
|
||
var pluginStates = {
|
||
'verstak.platform-test': {
|
||
status: 'loaded',
|
||
enabled: true,
|
||
manifest: {
|
||
schemaVersion: 1,
|
||
id: 'verstak.platform-test',
|
||
name: 'Platform Test',
|
||
version: '0.1.0',
|
||
apiVersion: '0.1.0',
|
||
description: 'Runtime test plugin for verifying the Verstak platform.',
|
||
source: 'official',
|
||
icon: '🧪',
|
||
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
|
||
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
|
||
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
|
||
frontend: { entry: 'frontend/dist/index.js' },
|
||
contributes: {
|
||
views: [
|
||
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
|
||
],
|
||
commands: [
|
||
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
|
||
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
|
||
],
|
||
sidebarItems: [
|
||
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
|
||
],
|
||
statusBarItems: [
|
||
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
|
||
],
|
||
settingsPanels: [
|
||
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
|
||
],
|
||
openProviders: [
|
||
{
|
||
id: 'verstak.platform-test.markdown-diagnostic',
|
||
title: 'Platform Test Markdown Diagnostic',
|
||
priority: 100,
|
||
component: 'MarkdownDiagnosticProvider',
|
||
supports: [
|
||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
},
|
||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||
error: ''
|
||
}
|
||
};
|
||
|
||
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||
var workbenchPreferences = {};
|
||
var openedResources = [];
|
||
var pluginSettings = {
|
||
'verstak.platform-test': { savedText: 'initial value' }
|
||
};
|
||
var vaultFiles = makeDefaultVaultFiles();
|
||
var workspaceTree = makeDefaultWorkspaceTree();
|
||
var reloadResponseMode = 'tuple';
|
||
|
||
// ── Helpers ────────────────────────────────────────────────────────
|
||
function makeDefaultWorkspaceTree() {
|
||
return {
|
||
status: 'initialized',
|
||
currentNodeId: 'case-alpha',
|
||
nodes: [
|
||
{ id: 'space-main', parentId: '', type: 'space', title: 'Main Space', status: 'active', order: 1 },
|
||
{ id: 'case-alpha', parentId: 'space-main', type: 'case', title: 'Alpha Case', status: 'active', order: 1 },
|
||
{ id: 'case-beta', parentId: 'space-main', type: 'case', title: 'Beta Case', status: 'active', order: 2 }
|
||
]
|
||
};
|
||
}
|
||
|
||
function cloneWorkspaceTree() {
|
||
return {
|
||
status: workspaceTree.status,
|
||
currentNodeId: workspaceTree.currentNodeId,
|
||
nodes: workspaceTree.nodes.map(function (n) { return Object.assign({}, n); })
|
||
};
|
||
}
|
||
|
||
function makeDefaultVaultFiles() {
|
||
return {
|
||
'': { type: 'folder', modifiedAt: new Date().toISOString() }
|
||
};
|
||
}
|
||
|
||
function normalizeVaultPath(relativePath, allowRoot) {
|
||
var p = String(relativePath || '');
|
||
if (p.indexOf('\x00') !== -1) return { error: 'invalid-path: null-byte' };
|
||
if (p.indexOf('\\') !== -1) return { error: 'invalid-path: backslash not allowed' };
|
||
if (p.indexOf('./') === 0) p = p.slice(2);
|
||
if (!allowRoot && !p) return { error: 'invalid-path: empty path' };
|
||
if (p.charAt(0) === '/' || /^[A-Za-z]:/.test(p)) return { error: 'invalid-path: absolute path rejected' };
|
||
var parts = p.split('/').filter(Boolean);
|
||
if (parts.indexOf('..') !== -1) return { error: 'invalid-path: path-traversal' };
|
||
if (parts[0] && parts[0].toLowerCase() === '.verstak') return { error: 'reserved-path: .verstak is internal' };
|
||
return { path: parts.join('/') };
|
||
}
|
||
|
||
function parentPath(path) {
|
||
var idx = path.lastIndexOf('/');
|
||
return idx === -1 ? '' : path.slice(0, idx);
|
||
}
|
||
|
||
function baseName(path) {
|
||
var idx = path.lastIndexOf('/');
|
||
return idx === -1 ? path : path.slice(idx + 1);
|
||
}
|
||
|
||
function fileEntry(path, node) {
|
||
var name = path ? baseName(path) : '';
|
||
var ext = '';
|
||
var dot = name.lastIndexOf('.');
|
||
if (dot > 0) ext = name.slice(dot + 1);
|
||
return {
|
||
name: name,
|
||
relativePath: path,
|
||
type: node.type,
|
||
size: node.type === 'file' ? (node.content || '').length : 0,
|
||
modifiedAt: node.modifiedAt || new Date().toISOString(),
|
||
extension: ext,
|
||
isHidden: name.charAt(0) === '.',
|
||
isReserved: false,
|
||
canRead: node.type === 'file' || node.type === 'folder',
|
||
canWrite: node.type === 'file' || node.type === 'folder'
|
||
};
|
||
}
|
||
|
||
function requirePluginPermission(pluginId, permission) {
|
||
var s = pluginStates[pluginId];
|
||
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
|
||
return 'plugin not enabled and loaded';
|
||
}
|
||
if (!s.manifest.permissions || s.manifest.permissions.indexOf(permission) === -1) {
|
||
return 'plugin lacks required permission ' + permission;
|
||
}
|
||
if (vaultStatus.status !== 'open') return 'vault-not-open';
|
||
return '';
|
||
}
|
||
|
||
function makePlugin(id) {
|
||
var s = pluginStates[id];
|
||
if (!s) return null;
|
||
return {
|
||
manifest: s.manifest,
|
||
status: s.status,
|
||
enabled: s.enabled,
|
||
rootPath: s.rootPath,
|
||
error: s.error
|
||
};
|
||
}
|
||
|
||
function allPlugins() {
|
||
return Object.keys(pluginStates).map(makePlugin).filter(Boolean);
|
||
}
|
||
|
||
function allCapabilities() {
|
||
var caps = [];
|
||
caps.push({ name: 'verstak/core/plugin-manager/v1', description: 'Plugin management', pluginId: 'verstak-desktop', status: 'stable' });
|
||
caps.push({ name: 'verstak/core/capability-registry/v1', description: 'Capability registry', pluginId: 'verstak-desktop', status: 'stable' });
|
||
caps.push({ name: 'verstak/core/files/v1', description: 'Files API', pluginId: 'verstak-desktop', status: 'stable' });
|
||
caps.push({ name: 'verstak/core/workbench/v1', description: 'Workbench routing', pluginId: 'verstak-desktop', status: 'stable' });
|
||
for (var id in pluginStates) {
|
||
var s = pluginStates[id];
|
||
if (s.status === 'loaded' && s.enabled && s.manifest && s.manifest.provides) {
|
||
s.manifest.provides.forEach(function (p) {
|
||
caps.push({ name: p, description: '', pluginId: id, status: 'stable' });
|
||
});
|
||
}
|
||
}
|
||
return caps;
|
||
}
|
||
|
||
function allPermissions() {
|
||
return [
|
||
{ name: 'vault.read', description: 'Read vault data', dangerous: false },
|
||
{ name: 'events.publish', description: 'Publish events', dangerous: false },
|
||
{ name: 'events.subscribe', description: 'Subscribe to events', dangerous: false },
|
||
{ name: 'ui.register', description: 'Register UI contributions', dangerous: false },
|
||
{ name: 'commands.register', description: 'Register commands', dangerous: false },
|
||
{ name: 'storage.namespace', description: 'Access plugin storage', dangerous: false },
|
||
{ name: 'files.read', description: 'Read vault files', dangerous: false },
|
||
{ name: 'files.write', description: 'Write vault files', dangerous: true },
|
||
{ name: 'files.delete', description: 'Trash vault files', dangerous: true },
|
||
{ name: 'workbench.open', description: 'Request Workbench open/edit routing', dangerous: false }
|
||
];
|
||
}
|
||
|
||
function allContributions() {
|
||
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [];
|
||
for (var id in pluginStates) {
|
||
var s = pluginStates[id];
|
||
var c = (s.manifest && s.manifest.contributes) || {};
|
||
if (c.views) c.views.forEach(function (v) { views.push(Object.assign({}, v, { pluginId: id })); });
|
||
if (c.commands) c.commands.forEach(function (cmd) { commands.push(Object.assign({}, cmd, { pluginId: id })); });
|
||
if (c.sidebarItems) c.sidebarItems.forEach(function (sb) { sidebarItems.push(Object.assign({}, sb, { pluginId: id })); });
|
||
if (c.statusBarItems) c.statusBarItems.forEach(function (st) { statusBarItems.push(Object.assign({}, st, { pluginId: id })); });
|
||
if (c.settingsPanels) c.settingsPanels.forEach(function (sp) { settingsPanels.push(Object.assign({}, sp, { pluginId: id })); });
|
||
if (c.openProviders) c.openProviders.forEach(function (op) { openProviders.push(Object.assign({}, op, { pluginId: id })); });
|
||
}
|
||
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders };
|
||
}
|
||
|
||
function requestExtension(request) {
|
||
if (request && request.extension) {
|
||
var explicit = String(request.extension).toLowerCase();
|
||
return explicit.charAt(0) === '.' ? explicit : '.' + explicit;
|
||
}
|
||
var p = String((request && request.path) || '').toLowerCase();
|
||
var slash = p.lastIndexOf('/');
|
||
var name = slash === -1 ? p : p.slice(slash + 1);
|
||
var dot = name.lastIndexOf('.');
|
||
return dot > 0 ? name.slice(dot) : '';
|
||
}
|
||
|
||
function requestContextName(request) {
|
||
var ctx = (request && request.context) || {};
|
||
if (ctx.notesMode || ctx.isInsideNotesFolder || ctx.sourceView === 'notes') return 'notes-markdown';
|
||
var ext = requestExtension(request);
|
||
if (ext === '.md' || ext === '.markdown') return 'generic-markdown';
|
||
return 'generic-text';
|
||
}
|
||
|
||
function providerSupports(provider, request) {
|
||
var ext = requestExtension(request);
|
||
var contextName = requestContextName(request);
|
||
return (provider.supports || []).some(function (support) {
|
||
if (support.kind && support.kind !== request.kind) return false;
|
||
if (support.extensions && support.extensions.length && support.extensions.map(function (e) { return String(e).toLowerCase(); }).indexOf(ext) === -1) return false;
|
||
if (support.contexts && support.contexts.length && support.contexts.indexOf(contextName) === -1) return false;
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function selectOpenProvider(request) {
|
||
var providers = allContributions().openProviders.filter(function (provider) {
|
||
var s = pluginStates[provider.pluginId];
|
||
return s && s.enabled && (s.status === 'loaded' || s.status === 'degraded') && providerSupports(provider, request);
|
||
});
|
||
providers.sort(function (a, b) {
|
||
var byPriority = (b.priority || 0) - (a.priority || 0);
|
||
if (byPriority) return byPriority;
|
||
return String(a.id).localeCompare(String(b.id));
|
||
});
|
||
return providers[0] || null;
|
||
}
|
||
|
||
function openWorkbenchResource(pluginId, request, forcedMode) {
|
||
var s = pluginStates[pluginId];
|
||
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
|
||
return Promise.resolve([{}, 'plugin not enabled and loaded']);
|
||
}
|
||
if (!s.manifest.permissions || s.manifest.permissions.indexOf('workbench.open') === -1) {
|
||
return Promise.resolve([{}, 'plugin lacks required permission workbench.open']);
|
||
}
|
||
var normalized = Object.assign({}, request || {});
|
||
normalized.kind = normalized.kind || 'vault-file';
|
||
normalized.mode = forcedMode || normalized.mode || 'view';
|
||
normalized.extension = requestExtension(normalized);
|
||
normalized.context = Object.assign({}, normalized.context || {}, { sourcePluginId: pluginId });
|
||
var provider = selectOpenProvider(normalized);
|
||
if (!provider) {
|
||
return Promise.resolve([{
|
||
status: 'no-provider',
|
||
request: normalized,
|
||
message: 'no open provider for resource'
|
||
}, '']);
|
||
}
|
||
var result = {
|
||
status: 'opened',
|
||
providerId: provider.id,
|
||
providerPluginId: provider.pluginId,
|
||
providerComponent: provider.component,
|
||
request: normalized
|
||
};
|
||
openedResources.push(Object.assign({ id: provider.id + ':' + openedResources.length, openedAt: new Date().toISOString() }, result));
|
||
return Promise.resolve([result, '']);
|
||
}
|
||
|
||
function platformTestBundle() {
|
||
return [
|
||
"(function(){",
|
||
"var DiagnosticsPanel={",
|
||
"mount:function(containerEl,props,api){",
|
||
"containerEl.innerHTML='';",
|
||
"containerEl.__ptCleanup=[];",
|
||
"function track(fn){if(typeof fn==='function')containerEl.__ptCleanup.push(fn);}",
|
||
"var root=document.createElement('div');",
|
||
"root.className='pt-root';",
|
||
"var title=document.createElement('h2');",
|
||
"title.className='pt-plugin-name';",
|
||
"title.textContent='Platform Diagnostics';",
|
||
"var pluginId=document.createElement('p');",
|
||
"pluginId.className='pt-plugin-id';",
|
||
"pluginId.textContent=api.pluginId;",
|
||
"var status=document.createElement('div');",
|
||
"status.className='pt-badge pt-badge-success';",
|
||
"status.textContent='Frontend Bundle Loaded';",
|
||
"var saved=document.createElement('div');",
|
||
"saved.className='pt-card pt-saved-setting';",
|
||
"saved.textContent='Saved setting: loading...';",
|
||
"var cap=document.createElement('div');",
|
||
"cap.className='pt-capability-result';",
|
||
"cap.textContent='Capabilities: loading...';",
|
||
"api.capabilities.list().then(function(caps){cap.textContent='Capabilities: '+caps.length+' available';});",
|
||
"api.settings.read('savedText').then(function(value){saved.textContent='Saved setting: '+(value||'');});",
|
||
"var input=document.createElement('input');",
|
||
"input.className='pt-setting-input';",
|
||
"input.setAttribute('aria-label','Saved setting');",
|
||
"input.value='changed value';",
|
||
"var button=document.createElement('button');",
|
||
"button.className='btn btn-primary pt-save-setting';",
|
||
"button.textContent='Save Setting';",
|
||
"button.addEventListener('click',function(){api.settings.write('savedText',input.value).then(function(){saved.textContent='Saved setting: '+input.value;});});",
|
||
"api.capabilities.has('verstak/platform-test/v1').then(function(ok){status.textContent='Frontend Bundle Loaded | capability '+(ok?'available':'missing');});",
|
||
"var command=document.createElement('div');",
|
||
"command.className='pt-command-result';",
|
||
"command.textContent='Command: registering...';",
|
||
"api.commands.register('verstak.platform-test.show-version',function(){return {version:'0.1.0',source:'bundled-frontend'};}).then(function(unregister){track(unregister);return api.commands.execute('verstak.platform-test.show-version',{});}).then(function(result){status.setAttribute('data-command-status',result.status||'');command.textContent='Command: '+result.status+' '+result.result.version+' from '+result.result.source;});",
|
||
"var eventResult=document.createElement('div');",
|
||
"eventResult.className='pt-event-result';",
|
||
"eventResult.textContent='Event: subscribing...';",
|
||
"api.events.subscribe('verstak.platform-test.echo',function(event){eventResult.textContent='Event: received '+event.payload.message;eventResult.setAttribute('data-event-status','received');}).then(function(unsubscribe){track(unsubscribe);return api.events.publish('verstak.platform-test.echo',{message:'hello-event'});});",
|
||
"var filesResult=document.createElement('div');",
|
||
"filesResult.className='pt-files-result';",
|
||
"filesResult.textContent='Files: running...';",
|
||
"var filesError=document.createElement('div');",
|
||
"filesError.className='pt-files-error-result';",
|
||
"filesError.textContent='Files error path: checking...';",
|
||
"var workbenchResult=document.createElement('div');",
|
||
"workbenchResult.className='pt-workbench-result';",
|
||
"workbenchResult.textContent='Workbench: ready';",
|
||
"function makeWorkbenchButton(cls,label,request){var b=document.createElement('button');b.className='btn btn-primary '+cls;b.textContent=label;b.addEventListener('click',function(){workbenchResult.textContent='Workbench: opening...';api.workbench.editResource(request).then(function(result){workbenchResult.textContent='Workbench: opened '+result.request.path+' with '+(result.providerId||'no-provider');workbenchResult.setAttribute('data-workbench-status',result.status==='opened'?'ok':result.status);}).catch(function(err){workbenchResult.textContent='Workbench error: '+(err&&err.message?err.message:String(err));workbenchResult.setAttribute('data-workbench-status','error');});});return b;}",
|
||
"var textWorkbenchButton=makeWorkbenchButton('pt-open-workbench-text','Open Text Diagnostic',{kind:'vault-file',path:'Docs/todo.txt',extension:'.txt',mime:'text/plain',context:{sourceView:'files'}});",
|
||
"var markdownWorkbenchButton=makeWorkbenchButton('pt-open-workbench-markdown','Open Markdown Diagnostic',{kind:'vault-file',path:'Docs/readme.md',extension:'.md',context:{sourceView:'files'}});",
|
||
"var notesWorkbenchButton=makeWorkbenchButton('pt-open-workbench-notes','Open Notes Diagnostic',{kind:'vault-file',path:'Notes/Overview.md',extension:'.md',context:{sourceView:'notes',isInsideNotesFolder:true,notesMode:true}});",
|
||
"api.files.createFolder('PlatformTest').catch(function(e){if(String(e).indexOf('conflict')===-1)throw e;}).then(function(){return api.files.writeText('PlatformTest/files-api.txt','hello files',{createIfMissing:true,overwrite:true});}).then(function(){return api.files.readText('PlatformTest/files-api.txt');}).then(function(text){if(text!=='hello files')throw new Error('read mismatch');return api.files.list('PlatformTest');}).then(function(entries){if(!entries.some(function(e){return e.relativePath==='PlatformTest/files-api.txt';}))throw new Error('list missing file');return api.files.move('PlatformTest/files-api.txt','PlatformTest/files-api-moved.txt',{overwrite:true});}).then(function(){return api.files.trash('PlatformTest/files-api-moved.txt');}).then(function(){filesResult.textContent='Files: wrote/read/listed/moved/trashed';filesResult.setAttribute('data-files-status','ok');}).catch(function(err){filesResult.textContent='Files error: '+(err&&err.message?err.message:String(err));filesResult.setAttribute('data-files-status','error');});",
|
||
"api.files.readText('.verstak/vault.json').then(function(){filesError.textContent='Files error path: unexpectedly allowed';filesError.setAttribute('data-files-error-status','error');}).catch(function(err){var message=err&&err.message?err.message:String(err);if(message.indexOf('reserved-path')===-1&&message.indexOf('.verstak')===-1){filesError.textContent='Files error path: wrong error '+message;filesError.setAttribute('data-files-error-status','error');return;}filesError.textContent='Files error path: rejected reserved-path';filesError.setAttribute('data-files-error-status','expected');});",
|
||
"root.appendChild(title);",
|
||
"root.appendChild(pluginId);",
|
||
"root.appendChild(status);",
|
||
"root.appendChild(saved);",
|
||
"root.appendChild(input);",
|
||
"root.appendChild(button);",
|
||
"root.appendChild(cap);",
|
||
"root.appendChild(command);",
|
||
"root.appendChild(eventResult);",
|
||
"root.appendChild(filesResult);",
|
||
"root.appendChild(filesError);",
|
||
"root.appendChild(textWorkbenchButton);",
|
||
"root.appendChild(markdownWorkbenchButton);",
|
||
"root.appendChild(notesWorkbenchButton);",
|
||
"root.appendChild(workbenchResult);",
|
||
"containerEl.appendChild(root);",
|
||
"},",
|
||
"unmount:function(containerEl){while(containerEl.__ptCleanup&&containerEl.__ptCleanup.length){containerEl.__ptCleanup.pop()();}containerEl.innerHTML='';}",
|
||
"};",
|
||
"var MarkdownDiagnosticProvider={",
|
||
"mount:function(containerEl,props,api){",
|
||
"containerEl.innerHTML='';",
|
||
"var root=document.createElement('div');",
|
||
"root.className='pt-root pt-workbench-result';",
|
||
"root.setAttribute('data-workbench-status','ok');",
|
||
"var req=(props&&props.request)||{};",
|
||
"var ctx=(req.context&&req.context.notesMode)||false?'notes-markdown':((req.extension==='.md'||req.extension==='.markdown')?'generic-markdown':'generic-text');",
|
||
"root.setAttribute('data-resource-path',req.path||'');",
|
||
"root.setAttribute('data-resource-mode',req.mode||'');",
|
||
"root.setAttribute('data-resource-context',ctx);",
|
||
"root.textContent='Workbench: opened '+(req.path||'')+' with '+((props&&props.providerId)||'')+' mode='+(req.mode||'')+' context='+ctx;",
|
||
"containerEl.appendChild(root);",
|
||
"},",
|
||
"unmount:function(containerEl){containerEl.innerHTML='';}",
|
||
"};",
|
||
"var PlatformTestSettings={",
|
||
"mount:function(containerEl,props,api){",
|
||
"containerEl.innerHTML='<div class=\"pt-root\"><h2>Platform Test Settings</h2><p>'+api.pluginId+'</p></div>';",
|
||
"},",
|
||
"unmount:function(containerEl){containerEl.innerHTML='';}",
|
||
"};",
|
||
"window.VerstakPluginRegister('verstak.platform-test',{components:{DiagnosticsPanel:DiagnosticsPanel,PlatformTestSettings:PlatformTestSettings,MarkdownDiagnosticProvider:MarkdownDiagnosticProvider}});",
|
||
"})();"
|
||
].join('');
|
||
}
|
||
|
||
// ── Mock API ───────────────────────────────────────────────────────
|
||
var mock = {
|
||
GetPlugins: function () { return Promise.resolve(allPlugins()); },
|
||
GetCapabilities: function () { return Promise.resolve(allCapabilities()); },
|
||
GetPermissions: function () { return Promise.resolve(allPermissions()); },
|
||
GetContributions: function () { return Promise.resolve(allContributions()); },
|
||
GetVaultStatus: function () { return Promise.resolve(vaultStatus); },
|
||
GetVaultPluginState: function () { return Promise.resolve(vaultPluginState); },
|
||
GetAppSettings: function () { return Promise.resolve(appSettings); },
|
||
GetPluginFrontendInfo: function (pluginId) {
|
||
var s = pluginStates[pluginId];
|
||
if (s && s.manifest && s.manifest.frontend) {
|
||
return Promise.resolve({ entry: s.manifest.frontend.entry });
|
||
}
|
||
return Promise.resolve({});
|
||
},
|
||
ReadPluginSettings: function (pluginId) {
|
||
return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']);
|
||
},
|
||
WritePluginSettings: function (pluginId, settings) {
|
||
pluginSettings[pluginId] = Object.assign({}, settings || {});
|
||
return Promise.resolve('');
|
||
},
|
||
ReadPluginSetting: function () { return Promise.resolve(null); },
|
||
WritePluginSetting: function () { return Promise.resolve(null); },
|
||
ReadPluginDataJSON: function () { return Promise.resolve({}); },
|
||
WritePluginDataJSON: function () { return Promise.resolve(null); },
|
||
OpenWorkbenchResource: function (pluginId, request) {
|
||
return openWorkbenchResource(pluginId, request || {}, '');
|
||
},
|
||
EditWorkbenchResource: function (pluginId, request) {
|
||
return openWorkbenchResource(pluginId, request || {}, 'edit');
|
||
},
|
||
GetWorkbenchOpenedResources: function () {
|
||
return Promise.resolve(openedResources.map(function (resource) {
|
||
return Object.assign({}, resource, { request: Object.assign({}, resource.request || {}) });
|
||
}));
|
||
},
|
||
GetWorkbenchPreferences: function () {
|
||
return Promise.resolve(Object.assign({}, workbenchPreferences));
|
||
},
|
||
UpdateWorkbenchPreferences: function (preferences) {
|
||
workbenchPreferences = Object.assign({}, workbenchPreferences, preferences || {});
|
||
return Promise.resolve('');
|
||
},
|
||
GetPluginAssetContent: function (pluginId, assetPath) {
|
||
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
|
||
return Promise.resolve(platformTestBundle());
|
||
}
|
||
return Promise.resolve('');
|
||
},
|
||
GetPluginCapability: function (pluginId, capId) {
|
||
var caps = allCapabilities();
|
||
var found = caps.find(function (cap) { return cap.name === capId; });
|
||
return Promise.resolve([found ? Object.assign({ available: true }, found) : { available: false, name: capId }, '']);
|
||
},
|
||
ListPluginCapabilities: function () { return Promise.resolve([allCapabilities(), '']); },
|
||
ExecutePluginCommand: function (pluginId, commandId, args) {
|
||
var s = pluginStates[pluginId];
|
||
var commands = ((s && s.manifest && s.manifest.contributes && s.manifest.contributes.commands) || []);
|
||
var found = commands.find(function (cmd) { return cmd.id === commandId; });
|
||
if (!found) return Promise.resolve([{}, 'command not declared']);
|
||
return Promise.resolve([{ status: 'declared', pluginId: pluginId, commandId: commandId, handler: found.handler, args: args || {} }, '']);
|
||
},
|
||
PublishPluginEvent: function () { return Promise.resolve(''); },
|
||
SubscribePluginEvent: function (pluginId, eventName) {
|
||
var s = pluginStates[pluginId];
|
||
if (!s || !s.enabled || s.status !== 'loaded') return Promise.resolve('plugin not enabled and loaded');
|
||
if (!eventName) return Promise.resolve('event name is empty');
|
||
if (!s.manifest.permissions || s.manifest.permissions.indexOf('events.subscribe') === -1) {
|
||
return Promise.resolve('plugin lacks required permission events.subscribe');
|
||
}
|
||
return Promise.resolve('');
|
||
},
|
||
ListVaultFiles: function (pluginId, relativeDir) {
|
||
var err = requirePluginPermission(pluginId, 'files.read');
|
||
if (err) return Promise.resolve([[], err]);
|
||
var norm = normalizeVaultPath(relativeDir, true);
|
||
if (norm.error) return Promise.resolve([[], norm.error]);
|
||
var dir = norm.path;
|
||
if (!vaultFiles[dir] || vaultFiles[dir].type !== 'folder') return Promise.resolve([[], 'not-found: ' + dir]);
|
||
var prefix = dir ? dir + '/' : '';
|
||
var entries = [];
|
||
Object.keys(vaultFiles).forEach(function (path) {
|
||
if (path === dir || path.indexOf(prefix) !== 0) return;
|
||
var rest = path.slice(prefix.length);
|
||
if (!rest || rest.indexOf('/') !== -1) return;
|
||
entries.push(fileEntry(path, vaultFiles[path]));
|
||
});
|
||
return Promise.resolve([entries, '']);
|
||
},
|
||
GetVaultFileMetadata: function (pluginId, relativePath) {
|
||
var err = requirePluginPermission(pluginId, 'files.read');
|
||
if (err) return Promise.resolve([{}, err]);
|
||
var norm = normalizeVaultPath(relativePath, false);
|
||
if (norm.error) return Promise.resolve([{}, norm.error]);
|
||
var node = vaultFiles[norm.path];
|
||
if (!node) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||
return Promise.resolve([fileEntry(norm.path, node), '']);
|
||
},
|
||
ReadVaultTextFile: function (pluginId, relativePath) {
|
||
var err = requirePluginPermission(pluginId, 'files.read');
|
||
if (err) return Promise.resolve(['', err]);
|
||
var norm = normalizeVaultPath(relativePath, false);
|
||
if (norm.error) return Promise.resolve(['', norm.error]);
|
||
var node = vaultFiles[norm.path];
|
||
if (!node) return Promise.resolve(['', 'not-found: ' + norm.path]);
|
||
if (node.type !== 'file') return Promise.resolve(['', 'not-regular-file: ' + norm.path]);
|
||
return Promise.resolve([node.content || '', '']);
|
||
},
|
||
WriteVaultTextFile: function (pluginId, relativePath, content, options) {
|
||
var err = requirePluginPermission(pluginId, 'files.write');
|
||
if (err) return Promise.resolve(err);
|
||
var norm = normalizeVaultPath(relativePath, false);
|
||
if (norm.error) return Promise.resolve(norm.error);
|
||
options = options || {};
|
||
var existing = vaultFiles[norm.path];
|
||
if (existing && existing.type !== 'file') return Promise.resolve('not-regular-file: ' + norm.path);
|
||
if (existing && !options.overwrite) return Promise.resolve('conflict: ' + norm.path);
|
||
if (!existing && !options.createIfMissing) return Promise.resolve('not-found: ' + norm.path);
|
||
var parent = parentPath(norm.path);
|
||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||
vaultFiles[norm.path] = { type: 'file', content: String(content == null ? '' : content), modifiedAt: new Date().toISOString() };
|
||
return Promise.resolve('');
|
||
},
|
||
CreateVaultFolder: function (pluginId, relativePath) {
|
||
var err = requirePluginPermission(pluginId, 'files.write');
|
||
if (err) return Promise.resolve(err);
|
||
var norm = normalizeVaultPath(relativePath, false);
|
||
if (norm.error) return Promise.resolve(norm.error);
|
||
if (vaultFiles[norm.path]) return Promise.resolve('conflict: ' + norm.path);
|
||
var parent = parentPath(norm.path);
|
||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||
vaultFiles[norm.path] = { type: 'folder', modifiedAt: new Date().toISOString() };
|
||
return Promise.resolve('');
|
||
},
|
||
MoveVaultPath: function (pluginId, fromRelativePath, toRelativePath, options) {
|
||
var err = requirePluginPermission(pluginId, 'files.write');
|
||
if (err) return Promise.resolve(err);
|
||
var from = normalizeVaultPath(fromRelativePath, false);
|
||
var to = normalizeVaultPath(toRelativePath, false);
|
||
if (from.error) return Promise.resolve(from.error);
|
||
if (to.error) return Promise.resolve(to.error);
|
||
options = options || {};
|
||
if (!vaultFiles[from.path]) return Promise.resolve('not-found: ' + from.path);
|
||
if (vaultFiles[from.path].type === 'folder' && (to.path === from.path || to.path.indexOf(from.path + '/') === 0)) {
|
||
return Promise.resolve('move-into-self: ' + from.path + ' -> ' + to.path);
|
||
}
|
||
if (vaultFiles[to.path] && !options.overwrite) return Promise.resolve('conflict: ' + to.path);
|
||
var parent = parentPath(to.path);
|
||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === from.path || path.indexOf(from.path + '/') === 0; });
|
||
moving.forEach(function (path) {
|
||
var suffix = path.slice(from.path.length);
|
||
vaultFiles[to.path + suffix] = vaultFiles[path];
|
||
delete vaultFiles[path];
|
||
});
|
||
return Promise.resolve('');
|
||
},
|
||
TrashVaultPath: function (pluginId, relativePath) {
|
||
var err = requirePluginPermission(pluginId, 'files.delete');
|
||
if (err) return Promise.resolve([{}, err]);
|
||
var norm = normalizeVaultPath(relativePath, false);
|
||
if (norm.error) return Promise.resolve([{}, norm.error]);
|
||
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
||
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
||
moving.forEach(function (path) { delete vaultFiles[path]; });
|
||
return Promise.resolve([{ originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString() }, '']);
|
||
},
|
||
GetCurrentWorkspaceNode: function () { return Promise.resolve(null); },
|
||
GetWorkspaceTree: function () { return Promise.resolve(cloneWorkspaceTree()); },
|
||
ArchiveWorkspaceNode: function () { return Promise.resolve(''); },
|
||
CreateWorkspaceNode: function () { return Promise.resolve({}); },
|
||
MoveWorkspaceNode: function () { return Promise.resolve(''); },
|
||
RenameWorkspaceNode: function () { return Promise.resolve(''); },
|
||
SetCurrentWorkspaceNode: function (id) {
|
||
var found = workspaceTree.nodes.some(function (n) { return n.id === id; });
|
||
if (!found) return Promise.resolve('workspace node not found: ' + id);
|
||
workspaceTree.currentNodeId = id;
|
||
return Promise.resolve('');
|
||
},
|
||
SelectDirectory: function () { return Promise.resolve(''); },
|
||
SelectVaultForOpen: function () { return Promise.resolve(''); },
|
||
CreateVault: function () { return Promise.resolve(null); },
|
||
OpenVault: function () { return Promise.resolve(null); },
|
||
CloseVault: function () { return Promise.resolve(null); },
|
||
SetCurrentVault: function () { return Promise.resolve(''); },
|
||
UpdateAppSettings: function () { return Promise.resolve(''); },
|
||
RecordDesiredPlugin: function () { return Promise.resolve(''); },
|
||
WriteFrontendLog: function () { return Promise.resolve(); },
|
||
|
||
EnablePlugin: function (pluginId) {
|
||
if (pluginStates[pluginId]) {
|
||
pluginStates[pluginId].status = 'loaded';
|
||
pluginStates[pluginId].enabled = true;
|
||
if (vaultPluginState.disabledPlugins.indexOf(pluginId) !== -1) {
|
||
vaultPluginState.disabledPlugins = vaultPluginState.disabledPlugins.filter(function (id) { return id !== pluginId; });
|
||
}
|
||
if (vaultPluginState.enabledPlugins.indexOf(pluginId) === -1) {
|
||
vaultPluginState.enabledPlugins.push(pluginId);
|
||
}
|
||
}
|
||
return Promise.resolve(null);
|
||
},
|
||
|
||
DisablePlugin: function (pluginId) {
|
||
if (pluginStates[pluginId]) {
|
||
pluginStates[pluginId].status = 'disabled';
|
||
pluginStates[pluginId].enabled = false;
|
||
if (vaultPluginState.enabledPlugins.indexOf(pluginId) !== -1) {
|
||
vaultPluginState.enabledPlugins = vaultPluginState.enabledPlugins.filter(function (id) { return id !== pluginId; });
|
||
}
|
||
if (vaultPluginState.disabledPlugins.indexOf(pluginId) === -1) {
|
||
vaultPluginState.disabledPlugins.push(pluginId);
|
||
}
|
||
}
|
||
return Promise.resolve(null);
|
||
},
|
||
|
||
ReloadPlugins: function () {
|
||
if (reloadResponseMode === 'raw-count') {
|
||
return Promise.resolve(Object.keys(pluginStates).length);
|
||
}
|
||
return Promise.resolve([Object.keys(pluginStates).length, 'Reloaded ' + Object.keys(pluginStates).length + ' plugin(s).']);
|
||
}
|
||
};
|
||
|
||
// ── Install bridge ─────────────────────────────────────────────────
|
||
if (!window['go']) window['go'] = {};
|
||
if (!window['go']['api']) window['go']['api'] = {};
|
||
window['go']['api']['App'] = mock;
|
||
|
||
// ── Test helpers (exposed for Playwright) ──────────────────────────
|
||
window.__wailsMock = {
|
||
reset: function () {
|
||
pluginStates = {
|
||
'verstak.platform-test': {
|
||
status: 'loaded',
|
||
enabled: true,
|
||
manifest: {
|
||
schemaVersion: 1,
|
||
id: 'verstak.platform-test',
|
||
name: 'Platform Test',
|
||
version: '0.1.0',
|
||
apiVersion: '0.1.0',
|
||
description: 'Runtime test plugin for verifying the Verstak platform.',
|
||
source: 'official',
|
||
icon: '🧪',
|
||
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
|
||
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
|
||
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
|
||
frontend: { entry: 'frontend/dist/index.js' },
|
||
contributes: {
|
||
views: [
|
||
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
|
||
],
|
||
commands: [
|
||
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
|
||
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
|
||
],
|
||
sidebarItems: [
|
||
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
|
||
],
|
||
statusBarItems: [
|
||
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
|
||
],
|
||
settingsPanels: [
|
||
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
|
||
],
|
||
openProviders: [
|
||
{
|
||
id: 'verstak.platform-test.markdown-diagnostic',
|
||
title: 'Platform Test Markdown Diagnostic',
|
||
priority: 100,
|
||
component: 'MarkdownDiagnosticProvider',
|
||
supports: [
|
||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
},
|
||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||
error: ''
|
||
}
|
||
};
|
||
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||
vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||
workbenchPreferences = {};
|
||
openedResources = [];
|
||
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
|
||
vaultFiles = makeDefaultVaultFiles();
|
||
workspaceTree = makeDefaultWorkspaceTree();
|
||
reloadResponseMode = 'tuple';
|
||
},
|
||
setPluginStatus: function (pluginId, status, enabled) {
|
||
if (pluginStates[pluginId]) {
|
||
pluginStates[pluginId].status = status;
|
||
pluginStates[pluginId].enabled = enabled;
|
||
}
|
||
},
|
||
getPluginState: function (pluginId) {
|
||
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
||
},
|
||
addSyntheticPlugins: function (count) {
|
||
var total = Number(count || 0);
|
||
for (var i = 1; i <= total; i++) {
|
||
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
||
pluginStates[id] = {
|
||
status: 'loaded',
|
||
enabled: true,
|
||
manifest: {
|
||
schemaVersion: 1,
|
||
id: id,
|
||
name: 'Synthetic Layout Plugin ' + i,
|
||
version: '0.0.' + i,
|
||
apiVersion: '0.1.0',
|
||
description: 'Synthetic plugin used by frontend layout tests.',
|
||
source: 'test',
|
||
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
||
requires: [],
|
||
optionalRequires: [],
|
||
permissions: [],
|
||
contributes: {
|
||
views: [],
|
||
commands: [],
|
||
sidebarItems: [],
|
||
statusBarItems: [],
|
||
settingsPanels: []
|
||
}
|
||
},
|
||
rootPath: '/tmp/verstak-test/plugins/synthetic-layout-' + i + '/with/a/long/path/for/responsive-checks',
|
||
error: ''
|
||
};
|
||
if (vaultPluginState.enabledPlugins.indexOf(id) === -1) {
|
||
vaultPluginState.enabledPlugins.push(id);
|
||
}
|
||
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
||
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: 'test' });
|
||
}
|
||
}
|
||
},
|
||
setVaultStatus: function (status) { vaultStatus = status; },
|
||
setVaultPluginState: function (state) { vaultPluginState = state; },
|
||
setReloadResponseMode: function (mode) { reloadResponseMode = mode || 'tuple'; }
|
||
};
|
||
|
||
window.__wailsMockReady = true;
|
||
console.log('[wails-mock] bridge installed');
|
||
})();
|