verstak-desktop/frontend/src/lib/test/wails-mock.js

757 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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');
})();