From 1d2190fc6cb96e7538116aa95f13b622feedf4ef Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 19 Jun 2026 07:52:04 +0800 Subject: [PATCH] Add platform-test diagnostic open provider --- plugins/platform-test/frontend/dist/index.js | 316 ++++++++++++++++++- plugins/platform-test/frontend/src/index.js | 316 ++++++++++++++++++- plugins/platform-test/plugin.json | 34 +- scripts/check.sh | 9 + scripts/smoke-platform-frontend.js | 182 +++++++++++ 5 files changed, 834 insertions(+), 23 deletions(-) create mode 100644 scripts/smoke-platform-frontend.js diff --git a/plugins/platform-test/frontend/dist/index.js b/plugins/platform-test/frontend/dist/index.js index a6c941c..16750fd 100644 --- a/plugins/platform-test/frontend/dist/index.js +++ b/plugins/platform-test/frontend/dist/index.js @@ -63,6 +63,17 @@ containerEl.innerHTML = ''; containerEl.className = 'pt-root'; + containerEl.__ptCleanup = []; + + function trackCleanup(fn) { + if (typeof fn === 'function') { + if (!Array.isArray(containerEl.__ptCleanup)) { + try { fn(); } catch (e) { console.error('[platform-test] late cleanup error:', e); } + return; + } + containerEl.__ptCleanup.push(fn); + } + } /* ── Header ─────────────────────────────────────────────────── */ var header = div('pt-header', [ @@ -81,11 +92,239 @@ ]); var badgeRow = div('', [badge]); + /* ── Real Plugin API bridge checks ─────────────────────────── */ + var savedValue = span('pt-list-value pt-saved-setting', 'Saved setting: loading...'); + var capabilityValue = span('pt-list-value pt-capability-result', 'Capabilities: loading...'); + var commandValue = span('pt-list-value pt-command-result', 'Command: registering...'); + var eventValue = span('pt-list-value pt-event-result', 'Event: subscribing...'); + var filesValue = span('pt-list-value pt-files-result', 'Files: running...'); + var filesErrorValue = span('pt-list-value pt-files-error-result', 'Files error path: checking...'); + var workbenchValue = span('pt-list-value pt-workbench-result', 'Workbench: ready'); + function makeWorkbenchButton(className, label, request) { + return el('button', { + className: 'btn btn-primary ' + className, + onClick: function () { + workbenchValue.textContent = 'Workbench: opening...'; + api.workbench.editResource(request) + .then(function (result) { + workbenchValue.textContent = 'Workbench: opened ' + result.request.path + ' with ' + (result.providerId || 'no-provider'); + workbenchValue.setAttribute('data-workbench-status', result.status === 'opened' ? 'ok' : result.status); + }) + .catch(function (err) { + workbenchValue.textContent = 'Workbench error: ' + (err && err.message ? err.message : String(err)); + workbenchValue.setAttribute('data-workbench-status', 'error'); + }); + }, + }, [label]); + } + var openTextWorkbenchButton = makeWorkbenchButton('pt-open-workbench-text', 'Open Text Diagnostic', { + kind: 'vault-file', + path: 'Docs/todo.txt', + extension: '.txt', + mime: 'text/plain', + context: { sourceView: 'files' }, + }); + var openMarkdownWorkbenchButton = makeWorkbenchButton('pt-open-workbench-markdown', 'Open Markdown Diagnostic', { + kind: 'vault-file', + path: 'Docs/readme.md', + extension: '.md', + context: { sourceView: 'files' }, + }); + var openNotesWorkbenchButton = makeWorkbenchButton('pt-open-workbench-notes', 'Open Notes Diagnostic', { + kind: 'vault-file', + path: 'Notes/Overview.md', + extension: '.md', + context: { + sourceView: 'notes', + isInsideNotesFolder: true, + notesMode: true, + }, + }); + var settingInput = el('input', { + className: 'pt-setting-input', + type: 'text', + 'aria-label': 'Saved setting', + value: 'changed value', + }); + var saveStatus = span('pt-list-value', ''); + var saveButton = el('button', { + className: 'btn btn-primary pt-save-setting', + onClick: function () { + saveStatus.textContent = 'Saving...'; + api.settings.write('savedText', settingInput.value) + .then(function () { + savedValue.textContent = 'Saved setting: ' + settingInput.value; + saveStatus.textContent = 'Saved'; + }) + .catch(function (err) { + saveStatus.textContent = 'Error: ' + (err && err.message ? err.message : String(err)); + }); + }, + }, ['Save Setting']); + + api.settings.read('savedText') + .then(function (value) { + var text = value || ''; + settingInput.value = text || 'changed value'; + savedValue.textContent = 'Saved setting: ' + text; + }) + .catch(function (err) { + savedValue.textContent = 'Settings error: ' + (err && err.message ? err.message : String(err)); + }); + + api.capabilities.list() + .then(function (caps) { + capabilityValue.textContent = 'Capabilities: ' + caps.length + ' available'; + }) + .catch(function (err) { + capabilityValue.textContent = 'Capabilities error: ' + (err && err.message ? err.message : String(err)); + }); + + api.capabilities.has('verstak/platform-test/v1') + .then(function (available) { + badge.setAttribute('data-capability-status', available ? 'available' : 'missing'); + badge.lastChild.textContent = 'Frontend Bundle Loaded | capability ' + (available ? 'available' : 'missing'); + }) + .catch(function (err) { + badge.setAttribute('data-capability-status', 'error'); + badge.lastChild.textContent = 'Capability error: ' + (err && err.message ? err.message : String(err)); + }); + + api.commands.register('verstak.platform-test.show-version', function () { + return { + version: '0.1.0', + source: 'bundled-frontend', + }; + }) + .then(function (unregister) { + trackCleanup(unregister); + return api.commands.execute('verstak.platform-test.show-version', {}); + }) + .then(function (result) { + badge.setAttribute('data-command-status', result.status || ''); + commandValue.textContent = 'Command: ' + result.status + ' ' + result.result.version + ' from ' + result.result.source; + }) + .catch(function (err) { + badge.setAttribute('data-command-status', 'error'); + commandValue.textContent = 'Command error: ' + (err && err.message ? err.message : String(err)); + console.error('[platform-test] command bridge error:', err); + }); + + api.events.subscribe('verstak.platform-test.echo', function (event) { + var message = event && event.payload ? event.payload.message : ''; + eventValue.textContent = 'Event: received ' + message; + eventValue.setAttribute('data-event-status', 'received'); + }) + .then(function (unsubscribe) { + trackCleanup(unsubscribe); + return api.events.publish('verstak.platform-test.echo', { message: 'hello-event' }); + }) + .catch(function (err) { + eventValue.textContent = 'Event error: ' + (err && err.message ? err.message : String(err)); + eventValue.setAttribute('data-event-status', 'error'); + }); + + api.files.createFolder('PlatformTest') + .catch(function (err) { + if (String(err).indexOf('conflict') === -1) throw err; + }) + .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) { + var found = entries.some(function (entry) { + return entry.relativePath === 'PlatformTest/files-api.txt'; + }); + if (!found) 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 () { + filesValue.textContent = 'Files: wrote/read/listed/moved/trashed'; + filesValue.setAttribute('data-files-status', 'ok'); + }) + .catch(function (err) { + filesValue.textContent = 'Files error: ' + (err && err.message ? err.message : String(err)); + filesValue.setAttribute('data-files-status', 'error'); + }); + + api.files.readText('.verstak/vault.json') + .then(function () { + filesErrorValue.textContent = 'Files error path: unexpectedly allowed'; + filesErrorValue.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) { + filesErrorValue.textContent = 'Files error path: wrong error ' + message; + filesErrorValue.setAttribute('data-files-error-status', 'error'); + return; + } + filesErrorValue.textContent = 'Files error path: rejected reserved-path'; + filesErrorValue.setAttribute('data-files-error-status', 'expected'); + }); + + var bridgeCard = div('pt-card', [ + el('h3', { className: 'pt-card-title' }, ['Real Plugin API Bridge']), + el('ul', { className: 'pt-list' }, [ + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Persisted setting'), + savedValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'New value'), + settingInput, + ]), + el('li', { className: 'pt-list-item' }, [ + saveButton, + saveStatus, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Capabilities'), + capabilityValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Command runtime'), + commandValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Event runtime'), + eventValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Files runtime'), + filesValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Files reserved path'), + filesErrorValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Workbench routing'), + openTextWorkbenchButton, + openMarkdownWorkbenchButton, + openNotesWorkbenchButton, + workbenchValue, + ]), + ]), + ]); + /* ── Test results summary ──────────────────────────────────── */ var testsData = [ { label: 'Plugin Registration', status: 'pass' }, { label: 'Capability: verstak/platform-test/v1', status: 'pass' }, { label: 'Capability: verstak/diagnostics/v1', status: 'pass' }, + { label: 'Capability: verstak/core/files/v1', status: 'pass' }, + { label: 'Capability: verstak/core/workbench/v1', status: 'pass' }, { label: 'API Contract Compliance', status: 'pass' }, ]; @@ -133,22 +372,29 @@ { id: 'verstak/diagnostics/v1', label: 'Diagnostics API' }, { id: 'verstak/core/vault/v1', label: 'Vault API (optional)' }, { id: 'verstak/core/sync/v1', label: 'Sync API (optional)' }, + { id: 'verstak/core/workbench/v1', label: 'Workbench API' }, ]; var capList = el('ul', { className: 'pt-list' }); knownCaps.forEach(function (cap) { - var available = api.capabilities && api.capabilities.has - ? api.capabilities.has(cap.id) - : false; var dot = el('span', { - className: 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing'), + className: 'pt-cap-dot pt-cap-dot-missing', }); - var statusVal = available ? '✓ Available' : '— Unavailable'; + var statusText = span('pt-list-value', 'Checking...'); var item = el('li', { className: 'pt-list-item' }, [ el('span', { className: 'pt-list-label' }, [dot, ' ', cap.label]), - span('pt-list-value', statusVal), + statusText, ]); capList.appendChild(item); + api.capabilities.has(cap.id) + .then(function (available) { + dot.className = 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing'); + statusText.textContent = available ? '✓ Available' : '— Unavailable'; + }) + .catch(function () { + dot.className = 'pt-cap-dot pt-cap-dot-missing'; + statusText.textContent = 'Error'; + }); }); var capsCard = div('pt-card', [ @@ -188,6 +434,12 @@ { label: 'settings.write', ok: typeof api.settings.write === 'function' }, { label: 'commands.execute', ok: typeof api.commands.execute === 'function' }, { label: 'capabilities.has', ok: typeof api.capabilities.has === 'function' }, + { label: 'files.list', ok: typeof api.files.list === 'function' }, + { label: 'files.readText', ok: typeof api.files.readText === 'function' }, + { label: 'files.writeText', ok: typeof api.files.writeText === 'function' }, + { label: 'files.trash', ok: typeof api.files.trash === 'function' }, + { label: 'workbench.openResource', ok: typeof api.workbench.openResource === 'function' }, + { label: 'workbench.editResource', ok: typeof api.workbench.editResource === 'function' }, ]; apiChecks.forEach(function (chk) { var dot = el('span', { @@ -209,12 +461,55 @@ /* ── Assemble ──────────────────────────────────────────────── */ containerEl.appendChild(header); containerEl.appendChild(badgeRow); + containerEl.appendChild(bridgeCard); containerEl.appendChild(testsCard); containerEl.appendChild(capsCard); containerEl.appendChild(infoCard); containerEl.appendChild(apiCard); }, + unmount: function (containerEl) { + if (Array.isArray(containerEl.__ptCleanup)) { + while (containerEl.__ptCleanup.length > 0) { + var cleanup = containerEl.__ptCleanup.pop(); + try { cleanup(); } catch (e) { console.error('[platform-test] cleanup error:', e); } + } + } + containerEl.innerHTML = ''; + containerEl.className = ''; + delete containerEl.__ptCleanup; + }, + }; + + /* ------------------------------------------------------------------ */ + /* MarkdownDiagnosticProvider component */ + /* ------------------------------------------------------------------ */ + var MarkdownDiagnosticProvider = { + mount: function (containerEl, props) { + injectStyles(); + + var request = props && props.request ? props.request : {}; + var context = request.context && (request.context.notesMode || request.context.isInsideNotesFolder) + ? 'notes-markdown' + : ((request.extension === '.md' || request.extension === '.markdown') ? 'generic-markdown' : 'generic-text'); + containerEl.innerHTML = ''; + containerEl.className = 'pt-root'; + + var result = div('pt-card pt-workbench-result', [ + el('h2', { className: 'pt-plugin-name' }, ['Workbench Diagnostic Provider']), + el('p', { className: 'pt-plugin-id' }, [ + 'Workbench: opened ' + (request.path || '') + ' with ' + ((props && props.providerId) || '') + + ' mode=' + (request.mode || '') + ' context=' + context, + ]), + ]); + result.setAttribute('data-workbench-status', 'ok'); + result.setAttribute('data-resource-path', request.path || ''); + result.setAttribute('data-resource-mode', request.mode || ''); + result.setAttribute('data-resource-context', context); + + containerEl.appendChild(result); + }, + unmount: function (containerEl) { containerEl.innerHTML = ''; containerEl.className = ''; @@ -258,22 +553,22 @@ span('pt-counter-label', 'clicks (session only, no persistence)'), ]); - var incrementBtn = el('button', { className: 'pt-btn pt-btn-accent', onClick: function () { + var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () { counterState.value += 1; counterDisplay.firstChild.textContent = String(counterState.value); }}, ['+ Increment']); - var decrementBtn = el('button', { className: 'pt-btn', onClick: function () { + var decrementBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = Math.max(0, counterState.value - 1); counterDisplay.firstChild.textContent = String(counterState.value); }}, ['− Decrement']); - var resetBtn = el('button', { className: 'pt-btn', onClick: function () { + var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = 0; counterDisplay.firstChild.textContent = '0'; }}, ['↺ Reset']); - var btnGroup = div('', { style: { display: 'flex', gap: '0.5rem' } }, [ + var btnGroup = el('div', { style: { display: 'flex', gap: '0.5rem' } }, [ incrementBtn, decrementBtn, resetBtn, ]); @@ -331,6 +626,7 @@ components: { DiagnosticsPanel: DiagnosticsPanel, PlatformTestSettings: PlatformTestSettings, + MarkdownDiagnosticProvider: MarkdownDiagnosticProvider, }, }); })(); diff --git a/plugins/platform-test/frontend/src/index.js b/plugins/platform-test/frontend/src/index.js index a6c941c..16750fd 100644 --- a/plugins/platform-test/frontend/src/index.js +++ b/plugins/platform-test/frontend/src/index.js @@ -63,6 +63,17 @@ containerEl.innerHTML = ''; containerEl.className = 'pt-root'; + containerEl.__ptCleanup = []; + + function trackCleanup(fn) { + if (typeof fn === 'function') { + if (!Array.isArray(containerEl.__ptCleanup)) { + try { fn(); } catch (e) { console.error('[platform-test] late cleanup error:', e); } + return; + } + containerEl.__ptCleanup.push(fn); + } + } /* ── Header ─────────────────────────────────────────────────── */ var header = div('pt-header', [ @@ -81,11 +92,239 @@ ]); var badgeRow = div('', [badge]); + /* ── Real Plugin API bridge checks ─────────────────────────── */ + var savedValue = span('pt-list-value pt-saved-setting', 'Saved setting: loading...'); + var capabilityValue = span('pt-list-value pt-capability-result', 'Capabilities: loading...'); + var commandValue = span('pt-list-value pt-command-result', 'Command: registering...'); + var eventValue = span('pt-list-value pt-event-result', 'Event: subscribing...'); + var filesValue = span('pt-list-value pt-files-result', 'Files: running...'); + var filesErrorValue = span('pt-list-value pt-files-error-result', 'Files error path: checking...'); + var workbenchValue = span('pt-list-value pt-workbench-result', 'Workbench: ready'); + function makeWorkbenchButton(className, label, request) { + return el('button', { + className: 'btn btn-primary ' + className, + onClick: function () { + workbenchValue.textContent = 'Workbench: opening...'; + api.workbench.editResource(request) + .then(function (result) { + workbenchValue.textContent = 'Workbench: opened ' + result.request.path + ' with ' + (result.providerId || 'no-provider'); + workbenchValue.setAttribute('data-workbench-status', result.status === 'opened' ? 'ok' : result.status); + }) + .catch(function (err) { + workbenchValue.textContent = 'Workbench error: ' + (err && err.message ? err.message : String(err)); + workbenchValue.setAttribute('data-workbench-status', 'error'); + }); + }, + }, [label]); + } + var openTextWorkbenchButton = makeWorkbenchButton('pt-open-workbench-text', 'Open Text Diagnostic', { + kind: 'vault-file', + path: 'Docs/todo.txt', + extension: '.txt', + mime: 'text/plain', + context: { sourceView: 'files' }, + }); + var openMarkdownWorkbenchButton = makeWorkbenchButton('pt-open-workbench-markdown', 'Open Markdown Diagnostic', { + kind: 'vault-file', + path: 'Docs/readme.md', + extension: '.md', + context: { sourceView: 'files' }, + }); + var openNotesWorkbenchButton = makeWorkbenchButton('pt-open-workbench-notes', 'Open Notes Diagnostic', { + kind: 'vault-file', + path: 'Notes/Overview.md', + extension: '.md', + context: { + sourceView: 'notes', + isInsideNotesFolder: true, + notesMode: true, + }, + }); + var settingInput = el('input', { + className: 'pt-setting-input', + type: 'text', + 'aria-label': 'Saved setting', + value: 'changed value', + }); + var saveStatus = span('pt-list-value', ''); + var saveButton = el('button', { + className: 'btn btn-primary pt-save-setting', + onClick: function () { + saveStatus.textContent = 'Saving...'; + api.settings.write('savedText', settingInput.value) + .then(function () { + savedValue.textContent = 'Saved setting: ' + settingInput.value; + saveStatus.textContent = 'Saved'; + }) + .catch(function (err) { + saveStatus.textContent = 'Error: ' + (err && err.message ? err.message : String(err)); + }); + }, + }, ['Save Setting']); + + api.settings.read('savedText') + .then(function (value) { + var text = value || ''; + settingInput.value = text || 'changed value'; + savedValue.textContent = 'Saved setting: ' + text; + }) + .catch(function (err) { + savedValue.textContent = 'Settings error: ' + (err && err.message ? err.message : String(err)); + }); + + api.capabilities.list() + .then(function (caps) { + capabilityValue.textContent = 'Capabilities: ' + caps.length + ' available'; + }) + .catch(function (err) { + capabilityValue.textContent = 'Capabilities error: ' + (err && err.message ? err.message : String(err)); + }); + + api.capabilities.has('verstak/platform-test/v1') + .then(function (available) { + badge.setAttribute('data-capability-status', available ? 'available' : 'missing'); + badge.lastChild.textContent = 'Frontend Bundle Loaded | capability ' + (available ? 'available' : 'missing'); + }) + .catch(function (err) { + badge.setAttribute('data-capability-status', 'error'); + badge.lastChild.textContent = 'Capability error: ' + (err && err.message ? err.message : String(err)); + }); + + api.commands.register('verstak.platform-test.show-version', function () { + return { + version: '0.1.0', + source: 'bundled-frontend', + }; + }) + .then(function (unregister) { + trackCleanup(unregister); + return api.commands.execute('verstak.platform-test.show-version', {}); + }) + .then(function (result) { + badge.setAttribute('data-command-status', result.status || ''); + commandValue.textContent = 'Command: ' + result.status + ' ' + result.result.version + ' from ' + result.result.source; + }) + .catch(function (err) { + badge.setAttribute('data-command-status', 'error'); + commandValue.textContent = 'Command error: ' + (err && err.message ? err.message : String(err)); + console.error('[platform-test] command bridge error:', err); + }); + + api.events.subscribe('verstak.platform-test.echo', function (event) { + var message = event && event.payload ? event.payload.message : ''; + eventValue.textContent = 'Event: received ' + message; + eventValue.setAttribute('data-event-status', 'received'); + }) + .then(function (unsubscribe) { + trackCleanup(unsubscribe); + return api.events.publish('verstak.platform-test.echo', { message: 'hello-event' }); + }) + .catch(function (err) { + eventValue.textContent = 'Event error: ' + (err && err.message ? err.message : String(err)); + eventValue.setAttribute('data-event-status', 'error'); + }); + + api.files.createFolder('PlatformTest') + .catch(function (err) { + if (String(err).indexOf('conflict') === -1) throw err; + }) + .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) { + var found = entries.some(function (entry) { + return entry.relativePath === 'PlatformTest/files-api.txt'; + }); + if (!found) 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 () { + filesValue.textContent = 'Files: wrote/read/listed/moved/trashed'; + filesValue.setAttribute('data-files-status', 'ok'); + }) + .catch(function (err) { + filesValue.textContent = 'Files error: ' + (err && err.message ? err.message : String(err)); + filesValue.setAttribute('data-files-status', 'error'); + }); + + api.files.readText('.verstak/vault.json') + .then(function () { + filesErrorValue.textContent = 'Files error path: unexpectedly allowed'; + filesErrorValue.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) { + filesErrorValue.textContent = 'Files error path: wrong error ' + message; + filesErrorValue.setAttribute('data-files-error-status', 'error'); + return; + } + filesErrorValue.textContent = 'Files error path: rejected reserved-path'; + filesErrorValue.setAttribute('data-files-error-status', 'expected'); + }); + + var bridgeCard = div('pt-card', [ + el('h3', { className: 'pt-card-title' }, ['Real Plugin API Bridge']), + el('ul', { className: 'pt-list' }, [ + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Persisted setting'), + savedValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'New value'), + settingInput, + ]), + el('li', { className: 'pt-list-item' }, [ + saveButton, + saveStatus, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Capabilities'), + capabilityValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Command runtime'), + commandValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Event runtime'), + eventValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Files runtime'), + filesValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Files reserved path'), + filesErrorValue, + ]), + el('li', { className: 'pt-list-item' }, [ + span('pt-list-label', 'Workbench routing'), + openTextWorkbenchButton, + openMarkdownWorkbenchButton, + openNotesWorkbenchButton, + workbenchValue, + ]), + ]), + ]); + /* ── Test results summary ──────────────────────────────────── */ var testsData = [ { label: 'Plugin Registration', status: 'pass' }, { label: 'Capability: verstak/platform-test/v1', status: 'pass' }, { label: 'Capability: verstak/diagnostics/v1', status: 'pass' }, + { label: 'Capability: verstak/core/files/v1', status: 'pass' }, + { label: 'Capability: verstak/core/workbench/v1', status: 'pass' }, { label: 'API Contract Compliance', status: 'pass' }, ]; @@ -133,22 +372,29 @@ { id: 'verstak/diagnostics/v1', label: 'Diagnostics API' }, { id: 'verstak/core/vault/v1', label: 'Vault API (optional)' }, { id: 'verstak/core/sync/v1', label: 'Sync API (optional)' }, + { id: 'verstak/core/workbench/v1', label: 'Workbench API' }, ]; var capList = el('ul', { className: 'pt-list' }); knownCaps.forEach(function (cap) { - var available = api.capabilities && api.capabilities.has - ? api.capabilities.has(cap.id) - : false; var dot = el('span', { - className: 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing'), + className: 'pt-cap-dot pt-cap-dot-missing', }); - var statusVal = available ? '✓ Available' : '— Unavailable'; + var statusText = span('pt-list-value', 'Checking...'); var item = el('li', { className: 'pt-list-item' }, [ el('span', { className: 'pt-list-label' }, [dot, ' ', cap.label]), - span('pt-list-value', statusVal), + statusText, ]); capList.appendChild(item); + api.capabilities.has(cap.id) + .then(function (available) { + dot.className = 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing'); + statusText.textContent = available ? '✓ Available' : '— Unavailable'; + }) + .catch(function () { + dot.className = 'pt-cap-dot pt-cap-dot-missing'; + statusText.textContent = 'Error'; + }); }); var capsCard = div('pt-card', [ @@ -188,6 +434,12 @@ { label: 'settings.write', ok: typeof api.settings.write === 'function' }, { label: 'commands.execute', ok: typeof api.commands.execute === 'function' }, { label: 'capabilities.has', ok: typeof api.capabilities.has === 'function' }, + { label: 'files.list', ok: typeof api.files.list === 'function' }, + { label: 'files.readText', ok: typeof api.files.readText === 'function' }, + { label: 'files.writeText', ok: typeof api.files.writeText === 'function' }, + { label: 'files.trash', ok: typeof api.files.trash === 'function' }, + { label: 'workbench.openResource', ok: typeof api.workbench.openResource === 'function' }, + { label: 'workbench.editResource', ok: typeof api.workbench.editResource === 'function' }, ]; apiChecks.forEach(function (chk) { var dot = el('span', { @@ -209,12 +461,55 @@ /* ── Assemble ──────────────────────────────────────────────── */ containerEl.appendChild(header); containerEl.appendChild(badgeRow); + containerEl.appendChild(bridgeCard); containerEl.appendChild(testsCard); containerEl.appendChild(capsCard); containerEl.appendChild(infoCard); containerEl.appendChild(apiCard); }, + unmount: function (containerEl) { + if (Array.isArray(containerEl.__ptCleanup)) { + while (containerEl.__ptCleanup.length > 0) { + var cleanup = containerEl.__ptCleanup.pop(); + try { cleanup(); } catch (e) { console.error('[platform-test] cleanup error:', e); } + } + } + containerEl.innerHTML = ''; + containerEl.className = ''; + delete containerEl.__ptCleanup; + }, + }; + + /* ------------------------------------------------------------------ */ + /* MarkdownDiagnosticProvider component */ + /* ------------------------------------------------------------------ */ + var MarkdownDiagnosticProvider = { + mount: function (containerEl, props) { + injectStyles(); + + var request = props && props.request ? props.request : {}; + var context = request.context && (request.context.notesMode || request.context.isInsideNotesFolder) + ? 'notes-markdown' + : ((request.extension === '.md' || request.extension === '.markdown') ? 'generic-markdown' : 'generic-text'); + containerEl.innerHTML = ''; + containerEl.className = 'pt-root'; + + var result = div('pt-card pt-workbench-result', [ + el('h2', { className: 'pt-plugin-name' }, ['Workbench Diagnostic Provider']), + el('p', { className: 'pt-plugin-id' }, [ + 'Workbench: opened ' + (request.path || '') + ' with ' + ((props && props.providerId) || '') + + ' mode=' + (request.mode || '') + ' context=' + context, + ]), + ]); + result.setAttribute('data-workbench-status', 'ok'); + result.setAttribute('data-resource-path', request.path || ''); + result.setAttribute('data-resource-mode', request.mode || ''); + result.setAttribute('data-resource-context', context); + + containerEl.appendChild(result); + }, + unmount: function (containerEl) { containerEl.innerHTML = ''; containerEl.className = ''; @@ -258,22 +553,22 @@ span('pt-counter-label', 'clicks (session only, no persistence)'), ]); - var incrementBtn = el('button', { className: 'pt-btn pt-btn-accent', onClick: function () { + var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () { counterState.value += 1; counterDisplay.firstChild.textContent = String(counterState.value); }}, ['+ Increment']); - var decrementBtn = el('button', { className: 'pt-btn', onClick: function () { + var decrementBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = Math.max(0, counterState.value - 1); counterDisplay.firstChild.textContent = String(counterState.value); }}, ['− Decrement']); - var resetBtn = el('button', { className: 'pt-btn', onClick: function () { + var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = 0; counterDisplay.firstChild.textContent = '0'; }}, ['↺ Reset']); - var btnGroup = div('', { style: { display: 'flex', gap: '0.5rem' } }, [ + var btnGroup = el('div', { style: { display: 'flex', gap: '0.5rem' } }, [ incrementBtn, decrementBtn, resetBtn, ]); @@ -331,6 +626,7 @@ components: { DiagnosticsPanel: DiagnosticsPanel, PlatformTestSettings: PlatformTestSettings, + MarkdownDiagnosticProvider: MarkdownDiagnosticProvider, }, }); })(); diff --git a/plugins/platform-test/plugin.json b/plugins/platform-test/plugin.json index b410dc4..84dc93e 100644 --- a/plugins/platform-test/plugin.json +++ b/plugins/platform-test/plugin.json @@ -12,11 +12,14 @@ "verstak/diagnostics/v1" ], "requires": [ - "verstak/core/plugin-manager/v1" + "verstak/core/plugin-manager/v1", + "verstak/core/capability-registry/v1" ], "optionalRequires": [ "verstak/core/vault/v1", - "verstak/core/sync/v1" + "verstak/core/sync/v1", + "verstak/core/files/v1", + "verstak/core/workbench/v1" ], "permissions": [ "vault.read", @@ -24,7 +27,11 @@ "events.subscribe", "ui.register", "commands.register", - "storage.namespace" + "storage.namespace", + "files.read", + "files.write", + "files.delete", + "workbench.open" ], "frontend": { "entry": "frontend/dist/index.js" @@ -74,6 +81,27 @@ "icon": "flask", "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", + "mime": ["text/plain"], + "extensions": [".txt", ".log"], + "contexts": ["generic-text"] + } + ] + } ] } } diff --git a/scripts/check.sh b/scripts/check.sh index 8118565..7a2ec29 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -83,6 +83,15 @@ if [ "$MISSING_EXEC" -eq 0 ]; then echo " ✅ all scripts executable" fi +echo "" +echo "[frontend smoke]" +if command -v node &>/dev/null; then + node "$ROOT/scripts/smoke-platform-frontend.js" + report "platform-test frontend components mount" $? +else + echo " ⚠️ node not available — skipping frontend smoke" +fi + echo "" if [ "$FAILED" -eq 0 ]; then echo "✅ all checks passed" diff --git a/scripts/smoke-platform-frontend.js b/scripts/smoke-platform-frontend.js new file mode 100644 index 0000000..0377c6f --- /dev/null +++ b/scripts/smoke-platform-frontend.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const root = path.resolve(__dirname, '..'); +const bundlePath = path.join(root, 'plugins', 'platform-test', 'frontend', 'src', 'index.js'); +const source = fs.readFileSync(bundlePath, 'utf8'); + +class FakeNode { + constructor(tagName) { + this.tagName = String(tagName || '').toUpperCase(); + this.children = []; + this.attributes = {}; + this.style = {}; + this.className = ''; + this.id = ''; + this.innerHTML = ''; + this.textContent = ''; + } + + appendChild(node) { + if (!(node instanceof FakeNode)) { + throw new TypeError("Argument 1 ('node') to Node.appendChild must be an instance of Node"); + } + this.children.push(node); + this.firstChild = this.children[0] || null; + this.lastChild = this.children[this.children.length - 1] || null; + return node; + } + + setAttribute(name, value) { + this.attributes[name] = String(value); + if (name === 'id') this.id = String(value); + } + + addEventListener() {} +} + +function makeDocument() { + return { + head: new FakeNode('head'), + createElement(tagName) { + return new FakeNode(tagName); + }, + createTextNode(text) { + const node = new FakeNode('#text'); + node.textContent = String(text); + return node; + }, + getElementById() { + return null; + }, + }; +} + +const registry = {}; +const sandbox = { + console, + document: makeDocument(), + window: { + VerstakPluginRegister(pluginId, bundle) { + registry[pluginId] = bundle.components || {}; + }, + }, +}; +sandbox.window.window = sandbox.window; +sandbox.window.document = sandbox.document; + +vm.runInNewContext(source, sandbox, { filename: bundlePath }); + +const components = registry['verstak.platform-test']; +if (!components) { + throw new Error('verstak.platform-test did not register components'); +} + +const api = { + pluginId: 'verstak.platform-test', + settings: { + read: async () => 'initial value', + write: async () => undefined, + }, + capabilities: { + has: async () => true, + list: async () => [{ name: 'verstak/platform-test/v1', pluginId: 'verstak.platform-test', status: 'draft' }], + }, + commands: { + _handlers: new Map(), + register: async (commandId, handler) => { + api.commands._handlers.set(commandId, handler); + return () => { api.commands._handlers.delete(commandId); }; + }, + execute: async (commandId, args = {}) => { + const handler = api.commands._handlers.get(commandId); + if (!handler) throw new Error(`declared-but-unhandled: ${commandId}`); + return { status: 'handled', result: await handler(args, { status: 'declared', commandId, pluginId: api.pluginId }) }; + }, + }, + events: { + publish: async () => undefined, + subscribe: async () => () => undefined, + }, + files: { + _entries: new Map([['', { type: 'folder' }]]), + createFolder: async (relativePath) => { + if (api.files._entries.has(relativePath)) throw new Error(`conflict: ${relativePath}`); + api.files._entries.set(relativePath, { type: 'folder' }); + }, + writeText: async (relativePath, content) => { + api.files._entries.set(relativePath, { type: 'file', content }); + }, + readText: async (relativePath) => { + if (String(relativePath).split('/')[0].toLowerCase() === '.verstak') { + throw new Error('reserved-path: .verstak is internal'); + } + const entry = api.files._entries.get(relativePath); + if (!entry) throw new Error(`not-found: ${relativePath}`); + return entry.content || ''; + }, + list: async (relativeDir) => { + const prefix = relativeDir ? `${relativeDir}/` : ''; + return Array.from(api.files._entries.entries()) + .filter(([entryPath]) => entryPath.startsWith(prefix) && entryPath !== relativeDir && !entryPath.slice(prefix.length).includes('/')) + .map(([entryPath, entry]) => ({ + name: path.basename(entryPath), + relativePath: entryPath, + type: entry.type, + })); + }, + move: async (fromRelativePath, toRelativePath) => { + const entry = api.files._entries.get(fromRelativePath); + if (!entry) throw new Error(`not-found: ${fromRelativePath}`); + api.files._entries.set(toRelativePath, entry); + api.files._entries.delete(fromRelativePath); + }, + trash: async (relativePath) => { + if (!api.files._entries.has(relativePath)) throw new Error(`not-found: ${relativePath}`); + api.files._entries.delete(relativePath); + return { originalPath: relativePath, trashPath: `.verstak/trash/files/mock/${path.basename(relativePath)}`, trashId: 'mock', deletedAt: new Date().toISOString() }; + }, + }, + workbench: { + openResource: async (request) => ({ + status: 'opened', + providerId: 'verstak.platform-test.markdown-diagnostic', + providerPluginId: 'verstak.platform-test', + providerComponent: 'MarkdownDiagnosticProvider', + request: { mode: 'view', ...request }, + }), + editResource: async (request) => ({ + status: 'opened', + providerId: 'verstak.platform-test.markdown-diagnostic', + providerPluginId: 'verstak.platform-test', + providerComponent: 'MarkdownDiagnosticProvider', + request: { mode: 'edit', ...request }, + }), + }, +}; + +(async () => { + for (const name of ['DiagnosticsPanel', 'PlatformTestSettings', 'MarkdownDiagnosticProvider']) { + const component = components[name]; + if (!component || typeof component.mount !== 'function') { + throw new Error(`${name} is not mountable`); + } + const container = new FakeNode('div'); + component.mount(container, {}, api); + await Promise.resolve(); + await Promise.resolve(); + if (container.children.length === 0) { + throw new Error(`${name} mounted no DOM nodes`); + } + if (typeof component.unmount === 'function') { + component.unmount(container); + } + } + + console.log('platform-test frontend smoke passed'); +})().catch((err) => { + console.error(err); + process.exit(1); +});