Skip to content

Commit 20c6c0a

Browse files
authored
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 63c8296. * 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.
1 parent 30a4c28 commit 20c6c0a

35 files changed

Lines changed: 1195 additions & 611 deletions

.github/workflows/ci.yml

Lines changed: 16 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,16 @@ jobs:
441441
OD_PLAYWRIGHT_FULLY_PARALLEL: "1"
442442
run: pnpm -C e2e exec playwright test -c playwright.config.ts ${{ matrix.files }} --grep '@critical'
443443

444-
ui_p0_smoke:
445-
name: UI P0 smoke
444+
ui_p0:
445+
name: UI P0 (${{ matrix.name }})
446446
needs: [scopes, runners]
447447
if: ${{ needs.scopes.outputs.run_ui_p0 == 'true' }}
448448
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).ui_hot }}
449-
timeout-minutes: 30
449+
timeout-minutes: 45
450+
strategy:
451+
fail-fast: false
452+
matrix:
453+
include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}
450454

451455
steps:
452456
- name: Checkout
@@ -476,14 +480,14 @@ jobs:
476480
- name: Clean Playwright state
477481
run: pnpm -C e2e exec tsx scripts/playwright.ts clean
478482

479-
- name: Run UI shell smoke
480-
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group smoke
483+
- name: Run UI P0 domain
484+
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group ${{ matrix.shard }}
481485

482-
- name: Upload Playwright debug artifact
483-
if: ${{ always() }}
486+
- name: Preserve project-runtime domain artifact
487+
if: ${{ success() && matrix.shard == 'project-runtime' }}
484488
uses: actions/upload-artifact@v7
485489
with:
486-
name: ui-p0-ci-${{ github.run_id }}-smoke
490+
name: ui-p0-ci-${{ github.run_id }}-${{ matrix.name }}-domain
487491
path: |
488492
e2e/ui/reports/playwright-html-report
489493
e2e/ui/reports/test-results
@@ -492,47 +496,9 @@ jobs:
492496
if-no-files-found: ignore
493497
retention-days: 7
494498

495-
ui_p0:
496-
name: UI P0 (${{ matrix.name }})
497-
needs: [scopes, runners]
498-
if: ${{ needs.scopes.outputs.run_ui_p0 == 'true' }}
499-
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).ui_hot }}
500-
timeout-minutes: 45
501-
strategy:
502-
fail-fast: false
503-
matrix:
504-
include: ${{ fromJSON(needs.scopes.outputs.ui_p0_matrix) }}
505-
506-
steps:
507-
- name: Checkout
508-
uses: actions/checkout@v6.0.2
509-
510-
- name: Configure CI parallelism
511-
uses: ./.github/actions/configure-ci-parallelism
512-
513-
- name: Setup workspace
514-
uses: ./.github/actions/setup-workspace
515-
with:
516-
runner-labels: ${{ toJSON(fromJSON(needs.runners.outputs.runs_on).ui_hot) }}
517-
518-
- name: Setup Playwright
519-
uses: ./.github/actions/setup-playwright
520-
with:
521-
package-json-path: e2e/package.json
522-
install-command: pnpm -C e2e exec playwright install --with-deps chromium
523-
runner-labels: ${{ toJSON(fromJSON(needs.runners.outputs.runs_on).ui_hot) }}
524-
525-
- name: Prebuild workspace type declarations
526-
run: |
527-
pnpm --filter @open-design/daemon build
528-
pnpm --filter @open-design/desktop build
529-
pnpm --filter @open-design/web build:sidecar
530-
531-
- name: Clean Playwright state
532-
run: pnpm -C e2e exec tsx scripts/playwright.ts clean
533-
534-
- name: Run UI P0 domain
535-
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group ${{ matrix.shard }}
499+
- name: Run UI critical extras
500+
if: ${{ matrix.shard == 'project-runtime' }}
501+
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group critical-extras
536502

