feat(errors): classify the install 'no store entry' failure
Some checks are pending
Security / Security check (push) Waiting to run

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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-22 13:48:31 +02:00
parent 08227410e6
commit e28fffee61
7 changed files with 87 additions and 2 deletions

View file

@ -204,6 +204,20 @@ FriendlyError? _matchHubPattern(String detail, AppLocalizations l) {
hint: l.errMcpUnreachableHint, 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 // Capability not in registry hub returns NotFound with this
// shape from the flow engine. // shape from the flow engine.
if (detail.toLowerCase().contains('no capability provider') || if (detail.toLowerCase().contains('no capability provider') ||

View file

@ -69,6 +69,8 @@
"errMcpUnreachableHint": "Der MCP-Server hat nicht geantwortet. Endpunkt in Einstellungen → Integrationen prüfen und sicherstellen, dass er läuft.", "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.", "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.", "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 <pfad>), 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", "welcomeDocClose": "Schließen",
"welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}", "welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}",
"@welcomeDocFailedToLoad": { "@welcomeDocFailedToLoad": {

View file

@ -77,6 +77,8 @@
"errMcpUnreachableHint": "The MCP server didn't respond. Check the endpoint in Settings → Integrations and confirm it's running.", "errMcpUnreachableHint": "The MCP server didn't respond. Check the endpoint in Settings → Integrations and confirm it's running.",
"errCapabilityNotInstalled": "Required capability is not installed.", "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.", "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 <path>), 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", "welcomeDocClose": "Close",
"welcomeDocFailedToLoad": "Could not load documentation: {error}", "welcomeDocFailedToLoad": "Could not load documentation: {error}",
"@welcomeDocFailedToLoad": { "@welcomeDocFailedToLoad": {

View file

@ -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.'** /// **'Open the flow\'s Text tab — the analyzer\'s Fix button installs the capability if it\'s in the store.'**
String get errCapabilityNotInstalledHint; 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 <path>), 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. /// No description provided for @welcomeDocClose.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -248,6 +248,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get errCapabilityNotInstalledHint => String get errCapabilityNotInstalledHint =>
'Im Text-Tab des Flows klicken Sie auf den Fix-Button — der installiert die Capability, sofern sie im Store ist.'; '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 <pfad>), 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 @override
String get welcomeDocClose => 'Schließen'; String get welcomeDocClose => 'Schließen';

View file

@ -248,6 +248,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get errCapabilityNotInstalledHint => String get errCapabilityNotInstalledHint =>
'Open the flow\'s Text tab — the analyzer\'s Fix button installs the capability if it\'s in the store.'; '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 <path>), 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 @override
String get welcomeDocClose => 'Close'; String get welcomeDocClose => 'Close';

View file

@ -40,12 +40,12 @@ void main() {
final l = await _loadL10n(const Locale('en')); final l = await _loadL10n(const Locale('en'));
final r = friendlyError( final r = friendlyError(
_FakeGrpcError(9, 'FAILED_PRECONDITION', _FakeGrpcError(9, 'FAILED_PRECONDITION',
'install error: no store entry for system.approval'), 'install error: module manifest is invalid'),
l, l,
); );
expect(r.headline, l.errFailedPrecondition); expect(r.headline, l.errFailedPrecondition);
expect(r.hint, l.errFailedPreconditionHint); expect(r.hint, l.errFailedPreconditionHint);
expect(r.detail, contains('system.approval')); expect(r.detail, contains('manifest'));
}); });
test('NOT_FOUND has a recovery hint', () async { test('NOT_FOUND has a recovery hint', () async {
@ -151,5 +151,45 @@ void main() {
); );
expect(r.headline, l.errMissingValue); 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 <term>` 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 <term>`",
),
l,
);
expect(r.headline, l.errNoStoreEntry);
expect(r.hint, contains('chain install --link'));
expect(r.hint, contains('Anbindung'));
});
} }