agent-browser-relaticle
GitHub针对 Relaticle 应用的 agent-browser 自动化测试指南,涵盖动态 URL 推导、登录流程、会话隔离及 Filament/Livewire 特定交互模式,支持自我修复。
Trigger Scenarios
Install
npx skills add relaticle/relaticle --skill agent-browser-relaticle -g -y
SKILL.md
Frontmatter
{
"name": "agent-browser-relaticle",
"description": "Use whenever driving agent-browser against the local Relaticle app (relaticle.test and its panels) for testing, QA, business review, or UI automation. Covers Filament v5 + Livewire v4 quirks specific to this codebase: panel URL derivation (domain-routed vs path-routed, never assumed), login flows for the app and sysadmin panels, seeded credentials, Select\/date-picker interaction, the $wire.mountAction gold pattern, tenant switching, Reverb\/queue hazards, and session isolation. Every hard fact here is a DATED CACHED HINT. When one fails, re-derive from the running app and update this file (self-heal). Not for other sites or generic browser automation."
}
agent-browser × Relaticle cookbook (cached hints, verified dates, self-healing)
Prime rule: facts below are cached hints, not truth. The app's URLs, routes,
selectors, and seeders change. When a documented pattern fails twice, stop retrying:
re-derive it from the running app (procedures below), make it work, then update this
file with the new pattern and today's verified: date.
1. URL derivation (NEVER hardcode; panels are conditionally domain-routed)
php artisan tinker --execute 'echo json_encode([
"base" => config("app.url"),
"app_domain" => config("app.app_panel_domain"),
"app_path" => config("app.app_panel_path", "app"),
"sysadmin_domain" => config("app.sysadmin_domain"),
"sysadmin_path" => config("app.sysadmin_path", "sysadmin"),
]);'
-
app panel =
https://{app_domain}if set, else{base}/{app_path} -
sysadmin =
https://{sysadmin_domain}if set, else{base}/{sysadmin_path} -
Routing mode is per-checkout. Derive it, and never carry it over from another workspace. Both modes are live in the wild:
- Conductor workspace
bamako,APP_PANEL_DOMAIN/SYSADMIN_DOMAINempty → path-routed:https://bamako.test/app,https://bamako.test/sysadmin(verified: 2026-08-12). - A checkout with the
*_DOMAINenvs set → domain-routed, e.g.https://app.relaticle.test,https://sysadmin.relaticle.test(verified: 2026-06-12).
Each Conductor workspace is served by Herd under its own
https://<workspace>.test, so the host changes too. Run thetinkerblock above every run and use what it returns. - Conductor workspace
-
Login entry points are Filament-registered routes; ground truth:
php artisan route:list --jsonfiltered forlogin(names likefilament.app.auth.login). If a URL 404s, check the route table before anything else. -
Host unreachable?
herd sites/herd linksshows what Herd actually serves this checkout as (catches renamed dirs / Polyscope clones)..envvsconfig()mismatch →php artisan config:clear.
2. Session setup (every time)
export AB_SESSION="<purpose>-<run-id>" # ALWAYS unique per agent; sessions are machine-global
agent-browser --session "$AB_SESSION" set viewport 1920 1080
agent-browser --session "$AB_SESSION" open "$APP_PANEL_URL"
Pass --session "$AB_SESSION" on EVERY call (or export AGENT_BROWSER_SESSION).
Default 1280x720 clips Filament modals (verified: 2026-05).
3. Credentials (seeded; re-derive when login fails)
| Surface | Login | Password | Source |
|---|---|---|---|
| app panel | manuk.minasyan1@gmail.com |
password |
database/seeders/LocalSeeder.php (verified: 2026-06-12) |
| sysadmin | sysadmin@relaticle.com |
password |
SystemAdministratorSeeder (verified: 2026-06-12) |
| per-run test users | br-rel-<run>-…@example.test |
password |
factory |
Login failing? In order: php artisan db:seed --class=LocalSeeder (local-gated; also
tops AI credits) → --class=SystemAdministratorSeeder → factory-create a namespaced
user (User::factory()->withPersonalTeam()->create([...])). If the seeder emails
changed, fix this table (self-heal).
Dev-login affordance: the app registers laravel-login-link (route loginLinkLogin,
POST laravel-login-link-login; verified 2026-06-12 via route:list). Local login
pages may render one-click "Login as …" links; prefer them over typing credentials when
present.
4. Login flow (both panels, Filament stock login)
UPDATE (verified: 2026-09-03, app panel, path-routed astana-v1): the app login is
now identifier-first (PR #285). /app/login renders only id="form.email" plus a
"Continue" submit; there is no form.password on the first step, so the eval recipe
below returns no-inputs. Two things that worked:
- Herd's cert fails Chromium's name check (
ERR_CERT_COMMON_NAME_INVALID). ExportAGENT_BROWSER_IGNORE_HTTPS_ERRORS=1(or pass--ignore-https-errors) on every call. - Local login pages render one-click
laravel-login-linkbuttons labelled by email (owner@relaticle.test,trial@relaticle.test, ...). Click one via eval:[...document.querySelectorAll("button[type=submit]")].find(b=>b.innerText.trim()==="owner@relaticle.test").click()and you land on/app/<team-slug>(acme-salesfor owner) with no password step.
CORRECTION (verified: 2026-06-12, review PR 336): the input[name="email"] selector
is WRONG. It matches a hidden input belonging to the laravel-login-link dev package
(the page has hidden _token/email/key/guard/user_model inputs from that form).
agent-browser fill against that hidden field hung the daemon (os error 35,
"daemon may be busy or unresponsive") and never submitted. The REAL Filament inputs have
NO name attribute. They are id="form.email" / id="form.password" with
wire:model="data.email" / data.password, inside the <form wire:submit="authenticate">.
The recipe that works when fill/type hang (eval-driven, daemon-safe):
export AGENT_BROWSER_SESSION="<unique>"
agent-browser open "$PANEL_URL/login"
agent-browser eval '(() => {
const e=document.getElementById("form.email"), p=document.getElementById("form.password");
e.value="'"$LOGIN"'"; e.dispatchEvent(new Event("input",{bubbles:true}));
p.value="password"; p.dispatchEvent(new Event("input",{bubbles:true}));
const f=[...document.querySelectorAll("form")].find(x=>x.getAttribute("wire:submit")==="authenticate");
f.requestSubmit(); return "submitted";
})()'
sleep 4
agent-browser eval 'location.pathname' # confirm you left /login (lands on /<team-slug>)
- Daemon hangs on
fill/typein this environment (verified: 2026-06-12). When a command returnsos error 35/ no output,pkill -9 -f agent-browser; sleep 3and re-open.open/eval/snapshot/screenshotare reliable;clickis flaky, so preferevalwithel.click()for<a wire:navigate>links. - Many stale
--sessionentries overload the daemon; keep ONE session per run and chain commands with&&in a single shell call (the daemon persists the browser).
Older recipe (fill+click), left here for reference; did NOT work on 2026-06-12
agent-browser --session "$AB" open "$PANEL_URL/login"
agent-browser --session "$AB" fill 'input[name="email"]' "$LOGIN"
agent-browser --session "$AB" fill 'input[name="password"]' "password"
sleep 1
agent-browser --session "$AB" click "Sign in"
agent-browser --session "$AB" wait --load networkidle
agent-browser --session "$AB" eval 'location.pathname' # confirm you left /login
click/filltake the element's VISIBLE TEXT or a CSS selector, NOTfind role button "<name>". That subcommand syntax errors on this binary (verified: 2026-06-12). Useagent-browser click "Sign in".- After app-panel login you land on the default team path
…/<team-slug>/…(e.g./tapix), so re-derive the slug fromlocation.pathnamebefore navigating further (verified: 2026-06-12). - After sysadmin login you land on
/(Dashboard) onsysadmin.relaticle.test(verified: 2026-06-12). - A "Developer Login" button is present on the login page but clicking it alone did not establish a session in testing, so prefer the fill+click recipe above (verified: 2026-06-12).
4b. Screenshot paths: ALWAYS absolute
agent-browser screenshot parses a RELATIVE path containing / as a CSS selector and
fails (Unexpected token "/" while parsing css selector). Always pass an absolute path:
agent-browser --session "$AB" screenshot "$(pwd)/.context/reviews/<dir>/case-X/shot.png"
(verified: 2026-06-12).
5. The gold patterns (Filament v5 + Livewire v4)
Prefer semantics over CSS selectors. a11y-role finds and Livewire state survive Blade/Tailwind refactors.
$wire is NOT in scope inside agent-browser eval (it's an Alpine magic; eval runs
in plain page context (verified 2026-06-12, after it cost a run 4 round-trips and one
self-inflicted 500 where the server was asked to call a method literally named
$wire). Resolve the component first, then use .set(...) / .call(...):
// by name (page components):
const meta = window.Livewire.all().find(c => /TasksBoard/.test(c.name)); // metadata ONLY: {id, name}
const comp = window.Livewire.find(meta.id); // the real component
await comp.call("moveCard", "<recordId>", "<columnId>");
// or from a DOM element (modals, nested components):
const comp2 = window.Livewire.find(el.closest("[wire\\:id]").getAttribute("wire:id"));
await comp2.set("mountedActions.0.data.title", "value", true);
await comp2.call("callMountedAction");
Livewire.all()entries have NO.call/.set. They are metadata, so always pass the id throughLivewire.find()(verified: 2026-06-12).- Select dropdowns (plain click is unreliable):
agent-browser find role combobox "<label>" clickthenagent-browser find role option "<option>" click, orcomp.set("data.company_id", 42, true). - Date pickers (plain type does nothing):
comp.set("data.closes_at", "2026-06-15", true). - Action modals (Delete, custom row actions), the single most useful pattern:
Every call must beawait comp.call("mountAction", "delete", { recordKey: 42 }); await comp.set("mountedActions.0.data.reason", "why", true); await comp.call("callMountedAction");await-ed. (verified: 2026-06-12 via the create-task modal) - Read Livewire state:
agent-browser eval '... JSON.stringify(comp.get("data"))' - Snapshots:
agent-browser snapshot -i -c -d 8(focused), never baresnapshot. Refs (@eXX) shift between snapshots, so keep snapshot→interaction adjacent or usefind role/text … click. - Modals fade ~300ms, so use
agent-browser wait '.fi-modal-window:not([data-state="open"])' 2000before asserting removal. - Tenant switching is browser-only (in-app switcher; tinker tenant-switch breaks the session → persistent 403s). After a switch the URL slug changes, so re-derive.
6. Environment hazards (dated)
- Factory/fresh teams redirect every app-panel page to /billing (verified:
2026-08-25):
EnsureHostedWorkspaceAccessallows only subscribed teams ortrial_ends_atin the future, and factory teams have neither, so login lands on/app/<slug>/billingand stays there. Fix before browsing:$team->forceFill(['trial_ends_at' => now()->addDays(14)])->save();(LocalSeeder's user is already provisioned; this bites ChatQaSeeder and factory users). - Shared local Redis across Herd apps: another app's Horizon can consume this app's
queue jobs (verified 2026-06-11, when Journey ate Relaticle chat jobs). Use a dedicated
REDIS_DBin.env; before queue-dependent testing, dispatch a sentinel job and confirm THIS checkout's worker consumed it. - Reverb/websockets: agent-browser's Chromium may use a wrong websocket host or a
stale built bundle. It looks like a dead page but is an env defect.
pnpm run build, checkagent-browser consolefor websocket errors (verified: 2026-06-10). - 419 CSRF after idle →
agent-browser reloadand retry once (verified: 2026-05). - A failed Livewire request leaves a full-screen error overlay in the DOM (Laravel
error page in a modal) that silently photobombs every later screenshot. The page
underneath still works, so nothing looks wrong until you read the PNG back. After ANY
errored
comp.call,agent-browser openthe page fresh (or remove the overlay) before shooting (verified 2026-06-12, when a stale overlay replaced the board in an evidence shot). - Stale session after branch switches → first action of a batch is a fresh login.
- AI credits drain during chat testing → re-seed
LocalSeederto top up before chat-heavy flows. - Screenshot pipeline can serve STALE FRAMES from a dead target (verified:
2026-08-18): after long sessions / viewport changes,
screenshotkept returning a frame that no longer matched the DOM (evals said dark theme + correct state; PNG showed an old light half-render). Detection:eval 'document.body.style.outline="40px solid red"'→ screenshot → if no red border, the pipeline is stale. Fix:pkill -9 -f agent-browser; sleep 3, new session, re-login. Don't debug the "bug" in the PNG before running the red-outline probe. UPDATE (verified: 2026-08-23): the pkill+new-session fix did NOT clear it, a fresh session's very first screenshots again failed the red-outline probe (dark-mode text read black in the PNG whilegetComputedStyle+elementFromPointat the same coordinates said white). When the probe fails twice, stop shooting: assert via DOM reads (computed styles, elementFromPoint, rects) and treat those as the truth for visual verification.
7. DB-assert (corroboration only; the UI is the proof)
php artisan tinker --execute '$c = \App\Models\Company::where("name", "br-rel-test")->first(); echo $c ? "found:".$c->id : "missing";'
Tenant-scoped query? Set context first:
\Relaticle\CustomFields\Services\TenantContextService::setTenantId($teamId);
Never use tinker or DB writes to fix or fake a result. An on-screen error is a finding.
8. Screenshots
For any deliverable screenshot, invoke Skill('screenshot-with-callout') per shot
(annotate → verify-crop → shoot → read-back). Throwaway debug shots exempt.
9. Eval and rendering hints (verified: 2026-09-07)
agent-browser evalruns every call in the same page scope. A top-levelconst xdeclared in one eval throwsIdentifier 'x' has already been declaredin the next. Wrap evals in an IIFE:agent-browser eval '(()=>{ const x=...; return JSON.stringify(x) })()'.- To screenshot a feature-flag branch without flipping the shared
.env, render it to a file and open that:php artisan tinker --execute '\Laravel\Pennant\Feature::define(\App\Features\Billing::class, false); file_put_contents(".context/off.html", view("pricing")->render());'thenagent-browser open "file://$(pwd)/.context/off.html". Vite assets resolve to the absoluteAPP_URL, so the page styles correctly fromfile://.
10. Turnstile on the signup step (verified: 2026-09-10)
- The widget lives in a closed shadow root, so
document.querySelector("iframe[src*=challenges]")is always null. Read the enclosing.fi-grid-colinstead:fi-hidden= silent pass, 70px tall = checkbox shown. The schema's grid child is.fi-grid-col, not.fi-fo-field; an empty in-flow column still costs one 24px grid gap, so measure password-field-bottom to button-top (24 = clean). Cloudflare's dummy sitekeys drive each state:1x…AApasses silently,3x…FFforces the checkbox,2x…ABalways fails; secret1x…AAaccepts the dummy token. - To click the checkbox use coordinates:
agent-browser mouse move X Y && mouse down && mouse upatrect.x+20, rect.y+32. Under zshmouse move $XYfails with "Missing arguments": an unquoted variable is not word-split, soread -r X Y <<< "$XY"first. agent-browser set media darkexists; togglingdocument.documentElement.classListalso works.
Version History
-
2e5f32c
Current 2026-09-23 05:34
无变更
-
f11c94b
2026-08-29 02:19
重构聊天功能,移除硬编码提示和重复的状态展示;修复面板路由判断逻辑及 UI 层级渲染问题。
- 1f433f8 2026-08-20 14:42


