fix(flows): only offer install for capabilities the store resolves
Some checks are pending
Security / Security check (push) Waiting to run

The capability set behind the flow editor's Install quick-fix and
the flow list's install badge ingested every store entry's
requiresCapabilities (dependencies, not provided capabilities) and
ignored entry status/kind. Clicking Install on such a capability
ended in the hub's "no store entry for '<name>'" error.

The set now mirrors the hub's install resolver (entry names only,
no planned or federated entries) via installableStoreCapabilities()
with unit tests for both classification states. Unresolvable
capabilities render the editor's 'not in store' state, which
explains the three recovery paths before any click; the editor pin
moves to 0.25.0 (commit-pinned until its tag exists) and the dialog
harness captures the badge states light+dark. Studio 0.78.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-22 13:46:08 +02:00
parent bef2dbe988
commit 08227410e6
8 changed files with 203 additions and 16 deletions

79
test/store_caps_test.dart Normal file
View file

@ -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<String> 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 '<provider>/<name>'":
// 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'});
});
});
}