Files
open-design/e2e/AGENTS.md
T
PerishFireandGitHub 20c6c0a6ed ci(e2e): shard the full UI functional pool into a merge-ready matrix (#5883)
* ci: benchmark deduplicated UI merge sentinel

* ci: benchmark restoration-hosted sentinel

* ci: benchmark distributed P0 sentinel

* ci: benchmark split P0 sentinel

* ci: serialize split sentinel benchmark

* ci: finalize deduplicated UI merge topology

* ci: prototype all-shades UI domain topology

* ci: bound full UI runtime by shade

* test(e2e): stabilize full UI suite

* fix(web): unclip DS picker popover and propagate patched project metadata

Two regressions surfaced by re-enabling the full UI pool:

- The New Project design-system popover lost its portal and was clipped by
  short viewports and the modal body. Render it through createPortal with a
  viewport-aware anchor that flips upward when space below runs out.

- After a working-dir pick, onProjectMetadataChange forwarded only the
  metadata onto a stale project copy, so ProjectView.currentProject's
  recency comparison let an older useProjectDetail snapshot shadow the
  accepted PATCH and the picker label never updated. The callback now
  carries the daemon's authoritative post-patch project (fresh updatedAt
  included), matching the design-system switch path, and ProjectView hands
  it straight to onProjectChange.

* test(e2e): repair remaining stale full-suite UI contracts

Second repair batch for the failures surfaced by full diagnostic run
29480362163, plus one CI-invisible case that was serially skipped behind
the BYOK failure in the same file:

- Point the working-dir metadata spec at a directory that exists on the
  runner instead of a hardcoded absolute path, and assert the label from
  the picked path (this spec is red without the ProjectView patched-project
  fix).
- Navigate back to All project files before asserting the plugin-folder
  cards, since a finished run auto-opens the produced file tab.
- Resolve the create-run response race in real-daemon-run's sendPrompt via
  a single waitForResponse alongside the click.
- Refresh selectors and copy for entry topbar, home hero rail, chat todo
  autoscroll, project management, workspace keyboard, design files, manual
  edit, restoration, and DS picker clipping flows to match the current UI
  contracts while keeping each spec's oracles.

* ci: distribute the full UI suite over a generic 8-shard matrix

Replace the diagnostic four-domain P0/P1/P2 serial topology with a single
all-shades Playwright invocation per shard (--shard=N/8, two fully
parallel workers, 20-minute cap) and drop the scheduled trigger; full runs
stay manual until the merge-queue wiring lands. Update the workflow
topology guard accordingly.

* ci: serialize full UI shards and widen the matrix to twelve

The eight-shard two-worker experiment left six light shards healthy
(260-477s test steps) but both shards holding runtime-heavy files
degraded: repeated two-minute timeouts with retry recovery in one, and a
consistent create-run response starvation failure in the other. This
matches the earlier finding that runtime-heavy scenarios cannot share a
runner under two fully parallel workers. Run each shard with a single
worker and rebalance to twelve shards; fully-parallel stays on only for
test-granularity sharding.

* fix(web): preserve prompt-extracted plugin inputs across apply reconcile

The active-plugin apply reconcile rebuilt the inputs from the bind-time
optimistic snapshot, so edits the user typed during the apply roundtrip
(already extracted into active.inputs by handlePromptChange) were silently
reverted to defaults — the submitted pluginInputs then disagreed with the
visible prompt. Merge the apply result's defaults into the current inputs
inside the functional updater instead, mirroring the guard the prompt-text
reconcile already had.

* test(e2e): stabilize home gallery flows for all-shades execution

Generic all-shades sharding executes tests in orders the shade-filtered
domain topology never produced, exposing a home-surface remount window:
the async projects fetch can mount or remount HomeTemplatesReveal after a
test starts interacting, collapsing the gallery (inert) under the test's
cursor. Stabilize the shared helpers instead of individual specs:

- gotoEntryHome arms a projects-list response waiter before navigating and
  holds until it settles.
