diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8ccadea..8ce00f7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -108,10 +108,15 @@ }, { "name": "flutter-patrol", - "source": "./plugins/flutter-patrol", - "description": "LeanCode Patrol E2E testing guidance (Modules/System/ApiClients, keys conventions) plus Patrol MCP setup for AI-assisted test development.", + "source": { + "source": "github", + "repo": "leancodepl/patrol" + }, + "strict": false, + "description": "Patrol E2E testing skills (write-test workflow and Modules/System/ApiClients architecture with keys conventions), sourced directly from the Patrol repository.", "skills": [ - "./skills/flutter-patrol-usage" + "./skills/patrol-write-test", + "./skills/patrol-test-architecture" ] }, { diff --git a/AGENTS.md b/AGENTS.md index bedb1b3..714833b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,23 @@ LeanCode's shared AI plugins for Claude Code, focused on Flutter development. Claude Code also documents support for per-plugin `agents/`, `commands/`, and `hooks/` directories. No plugin in this repo currently uses them — treat them as not-yet-exercised. +## Externally sourced plugins + +A plugin whose content is owned by another repository is registered in `.claude-plugin/marketplace.json` with an object source and `strict: false`: + +```json +{ + "name": "flutter-patrol", + "source": { "source": "github", "repo": "leancodepl/patrol" }, + "strict": false, + "skills": ["./skills/patrol-write-test", "./skills/patrol-test-architecture"] +} +``` + +The external repo is the single source of truth — there is no `plugins//` directory and no copy of its content here; do not vendor one. The validator skips external entries (no local directory to check); the `claude plugin validate` CI step covers the entry's schema. + +Currently external: `flutter-patrol` (from [leancodepl/patrol](https://github.com/leancodepl/patrol), which owns all Patrol AI support). + ## Working conventions - Keep plugins self-contained and independently installable. diff --git a/README.md b/README.md index 9733f55..87af138 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ To have Claude Code prompt collaborators to install the marketplace automaticall Most plugins are pure rules and skills with no setup. A few need one-time tooling or MCP setup — finish it from the plugin's `README.md`: -- [`flutter-patrol`](plugins/flutter-patrol/) - Patrol CLI and Patrol MCP +- [`flutter-patrol`](https://github.com/leancodepl/patrol) - Patrol CLI and Patrol MCP - [`flutter-marionette`](plugins/flutter-marionette/) - Marionette MCP and app-side binding ## Available plugins @@ -80,7 +80,7 @@ Most plugins are pure rules and skills with no setup. A few need one-time toolin ### UI and verification - [`flutter-ui`](plugins/flutter-ui/) - design-system-driven UI, loading/error patterns, localized presentation text, and UI implementation checklists -- [`flutter-patrol`](plugins/flutter-patrol/) - Patrol test architecture, key conventions, and Patrol MCP workflow for AI-assisted E2E work +- [`flutter-patrol`](https://github.com/leancodepl/patrol) - Patrol E2E test skills (write-test workflow, test architecture, key conventions, Patrol MCP), sourced directly from the Patrol repository's `skills/` — the single source of truth for Patrol AI support - [`flutter-marionette`](plugins/flutter-marionette/) - runtime interaction with a live debug app through Marionette MCP for exploration, smoke checks, and UI debugging - [`flutter-read-logs`](plugins/flutter-read-logs/) - read the running app's latest `flutter run` logs as on-demand context via `/read-logs` diff --git a/internal/pluginvalidation/marketplace.go b/internal/pluginvalidation/marketplace.go index db57055..1cf8a83 100644 --- a/internal/pluginvalidation/marketplace.go +++ b/internal/pluginvalidation/marketplace.go @@ -65,14 +65,17 @@ func (v *validator) validateMarketplaceEntries(where string, entries []marketpla seen[entry.Name] = i } } - v.requireNonEmpty(where, fmt.Sprintf("plugins[%d].source", i), entry.Source) + if !entry.External { + v.requireNonEmpty(where, fmt.Sprintf("plugins[%d].source", i), entry.Source) + } } } func (v *validator) registeredPlugins(entries []marketplacePlugin) map[string]struct{} { registered := map[string]struct{}{} for _, entry := range entries { - if entry.Name != "" { + // External plugins live in another repository: nothing local to validate. + if entry.Name != "" && !entry.External { registered[entry.Name] = struct{}{} } } diff --git a/internal/pluginvalidation/types.go b/internal/pluginvalidation/types.go index 3a12095..fa0b19b 100644 --- a/internal/pluginvalidation/types.go +++ b/internal/pluginvalidation/types.go @@ -1,14 +1,41 @@ package pluginvalidation +import "encoding/json" + type marketplace struct { Metadata map[string]any `json:"metadata"` Plugins []marketplacePlugin `json:"plugins"` } type marketplacePlugin struct { - Name string `json:"name"` - Source string `json:"source"` - Description string `json:"description"` + Name string + Source string + External bool + Description string +} + +// UnmarshalJSON accepts both source forms: a local path string and an object +// pointing at an external repository ({"source": "github", ...}). External +// plugins have no local directory, so the validator skips them. +func (p *marketplacePlugin) UnmarshalJSON(data []byte) error { + var raw struct { + Name string `json:"name"` + Source json.RawMessage `json:"source"` + Description string `json:"description"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + p.Name = raw.Name + p.Description = raw.Description + if len(raw.Source) == 0 { + return nil + } + if raw.Source[0] == '{' { + p.External = true + return nil + } + return json.Unmarshal(raw.Source, &p.Source) } type pluginManifest struct { diff --git a/plugins/flutter-marionette/README.md b/plugins/flutter-marionette/README.md index b6d88d4..359244e 100644 --- a/plugins/flutter-marionette/README.md +++ b/plugins/flutter-marionette/README.md @@ -11,7 +11,7 @@ LeanCode Flutter plugin for [Marionette MCP](https://github.com/leancodepl/mario ## Marionette vs Patrol -| | Marionette (this plugin) | Patrol ([`flutter-patrol`](../flutter-patrol/)) | +| | Marionette (this plugin) | Patrol ([`flutter-patrol`](https://github.com/leancodepl/patrol)) | |---|---|---| | Purpose | Runtime exploration, smoke verification | Deterministic E2E test suites | | Runs against | Live `flutter run` debug session | `patrol develop` / `patrol test` | @@ -124,5 +124,5 @@ Marionette relies on Flutter's VM Service and is intended for a live `flutter ru ## Related plugins -- [`flutter-patrol`](../flutter-patrol/) — deterministic E2E testing with Patrol MCP +- [`flutter-patrol`](https://github.com/leancodepl/patrol) — deterministic E2E testing with Patrol MCP - [`flutter-ui`](../flutter-ui/) — design-system guidance (relevant when configuring `isInteractiveWidget` for custom widgets) diff --git a/plugins/flutter-patrol/.claude-plugin/plugin.json b/plugins/flutter-patrol/.claude-plugin/plugin.json deleted file mode 100644 index ffea51b..0000000 --- a/plugins/flutter-patrol/.claude-plugin/plugin.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", - "name": "flutter-patrol", - "displayName": "Flutter Patrol", - "description": "LeanCode Patrol E2E testing guidance (Modules/System/ApiClients, keys conventions) plus Patrol MCP setup for AI-assisted test development.", - "version": "0.1.0", - "author": { - "name": "LeanCode" - }, - "homepage": "https://github.com/leancodepl/ai-plugins", - "repository": "https://github.com/leancodepl/ai-plugins", - "keywords": [ - "flutter", - "patrol", - "e2e-testing", - "automation", - "mcp" - ], - "skills": "./skills/" -} diff --git a/plugins/flutter-patrol/README.md b/plugins/flutter-patrol/README.md deleted file mode 100644 index cea5166..0000000 --- a/plugins/flutter-patrol/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# flutter-patrol - -LeanCode Flutter plugin for Patrol E2E testing. - -Contains the Patrol rules and workflow assets: - -- `skills/flutter-patrol-usage/references/patrol-tests.md` - Patrol test structure, API rules, native dialogs, and MCP workflow -- `skills/flutter-patrol-usage/references/patrol-keys.md` - key placement and naming conventions -- `skills/flutter-patrol-usage/SKILL.md` - entry point for this plugin - -## Example usage - -- `/flutter-patrol-usage` - get a short explanation of what this plugin does and which asset to use next - -## What this plugin is NOT about - -General cubit/bloc logic lives in [`flutter-bloc`](../flutter-bloc/). Route structure lives in [`flutter-navigation`](../flutter-navigation/). - -## Related plugins - -- [`flutter-bloc`](../flutter-bloc/) - cubit-driven flows exercised by Patrol tests -- [`flutter-navigation`](../flutter-navigation/) - navigation flows exercised by Patrol tests diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md deleted file mode 100644 index fc65ee4..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: flutter-patrol-usage -description: Explain what the `flutter-patrol` plugin does and how to use it. Use when the user invokes `/flutter-patrol-usage`, asks what this plugin covers, or needs help with Patrol E2E tests, test keys, native automation, or Patrol MCP. ---- - -# Patrol Usage - -## How to respond - -- If the user invoked this skill without a concrete task, start by explaining what this plugin does and when Patrol is the right tool. -- Show how to use it through concrete testing tasks this plugin can handle. -- Point to the relevant rule for tests, keys, or Patrol MCP usage. -- If the user already gave a concrete Patrol task, briefly explain why this plugin fits and then do the work. -- Do not reply with filler like "skill loaded", "ready for the task", or "what would you like to do?" before explaining the plugin. - -## What this plugin does - -- Covers LeanCode Patrol conventions for E2E UI tests. -- Guides Patrol test structure, assertions, key naming, custom finders, and native automation boundaries. -- Helps with Patrol MCP usage for agent-driven test runs and debugging. - -## How to use it - -- Ask to create or refactor a Patrol E2E test. -- Ask to add or review test keys for a feature flow. -- Ask how to handle native dialogs, permissions, notifications, or other platform UI. -- Ask how to run or debug a test through Patrol MCP. - -## Example requests - -- "Create a Patrol test for the login flow." -- "Review this feature for missing Patrol keys." -- "Help me run this test through Patrol MCP." - -## Reach for these assets - -- `references/patrol-tests.md` - test structure and Patrol API conventions. -- `references/patrol-keys.md` - key naming and composition rules. -- Related plugin: `flutter-bloc` - state-management conventions for cubit-driven flows under test. -- Related plugin: `flutter-navigation` - route structure and navigation flows exercised by Patrol tests. diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md deleted file mode 100644 index ed641b7..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md +++ /dev/null @@ -1,221 +0,0 @@ -# Patrol Keys - -## Core rules - -- Assign a key ONLY to widgets involved in testing. -- ONLY add the `key` parameter to existing widgets — never modify widget signatures or refactor existing code structure. -- Never hardcode keys in the app — always use keys from the `keys.dart` file. -- Never create a key that is not assigned to a widget. -- Always make sure each key value is unique. -- Sort keys alphabetically. -- Add keys as the **first parameter** to the widget constructor. - -## File structure - -- Create `keys.dart` in the feature/widget directory — NOT in the main `lib/` directory. -- Maximum one `keys.dart` per feature directory (the file can contain multiple classes). -- The main `Keys` class in `lib/keys.dart` should only import and aggregate feature-specific keys. -- Create new `keys.dart` files as needed. Add imports for them in the main aggregator. - -Example layout: - -``` -lib/features/home/keys.dart -lib/features/profile/keys.dart -lib/features/auth/keys.dart -lib/common/widgets/keys.dart -lib/keys.dart ← aggregator only -``` - -## Key usage pattern - -Always assign keys using this exact pattern: - -```dart -key: keys.feature.widgetName -``` - -(except when using parameterized keys — see below) - -## Grouping - -Group related keys in a class named after the screen (e.g. `HomeKeys`). Use a private `ValueKey` subclass to prefix all key values with the page or widget name. - -```dart -// lib/features/home/keys.dart -import 'package:flutter/widgets.dart'; - -class _HomeKey extends ValueKey { - const _HomeKey(String value) : super('home_$value'); -} - -class HomeKeys { - final menuIconButton = const _HomeKey('menuIconButton'); - _HomeKey navbarItem(String label) => _HomeKey('navbarItem_$label'); -} -``` - -## Main aggregator - -```dart -// lib/keys.dart -import 'features/home/keys.dart'; -import 'features/profile/keys.dart'; -import 'common/widgets/keys.dart'; - -final keys = Keys(); - -class Keys { - final home = HomeKeys(); - final profile = ProfileKeys(); - final widgets = WidgetKeys(); -} -``` - -## Grouping multiple screens per feature - -```dart -// lib/features/product/keys.dart -import 'package:flutter/widgets.dart'; - -class _ProductPageKey extends ValueKey { - const _ProductPageKey(String value) : super('productPage_$value'); -} - -class ProductPageKeys { - final menuIconButton = _ProductPageKey('menuIconButton'); - final productImage = _ProductPageKey('productImage'); - final productName = _ProductPageKey('productName'); -} - -class _ProductConnectingPageKey extends ValueKey { - const _ProductConnectingPageKey(String value) - : super('productConnectingPage_$value'); -} - -class ProductConnectingPageKeys { - final productName = _ProductConnectingPageKey('productName'); - final productImage = _ProductConnectingPageKey('productImage'); -} -``` - -## Common widgets - -For common widgets used across features, store keys in `WidgetKeys` placed in the directory of the common widget: - -```dart -// lib/common/widgets/keys.dart -import 'package:flutter/widgets.dart'; - -class _WidgetKey extends ValueKey { - const _WidgetKey(String value) : super('widget_$value'); -} - -class WidgetKeys { - final addButton = const _WidgetKey('addButton'); - final searchBar = const _WidgetKey('searchBar'); - final saveButton = const _WidgetKey('saveButton'); - final cancelButton = const _WidgetKey('cancelButton'); - final topBarHeaderMiddleText = const _WidgetKey('topBarHeaderMiddleText'); - final pickCurrencyButton = const _WidgetKey('pickCurrencyButton'); - _WidgetKey assetSlider(SelectAssetType type) => - _WidgetKey('assetSlider_$type'); - ValueKey assetRow(String coinTitle) => - _WidgetKey('assetRow_${coinTitle}'); -} -``` - -## External packages - -For widgets in a separate package (e.g. widgetbook), create a `keys.dart` in that package: - -```dart -// widgetbook/keys.dart -import 'package:flutter/widgets.dart'; - -final widgetBook = WidgetBookKeys(); - -class _WidgetBookKey extends ValueKey { - const _WidgetBookKey(String value) : super('widgetBook_$value'); -} - -class WidgetBookKeys { - final tile = const _WidgetBookKey('tile'); -} -``` - -Import to the main aggregator: - -```dart -import 'package:common_ui/widgets/keys.dart' as ds; - -final keys = Keys(); - -class Keys { - final designSystem = ds.dsKeys; - // ... -} -``` - -## Parameterized keys - -Use parameterized keys when: - -- Widgets are generated from dynamic data. -- Widgets are generated from a DTO or enum. -- Number of widgets is variable or large. -- Widgets are generated in loops or from lists. - -Use individual keys when: - -- Widgets are hardcoded and known at compile time. -- Widgets have distinct, meaningful names. - -### Parameterized key rules - -- ALWAYS prefer using enums or DTOs as the parameter if they already exist. -- Use existing widget properties for parameterized keys. -- NEVER assign a parameterized key in the app but use fixed values in the keys file (and vice versa). -- NEVER create helper methods — use parameterized keys instead. -- When widgets are generated from existing enums or DTOs, always use parameterized keys with those enum/DTO values. - -### Example - -```dart -// app widget -enum SizeDTO { small, medium, large } - -Widget _sizeButton(SizeDTO size) { - return _Button( - key: keys.pickSize.sizeButton(size), - value: size, - currentValue: value, - onPressed: onPressed, - ); -} - -@override -Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sizeButton(SizeDTO.small), - _sizeButton(SizeDTO.medium), - _sizeButton(SizeDTO.large), - ].spaced(24), - ); -} -``` - -```dart -// keys.dart -_PickSizeKey sizeButton(SizeDTO size) => - _PickSizeKey('sizeButton_${size.name}'); -``` - -## Steps to assign a key to a widget - -1. Identify the widget that is needed for testing. -2. Create the key assignment: `key: keys.feature.widgetName`. -3. Define the key in the feature's `keys.dart` file. -4. Verify the key is assigned to the widget. diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md deleted file mode 100644 index 8326032..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md +++ /dev/null @@ -1,172 +0,0 @@ -# Patrol Tests - -Patrol is LeanCode's Flutter-first E2E UI testing framework. It extends `integration_test` with native automation (`$.platform`) and a custom finder system. - -`patrol_mcp` (released 2026-03-31) exposes a Dart-based MCP server that wraps `patrol develop`, letting AI agents run tests, capture screenshots, inspect the native UI tree, and iterate on failing tests without manual handoff. - -## Order of actions when writing new tests - -1. Read provided test steps. -2. Inspect existing modules for functions that can be reused. -3. Consider whether an existing function can be adjusted to match both its existing usage and the new test. -4. Assign test keys to required elements if they are not assigned yet (see `patrol-keys`). -5. Start writing the test: reuse existing modules/functions and put new test steps in a new test file. -6. Write Patrol actions directly in the test file — do not create new methods in modules yet. -7. After the test passes, reorganize the new code into existing or new modules. -8. Recheck the test after refactoring. - -## Patrol MCP usage - -When the `patrol_mcp` MCP server is configured, use these tools instead of manual `patrol develop` runs: - -- `patrol-run({ "testFile": "patrol_test/your_test.dart" })` — run a test and wait for completion. If no session is running, starts a new one; if a session is running, restarts the current test. -- `patrol-screenshot({ "platform": "android" })` or `patrol-screenshot({ "platform": "ios" })` — capture a screenshot for debugging test failures. -- `patrol-quit({})` — gracefully quit the session. -- `patrol-status({})` — check current session status and recent output. -- `patrol-native-tree({})` — fetch the current native UI tree hierarchy (used for writing native interactions or interacting with apps other than the app under test). - -**Known limitation:** on iOS, `patrol develop` tends to time out after 360 seconds. - -See the plugin README for how to install `patrol_mcp` and register the MCP launcher. - -## Test architecture - -### Structure - -- Use the custom `testApp` wrapper for all tests. -- Each test file should contain **one** test. -- Organize tests using the `Modules`, `System`, and `ApiClients` pattern: - - **Modules** — one per user-perspective feature (e.g. `Auth`, `Home`, `Downloads`, `Player`). - - **System** — class for native interactions via `$.platform` that are not part of your app (e.g. enabling airplane mode to test offline mode). Do not put app-specific methods here. - - **ApiClients** — aggregates API clients used during testing (e.g. backend test client, mail server client, third-party services). - -### Method organization in modules - -- Do not write comments in methods — use descriptive method names instead. -- Split long methods when they become hard to read and represent multiple nameable logical steps. -- If a series of steps is reused across many tests as a whole (e.g. a long onboarding flow), create a wrapper method in the module that calls private methods for each step. - -## Patrol API rules - -- Any file that directly uses Patrol APIs (`$()`, `.scrollTo()`, `.tap()`, `.enterText()`, `.waitUntilVisible()`, etc.) must `import 'package:patrol/patrol.dart';`. -- ALWAYS inspect the Patrol API before implementing a test action: - - Search the codebase for existing Patrol API usage patterns. - - Check `$.platform` APIs for the specific action. - - If a method is not found in the codebase, check: https://patrol.leancode.co/. - - Only implement after confirming the correct API method. -- ALWAYS inspect `$.platform` methods before implementing native test actions. -- Do not use the `flutter_test` package. Use only Patrol API. -- Only run tests with the Patrol MCP server (for a single test) or `patrol test` (for all tests). Never use the `flutter test` command. -- Do not write `patrolSetUp` or `patrolTearDown` methods yourself. - -## Action rules - -- Do not use `$.pump`, `waitUntilVisible`, or `waitUntilExists` (or other wait methods) before or after `tap`, `scrollTo`, or `enterText`. Patrol handles waiting automatically. Use wait methods only at the end of the test for assertions. -- To find widgets, use only keys. -- Do not write `try/catch` blocks unless absolutely necessary. -- After writing a test, check that it works by running it with the MCP server; fix it if it fails. -- If a test fails because an element was not found, check whether it needs to be scrolled to in the app code and adjust the test if needed. - -## Assertion rules - -- Do not write assertions after actions. Write them at the end of the test. -- Prefer `waitUntilVisible` as the final assertion. -- Use `expect()` for assertions only when `waitUntilVisible` is not enough. - -## Native dialog handling - -- ALWAYS handle native dialogs that appear during the flow: - - Handle dialogs immediately after the action that triggers them. - - For native permissions, prefer `$.platform.mobile.grantPermissionWhenInUse` over `$.platform.mobile.tap`. - -## Code examples - -### Module - -```dart -// patrol_test/modules/home.dart -import 'package:patrol/patrol.dart'; -import 'module.dart'; - -final class Home extends Module { - Home(super.$); - - Future navigateToSettings() async { - await $(keys.home.settingsButton).scrollTo().tap(); - } - - Future searchForItem(String searchPhrase) async { - await $(keys.home.searchButton).scrollTo().tap(); - await $(keys.home.searchInput).enterText(searchPhrase); - await $(keys.home.searchSubmitButton).tap(); - } -} -``` - -### Modules aggregator - -```dart -// patrol_test/modules/modules.dart -final class Modules { - Modules(this._$); - final PatrolIntegrationTester _$; - - late final home = Home(_$); - late final auth = Auth(_$); -} -``` - -### System class - -```dart -// patrol_test/modules/system.dart -final class System extends PlatformAutomator { - System({required super.config}); - - Future checkIfNativePlayerIsVisible() async { - // Implementation - } -} -``` - -### ApiClients class - -```dart -// patrol_test/modules/api_clients.dart -final class ApiClients { - final backend = BackendClient(); - final mailpitClient = MailpitClient(); -} -``` - -### Complete test - -```dart -testApp('Download a chapter and play it offline', ($, modules, system, apiClients) async { - await modules.auth.getAuthToken(); - await apiClients.backend.addFavourites(); - await openApp($); - await modules.home.goToOldTestament(); - await modules.testament.expandBook(bookName: 'Ksiega Rodzaju'); - await modules.testament.chooseChapterOfBook( - bookName: 'Ksiega Rodzaju', - chapterIndex: 0, - ); - await modules.player.expandChapters(); - await modules.player.downloadChapter(chapterIndex: 9); - await modules.player.waitUntilDownloaded(); - // equivalent of await $.platform.mobile.enableAirplaneMode(); - await system.enableAirplaneMode(); - await modules.player.rollDownChapters(); - await modules.player.closeChapterPlayer(); - await modules.testament.closeTestament(); - await modules.bottomNavigation.goToLibrary(); - await modules.library.goToDownloads(); - await modules.downloads.expandOldTestament(); - await modules.downloads.goToBook(bookName: 'Ksiega Rodzaju'); - await modules.player.checkIfChapterIsCorrect(chapterIndex: 0); - await modules.player.playCurrentTrack(); - // uses a wrapper method that calls multiple $.platform methods - await system.checkIfNativePlayerIsVisible(); - }); -```