Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 7 additions & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ jobs:
- name: Check Windows test inventory
run: npm run windows:inventory

# Deliberately here and not in the e2e job: that job is conditional, and a
# tier this guard only checks when the tier already ran is a guard that
# grows back (#4761). Pure Node, no build -- it reads the spec sources.
- name: Check Electron e2e budget
run: npm run check:e2e-budget

# Runs on the PR merge result: after a sibling protocol change lands on
# main with the same epoch text, the silently merged tree still carries
# the current base parent's epoch and this fails instead of shipping two
Expand Down Expand Up @@ -357,37 +363,7 @@ jobs:

- name: Desktop e2e
if: steps.plan.outputs.e2e == 'true'
run: |
set -euo pipefail
display_base=90
worker_count=4
xvfb_pids=()
cleanup() {
kill "${xvfb_pids[@]}" 2>/dev/null || true
wait "${xvfb_pids[@]}" 2>/dev/null || true
}
trap cleanup EXIT
for ((index = 0; index < worker_count; index += 1)); do
display=$((display_base + index))
Xvfb ":$display" -screen 0 1280x1024x24 -nolisten tcp \
>"$RUNNER_TEMP/xvfb-$display.log" 2>&1 &
xvfb_pids+=("$!")
done
for ((index = 0; index < worker_count; index += 1)); do
display=$((display_base + index))
for _ in {1..50}; do
[[ -S "/tmp/.X11-unix/X$display" ]] && break
kill -0 "${xvfb_pids[$index]}" 2>/dev/null || break
sleep 0.1
done
if [[ ! -S "/tmp/.X11-unix/X$display" ]]; then
cat "$RUNNER_TEMP/xvfb-$display.log"
exit 1
fi
done
MAKA_E2E_X_DISPLAY_BASE="$display_base" \
npm exec -w @maka/desktop -- playwright test \
--config e2e/playwright.config.ts --workers="$worker_count"
run: xvfb-run -a npm exec -w @maka/desktop -- playwright test --config e2e/playwright.config.ts

# Playwright keeps a trace, a video and a screenshot for every failed
# test. Without this they die with the runner, and an e2e flake can only
Expand Down
73 changes: 73 additions & 0 deletions apps/desktop/e2e-budget.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"policy": [
"A test belongs in this tier only when it needs a real Electron window: an OS dialog, a main-process round trip, input released outside the window, persistence across a renderer reload, or a cross-process lifecycle.",
"Layout geometry, renderer state machines and pure CSS go to a Storybook play function; main-process logic goes to node --test. See #4761.",
"Every spec below records the Electron-owned mechanism it needs. If you cannot name one, the test does not belong here."
],
"specs": {
"composer-directory-reference.spec.ts": {
"tests": 1,
"electron": "the folder reference has to survive a renderer reload and still agree with the Host's session record"
},
"context-window-save.spec.ts": {
"tests": 1,
"electron": "the saved window is read back from the Host's connection snapshot, not from renderer state"
},
"new-task-reload.spec.ts": {
"tests": 1,
"electron": "renderer reload is the whole contract: an explicit new task must not reopen history"
},
"proxy-password-editing.spec.ts": {
"tests": 1,
"electron": "the password never reaches the renderer; only the Host can report passwordConfigured and authenticate offline"
},
"quote-window-boundary.spec.ts": {
"tests": 1,
"electron": "a pointer capture released outside the window -- there is no window edge to leave in a browser tab"
},
"session-draft-focus.spec.ts": {
"tests": 1,
"electron": "needs a second real Session (Host round trip) to switch to; the focus and draft-restore halves alone would not earn a window"
},
"session-workbar.spec.ts": {
"tests": 5,
"electron": "Git changes re-read on window focus, terminal PTY ownership across Sessions, Side Chat's fork lifecycle, and a first send that has to reach the Host; the composer-usage test is renderer-only and rides along on those windows until app-shell.tsx's composer-to-workbar wiring has a story host"
},
"settings.spec.ts": {
"tests": 4,
"electron": "the preload makaE2eLatch holds the settings chunk mid-load, and the rename it commits is a Host write; the workbar-chrome test rides along on that window and would move to a story the day app-shell.tsx's settings wiring has one"
},
"sidebar-project-reload.spec.ts": {
"tests": 1,
"electron": "rail grouping has to be rebuilt from persisted project state after a renderer reload"
},
"skill-draft-lifecycle.spec.ts": {
"tests": 2,
"electron": "revision retry and cancel are Host-owned draft transitions across a parent and a child Session"
},
"slash-command-compact.spec.ts": {
"tests": 1,
"electron": "/compact is a Host round trip that ends in a real terminal Turn state"
},
"streaming-remount.spec.ts": {
"tests": 4,
"electron": "observation seeding, reconnect and settle are Host subscriptions surviving a renderer remount"
},
"transcript-scroll-cost.spec.ts": {
"tests": 3,
"electron": "the perf budget is measured from CDP wheel input and the browser's own render skipping"
},
"workhub-layout.spec.ts": {
"tests": 1,
"electron": "Coordination startup failure comes from the Host, and recovery needs a default model written back to it"
},
"workhub-reconstruction.spec.ts": {
"tests": 2,
"electron": "delegation linkage is rebuilt by the Host across navigation and across Sessions"
},
"zh-tw-locale.spec.ts": {
"tests": 1,
"electron": "the locale is a persisted Host setting and only takes effect through a renderer reload"
}
}
}
57 changes: 0 additions & 57 deletions apps/desktop/e2e/fixture-thread-search.spec.ts

