协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
flutter-mcp-e2e-harness【开发与工程】Use this skill when writing or running repeatable E2E scenarios for Flutter apps as checked-in Dart with the `flutter_mcp_harness` package (`packages/harness`) — build/launch the app, attach to its VM service, drive widgets (snapshot/tap/enter text) and assert without MCP; also covers the repo showcase launcher (`make showcase`, `--web`, `--stop`) an…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
flutter_mcp_harness package (packages/harness) — build/launch the app, attach to its VM service, drive widgets (snapshot/tap/enter text) and assert without MCP; also covers the repo showcase launcher (make showcase, --web, --stop) and multi-instance desktop compositions. For interactive assistant-driven debugging through MCP fmt_* tools, use flutter-mcp instead.Golden path for programmatic E2E over the toolkit: packages/harness
(flutter_mcp_harness) owns the whole lifecycle — build the app, launch the
process, attach to its Dart VM service, and only then drive/assert. It speaks
the same ext.mcp.toolkit.* service extensions as the MCP server, as plain
Dart. ADR: decisions/0015_flutter_mcp_harness_extraction.mdx.
The driving layer speaks the universal AutomationDriver contract
(ADR-0038, universal_automation_interface): ToolkitDriver exposes the
observe/act/verify loop (snapshot → semantic AxNode tree with refs and
bounds; perform click/type/key/named-route-navigate/evaluate; screenshot
PNG), so the same agent vocabulary drives Flutter apps, browsers
(CdpDriver), and OS-native targets. IntentDriverRouter additionally
routes an intent's automation hint (transport + verb + locator) onto the
bound driver. Frame pipelines are deliberately NOT a harness dependency —
they are composition-level (showcase/drivers, which also holds the live
end-to-end drives: make drive-flutter / drive-flutter-chrome /
drive-web).
App-specific verbs use the invoke tier (ADR-0017): the app's intent
registry IS the action catalog — driver.actions() lists it, and
InvokeAction(name, args) dispatches to a registered entry with
schema-validated arguments (custom IntentAutomationAction.custom hints
route the same way). Web surfaces without a VM service (Jaspr, plain JS)
compose identically over CdpDriver via the window.__mcpActions page
registry.
| Need | Use |
|---|---|
| Repeatable scenario as code, CI-friendly exit codes | this skill (flutter_mcp_harness) |
| One-off interactive debugging from chat/editor | flutter-mcp (MCP fmt_* tools) |
| Browser/WebMCP dogfood of the web showcase | flutter-mcp-toolkit-maintain-web |
| Live end-to-end showcase drives (both tiers) | showcase/drivers (make drive-flutter / drive-web) |
The #1 rule: at most one owning session per app. A second
flutter run/flutter attach on a session someone else owns is the
ADR-0014 failure mode (the second attach can kill the first).
| Target | When | Notes |
|---|---|---|
FlutterRunTarget | You want the flutter tool to own build + hot reload (r/R/q via stdin). The showcase launch path. | device (macos, chrome, …), extraArgs, onLine tee for long cold builds; VM URI scraped from the tool's own announcement. |
BinaryAppTarget / MacosAppTarget / WindowsAppTarget | Fresh full flutter build <os> --debug → launch the binary directly. No compile channel afterwards. | Pass launch(build: false) to reuse an existing binary. Never opens an owning session. |
External runner (e.g. oka dev) owns the app | Construct LaunchedApp directly around the owning process and read the VM URI from the runner-session contract, not stdout. | This package never imports the runner; callers wire it (LaunchedApp(name:, process:, stdout:, vmUri:)). |
FlutterRunTarget.launch(build:) ignores the flag — flutter run always
builds. A failed bring-up reaps the session (SIGTERM → child-reaping pkill →
SIGKILL) before rethrowing.
final scenario = Scenario('checkout E2E', steps: [
('launch app', (context) async {
final app = await MacosAppTarget(projectDir: appDir, binaryPath: binary)
.launch(build: build);
launched = app; // for finally-stop
await app.stdout.waitFor('app ready'); // no sleeps — wait on logs
context.bag['app'] = app; // hand values between steps
}),
('drive + assert', (context) async {
final driver = WidgetDriver(await context.take<LaunchedApp>('app').vm());
final ref = await driver.findRef('checkout');
if (ref == null) return context.report.fail('no checkout button');
await driver.tap(ref);
final total = await driver.findValue((v) => v.startsWith(r'#x27;));
total != null
? context.report.pass('total rendered: $total')
: context.report.fail('no total in snapshot');
}),
]);
var passed = false;
try {
passed = await scenario.run();
scenario.report.printSummary();
} finally {
await launched?.stop(); // cleanup always runs
}
exit(passed ? 0 : 1);
report.fail) aborts the
rest — cleanup still happens in the caller's finally.HarnessContext.bag passes values between steps; take<T>(key) reads them.retry(action) polls until non-null (for app-side readiness races).packages/harness/example/desktop_pair.dart.WidgetDriver)snapshot() → (label, ref) pairs; findRef(needle) is case-insensitive
substring + retry until timeout.tap(ref), tapUntil(ref, until: …) (re-tap until a predicate fires),
enterText(ref, text), scroll(direction:, distance:) — off-screen
semantics only exist once scrolled into view.findValue(predicate) reads a node's selectable-text value — the
deterministic way to read app-rendered data (clipboards and icon-only copy
buttons are not).-32601 with an empty snapshot so
findRef's retry loop rides it out instead of aborting — do not treat one
empty snapshot as proof the UI is empty.VmClient extras: extensionNames() (discoverability), evaluate(expr),
hotReload(). Raw reloadSources cannot compile without an owning
flutter run/attach session — treat true as "asked", not "recompiled".packages/harness/tool/showcase.dart)make showcase # macOS showcase, interactive foreground (r/R/q relayed)
make web-showcase # Chrome + WebMCP flags; WEB_PORT/VM_HOST_PORT/FLIGHT… env
make showcase-stop # kill stray sessions, free VM port 8181 (idempotent)
--web --detach (or make-less dart run packages/harness/tool/showcase.dart --web --detach)
spawns headless-ish, waits for the ws:// VM URI in .showcase/web_app.log,
prints WS_URI=…, exits 0 — the CI-friendly variant..showcase/: flutter_app.log / web_app.log (teed
output) and flutter.pid / web_flutter.pid pid files.dart run packages/harness/tool/intentcall_session.dart [demo|serve-link|serve-debug] — resolves the VM service URI from the
showcase log (--vm-service-uri pins one) and the IntentCall CLI from
INTENTCALL_ROOT, sibling checkouts, or PATH.make exec-sweep greps)
and tears the session down on exit or Ctrl-C.grep -Eo 'ws://127\.0\.0\.1:[0-9]+/[A-Za-z0-9_=-]+/ws' .showcase/flutter_app.log | tail -1.ToolkitExtensions mirrors
mcp_toolkit/lib/src/toolkits/interaction_toolkit.dart; keep the lists in
lockstep — drift is caught by extension calls failing at runtime today
(shared-constants follow-up will make it a compile error).BinaryAppTarget — full fresh builds only.flutter_harness experiment (ADR-0012) is retired (ADR-0017).
Composition roots are consuming-project code (see example/).oka_harness, ADR-0014) — not this package.cd packages/harness && dart test # unit tests (canned envelopes, fake flutter)
dart run example/desktop_pair.dart --skip-build
flutter-mcp — the interactive MCP loop over the same extensionsflutter-mcp-automation-chain — cross-tier wiring for your own app (surface picker, oka integration, runner sessions)flutter-mcp-toolkit-maintain-macos / -maintain-web — showcase + platform lanespackages/harness/README.md, ADR-0014 (runner delegation), ADR-0015 (extraction)