From e28fffee6140942ec5ec1a38a43dd90f5ac1f483 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Wed, 22 Jul 2026 13:48:31 +0200 Subject: [PATCH] feat(errors): classify the install 'no store entry' failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even with the honest install badge the hub error stays reachable (stale store snapshot, race with a store refresh, older hub). The friendly-error mapper now gives it its own headline plus a hint naming the three acquisition paths — local module install, adding the providing store, configuring the MCP/n8n integration — in EN and DE, with the verbatim hub message kept copyable. Matcher unit tests EN+DE guard the classification. Signed-off-by: flemming-it --- lib/data/friendly_error.dart | 14 ++++++++++ lib/l10n/app_de.arb | 2 ++ lib/l10n/app_en.arb | 2 ++ lib/l10n/app_localizations.dart | 12 ++++++++ lib/l10n/app_localizations_de.dart | 8 ++++++ lib/l10n/app_localizations_en.dart | 7 +++++ test/friendly_error_test.dart | 44 ++++++++++++++++++++++++++++-- 7 files changed, 87 insertions(+), 2 deletions(-) diff --git a/lib/data/friendly_error.dart b/lib/data/friendly_error.dart index a375b2a..57e9eed 100644 --- a/lib/data/friendly_error.dart +++ b/lib/data/friendly_error.dart @@ -204,6 +204,20 @@ FriendlyError? _matchHubPattern(String detail, AppLocalizations l) { hint: l.errMcpUnreachableHint, ); } + // Install target unresolvable — the hub's install resolver found + // no store entry with that exact name ("no store entry for 'X' — + // hint"). Distinct from capability-not-installed below: the store + // cannot deliver it at all, so the recovery is one of the three + // acquisition paths, not the Fix button. Reachable despite the + // honest badge (stale store snapshot, race with a store refresh, + // older hub). + if (detail.contains('no store entry for')) { + return FriendlyError( + headline: l.errNoStoreEntry, + detail: detail, + hint: l.errNoStoreEntryHint, + ); + } // Capability not in registry — hub returns NotFound with this // shape from the flow engine. if (detail.toLowerCase().contains('no capability provider') || diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8543a2f..f976bfc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -69,6 +69,8 @@ "errMcpUnreachableHint": "Der MCP-Server hat nicht geantwortet. Endpunkt in Einstellungen → Integrationen prüfen und sicherstellen, dass er läuft.", "errCapabilityNotInstalled": "Benötigte Capability nicht installiert.", "errCapabilityNotInstalledHint": "Im Text-Tab des Flows klicken Sie auf den Fix-Button — der installiert die Capability, sofern sie im Store ist.", + "errNoStoreEntry": "Kein Store-Eintrag bietet diese Capability an.", + "errNoStoreEntryHint": "Drei Wege: lokales Modul installieren (chain install --link ), den passenden Store hinzufügen (Einstellungen → Stores) oder die Anbindung (MCP/n8n) einrichten, die sie bereitstellt. Falls gerade erst veröffentlicht: zuerst die Store-Seite aktualisieren.", "welcomeDocClose": "Schließen", "welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}", "@welcomeDocFailedToLoad": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d0f72b2..13864d3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -77,6 +77,8 @@ "errMcpUnreachableHint": "The MCP server didn't respond. Check the endpoint in Settings → Integrations and confirm it's running.", "errCapabilityNotInstalled": "Required capability is not installed.", "errCapabilityNotInstalledHint": "Open the flow's Text tab — the analyzer's Fix button installs the capability if it's in the store.", + "errNoStoreEntry": "No store entry provides this capability.", + "errNoStoreEntryHint": "Three ways to get it: install a local module (chain install --link ), add the store that provides it (Settings → Stores), or configure the integration (MCP/n8n) that supplies it. If it was just published, refresh the Store page first.", "welcomeDocClose": "Close", "welcomeDocFailedToLoad": "Could not load documentation: {error}", "@welcomeDocFailedToLoad": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0e1e9e5..9adc9af 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -512,6 +512,18 @@ abstract class AppLocalizations { /// **'Open the flow\'s Text tab — the analyzer\'s Fix button installs the capability if it\'s in the store.'** String get errCapabilityNotInstalledHint; + /// No description provided for @errNoStoreEntry. + /// + /// In en, this message translates to: + /// **'No store entry provides this capability.'** + String get errNoStoreEntry; + + /// No description provided for @errNoStoreEntryHint. + /// + /// In en, this message translates to: + /// **'Three ways to get it: install a local module (chain install --link ), add the store that provides it (Settings → Stores), or configure the integration (MCP/n8n) that supplies it. If it was just published, refresh the Store page first.'** + String get errNoStoreEntryHint; + /// No description provided for @welcomeDocClose. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e76d4c2..2246e2a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -248,6 +248,14 @@ class AppLocalizationsDe extends AppLocalizations { String get errCapabilityNotInstalledHint => 'Im Text-Tab des Flows klicken Sie auf den Fix-Button — der installiert die Capability, sofern sie im Store ist.'; + @override + String get errNoStoreEntry => + 'Kein Store-Eintrag bietet diese Capability an.'; + + @override + String get errNoStoreEntryHint => + 'Drei Wege: lokales Modul installieren (chain install --link ), den passenden Store hinzufügen (Einstellungen → Stores) oder die Anbindung (MCP/n8n) einrichten, die sie bereitstellt. Falls gerade erst veröffentlicht: zuerst die Store-Seite aktualisieren.'; + @override String get welcomeDocClose => 'Schließen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b9c4777..f2edce7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -248,6 +248,13 @@ class AppLocalizationsEn extends AppLocalizations { String get errCapabilityNotInstalledHint => 'Open the flow\'s Text tab — the analyzer\'s Fix button installs the capability if it\'s in the store.'; + @override + String get errNoStoreEntry => 'No store entry provides this capability.'; + + @override + String get errNoStoreEntryHint => + 'Three ways to get it: install a local module (chain install --link ), add the store that provides it (Settings → Stores), or configure the integration (MCP/n8n) that supplies it. If it was just published, refresh the Store page first.'; + @override String get welcomeDocClose => 'Close'; diff --git a/test/friendly_error_test.dart b/test/friendly_error_test.dart index 2577e79..695548b 100644 --- a/test/friendly_error_test.dart +++ b/test/friendly_error_test.dart @@ -40,12 +40,12 @@ void main() { final l = await _loadL10n(const Locale('en')); final r = friendlyError( _FakeGrpcError(9, 'FAILED_PRECONDITION', - 'install error: no store entry for system.approval'), + 'install error: module manifest is invalid'), l, ); expect(r.headline, l.errFailedPrecondition); expect(r.hint, l.errFailedPreconditionHint); - expect(r.detail, contains('system.approval')); + expect(r.detail, contains('manifest')); }); test('NOT_FOUND has a recovery hint', () async { @@ -151,5 +151,45 @@ void main() { ); expect(r.headline, l.errMissingValue); }); + + test('no-store-entry maps to the three-path recovery hint', () async { + // The flow quick-fix dead end: even with the honest badge this + // error stays reachable (stale store snapshot, refresh race, + // older hub) — it must never render as the generic + // FAILED_PRECONDITION copy again. + final l = await _loadL10n(const Locale('en')); + final r = friendlyError( + _FakeGrpcError( + 9, + 'FAILED_PRECONDITION', + "install error: no store entry for 'example-provider/tool.summarize'" + ' — run `chain store search ` to see what\'s available', + ), + l, + ); + expect(r.headline, l.errNoStoreEntry); + expect(r.hint, l.errNoStoreEntryHint); + // The hint names all three acquisition paths. + expect(r.hint, contains('chain install --link')); + expect(r.hint, contains('Stores')); + expect(r.hint, contains('integration')); + // The verbatim hub message stays copyable. + expect(r.detail, contains("example-provider/tool.summarize")); + }); + + test('no-store-entry is classified in German too', () async { + final l = await _loadL10n(const Locale('de')); + final r = friendlyError( + _FakeGrpcError( + 9, + 'FAILED_PRECONDITION', + "no store entry for 'x.y' — run `chain store search `", + ), + l, + ); + expect(r.headline, l.errNoStoreEntry); + expect(r.hint, contains('chain install --link')); + expect(r.hint, contains('Anbindung')); + }); }