Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b56b812
Add AGENTS.md with Cursor Cloud specific instructions
cursoragent Apr 13, 2026
efb61da
e2e: post provisioned server URLs as PR comment when E2E tests start
cursoragent Apr 13, 2026
649ede8
e2e: post provisioned server URLs as PR comment when E2E tests start
cursoragent Apr 13, 2026
30b05ba
e2e: fix script injection in post-server-info workflow step
cursoragent Apr 13, 2026
6a712bd
e2e: address CodeRabbit review comments on post-server-info
cursoragent Apr 13, 2026
5b3ee16
e2e: disable E2E/Run label removal to keep servers alive for agent-dr…
cursoragent Apr 13, 2026
9af4cda
e2e: disable label removal and fix post-login tabsDisabled race
cursoragent Apr 13, 2026
76dac80
e2e: fix lint errors and apply remaining CodeRabbit review fixes
cursoragent Apr 13, 2026
146f4d0
e2e: fix security issues and address remaining CodeRabbit review
cursoragent Apr 13, 2026
9f6cb9a
Merge cursor/e2e-post-server-info-pr-comment-8995 into cursor/setup-a…
cursoragent Apr 13, 2026
634ee0b
e2e: fix CodeRabbit review comments and comment out removeE2ELabel
cursoragent Apr 13, 2026
526681e
e2e: add zero-intervention fix trigger workflow and CodeRabbit fixes
cursoragent Apr 13, 2026
2fe719d
e2e: add @cursor command workflow for PR comment-driven agent dispatch
cursoragent Apr 13, 2026
ecbff2d
docs: add GitHub Actions coding practice rules to AGENTS.md
cursoragent Apr 13, 2026
322cd0c
e2e: fix loginToMattermost for fresh servers landing on /select_team
cursoragent Apr 13, 2026
acbb8a7
e2e: fix ESLint lines-around-comment errors in login.ts
cursoragent Apr 13, 2026
7ab93ab
e2e: provision test server team before suite starts
cursoragent Apr 13, 2026
07ed7b1
merge: resolve login.ts conflict — keep clean version without /select…
cursoragent Apr 13, 2026
66adb6a
merge: resolve conflicts with master — keep label-removal disabled
yasserfaraazkhan Apr 14, 2026
f39426d
ci: fix security issues and improve Cursor E2E automation workflows (…
yasserfaraazkhan Apr 15, 2026
ea62893
ci: fix security issues and improve Cursor E2E automation workflows (…
yasserfaraazkhan Apr 15, 2026
2366170
fix: skip check-for-failures when install-deps was canceled
yasserfaraazkhan Apr 15, 2026
8642b46
e2e: revert newTabButton :not([disabled]) selector change
cursoragent Apr 15, 2026
21ca4e3
e2e: fix login state propagation race with main-process polling
cursoragent Apr 15, 2026
9d04822
refactor: remove CI workflow automation, use AGENTS.md for Cursor Clo…
cursoragent Apr 16, 2026
c80551d
Merge remote-tracking branch 'origin/master' into cursor/setup-agents…
cursoragent Apr 16, 2026
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
91 changes: 91 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,94 @@ Open Settings (`Ctrl/Cmd+,`) → switch logging to **Debug** → reproduce → *
3. **Restart** app (and computer if needed).
4. **Reset data** — **View → Clear All Data**, or delete the config directory.
5. **Collect debug logs and heap snapshots**.

## Cursor Cloud-specific instructions

### Node version

The project requires Node.js v20.15.0 (specified in `.nvmrc`). Use `nvm` to switch:

```bash
source ~/.nvm/nvm.sh && nvm use 20.15.0
```

### Running on headless Linux (Cloud VM)

- The VM has an X server on display `:1`. Set `DISPLAY=:1` before launching Electron.
- Chrome sandbox requires root ownership: `sudo chown root:root ./node_modules/electron/dist/chrome-sandbox && sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox`. This is normally handled by `npm run linux-dev-setup` (called by `npm start` and `npm run watch`), but that script uses `sudo` which may prompt.
- To launch the built app directly: `DISPLAY=:1 npx electron dist/ --disable-dev-mode --no-sandbox`
- DBus errors in the container logs are expected and harmless (no system bus in containers).
- The "Failed to load configuration file" message on first run is normal — the app creates defaults.

### Native modules

The `postinstall` script runs `electron-builder install-app-deps` to rebuild native modules (registry-js, cf-prefs, etc.) for the current Electron version. If you see native module errors after `npm install`, ensure postinstall completed successfully.

### Starting a local Mattermost server with Docker

Server-backed E2E tests require a running Mattermost instance. Use Docker to spin one up locally:

```bash
docker run -d \
--name mattermost-e2e \
-p 8065:8065 \
--restart unless-stopped \
mattermost/mattermost-preview:latest

until curl -sf http://localhost:8065/api/v4/system/ping >/dev/null 2>&1; do
echo "Waiting for Mattermost to start..."
sleep 3
done
echo "Mattermost is ready at http://localhost:8065"
```

On first launch, create the admin user and team via the API:

```bash
curl -sf http://localhost:8065/api/v4/users \
-H 'Content-Type: application/json' \
-d '{"email":"admin@test.com","username":"admin","password":"admin","auth_service":""}' || true

TOKEN=$(curl -sf http://localhost:8065/api/v4/users/login \
-H 'Content-Type: application/json' \
-d '{"login_id":"admin","password":"admin"}' \
-D - 2>/dev/null | grep -i '^token:' | awk '{print $2}' | tr -d '\r')

curl -sf http://localhost:8065/api/v4/teams \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"e2e-team","display_name":"E2E Team","type":"O"}' || true
```

### Running E2E tests

```bash
source ~/.nvm/nvm.sh && nvm use 20.15.0
npm ci && cd e2e && npm ci && cd ..
npm run build-test

cd e2e
export DISPLAY=:1
export MM_TEST_SERVER_URL=http://localhost:8065
export MM_TEST_USER_NAME=admin
export MM_TEST_PASSWORD=admin
npx playwright test <spec-file> --reporter=list --workers=1
cd ..
```

If a run leaves Electron hanging: `killall Electron 2>/dev/null || true`

### Fixing E2E tests

When asked to fix E2E failures:

1. **Start the Mattermost server** using the Docker instructions above.
2. **Read the CI logs** to identify which spec files failed. Use `gh run view --job <job-id> --log-failed`.
3. **Build the test bundle**: `npm run build-test`
4. **Reproduce** each failure locally before editing.
5. **Fix the test** — see `e2e/AGENTS.md` for classification and design rules.
6. **Re-run the spec** to confirm the fix, then commit.

### Login state propagation (common E2E flake)

After `loginToMattermost()` completes, the desktop app's `isLoggedIn` flag must travel through a multi-hop IPC chain before the renderer enables tab-bar interactions (`#newTabButton`). The `waitForLoggedIn()` helper in `e2e/helpers/login.ts` polls the main-process `ServerManager` directly, which is more reliable than waiting for the DOM element. Use it in `beforeAll` blocks for any test that interacts with the tab bar after login.
49 changes: 49 additions & 0 deletions e2e/helpers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import type {ServerView} from './serverView';

type ElectronApplication = Awaited<ReturnType<typeof import('playwright')['_electron']['launch']>>;

async function waitForAppShell(win: ServerView, timeout: number) {
const results = await Promise.allSettled([
win.waitForSelector('#post_textbox', {timeout}),
Expand Down Expand Up @@ -56,3 +58,50 @@ export async function loginToMattermost(win: ServerView): Promise<void> {
throw new Error(`loginToMattermost: login succeeded but the app shell never became ready. Current URL: ${await win.url()}`);
}
}

/**
* Poll the main process until ServerManager reports isLoggedIn=true for the
* current server, then wait for the renderer to reflect it (the #newTabButton
* appearing in the main window DOM).
*
* The web app's desktopAPI.onLogin() → TAB_LOGIN_CHANGED → ServerManager.setLoggedIn
* chain can lag behind the web app shell becoming visible. Tests that interact
* with the tab bar (new tab, drag-and-drop, close tab) need isLoggedIn=true in
* the renderer before proceeding.
*/
export async function waitForLoggedIn(
electronApp: ElectronApplication,
mainWindow: import('playwright').Page,
timeout = 60_000,
): Promise<void> {
const pollInterval = 500;
const deadline = Date.now() + timeout;

while (Date.now() < deadline) {
const loggedIn = await electronApp.evaluate(() => {
const refs = (global as any).__e2eTestRefs; // eslint-disable-line @typescript-eslint/no-explicit-any
if (!refs?.ServerManager) {
return false;
}
const serverId = refs.ServerManager.getCurrentServerId?.();
if (!serverId) {
return false;
}
const server = refs.ServerManager.getServer?.(serverId);
return Boolean(server?.isLoggedIn);
}).catch(() => false);

if (loggedIn) {
break;
}

if (Date.now() + pollInterval > deadline) {
throw new Error(
`waitForLoggedIn: ServerManager.isLoggedIn never became true within ${timeout}ms`,
);
}
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}

await mainWindow.waitForSelector('#newTabButton', {timeout: Math.max(deadline - Date.now(), 5_000)});
}
3 changes: 2 additions & 1 deletion e2e/specs/menu_bar/window_menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {waitForAppReady} from '../../helpers/appReadiness';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {buildServerMap} from '../../helpers/serverMap';
import {appDir, cmdOrCtrl, demoMattermostConfig, electronBinaryPath, writeConfigFile} from '../../helpers/config';
import {loginToMattermost} from '../../helpers/login';
import {loginToMattermost, waitForLoggedIn} from '../../helpers/login';

const windowMenuConfig = {
...demoMattermostConfig,
Expand Down Expand Up @@ -333,6 +333,7 @@ test.describe('Menu/window_menu', () => {
serverMap = await buildServerMap(electronApp);

await loginToMattermost(getMattermostServer());
await waitForLoggedIn(electronApp, mainWindow);
await focusMainWindow();
});

Expand Down
11 changes: 7 additions & 4 deletions e2e/specs/server_management/bad_servers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,17 @@ test.describe('Bad Server Configurations', () => {
};
const {app, userDataDir: badCertUserDataDir} = await launchWithConfig(testInfo, badConfig);
try {
// Ensure the renderer has mounted its IPC listeners before the load failure
// fires, then reload to re-trigger the failure so it reaches the UI.
await waitForRendererThenReload(app);

const mainWindow = app.windows().find((w) => w.url().includes('index'));
expect(mainWindow).toBeDefined();
await mainWindow!.waitForSelector('.ErrorView', {timeout: 30000});
const errorView = await mainWindow!.$('.ErrorView');

let errorView = await mainWindow!.$('.ErrorView');
if (!errorView) {
await waitForRendererThenReload(app);
await mainWindow!.waitForSelector('.ErrorView', {timeout: 60_000});
errorView = await mainWindow!.$('.ErrorView');
}
expect(errorView).toBeDefined();

const errorInfo = await mainWindow!.innerText('.ErrorView-techInfo');
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/server_management/drag_and_drop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {electronBinaryPath, appDir, demoMattermostConfig, writeConfigFile} from '../../helpers/config';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {loginToMattermost} from '../../helpers/login';
import {loginToMattermost, waitForLoggedIn} from '../../helpers/login';
import {buildServerMap} from '../../helpers/serverMap';

if (!process.env.MM_TEST_SERVER_URL) {
Expand Down Expand Up @@ -198,7 +198,7 @@ test.describe('server_management/drag_and_drop', () => {
mainWindow = await waitForWindow(electronApp, 'index');
const mmServer = await getMattermostServer();
await loginToMattermost(mmServer);
await mainWindow.waitForSelector('#newTabButton', {timeout: 30_000});
await waitForLoggedIn(electronApp, mainWindow);
});

test.beforeEach(async () => {
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/server_management/popout_windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {electronBinaryPath, appDir, demoMattermostConfig, writeConfigFile} from '../../helpers/config';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {loginToMattermost} from '../../helpers/login';
import {loginToMattermost, waitForLoggedIn} from '../../helpers/login';
import {buildServerMap} from '../../helpers/serverMap';

const config = {
Expand Down Expand Up @@ -163,7 +163,7 @@ test.describe('server_management/popout_windows', () => {
mainWindow = await waitForWindow(electronApp, 'index');
const mmServer = await getMattermostServer();
await loginToMattermost(mmServer);
await mainWindow.waitForSelector('#newTabButton', {timeout: 30_000});
await waitForLoggedIn(electronApp, mainWindow);
});

test.beforeEach(async () => {
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/server_management/tab_management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {electronBinaryPath, appDir, demoMattermostConfig, writeConfigFile} from '../../helpers/config';
import {waitForWindow, closeElectronApp} from '../../helpers/electronApp';
import {loginToMattermost} from '../../helpers/login';
import {loginToMattermost, waitForLoggedIn} from '../../helpers/login';
import {buildServerMap} from '../../helpers/serverMap';

const config = demoMattermostConfig;
Expand Down Expand Up @@ -79,7 +79,7 @@ test.describe('server_management/tab_management', () => {
mainWindow = await waitForWindow(electronApp, 'index');
const mmServer = await getMattermostServer();
await loginToMattermost(mmServer);
await mainWindow.waitForSelector('#newTabButton', {timeout: 30_000});
await waitForLoggedIn(electronApp, mainWindow);
});

test.beforeEach(async () => {
Expand Down
Loading