537503
- name: Upload Playwright debug artifact
538504
if: ${{ always() }}
@@ -648,7 +614,6 @@ jobs:
648614
- web_workspace_tests
649615
- e2e_vitest
650616
- playwright_critical
651-
- ui_p0_smoke
652617
- ui_p0
653618
- playwright_visual
654619
if: ${{ always() }}
@@ -695,7 +660,7 @@ jobs:
695660
+ when($out.run_web_workspace_tests == "true"; ["web_workspace_tests"])
696661
+ when($out.run_e2e_vitest == "true"; ["e2e_vitest"])
697662
+ when($out.run_playwright_critical == "true"; ["playwright_critical"])
698-
+ when($out.run_ui_p0 == "true"; ["ui_p0_smoke", "ui_p0"])
663+
+ when($out.run_ui_p0 == "true"; ["ui_p0"])
699664
+ when($out.run_playwright_visual == "true"; ["playwright_visual"])
700665
)[]
701666
| select(($needs[.].result // "missing") != "success")

.github/workflows/ui-extended-main.yml

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: ui-extended-main
22

33
on:
4-
schedule:
5-
# 02:00 UTC = 10:00 Asia/Shanghai
6-
- cron: "0 2 * * *"
74
workflow_dispatch:
85
inputs:
96
suite:
@@ -24,17 +21,37 @@ concurrency:
2421
cancel-in-progress: true
2522

2623
jobs:
24+
p0_runners:
25+
name: Resolve UI runner profiles
26+
if: ${{ github.repository == 'nexu-io/open-design' }}
27+
runs-on: ubuntu-24.04
28+
outputs:
29+
runs_on: ${{ steps.runners.outputs.runs_on }}
30+
31+
steps:
32+
- name: Checkout
33+
uses: actions/checkout@v6.0.2
34+
35+
- name: Resolve runner profiles
36+
id: runners
37+
env:
38+
OD_CI_RUNNER_MODE: ${{ vars.OD_CI_RUNNER_MODE }}
39+
run: python3 .github/scripts/runners.py
40+
2741
ui_p0:
2842
name: UI P0 (${{ matrix.name }})
43+
needs: [p0_runners]
2944
if: ${{ github.repository == 'nexu-io/open-design' && github.event_name == 'workflow_dispatch' && inputs.suite != 'full' }}
30-
runs-on: ubuntu-latest
45+
runs-on: ${{ fromJSON(needs.p0_runners.outputs.runs_on).ui_hot }}
3146
timeout-minutes: 45
3247
strategy:
3348
fail-fast: false
3449
matrix:
3550
include:
3651
- name: entry-settings
3752
shard: entry-settings
53+
- name: project-workspace
54+
shard: project-workspace
3855
- name: project-runtime
3956
shard: project-runtime
4057
- name: workspace-restoration
@@ -44,14 +61,20 @@ jobs:
4461
- name: Checkout
4562
uses: actions/checkout@v6.0.2
4663

64+
- name: Configure CI parallelism
65+
uses: ./.github/actions/configure-ci-parallelism
66+
4767
- name: Setup workspace
4868
uses: ./.github/actions/setup-workspace
69+
with:
70+
runner-labels: ${{ toJSON(fromJSON(needs.p0_runners.outputs.runs_on).ui_hot) }}
4971

5072
- name: Setup Playwright
5173
uses: ./.github/actions/setup-playwright
5274
with:
5375
package-json-path: e2e/package.json
5476
install-command: pnpm -C e2e exec playwright install --with-deps chromium
77+
runner-labels: ${{ toJSON(fromJSON(needs.p0_runners.outputs.runs_on).ui_hot) }}
5578

5679
- name: Prebuild workspace type declarations
5780
run: |
@@ -62,17 +85,31 @@ jobs:
6285
- name: Clean Playwright state
6386
run: pnpm -C e2e exec tsx scripts/playwright.ts clean
6487

65-
- name: Run UI shell smoke
66-
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group smoke
67-
6888
- name: Run UI P0 domain
6989
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group ${{ matrix.shard }}
7090

91+
- name: Preserve project-runtime domain artifact
92+
if: ${{ success() && matrix.shard == 'project-runtime' }}
93+
uses: actions/upload-artifact@v7
94+
with:
95+
name: ui-extended-main-${{ github.run_id }}-p0-${{ matrix.name }}-domain
96+
path: |
97+
e2e/ui/reports/playwright-html-report
98+
e2e/ui/reports/test-results
99+
e2e/ui/reports/results.json
100+
e2e/ui/test-results
101+
if-no-files-found: ignore
102+
retention-days: 7
103+
104+
- name: Run UI critical extras
105+
if: ${{ matrix.shard == 'project-runtime' }}
106+
run: pnpm -C e2e exec tsx scripts/playwright.ts run-ui-group critical-extras
107+
71108
- name: Upload Playwright debug artifact
72109
if: ${{ always() }}
73110
uses: actions/upload-artifact@v7
74111
with:
75-
name: ui-p0-${{ github.run_id }}-${{ matrix.name }}
112+
name: ui-extended-main-${{ github.run_id }}-p0-${{ matrix.name }}
76113
path: |
77114
e2e/ui/reports/playwright-html-report
78115
e2e/ui/reports/test-results
@@ -92,10 +129,10 @@ jobs:
92129
include:
93130
- name: P1 shard 1
94131
part: p1
95-
command: pnpm -C e2e exec playwright test -c playwright.config.ts ui --grep '\[P1\]' --shard=1/2
132+
command: pnpm -C e2e exec playwright test -c playwright.config.ts ui --grep '\[P1\]' --grep-invert '@merge-extra' --shard=1/2
96133
- name: P1 shard 2
97134
part: p1
98-
command: pnpm -C e2e exec playwright test -c playwright.config.ts ui --grep '\[P1\]' --shard=2/2
135+
command: pnpm -C e2e exec playwright test -c playwright.config.ts ui --grep '\[P1\]' --grep-invert '@merge-extra' --shard=2/2
99136

100137
steps:
101138
- name: Checkout
@@ -136,23 +173,34 @@ jobs:
136173
retention-days: 7
137174

138175
ui_full:
139-
name: Full UI regression
140-
if: ${{ github.repository == 'nexu-io/open-design' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.suite == 'full')) }}
141-
runs-on: ubuntu-latest
142-
timeout-minutes: 120
176+
name: Full UI (shard ${{ matrix.shard }}/12)
177+
needs: [p0_runners]
178+
if: ${{ github.repository == 'nexu-io/open-design' && github.event_name == 'workflow_dispatch' && inputs.suite == 'full' }}
179+
runs-on: ${{ fromJSON(needs.p0_runners.outputs.runs_on).ui_hot }}
180+
timeout-minutes: 20
181+
strategy:
182+
fail-fast: false
183+
matrix:
184+
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
143185

