← Test Lens
API
Tokens

Run Test Lens from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page scans the Dart - every call matched to its closing bracket, its named arguments read at their own depth - - reads every test, testWidgets, group and setUp in order, and checks awaits and pumps, assertions, app context, isolation from real dependencies and flaky waits, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans a rewritten test file with the same scanner (testreply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"test-lens"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, suite, code, facts) is missing. The page always sends all five of task, suite, code, carry, facts; carry is an empty string for review.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - inspect, then produce:

A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.

The input

FieldRequiredWhat
taskyesreview or rewrite. If it is missing or neither, do review and set "task": "review".
suiteyesthe test file's path, where it runs, and how many unit, widget and integration tests it holds.
codeyesthe test file. A line starting "// [... N lines cut by the page" marks code the page scanned but did not send.
carrysend it (empty for review)rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty.
factsyesthe page's scan: one finding per line as id | area | severity | line | where | evidence | source, and every test with its line, kind, name and counts of expects, actions and pumps. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @flutter/flutter-testing-apps - the only standard the run applies:

House rules

The replies

review

Confirm or clear each of the page's findings, add what a scan cannot see, and put the fixes in order.

  1. summary - one sentence, at most 40 words: what these tests really prove today (often less than they claim), and the first fix.
  2. findings - one object per page finding you confirm, in id order: id (the page's id), widget (the test, e.g. "testWidgets - adding an item updates the badge"), issue (one sentence: what the test fails to prove, or how it misleads), fix (one sentence: the change), code (the corrected Dart for that piece only, at most 6 lines; "" when fix says it all).
  3. cleared - one object per page finding that is not a problem in this code, with id and why (one sentence tied to the code). Every page id appears exactly once, in findings or in cleared.
  4. extra - at most 5 gaps the scan cannot see, each tied to a line of the code: id (X1, X2, ...), area (async, assertions, setup, isolation or flaky), line (a number), widget, issue, fix. Examples: an expect that checks the wrong thing (findsWidgets where one is meant), a finder that matches more than the widget under test, a missing edge case (empty, error, loading), state shared between tests through a top-level variable, a layer the skill wants covered and the file leaves out. [] when there are none.
  5. plan - 2 to 10 short strings, the fixes in the order to make them (what makes the file not compile or not run first, then awaits and pumps, then assertions, then isolation and flakiness), each naming the ids it covers (e.g. "Await the tap and pump before the expect (C3, C4)"). Every confirmed id appears in a step. [] only when nothing is confirmed.
  6. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "review",
  "summary": "one sentence",
  "findings": [ { "id": "C1", "widget": "testWidgets - adding an item updates the badge", "issue": "one sentence", "fix": "one sentence", "code": "await tester.tap(find.byIcon(Icons.add));\nawait tester.pump();" } ],
  "cleared": [ { "id": "C4", "why": "one sentence" } ],
  "extra": [ { "id": "X1", "area": "assertions", "line": 10, "widget": "testWidgets - adding an item updates the badge", "issue": "one sentence", "fix": "one sentence" } ],
  "plan": [ "Await the tap and pump before the expect (C3, C4)" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in testreply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link in the prose.

rewrite

Rewrite the test file with the fixes applied.

  1. summary - one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.
  2. code - the complete rewritten test file. Keep every import, group, setUp and test, with every test's name word for word; change only what the findings name, plus the imports a change needs. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. A tester call not awaited - await it. An async callback missing - (tester) async { ... }. An action with no pump before the expect - await tester.pump(); after it (pumpAndSettle() when an animation must finish). A test with no expect - add the expect its name promises (a bare find.text(...) line becomes expect(find.text(...), findsOneWidget)). A widget pumped with no app - MaterialApp(home: the widget). A real repository or service - a Fake of it (FakeCartRepository()), declared as used; do not write the fake's body when the file does not hold the real class - say so in a caveat. Real I/O in a unit test - the fake or mock the code under test accepts. A real wait - await tester.pump(the same Duration) in a widget test; in a unit test, remove it when the awaited call already completes, or say what the team must inject. pumpAndSettle over a spinner - pump() instead. No integration binding - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); first in main().
  3. changes - one object per page finding you fixed, in id order: id, change (one sentence: what changed, naming the test). Extra ids (X1, ...) from the carried review may appear too.
  4. kept - one object per page finding you did not change: id, why (one sentence - the review cleared it, or it needs a decision the code cannot make). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.
  5. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "rewrite",
  "summary": "one sentence",
  "code": "the complete rewritten test file",
  "changes": [ { "id": "C1", "change": "one sentence" } ],
  "kept": [ { "id": "C4", "why": "one sentence" } ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every widget class of the paste survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the rewritten code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A test the rewrite drops or renames is flagged.

Worked examples

Three runs from the page's free samples (invented apps): the review of a widget test that passes without testing anything, the rewrite of the same file carrying that review, and the review of an integration test with no binding. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

review - A widget test that passes without testing anything

The page read it as: Tests are broken - 1 blocker, 6 major, 0 minor; findings C1 no-app, C2 real-dependency, C3 not-awaited, C4 no-pump, C5 no-expect, C6 real-dependency, C7 real-wait.

Input (facts is 2402 characters; the first 1398 are shown):

{
 "task": "review",
 "suite": "File: test/cart_screen_test.dart\nRuns on: a device or emulator\nKinds: 0 unit, 2 widget, 0 integration",
 "code": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shop/cart/cart_screen.dart';\nimport 'package:shop/cart/cart_repository.dart';\n\nvoid main() {\n  testWidgets('adding an item updates the badge', (tester) async {\n    await tester.pumpWidget(CartScreen(repository: CartRepository()));\n    tester.tap(find.byIcon(Icons.add));\n    expect(find.text('1'), findsOneWidget);\n  });\n\n  testWidgets('shows the empty state', (tester) async {\n    await tester.pumpWidget(MaterialApp(home: CartScreen(repository: CartRepository())));\n    await Future.delayed(const Duration(seconds: 1));\n    find.text('Your cart is empty');\n  });\n}\n",
 "carry": "",
 "facts": "The page's scan of 19 lines - 2 tests (0 unit, 2 widget, 0 integration), 0 groups, 0 setUp/tearDowns; verdict: Tests are broken - 1 blocker, 6 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- C1 | setup | major | line 8 | testWidgets 'adding an item updates the badge' | pumpWidget is given CartScreen(repository: CartRepository()) with no MaterialApp around it, so Text, Scaffold and navigation fail for want of Directionality, MediaQuery or a Navigator. | source: await tester.pumpWidget(CartScreen(repository: CartRepository()));\n- C2 | isolation | major | line 8 | testWidgets 'adding an item updates the badge' | The test builds the real CartRepository, so it depends on whatever CartRepository talks to. | source: await tester.pumpWidget(CartScreen(repository: CartRepository()));\n- C3 | async | blocker | line 9 | testWidgets 'adding an item updates the badge' | tester.tap(...) is not awaited, so the next line runs before it finishes and the test can pass without testing anything. | source: tester.tap(find.byIcon(Icons.add));\n- C4 | async | major | line 10 | testWidgets 'adding an item updates the badge' | expect runs right after tester.tap (line 9) with no pump between, so it checks the frame before the tap took effect. | source: expect(find.text('1'), findsOneWidget);\n- C5 | assertions | major | line 13 | testWidgets 'shows the empty state' |\n\n[... 1004 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "Today neither test proves anything: the badge test checks a frame before an unawaited tap lands, and the empty-state test has no expect; first await the tap and pump before the badge expect.",
 "findings": [
  {
   "id": "C1",
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "CartScreen is pumped bare, so without the Directionality, MediaQuery and Navigator a MaterialApp supplies the screen can throw on build instead of showing the badge.",
   "fix": "Wrap CartScreen in MaterialApp(home: ...) as the second test already does.",
   "code": "await tester.pumpWidget(MaterialApp(\n  home: CartScreen(repository: FakeCartRepository()),\n));"
  },
  {
   "id": "C2",
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "The badge test builds the real CartRepository, so its result depends on whatever storage or network that class reaches rather than on CartScreen alone.",
   "fix": "Pass a FakeCartRepository that starts with no items, so one tap on the add icon has a known outcome.",
   "code": "CartScreen(repository: FakeCartRepository())"
  },
  {
   "id": "C3",
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "tester.tap(find.byIcon(Icons.add)) is not awaited, so the expect on line 10 runs before the tap is delivered and the test can pass or fail without exercising the add button.",
   "fix": "Await the tap.",
   "code": "await tester.tap(find.byIcon(Icons.add));"
  },
  {
   "id": "C4",
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "No frame is pumped between the tap and expect(find.text('1'), findsOneWidget), so the expect reads the tree from before the badge could rebuild.",
   "fix": "Call await tester.pump() after the awaited tap and before the expect.",
   "code": "await tester.tap(find.byIcon(Icons.add));\nawait tester.pump();\nexpect(find.text('1'), findsOneWidget);"
  },
  {
   "id": "C5",
   "widget": "testWidgets - shows the empty state",
   "issue": "Line 16 builds find.text('Your cart is empty') but never passes it to expect, so the test passes whether or not the empty state appears.",
   "fix": "Turn the bare finder into an expect with findsOneWidget.",
   "code": "expect(find.text('Your cart is empty'), findsOneWidget);"
  },
  {
   "id": "C6",
   "widget": "testWidgets - shows the empty state",
   "issue": "The empty-state test uses the real CartRepository, so the cart is only empty if whatever that class reads happens to hold no items.",
   "fix": "Pass a FakeCartRepository that returns no items, so the empty state is the defined input of the test.",
   "code": "await tester.pumpWidget(MaterialApp(\n  home: CartScreen(repository: FakeCartRepository()),\n));"
  },
  {
   "id": "C7",
   "widget": "testWidgets - shows the empty state",
   "issue": "await Future.delayed(const Duration(seconds: 1)) waits on the fake clock inside testWidgets, which no pump advances, so the test can hang or time out instead of letting the screen load.",
   "fix": "Replace the real wait with await tester.pump(const Duration(seconds: 1)) to advance the fake clock by the same amount.",
   "code": "await tester.pump(const Duration(seconds: 1));"
  }
 ],
 "cleared": [],
 "extra": [
  {
   "id": "X1",
   "area": "assertions",
   "line": 10,
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "find.text('1') matches any Text reading 1 on the whole screen, such as a quantity or price, so it does not prove the badge itself shows the count.",
   "fix": "Scope the finder to the badge, for example with find.descendant under the badge widget, once the team names which widget in CartScreen is the badge."
  },
  {
   "id": "X2",
   "area": "assertions",
   "line": 10,
   "widget": "testWidgets - adding an item updates the badge",
   "issue": "The test never checks the badge before the tap, so a badge that already read 1 would pass as well.",
   "fix": "Expect find.text('1') to be findsNothing right after pumpWidget and before the tap."
  },
  {
   "id": "X3",
   "area": "setup",
   "line": 4,
   "widget": "CartRepository",
   "issue": "The file imports CartRepository but nothing covers it with a unit test, and the skill wants every repository unit tested with its own dependencies mocked.",
   "fix": "Add a unit test file for CartRepository that fakes whatever it talks to and checks adding an item and reading an empty cart."
  }
 ],
 "plan": [
  "Wrap the badge test's CartScreen in MaterialApp(home: ...) so it can build (C1)",
  "Await the tap on the add icon and call await tester.pump() before the badge expect (C3, C4)",
  "Replace Future.delayed with await tester.pump(const Duration(seconds: 1)) in the empty-state test (C7)",
  "Turn the bare find.text('Your cart is empty') into an expect with findsOneWidget (C5)",
  "Check the badge reads nothing before the tap and scope the '1' finder to the badge (X1, X2)",
  "Swap the real CartRepository for a FakeCartRepository in both tests (C2, C6)",
  "Add a unit test for CartRepository with its dependencies faked (X3)"
 ],
 "caveats": [
  "FakeCartRepository is not in this file: the team writes it from the real CartRepository, which the page did not send.",
  "The widget that holds the badge in CartScreen is not shown, so the scoped finder for X1 needs its real type or key."
 ]
}

What step 5 prints for it: C1: Wrap CartScreen in MaterialApp(home: ...) as the second test already does. | C2: Pass a FakeCartRepository that starts with no items, so one tap on the add icon has a known outcome. | C3: Await the tap. | C4: Call await tester.pump() after the awaited tap and before the expect. | C5: Turn the bare finder into an expect with findsOneWidget. | C6: Pass a FakeCartRepository that returns no items, so the empty state is the defined input of the test. | C7: Replace the real wait with await tester.pump(const Duration(seconds: 1)) to advance the fake clock by the same amount.

The page's check: kept - nothing to flag

rewrite - A widget test that passes without testing anything

The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the rewritten code: 7 findings before, 0 after.

Input (facts is 2402 characters; the first 1398 are shown; carry is 1508 characters; the first 1399 are shown):

{
 "task": "rewrite",
 "suite": "File: test/cart_screen_test.dart\nRuns on: a device or emulator\nKinds: 0 unit, 2 widget, 0 integration",
 "code": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shop/cart/cart_screen.dart';\nimport 'package:shop/cart/cart_repository.dart';\n\nvoid main() {\n  testWidgets('adding an item updates the badge', (tester) async {\n    await tester.pumpWidget(CartScreen(repository: CartRepository()));\n    tester.tap(find.byIcon(Icons.add));\n    expect(find.text('1'), findsOneWidget);\n  });\n\n  testWidgets('shows the empty state', (tester) async {\n    await tester.pumpWidget(MaterialApp(home: CartScreen(repository: CartRepository())));\n    await Future.delayed(const Duration(seconds: 1));\n    find.text('Your cart is empty');\n  });\n}\n",
 "carry": "C1 | confirmed | testWidgets - adding an item updates the badge | Wrap CartScreen in MaterialApp(home: ...) as the second test already does.\nC2 | confirmed | testWidgets - adding an item updates the badge | Pass a FakeCartRepository that starts with no items, so one tap on the add icon has a known outcome.\nC3 | confirmed | testWidgets - adding an item updates the badge | Await the tap.\nC4 | confirmed | testWidgets - adding an item updates the badge | Call await tester.pump() after the awaited tap and before the expect.\nC5 | confirmed | testWidgets - shows the empty state | Turn the bare finder into an expect with findsOneWidget.\nC6 | confirmed | testWidgets - shows the empty state | Pass a FakeCartRepository that returns no items, so the empty state is the defined input of the test.\nC7 | confirmed | testWidgets - shows the empty state | Replace the real wait with await tester.pump(const Duration(seconds: 1)) to advance the fake clock by the same amount.\nX1 | extra | testWidgets - adding an item updates the badge (line 10) | Scope the finder to the badge, for example with find.descendant under the badge widget, once the team names which widget in CartScreen is the badge.\nX2 | extra | testWidgets - adding an item updates the badge (line 10) | Expect find.text('1') to be findsNothing right after pumpWidget and before the tap.\nX3 | extra | CartRepository (line 4) | Add a unit test\n\n[... 109 more characters cut from this display ...]",
 "facts": "The page's scan of 19 lines - 2 tests (0 unit, 2 widget, 0 integration), 0 groups, 0 setUp/tearDowns; verdict: Tests are broken - 1 blocker, 6 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- C1 | setup | major | line 8 | testWidgets 'adding an item updates the badge' | pumpWidget is given CartScreen(repository: CartRepository()) with no MaterialApp around it, so Text, Scaffold and navigation fail for want of Directionality, MediaQuery or a Navigator. | source: await tester.pumpWidget(CartScreen(repository: CartRepository()));\n- C2 | isolation | major | line 8 | testWidgets 'adding an item updates the badge' | The test builds the real CartRepository, so it depends on whatever CartRepository talks to. | source: await tester.pumpWidget(CartScreen(repository: CartRepository()));\n- C3 | async | blocker | line 9 | testWidgets 'adding an item updates the badge' | tester.tap(...) is not awaited, so the next line runs before it finishes and the test can pass without testing anything. | source: tester.tap(find.byIcon(Icons.add));\n- C4 | async | major | line 10 | testWidgets 'adding an item updates the badge' | expect runs right after tester.tap (line 9) with no pump between, so it checks the frame before the tap took effect. | source: expect(find.text('1'), findsOneWidget);\n- C5 | assertions | major | line 13 | testWidgets 'shows the empty state' |\n\n[... 1004 more characters cut from this display ...]"
}

Reply (long strings shortened for display - see the cuts below):

{
 "task": "rewrite",
 "summary": "Both tests now run inside MaterialApp on a FakeCartRepository, await the tap and pump before each expect, and the empty-state test asserts its text; the team still writes the fake and names the badge widget.",
 "code": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shop/cart/cart_screen.dart';\nimport 'package:shop/cart/cart_repository.dart';\n\nvoid main() {\n  testWidgets('adding an item updates the badge', (tester) async {\n    await tester.pumpWidget(MaterialApp(home: [... 483 more characters cut from this display]",
 "changes": [
  {
   "id": "C1",
   "change": "In 'adding an item updates the badge', CartScreen is now pumped as MaterialApp(home: CartScreen(...)), as the second test already did."
  },
  {
   "id": "C2",
   "change": "In 'adding an item updates the badge', the real CartRepository() is replaced by FakeCartRepository(), which should start with no items."
  },
  {
   "id": "C3",
   "change": "In 'adding an item updates the badge', the tap on the add icon is now awaited."
  },
  {
   "id": "C4",
   "change": "In 'adding an item updates the badge', await tester.pump() now runs after the tap and before the expect on find.text('1')."
  },
  {
   "id": "C5",
   "change": "In 'shows the empty state', the bare finder became expect(find.text('Your cart is empty'), findsOneWidget)."
  },
  {
   "id": "C6",
   "change": "In 'shows the empty state', the real CartRepository() is replaced by FakeCartRepository(), which should return no items."
  },
  {
   "id": "C7",
   "change": "In 'shows the empty state', the real one-second Future.delayed became await tester.pump(const Duration(seconds: 1)) on the fake clock."
  },
  {
   "id": "X2",
   "change": "In 'adding an item updates the badge', expect(find.text('1'), findsNothing) now runs after pumpWidget and before the tap, so the test proves the tap made the change."
  }
 ],
 "kept": [],
 "caveats": [
  "FakeCartRepository is not defined in this file and its body is not written here; the team writes it from the real CartRepository, empty by default, and imports it where it lives.",
  "X1 is left open: find.text('1') matches any '1' on the screen, so scope it with find.descendant once the team names the badge widget in CartScreen.",
  "X3 is left for a new file: a unit test for CartRepository that fakes whatever it talks to and checks adding an item and reading an empty cart."
 ]
}

Cut from this display only (the saved reply and the check use the full text): code (793 characters, 310 shown).

What step 5 prints for it: C1: In 'adding an item updates the badge', CartScreen is now pumped as MaterialApp(home: CartScreen(...)), as the second test already did. | C2: In 'adding an item updates the badge', the real CartRepository() is replaced by FakeCartRepository(), which should start with no items. | C3: In 'adding an item updates the badge', the tap on the add icon is now awaited. | C4: In 'adding an item updates the badge', await tester.pump() now runs after the tap and before the expect on find.text('1'). | C5: In 'shows the empty state', the bare finder became expect(find.text('Your cart is empty'), findsOneWidget). | C6: In 'shows the empty state', the real CartRepository() is replaced by FakeCartRepository(), which should return no items. | C7: In 'shows the empty state', the real one-second Future.delayed became await tester.pump(const Duration(seconds: 1)) on the fake clock. | X2: In 'adding an item updates the badge', expect(find.text('1'), findsNothing) now runs after pumpWidget and before the tap, so the test proves the tap made the change.

The page's check: kept - nothing to flag

review - An integration test with no binding

The page read it as: Tests are broken - 1 blocker, 1 major, 0 minor; findings C1 no-binding, C2 no-pump.

Input:

{
 "task": "review",
 "suite": "File: integration_test/notes_test.dart\nRuns on: a Linux CI machine\nKinds: 0 unit, 0 widget, 1 integration",
 "code": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:notes/main.dart';\n\nvoid main() {\n  group('end-to-end', () {\n    testWidgets('create a note', (tester) async {\n      await tester.pumpWidget(const NotesApp());\n      await tester.tap(find.byKey(const ValueKey('new-note')));\n      await tester.pumpAndSettle();\n      await tester.enterText(find.byType(TextField), 'Buy milk');\n      await tester.tap(find.text('Save'));\n      expect(find.text('Buy milk'), findsOneWidget);\n    });\n  });\n}\n",
 "carry": "",
 "facts": "The page's scan of 18 lines - 1 test (0 unit, 0 widget, 1 integration), 1 group, 0 setUp/tearDowns; verdict: Tests are broken - 1 blocker, 1 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- C1 | setup | blocker | line 6 | main | The file imports integration_test but never calls IntegrationTestWidgetsFlutterBinding.ensureInitialized(), so the tests do not run on the device's binding. | source: void main() {\n- C2 | async | major | line 14 | testWidgets 'create a note' | expect runs right after tester.tap (line 13) with no pump between, so it checks the frame before the tap took effect. | source: expect(find.text('Buy milk'), findsOneWidget);\nThe tests (line | kind | name | expects | actions | pumps):\n- line 8 | integration | create a note | 1 | 3 | 2\nmust: account for every finding id above exactly once - in findings (confirmed) or in cleared (with the reason)."
}

Reply:

{
 "task": "review",
 "summary": "Today the test proves only that 'Buy milk' can be typed into a field, since the expect also matches the TextField itself; first call IntegrationTestWidgetsFlutterBinding.ensureInitialized() at the top of main().",
 "findings": [
  {
   "id": "C1",
   "widget": "main",
   "issue": "The file imports integration_test but main() never initialises its binding, so the test runs on the default test binding rather than the device's and its results are not reported back to the driver.",
   "fix": "Call IntegrationTestWidgetsFlutterBinding.ensureInitialized() as the first statement of main(), before the group.",
   "code": "void main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  group('end-to-end', () {"
  },
  {
   "id": "C2",
   "widget": "testWidgets - create a note",
   "issue": "The expect on line 14 runs straight after the Save tap with no frame pumped, so it inspects the editor as it was before Save took effect.",
   "fix": "Add await tester.pumpAndSettle() after the Save tap so the save and any route change finish before the expect.",
   "code": "await tester.tap(find.text('Save'));\nawait tester.pumpAndSettle();\nexpect(find.text('Buy milk'), findsOneWidget);"
  }
 ],
 "cleared": [],
 "extra": [
  {
   "id": "X1",
   "area": "assertions",
   "line": 14,
   "widget": "testWidgets - create a note",
   "issue": "find.text('Buy milk') also matches the text inside the TextField filled on line 12, so the expect passes even if Save does nothing and the note never reaches the list.",
   "fix": "After the pump, first assert expect(find.byType(TextField), findsNothing) to prove the editor closed, then check find.text('Buy milk') with findsOneWidget."
  },
  {
   "id": "X2",
   "area": "assertions",
   "line": 13,
   "widget": "testWidgets - create a note",
   "issue": "Only the happy path is covered; nothing checks what tapping Save does with an empty TextField, a critical-flow edge case the file leaves out.",
   "fix": "Add a second testWidgets that taps Save without entering text and expects whatever the app shows for an empty note, with the expected text taken from the app code."
  }
 ],
 "plan": [
  "Call IntegrationTestWidgetsFlutterBinding.ensureInitialized() first in main() (C1)",
  "Pump with await tester.pumpAndSettle() after the Save tap, before the expect (C2)",
  "Assert the editor closed with findsNothing on the TextField before checking the note text (X1)",
  "Add an empty-note test that taps Save with no text entered (X2)"
 ],
 "caveats": [
  "The code does not show what NotesApp displays for an empty note, so the X2 expect needs the team to supply that value.",
  "If NotesApp saves notes to real storage, an earlier run's 'Buy milk' note may still be listed; clear that storage before the test."
 ]
}

What step 5 prints for it: C1: Call IntegrationTestWidgetsFlutterBinding.ensureInitialized() as the first statement of main(), before the group. | C2: Add await tester.pumpAndSettle() after the Save tap so the save and any route change finish before the expect.

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./testkit.js"); require("./testreply.js")), then:

const K = TestKit, R = TestReply;
const f = K.form({ name: "test/counter_test.dart", targets: "mobile", carry: "",
  code: "void main() {\n  testWidgets('adds', (tester) async {\n    await tester.pumpWidget(const Counter());\n    tester.tap(find.byIcon(Icons.add));\n  });\n}" });
// f.findings, f.scan.inventory (every test), f.checklist, f.commands (how to run the file)
// K.findingsCsv(f), K.testsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review");                         // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b)                                         // {status: kept or flags, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://test-lens.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"test-lens"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "review", "suite": "File: test/cart_screen_test.dart
Runs on: a device or emulator
Kinds: 0 unit, 2 widget, 0 integration", "code": "import 'package:flutter_test/flutter_test.dart';
...", "carry": "", "facts": "The page's scan of 19 lines - 2 tests ...
Findings (id | area | severity | line | where | evidence | source):
- C1 | setup | major | line 8 | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses test-lens:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review reply carries summary, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.

KEY="test-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rewrite" then (.changes[] | "\(.id): \(.change)") 
       else (.findings[] | "\(.id): \(.fix)") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: test-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json