- revealHomeTemplates requires the revealed state (or a true pass-through)
  to survive a settle window, retrying when the wrapper mounts late; a
  card's isVisible() is true inside the collapsed container, so
  openHomePluginDetails now always routes through the reveal path.
- The browse-registry spec reveals before clicking.
- The DS-picker options moved out of the new-project-modal subtree with the
  portal fix; query the page-level popover per the clipping spec's
  convention.
- real-daemon-run's sendPrompt reports on timeout whether POST /api/runs
  was ever issued, so the CI-only BYOK failure can be split into
  swallowed-click vs unanswered-request on the next reproduction.

* test(e2e): make BYOK availability hermetic and reveal remaining gallery clicks

The BYOK missing-provider spec could only pass on machines with a real
`opencode` binary: daemon detection maps byok-opencode's availability
through the `opencode` agent's configured cli env, and the web composer
refuses to POST /api/runs for an unavailable agent (the sendPrompt
diagnostic confirmed "was never issued" on CI). Point the helper's
agentCliEnv at the fake opencode runtime — the run still fails pre-spawn
on the missing provider config, so the fake binary never executes.

Also route the two remaining collapsed-container gallery clicks (the
browse-registry starter journey and the detail-use force-click) through
revealHomeTemplates; an inert container swallows force-clicks silently.

* ci: probe a 24-shard full UI matrix against the four-minute wall target

The first green 12-shard run put eleven shards at 141-354s test steps but
concentrated the contiguous runtime-heavy chain in one 894s shard, setting
a ~16-minute wall tail. With ~3568 total test-seconds and ~90s per-job
overhead, 24 shards should land every job near four minutes.

* ci: settle the full UI matrix on twelve shards

The 24-shard probe stayed green but proved the runtime-heavy chain is a
serial group (test.describe.configure serial in real-daemon-run), which
Playwright keeps atomic within one shard: the tail stayed ~16 minutes while
the extra twelve cold starts raised aggregate runner time from ~76 to ~102
minutes. Twelve shards dominate; lowering the wall below the serial-chain
floor is a de-serialization follow-up, not a shard-count knob.

* test(e2e): drop serial mode from shard-splittable files

Playwright keeps a serial group atomic within one shard, so
real-daemon-run's file-level serial made its ~16-minute chain the floor of
the full UI matrix wall regardless of shard count. Every test in the three
serial files already performs its full setup per test (config reset,
route stubs, own project), so serial only added skip-after-failure and
shard atomicity. Verified order independence by running each file's two
standalone shard halves locally (fully parallel sharding, one worker):
21/21, 57/57, and 12/12 across the halves.

* test(e2e): settle the projects fetch in real-daemon-run home navigation

With serial mode gone, the plugin-authoring spec runs after arbitrary
shard predecessors, so the daemon holds more projects when it navigates
home; the late-resolving projects list re-renders the hero and left the
shortcuts trigger unstable for the whole click timeout on CI. Port the
projects-settle waiter already used by entry-chrome-flows into this
file's gotoEntryHome.

* test(e2e): name the plugins-load gate in the plugin authoring spec

The home shortcuts trigger is disabled while the plugins list loads, and
this spec's mid-flow reload re-fires that fetch after gotoEntryHome's
settle has already passed — on CI the click then times out opaquely for
120 seconds. Arm a response waiter across the reload and assert the
trigger's enabled state explicitly, so a slow or hung /api/plugins fails
with the actual cause named.

* test(e2e): enter plugin authoring through the plugins page create button

The home shortcuts trigger sits disabled on CI runners while a home
plugin apply hangs (plugins list resolves, the pending-apply gate never
clears); three reproduction attempts confirmed the anomaly is CI-only
and independent of this journey. The create button on the Plugins page
drives the same queuePluginAuthoring flow, and this spec's oracle is the
generated scaffold plus its action cards, so route the entry there and
track the disabled-trigger anomaly separately.

* ci: let configure-ci-parallelism drive full UI shard workers