This file was deleted.

35 changes: 1 addition & 34 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,34 +523,13 @@ type E2eTestFixtures = {
parentRemovalWindow: Page;
railRenderWindow: Page;
promptRailWindow: Page;
threadSearchWindow: Page;
requestHeaderRowWindow: Page;
newTaskTargetWindow: Page;
directoryReferenceWindow: { page: Page; folder: string };
accessibilityNarrativeWindow: Page;
};

type E2eWorkerFixtures = {
isolatedDisplay: void;
};

export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
isolatedDisplay: [async ({}, use, workerInfo) => {
const base = process.env.MAKA_E2E_X_DISPLAY_BASE;
if (base === undefined) {
await use();
return;
}
if (!/^\d+$/.test(base)) throw new Error(`Invalid E2E X display base: ${base}`);
const previous = process.env.DISPLAY;
process.env.DISPLAY = `:${Number(base) + workerInfo.parallelIndex}`;
try {
await use();
} finally {
if (previous === undefined) delete process.env.DISPLAY;
else process.env.DISPLAY = previous;
}
}, { scope: 'worker', auto: true }],
export const test = base.extend<E2eTestFixtures>({
directoryReferenceWindow: async ({}, use) => {
await withE2eWindow(
{ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh-CN', showWindow: true },
Expand Down Expand Up @@ -661,18 +640,6 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
showWindow: true,
}, use);
},
// The same seeded transcript, on a window of its own. Search reads the Host
// through the bridge and renders nothing, so it needs neither the warm
// window's compositor nor its between-test reset — and taking it off the
// reused window is what retires the readiness gate's cross-test bleed (#4707).
threadSearchWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'chat-prompt-rail',
locale: 'zh-CN',
}, use);
},
// Settings → 模型, where `no-models` is the seeded openai-compatible relay —
// the connection type whose detail page owns the custom request headers
// editor. Shown, because what this window is for is a rendered box
Expand Down
17 changes: 5 additions & 12 deletions apps/desktop/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,19 @@ import { defineConfig } from '@playwright/test';
/**
* Playwright config for the desktop Electron E2E suite.
*
* Most tests launch a real Electron window backed by the deterministic fake
* backend (MAKA_E2E=1) against their OWN throwaway userData dir. The read-only
* prompt-rail scenario instead keeps its Electron + Host composition warm for
* a worker and resets its Host range and renderer state per test. Keep the
* local default at one worker: concurrent windows share OS focus, invalidating
* geometry and focus contracts. Developers can still pass `--workers`
* explicitly for a subset that has neither concern.
* Every test launches a real Electron window backed by the deterministic fake
* backend (MAKA_E2E=1) against its OWN throwaway userData dir. One worker,
* everywhere: what is left in this tier is here because it needs a native
* window, and concurrent windows share OS focus, which invalidates exactly the
* focus, pointer and geometry contracts that kept these tests here.
* Deliberately no test count here — the previous note carried a stale one that
* outlived two rounds of pruning. `playwright test --list` is the only figure
* that cannot rot.
*
* CI gives every Playwright worker an isolated X display, so they overlap
* without sharing focus or a compositor. Local parallelism is opt-in for the
* same reason.
*
* Run from apps/desktop via `npm run e2e`, which builds the app first.
*/
export default defineConfig({
testDir: '.',
fullyParallel: true,
workers: 1,
// CI publishes no Playwright report that consumes Git metadata. Disable its
// best-effort shallow-history fetch, which otherwise waits on a fixed timeout.
Expand Down
Loading