fix: localize plugin operation errors
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
function frontendSources(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const target = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return frontendSources(target);
|
||||
return /\.(?:js|svelte)$/.test(entry.name) ? [target] : [];
|
||||
});
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(path.join(root, 'plugins'))
|
||||
.flatMap((plugin) => frontendSources(path.join(root, 'plugins', plugin, 'frontend', 'src')));
|
||||
|
||||
const renderPatterns = [
|
||||
/statusText\s*=/,
|
||||
/state\.error\s*=/,
|
||||
/errorMsg\s*=/,
|
||||
/setStatus\(/,
|
||||
/setCreateError\(/,
|
||||
/setRenameError\(/,
|
||||
/renderEmpty\(/,
|
||||
/output\.errors\.push\(/,
|
||||
/window\.alert\(/,
|
||||
/showExternalFallback\(/,
|
||||
/de-error-msg/,
|
||||
/files-error-msg/,
|
||||
/body\.textContent\s*=/
|
||||
];
|
||||
|
||||
const violations = [];
|
||||
for (const file of files) {
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
||||
lines.forEach((line, index) => {
|
||||
if (!/(?:err(?:or)?\.message|String\(err(?:or)?\b)/.test(line)) return;
|
||||
if (!renderPatterns.some((pattern) => pattern.test(line))) return;
|
||||
violations.push(`${path.relative(root, file)}:${index + 1}: technical error text reaches a user-facing UI sink`);
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length) {
|
||||
console.error('User-facing UI must not render raw backend/plugin errors:');
|
||||
violations.forEach((violation) => console.error(` ${violation}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('user-facing error messages do not expose raw backend details');
|
||||
@@ -101,6 +101,18 @@ else
|
||||
echo " ⚠️ node not available — skipping select style validation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "[user-facing errors]"
|
||||
if command -v node &>/dev/null; then
|
||||
set +e
|
||||
node "$ROOT/scripts/check-user-facing-errors.js"
|
||||
STATUS=$?
|
||||
set -e
|
||||
report "user-facing errors" "$STATUS"
|
||||
else
|
||||
echo " ⚠️ node not available — skipping user-facing error validation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Guard official plugins against bypassing the v2 plugin API for note features.
|
||||
echo "[frontend API boundary]"
|
||||
|
||||
@@ -6,6 +6,7 @@ const vm = require('vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const technicalErrors = [];
|
||||
|
||||
class FakeNode {
|
||||
constructor(tagName) {
|
||||
@@ -113,7 +114,15 @@ function makeDocument() {
|
||||
function loadComponents(document) {
|
||||
const registry = {};
|
||||
const sandbox = {
|
||||
console,
|
||||
console: {
|
||||
...console,
|
||||
warn(...args) {
|
||||
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
error(...args) {
|
||||
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
},
|
||||
Date,
|
||||
document,
|
||||
window: {
|
||||
@@ -738,9 +747,12 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||
if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) {
|
||||
throw new Error('failed conversion removed capture from queue');
|
||||
}
|
||||
if (!failedConversionView.container.textContent.includes('Could not create note')) {
|
||||
if (!failedConversionView.container.textContent.includes('Could not create the note. Please try again.')) {
|
||||
throw new Error('failed conversion did not render an error status');
|
||||
}
|
||||
if (failedConversionView.container.textContent.includes('file already exists')) {
|
||||
throw new Error('failed conversion exposed a raw backend error');
|
||||
}
|
||||
if (failedConversionApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||
throw new Error('failed conversion published converted event');
|
||||
}
|
||||
@@ -827,7 +839,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||
if (!failedLinkApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link-conflict')) {
|
||||
throw new Error('failed link conversion removed capture from queue');
|
||||
}
|
||||
if (!failedLinkView.container.textContent.includes('Could not create link')) {
|
||||
if (!failedLinkView.container.textContent.includes('Could not create the link. Please try again.')) {
|
||||
throw new Error('failed link conversion did not render an error status');
|
||||
}
|
||||
if (failedLinkApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||
@@ -934,7 +946,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||
if (!failedFileApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file-conflict')) {
|
||||
throw new Error('failed file conversion removed capture from queue');
|
||||
}
|
||||
if (!failedFileView.container.textContent.includes('Could not create file')) {
|
||||
if (!failedFileView.container.textContent.includes('Could not create the file. Please try again.')) {
|
||||
throw new Error('failed file conversion did not render an error status');
|
||||
}
|
||||
if (failedFileApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||
@@ -942,6 +954,10 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||
}
|
||||
component.unmount && component.unmount(failedFileView.container);
|
||||
|
||||
if (!technicalErrors.some((entry) => entry.includes('file already exists'))) {
|
||||
throw new Error('failed conversion did not retain its technical details in the console log');
|
||||
}
|
||||
|
||||
console.log('browser inbox plugin smoke passed');
|
||||
})().catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -8,6 +8,7 @@ const sourcePath = path.join(root, 'plugins', 'search', 'frontend', 'src', 'inde
|
||||
const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const technicalErrors = [];
|
||||
|
||||
class FakeNode {
|
||||
constructor(tagName) {
|
||||
@@ -100,7 +101,15 @@ function makeDocument() {
|
||||
function loadComponent(document) {
|
||||
const registry = {};
|
||||
vm.runInNewContext(source, {
|
||||
console,
|
||||
console: {
|
||||
...console,
|
||||
warn(...args) {
|
||||
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
error(...args) {
|
||||
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
},
|
||||
document,
|
||||
window: {
|
||||
VerstakPluginRegister(pluginId, bundle) {
|
||||
@@ -253,11 +262,13 @@ async function wait(ms) {
|
||||
if (!container.textContent.includes('Project/Target Assets')) throw new Error('typing should search folder paths');
|
||||
if (!container.textContent.includes('Project/External/target.note')) throw new Error('external provider result should be rendered');
|
||||
if (!container.textContent.includes('External Notes')) throw new Error('external provider label should be rendered');
|
||||
if (!container.textContent.includes('provider unavailable')) throw new Error('provider failure should be reported without failing search');
|
||||
if (!container.textContent.includes('A search provider is unavailable.')) throw new Error('provider failure should be reported without failing search');
|
||||
if (container.textContent.includes('provider unavailable')) throw new Error('provider failure leaked a raw backend error');
|
||||
if (!container.textContent.includes('Content match')) throw new Error('content result type was not rendered');
|
||||
if (!container.textContent.includes('Folder name')) throw new Error('folder result type was not rendered');
|
||||
if (!pluginData['search-index'] || !Array.isArray(pluginData['search-index'].files)) throw new Error('search index was not written to plugin data storage');
|
||||
if (providerCalls.some((call) => call.pluginId === 'verstak.search')) throw new Error('search must not call itself as an external provider');
|
||||
if (!technicalErrors.some((entry) => entry.includes('provider unavailable'))) throw new Error('provider failure was not retained in the console log');
|
||||
|
||||
input = queryInput();
|
||||
input.value = 'image';
|
||||
|
||||
@@ -101,6 +101,9 @@ function loadComponent(document, errorLog) {
|
||||
vm.runInNewContext(source, {
|
||||
console: {
|
||||
...console,
|
||||
warn(...args) {
|
||||
errorLog.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
error(...args) {
|
||||
errorLog.push(args.map((value) => String(value)).join(' '));
|
||||
},
|
||||
|
||||
@@ -9,14 +9,17 @@ const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
if (!source.includes('settings.lastError')) {
|
||||
throw new Error('SyncSettings must render persisted settings.lastError');
|
||||
}
|
||||
if (!source.includes('sanitizeError(settings.lastError)')) {
|
||||
throw new Error('SyncSettings must sanitize persisted sync errors before rendering');
|
||||
if (!source.includes('function reportError')) {
|
||||
throw new Error('SyncSettings must map technical failures to localized action-specific errors');
|
||||
}
|
||||
if (!source.includes('Last sync error')) {
|
||||
if (/sanitizeError\(/.test(source) || /\{ error: sanitizeError/.test(source)) {
|
||||
throw new Error('SyncSettings must not interpolate raw technical errors into user-facing messages');
|
||||
}
|
||||
if (!source.includes("tr('ui.lastSyncError'")) {
|
||||
throw new Error('SyncSettings must label the persisted sync error');
|
||||
}
|
||||
if (!source.includes('function formatSyncConflict')) {
|
||||
throw new Error('SyncSettings must format individual sync conflicts');
|
||||
if (!source.includes("tr('ui.syncConflictItem'")) {
|
||||
throw new Error('SyncSettings must hide technical conflict identifiers behind a user-facing summary');
|
||||
}
|
||||
if (!source.includes('conflictDetails')) {
|
||||
throw new Error('SyncSettings must store sync conflict details after Sync Now');
|
||||
|
||||
Reference in New Issue
Block a user