144186
steps:
145187
- name: Checkout
146188
uses: actions/checkout@v6.0.2
147189

190+
- name: Configure CI parallelism
191+
uses: ./.github/actions/configure-ci-parallelism
192+
148193
- name: Setup workspace
149194
uses: ./.github/actions/setup-workspace
195+
with:
196+
runner-labels: ${{ toJSON(fromJSON(needs.p0_runners.outputs.runs_on).ui_hot) }}
150197

151198
- name: Setup Playwright
152199
uses: ./.github/actions/setup-playwright
153200
with:
154201
package-json-path: e2e/package.json
155202
install-command: pnpm -C e2e exec playwright install --with-deps chromium
203+
runner-labels: ${{ toJSON(fromJSON(needs.p0_runners.outputs.runs_on).ui_hot) }}
156204

157205
- name: Prebuild workspace type declarations
158206
run: |
@@ -163,14 +211,19 @@ jobs:
163211
- name: Clean Playwright state
164212
run: pnpm -C e2e exec tsx scripts/playwright.ts clean
165213

166-
- name: Run full UI
167-
run: pnpm -C e2e test:ui
214+
- name: Run full UI shard
215+
# Worker count comes from configure-ci-parallelism (nproc/2, so two
216+
# on the four-core ui_hot profile); fully-parallel stays on for
217+
# test-granularity sharding.
218+
env:
219+
OD_PLAYWRIGHT_FULLY_PARALLEL: "1"
220+
run: pnpm -C e2e exec playwright test -c playwright.config.ts ui --shard=${{ matrix.shard }}/12
168221