The single-worker override predates the repairs that explained the
two-worker instability (BYOK agent availability and the reveal remount
race, both test-side and since fixed), so it was pinning throughput on a
stale diagnosis. Drop it and let the repo's nproc/2 convention apply —
two workers on the four-core ui_hot profile — halving the per-shard test
step if the runtime holds.

* ci: probe three full UI workers against Blacksmith's actual vCPU capacity

The ui_hot profile resolves to blacksmith-4vcpu runners that reportedly
provide six actual vCPUs while nproc's cgroup view says four, so the
nproc/2 convention derives two workers and idles capacity. Pin three
workers for this probe and print the CPU topology alongside for
verification.

* Revert "ci: probe three full UI workers against Blacksmith's actual vCPU capacity"

This reverts commit 63c8296ae0ead2c3dadfbfda41f6e068d58331ef.

* ci: skip the critical fallback for pure packaged-leaf changes

Playwright starts tools-dev web (daemon + web runtimes) and never
launches the desktop, packaged, or tools-pack entrypoints, so PRs
confined to apps/desktop, apps/packaged, or tools/pack were paying the
two-job critical fallback (5.4-13.4 minutes of wall each) for coverage
their package tests and preflight already provide — seven of the eight
fallback PRs in the latest 100-merge sample were such false positives.

Add a fail-closed ui_critical_validation_required scope: it clears only
when every non-exempt changed file sits under one of those three leaf
roots. tools-dev, any transitive package (including the undeclared
metatool edge), scripts, runtime resources, unknown roots, and mixed
leaf + runtime changes keep the fallback armed; root manifests keep
enabling P0 with the existing critical/P0 mutual exclusion; merge-group
full mode is unchanged.

* test(e2e): align question-form specs with inline Studio discovery

Rebasing onto main picked up #5496, which moved clarification forms
inline into the chat flow and removed the legacy Questions workspace
tab. Re-point the submit-persistence spec at the inline form, and fix
the oracles that no other lane had verified since: the answered summary
echoes the picked visual-style card title (not the option label), and
the submitted answer message carries the card title plus its value id.

* test(e2e): absorb post-rebase drift in BYOK gating and zh-CN hero copy

Two more contracts only the full pool guards moved under the rebase:

- The composer's BYOK gate silently swallows submits until the
  incremental /api/agents stream has delivered byok-opencode's
  availability, so a fast submit races the stream (the sendPrompt
  diagnostic reports the POST was never issued). Retry through that
  window; runErrorCard reads the last card, so swallowed attempts cannot
  shadow the provider-validation oracle. The gate erroring instead of
  waiting while agents are still loading is a product-side follow-up.
- The home hero title copy changed in every locale; re-point the zh-CN
  P2 smoke at the current translation.

* test(e2e): align gallery specs with the all-types default slice

