diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f32ff..4c64871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ lockstep. ## Unreleased +### Fixed — honest install badge in the flow editor (0.78.0) + +- **Install is only offered when the store can deliver.** The + capability set behind the flow editor's Install quick-fix and the + flow list's install badge previously ingested every store entry's + `requiresCapabilities` (dependencies, not provided capabilities) + and ignored entry status/kind — so clicking Install on such a + capability ended in the hub's "no store entry for ''" error. + The set now mirrors the hub's install resolver: entry names only, + no `planned` entries, no federated entries + (`lib/data/store_caps.dart`). Capabilities the store cannot + resolve render the editor's "not in store" state, which explains + the three recovery paths (local install, add store, configure + integration) before any click. +- Flow editor pinned at 0.25.0 (store-resolvability split in the + flow list, three-path analyzer message). + ### Added — real trust + exposure data (0.77.0) - **Per-entry install verification.** The hub now reports (since diff --git a/integration_test/dialog_shots_test.dart b/integration_test/dialog_shots_test.dart index a7c2659..25646f8 100644 --- a/integration_test/dialog_shots_test.dart +++ b/integration_test/dialog_shots_test.dart @@ -16,6 +16,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:chain_studio_flow_editor/src/l10n.dart'; +import 'package:chain_studio_flow_editor/src/widgets/missing_modules_badge.dart'; + import 'package:chain_studio/data/hub.dart' show StoreItem; import 'package:chain_studio/l10n/app_localizations.dart'; import 'package:chain_studio/pages/federation.dart'; @@ -104,6 +107,65 @@ void main() { }); } + // Flow-list resolvability badge, both classified states side by + // side: store-resolvable missing capability (amber chip + + // Install link) vs. capability no store can install (quiet + // "nicht im Store" chip; recovery paths live in its tooltip). + for (final (themeName, mode) in [ + ('light', ThemeMode.light), + ('dark', ThemeMode.dark), + ]) { + testWidgets('flow-list badge states — $themeName', (tester) async { + SharedPreferences.setMockInitialValues({}); + await tester.pumpWidget( + MaterialApp( + debugShowCheckedModeBanner: false, + themeMode: mode, + theme: ThemeData.light(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('de'), + builder: (context, child) => + RepaintBoundary(key: _shotKey, child: child), + home: Scaffold( + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 260), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MissingModulesBadge( + installable: const ['text.extract'], + notInStore: const [], + strings: + const FlowEditorStrings(FlowEditorLocale.de), + onInstall: () {}, + ), + const SizedBox(height: 16), + MissingModulesBadge( + installable: const [], + notInStore: const ['acme.internal/lookup'], + strings: + const FlowEditorStrings(FlowEditorLocale.de), + onInstall: null, + ), + ], + ), + ), + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 200)); + await _shot(tester, 'flow-badge-states-$themeName'); + expect(find.text('1 Modul fehlt'), findsOneWidget); + expect(find.text('Installieren'), findsOneWidget); + expect(find.text('nicht im Store'), findsOneWidget); + }); + } + // Trust gate with the hub-reported verification statement — the // pinned-key (good) and policy-off (warning) variants. for (final (name, verification) in [ diff --git a/lib/data/about_info.dart b/lib/data/about_info.dart index 1371790..b241df1 100644 --- a/lib/data/about_info.dart +++ b/lib/data/about_info.dart @@ -4,7 +4,7 @@ /// Studio's own build version. Bump on every UI release so the /// running app self-identifies. -const String kStudioVersion = '0.77.0'; +const String kStudioVersion = '0.78.0'; const String kProductName = 'Ch∆In Studio'; const String kVendorName = 'Flemming.AI (F∆I)'; diff --git a/lib/data/store_caps.dart b/lib/data/store_caps.dart new file mode 100644 index 0000000..7d50d51 --- /dev/null +++ b/lib/data/store_caps.dart @@ -0,0 +1,30 @@ +// Store-resolvability: which capabilities can the store actually +// install? This is the data source behind the flow editor's +// "Install" quick-fix and the flow list's install badge — it must +// mirror the hub's install resolver (exact entry name, an +// installable version, native bundle), or the UI offers installs +// that end in "no store entry for ''". + +import 'hub.dart'; + +/// Bare capability names a store entry can install right now. +/// +/// Mirrors the hub's `resolve_install_plan` acceptance rules: +/// - only the entry's own `name` counts — `requiresCapabilities` +/// are the entry's DEPENDENCIES, not things it provides, and +/// must never be offered as installable; +/// - `planned` entries have no downloadable version yet; +/// - `federated` entries route through a bridge (MCP/n8n), not +/// the bundle-install path. +/// An empty `status` (older hub without the field) keeps the +/// entry installable — the classified install error is the net +/// underneath that skew. +Set installableStoreCapabilities(Iterable items) { + final caps = {}; + for (final item in items) { + if (item.isFederated) continue; + if (item.status == 'planned') continue; + caps.add(item.name); + } + return caps; +} diff --git a/lib/pages/flows.dart b/lib/pages/flows.dart index b5b8b55..01e78d4 100644 --- a/lib/pages/flows.dart +++ b/lib/pages/flows.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; import '../data/flow_run_driver.dart'; import '../data/hub.dart'; +import '../data/store_caps.dart'; import '../data/workspace.dart'; import '../l10n/app_localizations.dart'; import '../widgets/chain_install_confirm.dart'; @@ -55,18 +56,14 @@ class _FlowsPageState extends State { try { final items = await HubService.instance.searchStore(limit: 500); if (!mounted) return; - // Store items carry a list of provided capabilities. We - // ingest all of them flat so a single store entry - // shipping multiple caps still reports each as installable. - // Falls back to the item name for legacy entries. - final caps = {}; - for (final item in items) { - if (item.requiresCapabilities.isNotEmpty) { - caps.addAll(item.requiresCapabilities); - } - caps.add(item.name); - } - setState(() => _storeCaps = caps.toList()..sort()); + // Only capabilities the store can actually install — the + // editor's Install quick-fix and list badge promise exactly + // what the hub's install resolver accepts. (This set used to + // ingest requiresCapabilities too, which offered installs + // that ended in "no store entry for ''".) + setState( + () => _storeCaps = installableStoreCapabilities(items).toList()..sort(), + ); } catch (_) { // Soft-fail: empty list disables the Install button on // unknown-cap fixes, but Add-source remains available. diff --git a/pubspec.lock b/pubspec.lock index b451d3f..8235b00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -46,7 +46,7 @@ packages: path: "../fai_chain_studio_flow_editor" relative: true source: path - version: "0.24.1" + version: "0.25.0" characters: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7dcbc3d..8b494a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: chain_studio description: "Ch∆In Studio — desktop GUI for the Ch∆In hub" publish_to: 'none' -version: 0.77.0 +version: 0.78.0 environment: sdk: ^3.11.0-200.1.beta @@ -45,7 +45,9 @@ dependencies: # Tag-pinned so a CI (override-free) build is reproducible and an # editor push can't retroactively change what a Studio release # builds against. Bump this in lockstep with the editor tag. - ref: v0.24.1 + # Commit-pinned to editor 0.25.0 until its v0.25.0 tag exists + # (equally reproducible); switch back to the tag then. + ref: ab97e5e dev_dependencies: flutter_test: diff --git a/test/store_caps_test.dart b/test/store_caps_test.dart new file mode 100644 index 0000000..d879ace --- /dev/null +++ b/test/store_caps_test.dart @@ -0,0 +1,79 @@ +import 'package:chain_studio/data/hub.dart'; +import 'package:chain_studio/data/store_caps.dart'; +import 'package:flutter_test/flutter_test.dart'; + +StoreItem _item( + String name, { + String status = 'published', + String kind = 'native', + List requires = const [], +}) { + return StoreItem( + name: name, + taglineEn: '', + taglineDe: '', + descriptionEn: '', + descriptionDe: '', + category: 'data', + tags: const [], + requiresCapabilities: requires, + requiresServices: const [], + license: 'Apache-2.0', + repository: '', + bestVersion: '1.0.0', + status: status, + installed: false, + featured: false, + iconUrl: '', + screenshotUrls: const [], + docsUrl: '', + kind: kind, + provider: '', + ); +} + +void main() { + group('installableStoreCapabilities', () { + test('published and alpha entries are installable by name', () { + final caps = installableStoreCapabilities([ + _item('text.extract'), + _item('text.anonymize', status: 'alpha'), + ]); + expect(caps, {'text.extract', 'text.anonymize'}); + }); + + test('required capabilities of an entry are NOT installable', () { + // The repro class behind "no store entry for '/'": + // a capability that only appears as some entry's requirement + // must never be offered as an install target. + final caps = installableStoreCapabilities([ + _item( + 'doc.pipeline', + requires: ['example-provider/tool.summarize', 'llm.generate'], + ), + ]); + expect(caps, {'doc.pipeline'}); + expect(caps.contains('example-provider/tool.summarize'), isFalse); + expect(caps.contains('llm.generate'), isFalse); + }); + + test('planned entries have nothing to download and are excluded', () { + final caps = installableStoreCapabilities([ + _item('web.scrape', status: 'planned'), + ]); + expect(caps, isEmpty); + }); + + test('federated entries do not use the bundle-install path', () { + final caps = installableStoreCapabilities([ + _item('mcp.files.read', kind: 'federated'), + ]); + expect(caps, isEmpty); + }); + + test('empty status (older hub) keeps the entry installable', () { + final caps = installableStoreCapabilities([_item('debug.echo', status: '')]); + expect(caps, {'debug.echo'}); + }); + }); +}