169-
- name: Upload Playwright debug artifact
222+
- name: Upload full UI shard artifact
170223
if: ${{ always() }}
171224
uses: actions/upload-artifact@v7
172225
with:
173-
name: ui-full-${{ github.run_id }}
226+
name: ui-full-${{ github.run_id }}-shard-${{ matrix.shard }}-of-12
174227
path: |
175228
e2e/ui/reports/playwright-html-report
176229
e2e/ui/reports/test-results

apps/web/src/components/ChatComposer.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,12 @@ interface Props {
260260
onOpenPetSettings?: () => void;
261261
researchAvailable?: boolean;
262262
projectMetadata?: ProjectMetadata;
263-
onProjectMetadataChange?: (metadata: ProjectMetadata) => void;
263+
// Fired after the daemon accepts a metadata PATCH, with the authoritative
264+
// post-patch project (fresh `updatedAt` included). Callers must replace
265+
// their whole project copy with it: forwarding only the metadata onto a
266+
// stale copy lets an older detail snapshot win recency comparisons and
267+
// shadow the change (e.g. the working-dir label never updating).
268+
onProjectMetadataChange?: (updated: Project) => void;
264269
activeWorkspaceContext?: WorkspaceContextItem | null;
265270
initialWorkspaceContexts?: WorkspaceContextItem[];
266271
workspaceContexts?: WorkspaceContextItem[];
@@ -1338,7 +1343,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
13381343
onShowToast?.(t('homeWorkingDir.applyFailed'));
13391344
return false;
13401345
}
1341-
onProjectMetadataChange?.(result.metadata);
1346+
onProjectMetadataChange?.(result);
13421347
for (const trimmed of trimmedDirs) void rememberRecentDir(trimmed);
13431348
}
13441349
return trackedByDir;
@@ -1668,7 +1673,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
16681673
onShowToast?.(t('homeWorkingDir.applyFailed'));
16691674
return false;
16701675
}
1671-
onProjectMetadataChange?.(result.metadata);
1676+
onProjectMetadataChange?.(result);
16721677
setWorkspaceLinkedDirAdds((current) => {
16731678
const { [id]: _removed, ...rest } = current;
16741679
return rest;
@@ -2112,7 +2117,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
21122117
onShowToast?.(t('homeWorkingDir.applyFailed'));
21132118
return;
21142119
}
2115-
onProjectMetadataChange?.(result.metadata);
2120+
onProjectMetadataChange?.(result);
21162121
const promotedDir = dir.trim();
21172122
setPromotedWorkspaceContextDir(
21182123
selectedWorkspaceContextDirs.includes(promotedDir) ? promotedDir : null,
@@ -2141,7 +2146,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
21412146
const result = await patchProject(projectId, { metadata });
21422147
if (result?.metadata) {
21432148
setPromotedWorkspaceContextDir(null);
2144-
onProjectMetadataChange?.(result.metadata);
2149+
onProjectMetadataChange?.(result);
21452150
}
21462151
}
21472152

apps/web/src/components/ChatPane.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,9 @@ interface Props {
632632
onTogglePet?: () => void;
633633
onOpenPetSettings?: () => void;
634634
projectMetadata?: ProjectMetadata;
635-
onProjectMetadataChange?: (metadata: ProjectMetadata) => void;
635+
// Authoritative post-patch project from the daemon — see ChatComposer's
636+
// prop of the same name for the recency invariant.
637+
onProjectMetadataChange?: (updated: Project) => void;
636638
activeWorkspaceContext?: WorkspaceContextItem | null;
637639
initialWorkspaceContexts?: WorkspaceContextItem[];
638640
workspaceContexts?: WorkspaceContextItem[];

0 commit comments

Comments
 (0)