The Community gallery now defaults to an All category pill (#5759)
instead of pre-selecting Deck: assert the new default state in the hero
and category-tab specs, and have the facet-filter spec pick Deck
explicitly before asserting the deck-only visibility set.

* test(e2e): align the first-run reveal spec with the all-types default slice

Same #5759 drift family as the entry-chrome-flows gallery specs, missed
by the first sweep because it lives in home-hero-rail: the revealed
gallery now shows the All pill selected instead of hiding it with
live-artifact pre-selected.

* docs(e2e): add UI test stability rules distilled from the full-pool work

Capture the invariants a UI test must hold to stay green under the generic
sharded full pool, learned from stabilizing that suite and two rebases:

- order independence (no serial groups; setup is per-file, not universal),
  and that neither shade-filtered local runs nor merge-path P0 prove it;
- retry-only passes are an order-leak/readiness signal, not flake;
- settle async surfaces (best-effort waiter + a state-survives-a-window
  check) before interacting;
- an enabled control is not a ready control when a streamed precondition
  (e.g. BYOK-OpenCode agent availability over the /api/agents SSE stream)
  gates it;
- never force-click into inert/collapsed reveal containers; use
  actionability, not visibility, as the readiness signal;
- hermetic agent availability via route mocks (with the terminal SSE `done`
  event) or fake CLI runtimes + agentCliEnv, never host binaries;
- realign stale oracles against the product and confirm them in the lane
  that actually executes the case, since a merged PR does not verify P1/P2
  or unregistered-file P0 oracles;
- name failure causes so a CI-only reproduction carries its own evidence.

Cross-linked from the top-of-file coverage-status pointer.

* test(e2e): realign BYOK missing-provider spec to the #5745 client-side preflight

main #5745 added a client-side BYOK preflight that blocks a missing-provider
run before any POST and opens the execution Settings section, instead of
letting the run reach the daemon and fail there. Rewrite the spec to the new
"blocked before spawn" behavior: submitting opens the execution-mode dialog,
no create-run request is issued, and no artifact is produced (persisted
across reload). This drops the now-obsolete daemon-failed-run assertions and
the SSE-availability retry loop, which the preflight makes unnecessary.

* docs(e2e): explain why the functional config ignores visual specs

Clarify that visual-*.test.ts are owned by playwright.visual.config.ts and
its playwright_visual lane, so excluding them from the functional config is
intentional scope (it keeps a bare test:ui or the full-pool shard run from
executing visual specs without their visual config), not a disabled lane.

* fix(web): portal the New Project brand-preview flyout with the popover

The earlier DS-picker unclip portaled the popover list but left its companion
brand-preview flyout on the old inline `position: absolute` against the
trigger wrapper. Once the popover moved to a body portal, the flyout for a
finalized `user:<id>` brand was still clipped by the modal body and no longer
anchored to the popover. Portal the flyout too and place it beside the
popover with the same viewport-aware logic (right of it when the 320px card
fits, else flipped left, then clamped), mirroring the popover's up/down
vertical anchor.

Red spec: e2e/ui/new-project-ds-picker-clipping.test.ts
"finalized-brand preview flyout is not clipped by the modal body" — red with
the popover portaled but the flyout inline, green with this change.
2026-07-20 09:25:09 +00:00

16 KiB

e2e/AGENTS.md

Follow the root AGENTS.md first. This package owns user-level end-to-end smoke tests and Playwright UI automation only.

For the current coverage posture, recent hardening work, grouped-run status, and known intentional gaps, see docs/testing/e2e-coverage/status.md. For the invariants new and repaired UI tests must hold under the sharded full pool, see UI test stability rules below.

Directory layout

  • specs/: highest-ROI, long-running core business capability regressions suitable for PR or release gating. Each spec should describe one nearly orthogonal product capability chain, such as main dialog generation, Pet, Orbit, or packaged runtime. Keep this layer small and expand it only when a core capability deserves always-on signal.
  • tests/: broader user-level end-to-end coverage and local hotspot checks that intentionally span app/package/resource boundaries. Prefer adding tests here when a repeated or high-risk local capability naturally falls out of a core spec. Do not build a speculative coverage matrix before the core spec needs it.
  • tests/scripts/: behavior-contract coverage for root operational scripts whose regressions affect install, CI, or release flows. Keep fixtures hermetic and runnable through e2e Vitest; do not put *.test.ts siblings directly under root scripts/.
  • ui/: flat Playwright UI automation test files only. Keep helpers, resources, and non-Playwright harnesses out of this directory.
  • resources/: declarative resources for e2e suites, such as Playwright UI scenario lists.
  • lib/fake-agents.ts: shared fake local agent CLI harness used by UI and pure-inspect daemon specs.
  • lib/timeouts.ts: CI-scaled timeout constants (T.short, T.medium, T.long, T.xlong). Import as { T } from @/timeouts. Use these instead of hardcoded millisecond values in UI tests.
  • lib/tools-dev/: framework-neutral tools-dev runtime lifecycle. It owns namespace/path construction, port reservation, tools-dev ... --json execution, status/log/check reads, URL construction, and start/stop semantics. It must not import Vitest or Playwright.
  • lib/playwright/suite.ts: Playwright-only suite assembly. It provides the worker-scoped tools-dev fixture, dynamic baseURL, and failure attachments. UI tests import test/expect from @/playwright/suite.
  • lib/vitest/suite.ts: Vitest-only suite assembly. It composes the neutral tools-dev runtime with report creation, scratch preservation, and Vitest assertions for non-UI smoke suites.
  • lib/playwright/mock-factory.ts: shared Playwright mock helpers. applyStandardMocks(page) seeds localStorage and intercepts /api/agents and /api/app-config with standard daemon/mock-agent fixtures. Use in beforeEach for tests that do not need a custom agent or protocol setup.
  • lib/vitest/: Vitest-specific atomic helpers only. Helpers describe actions such as mock servers, HTTP calls, and reports; tools-dev lifecycle belongs in lib/tools-dev/ and is only composed through lib/vitest/suite.ts.
  • lib/vitest/report.ts: the report boundary. Specs save curated output through report.save(<relpath>, <blob>) or report.json(<relpath>, value); release workflows should consume only the final report path, not its internal file layout.
  • createSmokeSuite(...).with.*: suite-owned lifecycle composition from @/vitest/suite. Prefer this shape for namespace-bound resources such as suite.with.toolsDev(...) so specs keep business workflow code in the foreground.
  • Temporary e2e Vitest env/PATH mutations, AMR fake endpoint URLs, and packaged smoke default namespaces belong behind @/vitest/suite helpers such as suite.with.env(...), suite.with.pathEntry(...), suite.amr, and resolvePackagedSmokeNamespace(...). Do not hand-roll save/restore blocks or fixed localhost ports in individual specs.
  • lib/playwright/: Playwright-specific fixtures, resource accessors, route helpers, and UI actions.
  • scripts/playwright.ts: Playwright auxiliary subcommands such as artifact cleanup; it must not wrap playwright test.

Spec and test model

  • Start from specs/: define orthogonal long-form core capabilities first, then let supporting tests/ and lib/ grow from those chains.
  • specs/ should read as business/system workflows, for example dialog/main.spec.ts, orbit/run.spec.ts, or pet/main.spec.ts.
  • tests/ should pin reusable local hotspots, such as tools-dev/inspect.test.ts, provider mocks, report lifecycle, artifact file shape, or namespace cleanup.
  • High-confidence infrastructure checks may be added to tests/ before a full core spec exists, but most tests should be extracted only after a spec proves the local hotspot matters.
  • Treat tests/ as maintainable support material, not permanent coverage inventory. Merge, split, shrink, or delete tests as product capabilities evolve.
  • Keep new non-UI e2e smoke chains pure inspect by default. Do not use Playwright for these chains; use daemon/web APIs, sidecar IPC, tools-dev/tools-pack inspect, logs, reports, and screenshots when available.
  • External service dependencies must use temporary server-level mocks. Do not rely on real API keys, real provider accounts, or UI-level route patching for core e2e smoke.
  • Every atomic suite must run in an isolated namespace. Successful suites should keep only curated reports and high-value artifacts, then clean process/runtime scratch. Failed suites should preserve runtime scratch, logs, mock requests, screenshots, and report pointers for diagnosis.

UI test stability rules

These invariants complement the coverage posture in docs/testing/e2e-coverage/status.md; this section is the source of truth for how a UI test must be written to stay green under the sharded full pool.

The ui-extended-main full pool (workflow_dispatch with suite=full) executes every non-visual functional shade in one generically sharded matrix (visual-*.test.ts is excluded by the config's testIgnore and runs in its own lane): arbitrary P0/P1/P2 interleavings, contiguous shard slices that start mid-file, an isolated tools-dev runtime per Playwright worker (nproc / 2, so two on the ui_hot runner) with OD_PLAYWRIGHT_FULLY_PARALLEL=1, and slow CI runners. It is the only lane that runs the whole non-visual ui suite together — every P1/P2 shade, plus the P0 cases no merge lane covers: ci.yml's ui_p0 runs only the files listed in a uiP0Groups group, and playwright_critical only its own @critical file matrix, so a [P0]/@critical tag does not enroll a new file (the P0 cases in automations-page.test.ts and home-hero-rail.test.ts, for instance, run nowhere but the full pool). Two order hazards then hide from narrower runs: within-file interleaving (the tests of one file racing under fully-parallel workers) and cross-file carry-over (the worker-scoped tools-dev runtime — suite.ts, scope: 'worker' — retaining daemon/config/project state between the files a worker runs in sequence). Merge lanes touch each in part — playwright_critical runs fully-parallel, ui_p0's single-worker multi-file groups accumulate carry-over — but only the full pool exercises the whole suite interleaved with mid-file shards, so treat it as the acceptance gate. New and repaired UI tests must hold the following invariants.

  • Order independence is the contract. Each test performs its own complete setup — whatever that file's model requires — and never relies on a predecessor's side effects; any contiguous subset of the suite must pass with the rest absent. There is no universal setup list, and the axis that matters is test-scoped browser mocks versus worker-scoped daemon state: the daemon/data root is shared across a worker's tests (suite.ts), while localStorage seeds and applyStandardMocks route interception are per-test. Real-daemon specs reset config and create real projects; entry-surface specs route-mock the same endpoints and may also create real daemon projects. Match the file's existing model rather than adding route stubs a core smoke chain forbids. Do not add test.describe.configure({ mode: 'serial' }): a serial group is atomic within one shard (it cannot be split across the matrix) and adds skip-after-failure, which floors the pool's wall time. Running a file's standalone halves — --shard=1/2 and --shard=2/2 with OD_PLAYWRIGHT_FULLY_PARALLEL=1 — checks within-file split-independence, but each is a fresh process, so it cannot expose the cross-file, same-worker carry-over above; run the whole ui folder (one worker runtime spans the files) to surface that, or the full pool, which additionally stresses it with fully-parallel mid-file shards.
  • Treat a retry-only pass as a signal, not flake. CI retries a failed functional test once (the visual config sets retries: 0), and the retry runs after the Playwright worker restarts with a fresh tools-dev runtime — so a first attempt that failed on a dirty predecessor state can pass on the clean retry and leave the run green. A test that only ever fails on its first attempt is telling you something: it may be carried-over predecessor state, worker-startup instability, or an in-test async-readiness race (the settle and readiness rules below). Don't wave it through — reproduce it locally (CI keeps only the failed roots' paths and the retry's trace, not the failed attempt's runtime) and fix the actual cause.
  • Settle async surfaces before interacting. Late-resolving fetches re-render the home surface and a remount silently resets transient UI state: the projects list can remount the templates reveal container. Arm a best-effort response waiter before the navigation or reload that triggers the fetch (entry-chrome-flows's gotoEntryHome waits on /api/projects, but swallows its own timeout, so it only narrows the race — and the many other same-named helpers do not arm it at all). The load-bearing half is requiring the observed state to survive a settle window rather than trusting the first observation, as revealHomeTemplates does by retrying on reveal regression.
  • An enabled control is not a ready control. A precondition that arrives over a stream is a distinct gate from a remount reset. Agents load through an incremental /api/agents SSE stream, and a BYOK-OpenCode run checks that agent's availability before it will POST: until the stream publishes, ProjectView rejects the submit with a visible unavailable error instead of creating a run — even though chat-send already reports enabled and the fake runtime env makes availability possible. This pre-POST check is BYOK-OpenCode-specific, not a universal composer invariant, but the pattern generalizes: when a control's readiness depends on a streamed precondition, wait for that signal — ideally a side-effect-free one — rather than for toBeEnabled. The BYOK spec in ui/real-daemon-run.test.ts instead retries the submit itself until the stream catches up (its sendPrompt reports whether a create-run request was issued at all); that is safe only because its oracle tolerates the repeated rejected attempts, and a submit that persisted state on each try would need an idempotent oracle or a non-mutating readiness wait.
  • Never force-click into gated containers. An inert container swallows force-clicks with no error, and isVisible() is true for content inside a collapsed reveal container. Route gallery interactions through the reveal-aware helpers and treat actionability (hit-target), not visibility, as the readiness signal.
  • Hermetic dependencies only. CI runners have no host binaries and no provider accounts, so agent availability must come from the harness, by one of two mechanisms. Route-mocked specs intercept /api/agents with routeAgents / fulfillAgentsRoute (via applyStandardMocks); that mock must serve both the JSON and the ?stream=1 SSE shape including the terminal done event — without it the streamed availability lands transiently and is then cleared when the client rejects the incomplete stream. Real-daemon specs instead run a fake CLI through createFakeAgentRuntimes + agentCliEnv — and daemon detection resolves byok-opencode through the opencode agent's env. A spec that only passes where a real CLI happens to be installed is broken.
  • Oracles assert the running product's observable behavior. When a spec goes stale, realign it against the current product, not the spec's own history. A merged product PR is not proof its oracle is verified: what runs on the merge path is governed by registration, not priority tag — the ui_p0 group files and the @merge-extra P1 subset (playwright_critical is a mutually-exclusive PR fallback — run_playwright_critical is && !runUiP0 — so @critical does not add coverage on the merge queue). A non-visual P1 case outside those also runs in the manual p0p1 lane; a non-visual P2 case, or a P0 case in a file no group lists, runs only in the full pool (visual tests have their own lane). Confirm a realigned oracle in the lane that actually executes it, not by the PR merging.
  • Name your failure causes. A long wait should fail with a diagnosis, not an opaque timeout. sendPrompt in ui/real-daemon-run.test.ts tracks whether the create-run request was ever issued, so its failure separates "no request left the page" (a composer/overlay gate) from "no accepted response arrived" — enough to point at the right layer in a CI-only reproduction. Assert gate preconditions explicitly for the same reason. A hang is usually a missing gate, not a too-short budget — diagnose the state the wait targets before raising it.

Naming and tools

  • specs/ files must be *.spec.ts; tests/ files must be *.test.ts.
  • Prefer directory hierarchy over long file names. Basenames should normally be three words or fewer, such as main.spec.ts, run.spec.ts, inspect.test.ts, or report.test.ts.
  • ui/ files must be flat *.test.ts Playwright tests. Do not add subdirectories, TSX, Vitest, jsdom, Testing Library, or React harness tests under ui/.
  • ui/ tests must import runtime-bound test/expect from @/playwright/suite; use @playwright/test only for type imports or low-level helper modules that do not own test lifecycle.
  • E2E Vitest tests use Node APIs; do not add JSX/TSX, jsdom, or browser-component tests under specs/ or tests/.
  • Web component/runtime tests belong in apps/web/tests/, not e2e/ui/.
  • E2E tests may validate cross-app/resource consistency, but must not treat one app's private implementation as a shared helper for another app. Keep test-only helpers local to e2e/lib/ or promote reusable logic to a pure package such as packages/contracts.
  • E2E imports may use @/* for lib/*; keep this alias local to the e2e package.

Commands

Run commands from this directory:

pnpm test specs/mac.spec.ts
pnpm test tests/tools-dev/inspect.test.ts
pnpm test specs
pnpm test tests
pnpm test:p0
pnpm test:p0p1
pnpm test:ui:p0
pnpm test:ui:p0p1
pnpm typecheck
pnpm exec tsx scripts/playwright.ts clean
pnpm exec playwright test -c playwright.config.ts --list
pnpm exec playwright test -c playwright.config.ts

Use a specific file path when validating a single case. Do not add root e2e aliases or extra package scripts for individual cases.

Case-level priority tags use test-name prefixes: [P0], [P1], [P2].

Playwright UI runs use one tools-dev daemon/web/data root per Playwright worker. The single-worker fallback is --workers=1 (or OD_PLAYWRIGHT_WORKERS=1); do not reintroduce a shared daemon/web runtime mode.