# WP2 — BE runner accepts an optional filter (single/group run)

## Goal
Let the UI run a SUBSET of scenarios (one test or one @tag group) instead of always all — by passing an optional filter that becomes `playwright test --grep <filter>`.

## Repo & branch
BE repo `/home/moonui/moon-erp-be`, branch `hazemdev`. Commit `feat(e2e):`.

## Files to MODIFY
- `Modules/E2eRunner/database/migrations/<new_ts>_add_filter_to_e2e_runs.php` — NEW migration: add nullable `string filter` column to `e2e_runs`. Timestamp sorts after existing. **Run it on dev this step**: `php artisan migrate --force` (⛔ never migrate:fresh). Add `filter` to the model `$fillable` (`Modules/E2eRunner/app/Models/E2eRun.php`).
- `Modules/E2eRunner/app/Http/Controllers/E2eRunController.php` — `store()`: read optional `$request->input('filter')` (nullable string, trim, cap length e.g. 200). Store it on the created `e2e_runs` row (`filter` column). Pass it to the spawned command. The spawn currently builds `nohup php artisan e2e:run {id} ...` — add `--filter=<escapeshellarg(filter)>` ONLY IF non-empty (the id stays `%d`; the filter goes through escapeshellarg since this is the shell-string spawn path — that's still safe). Keep the 3 gates + single-flight unchanged. Also surface `filter` in `latest()` response so the FE can show what ran.
- `Modules/E2eRunner/app/Console/RunE2eCommand.php` — signature `e2e:run {run} {--filter=}`. When `--filter` is present and non-empty, build the playwright args as `['npx','playwright','test','--config',$cfg,'--grep',$filter]` (ARRAY form — no shell interpolation, injection-safe). Empty/absent filter → run all (unchanged). If `--grep` matches zero tests, playwright exits 0 with 0 tests — treat as a valid "no matching tests" run (status passed, total 0) or a clear note; don't crash.

## Interfaces exposed to WP4
- `POST /api/e2e/run` body: optional `{ "filter": "@lis" }` or `{ "filter": "<exact test title>" }`. No filter = run all. Returns `{id,status,already_running}` as before.
- `GET /api/e2e/runs/latest` includes `filter` (string|null) so the UI shows the last run's scope.

## Acceptance criteria
- [ ] Migration adds `e2e_runs.filter` and RUNS on dev.
- [ ] `POST /e2e/run` with `{"filter":"@results"}` runs only the results scenario; no filter runs all — verify by a real triggered run (env is enabled on dev) OR by asserting the command line built. Confirm the grep arg is array-form (injection-safe).
- [ ] `latest()` returns `filter`.
- [ ] Pest: extend `E2eRunnerTest` — a run created with a filter stores it; single-flight still holds; gates unchanged. Green on sqlite, no regressions.
- [ ] Injection-safe: the filter never reaches a shell as an unescaped string in the array-form command path (the artisan-spawn path uses escapeshellarg).

## Out of scope
FE (WP4). Streaming/screenshots (WP3, deferred). Do NOT change the gates or single-flight logic.

## Flags
**Migration: YES** (`e2e_runs.filter`) — run on dev same step. Security-relevant (filter → grep) but low risk with array-form + escapeshellarg; native review suffices (Fable emergency-only).
