From 19be861c498cc33d4d138a5d7cb3d9dbab628453 Mon Sep 17 00:00:00 2001 From: Thanadon Kongkanun <71448810+thanadon-dev@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:49:23 +0700 Subject: [PATCH] fix(kv): hide internal agent error data in browser messages --- .../src/components/etcd/EtcdKeyBrowser.vue | 25 ++++++++++--------- .../src/components/kv/KvKeyBrowser.vue | 17 +++++++------ .../KvKeyBrowserErrorMessages.spec.ts | 23 +++++++++++++++++ .../KvKeyBrowserSearchAndSplit.spec.ts | 2 +- 4 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 apps/desktop/src/components/kv/__tests__/KvKeyBrowserErrorMessages.spec.ts diff --git a/apps/desktop/src/components/etcd/EtcdKeyBrowser.vue b/apps/desktop/src/components/etcd/EtcdKeyBrowser.vue index 962e1d9412..10b114997c 100644 --- a/apps/desktop/src/components/etcd/EtcdKeyBrowser.vue +++ b/apps/desktop/src/components/etcd/EtcdKeyBrowser.vue @@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useToast } from "@/composables/useToast"; import { useTabUiState } from "@/lib/tabs/tabUiState"; import * as api from "@/lib/backend/api"; +import { formatError } from "@/lib/backend/errorUtils"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { isKeyInKvExportScope, kvExportFilenameStem, kvValueByteIdentity, type KvExportScopeKind, type KvExportScopeRequest } from "@/lib/kv/kvExportScope"; import { detectKvValueFormat } from "@/lib/kv/kvValueFormat"; @@ -596,7 +597,7 @@ async function exportTreeSelection(format: EtcdExportFormat) { const exported = await downloadExport(file); if (exported) toast(t("etcd.exported", { count: entries.length }), 2500); } catch (error) { - toast(error instanceof Error ? error.message : String(error), 4000); + toast(formatError(error), 4000); } } @@ -623,7 +624,7 @@ async function deleteSelectedTreeKeys() { toast(t("etcd.batchDeleteSuccess", { count: deleted }), 3000); } catch (error) { failed = true; - const message = error instanceof Error ? error.message : String(error); + const message = formatError(error); toast(t("etcd.batchDeletePartial", { count: deleted, error: message }), 5000); } finally { if (failed) browserRef.value?.clearMultiSelection(); @@ -711,7 +712,7 @@ async function exportAll(format: EtcdExportFormat) { const exported = await downloadExport(file); if (exported) toast(t("etcd.exported", { count: scan.entries.length }), 2500); } catch (error) { - toast(error instanceof Error ? error.message : String(error), 4000); + toast(formatError(error), 4000); } } @@ -744,7 +745,7 @@ async function exportEtcdNodeScope(connectionId: string, request: KvExportScopeR const exported = await downloadExport(file); if (exported) toast(t("etcd.exported", { count: entries.length }), 2500); } catch (error) { - toast(error instanceof Error ? error.message : String(error), 4000); + toast(formatError(error), 4000); } } @@ -776,7 +777,7 @@ async function onImportFile(event: Event) { transferOpen.value = true; await previewTransfer(); } catch (error) { - toast(error instanceof Error ? error.message : String(error), 4000); + toast(formatError(error), 4000); } } @@ -825,7 +826,7 @@ async function loadSyncPreview() { transferBundle.value = bundleFromSummaries(scan.entries, prefix, scan.revision); await previewTransfer(); } catch (error) { - transferError.value = error instanceof Error ? error.message : String(error); + transferError.value = formatError(error); } finally { transferLoading.value = false; transferLoadingDetail.value = ""; @@ -870,7 +871,7 @@ async function previewTransfer() { if (transferMode.value === "sync") syncConfigurationExpanded.value = false; } catch (error) { if (generation !== transferPreviewGeneration) return; - transferError.value = error instanceof Error ? error.message : String(error); + transferError.value = formatError(error); transferPreviewLoaded.value = false; } finally { if (generation === transferPreviewGeneration) { @@ -915,7 +916,7 @@ async function applyTransfer() { transferOpen.value = false; if (targetId === props.connectionId) browserRef.value?.refresh(); } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = formatError(error); // Rebuild the preview so an ambiguous network failure or a successful // prefix of the batch is reflected before the user retries. let previewError = ""; @@ -1032,7 +1033,7 @@ async function runSearch() { return { id: `${identity}:${summary.modRevision || ""}`, displayKey: shown, keyIdentity: identity, summary: { ...summary, key: shown, keyBytes: bytes, keyIdentity: identity }, matchesKey, matchesValue, selected: true }; }); } catch (error) { - searchError.value = error instanceof Error ? error.message : String(error); + searchError.value = formatError(error); } finally { searchRunning.value = false; } @@ -1063,7 +1064,7 @@ async function openOperations(nextMode: Extract { + it("keeps the internal Agent payload out of a compacted-revision history error", () => { + const expected = "ETCD_COMPACTED: requested history was compacted at revision 6538169"; + + expect(sanitizeBackendErrorMessage(COMPACTED_HISTORY_ERROR)).toBe(expected); + expect(formatError(new Error(COMPACTED_HISTORY_ERROR))).toBe(expected); + }); + + it("leaves an error without the internal marker untouched", () => { + const message = "etcdserver: permission denied"; + + expect(formatError(new Error(message))).toBe(message); + expect(formatError(message)).toBe(message); + }); +}); diff --git a/apps/desktop/src/components/kv/__tests__/KvKeyBrowserSearchAndSplit.spec.ts b/apps/desktop/src/components/kv/__tests__/KvKeyBrowserSearchAndSplit.spec.ts index f93c18a79e..ebf119e2f7 100644 --- a/apps/desktop/src/components/kv/__tests__/KvKeyBrowserSearchAndSplit.spec.ts +++ b/apps/desktop/src/components/kv/__tests__/KvKeyBrowserSearchAndSplit.spec.ts @@ -42,7 +42,7 @@ describe("KvKeyBrowser search and split layout", () => { it("shows list request errors instead of the empty-state message", () => { expect(browserSource).toContain('const listError = ref("");'); - expect(browserSource.match(/listError\.value = error instanceof Error \? error\.message : String\(error\);/g)).toHaveLength(2); + expect(browserSource.match(/listError\.value = formatError\(error\);/g)).toHaveLength(2); expect(browserSource.indexOf('v-else-if="listError"')).toBeLessThan(browserSource.indexOf('v-else-if="visibleRows.length === 0"')); });