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
143 changes: 143 additions & 0 deletions .github/workflows/playwright-mock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Playwright E2E (Mock LLM)

on:
pull_request:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual e2e run'

permissions:
contents: read

concurrency:
group: playwright-mock-${{ github.ref }}
cancel-in-progress: true

env:
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'

jobs:
e2e:
name: Tier-1 smoke (headless Chromium)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Use Node.js 24.16.0
uses: actions/setup-node@v4
with:
node-version: '24.16.0'

- name: Restore node_modules cache
id: cache-node-modules
uses: actions/cache@v4
with:
path: |
node_modules
client/node_modules
packages/client/node_modules
packages/data-provider/node_modules
packages/data-schemas/node_modules
packages/api/node_modules
api/node_modules
key: node-modules-e2e-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci

- name: Restore data-provider build cache
id: cache-data-provider
uses: actions/cache@v4
with:
path: packages/data-provider/dist
key: build-data-provider-${{ runner.os }}-${{ hashFiles('package-lock.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}

- name: Build data-provider
if: steps.cache-data-provider.outputs.cache-hit != 'true'
run: npm run build:data-provider

- name: Restore data-schemas build cache
id: cache-data-schemas
uses: actions/cache@v4
with:
path: packages/data-schemas/dist
key: build-data-schemas-${{ runner.os }}-${{ hashFiles('package-lock.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}

- name: Build data-schemas
if: steps.cache-data-schemas.outputs.cache-hit != 'true'
run: npm run build:data-schemas

- name: Restore api build cache
id: cache-api
uses: actions/cache@v4
with:
path: packages/api/dist
key: build-api-${{ runner.os }}-${{ hashFiles('package-lock.json', 'packages/api/src/**', 'packages/api/tsconfig*.json', 'packages/api/server-rollup.config.js', 'packages/api/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json') }}

- name: Build api
if: steps.cache-api.outputs.cache-hit != 'true'
run: npm run build:api

- name: Restore client-package build cache
id: cache-client-package
uses: actions/cache@v4
with:
path: packages/client/dist
key: build-client-package-${{ runner.os }}-${{ hashFiles('package-lock.json', 'packages/client/src/**', 'packages/client/tsconfig*.json', 'packages/client/rollup.config.js', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}

- name: Build client-package
if: steps.cache-client-package.outputs.cache-hit != 'true'
run: npm run build:client-package

- name: Restore client app build cache
id: cache-client-app
uses: actions/cache@v4
with:
path: client/dist
key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package-lock.json', 'client/src/**', 'client/public/**', 'client/scripts/post-build.cjs', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tsconfig*.json', 'packages/client/rollup.config.js', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}

- name: Build client app
if: steps.cache-client-app.outputs.cache-hit != 'true'
run: npm run build:client

- name: Resolve Playwright version
id: pw
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"

- name: Cache Playwright browsers
id: cache-playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }}

- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium

- name: Run mock-LLM Tier-1 e2e
run: npx playwright test --config=e2e/playwright.config.mock.ts
env:
CI: 'true'

- name: Upload Playwright HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: e2e/playwright-report/**
Comment thread
danny-avila marked this conversation as resolved.
retention-days: 7
if-no-files-found: ignore

- name: Upload traces & screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-test-results
path: e2e/specs/.test-results/**
retention-days: 7
if-no-files-found: ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ archive
.vscode/settings.json
src/style - official.css
/e2e/specs/.test-results/
/e2e/.generated/
/e2e/playwright-report/
/playwright/.cache/
.DS_Store
Expand Down
5 changes: 4 additions & 1 deletion api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ const startServer = async () => {

// Configure stream services (auto-detects Redis from USE_REDIS env var)
const streamServices = createStreamServices();
GenerationJobManager.configure(streamServices);
GenerationJobManager.configure({
...streamServices,
cleanupOnComplete: !isEnabled(process.env.STREAM_KEEP_COMPLETED_JOBS),
});
GenerationJobManager.initialize();

const inspectFlags = process.execArgv.some((arg) => arg.startsWith('--inspect'));
Expand Down
11 changes: 8 additions & 3 deletions api/server/routes/oauth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jest.mock('@librechat/data-schemas', () => ({
}));

jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
ErrorTypes: {
AUTH_FAILED: 'auth_failed',
},
Expand Down Expand Up @@ -99,8 +100,6 @@ jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn(),
}));

const oauthRouter = require('./oauth');

afterAll(() => {
if (originalDomainClient === undefined) {
delete process.env.DOMAIN_CLIENT;
Expand All @@ -109,6 +108,11 @@ afterAll(() => {
process.env.DOMAIN_CLIENT = originalDomainClient;
});

function getOAuthRouter() {
jest.resetModules();
return require('./oauth');
}

function createApp(sessionMessages) {
const app = express();
app.use((req, _res, next) => {
Expand All @@ -117,7 +121,7 @@ function createApp(sessionMessages) {
}
next();
});
app.use('/oauth', oauthRouter);
app.use('/oauth', getOAuthRouter());
app.use((err, _req, res, _next) => {
res.status(500).json({ message: err.message });
});
Expand All @@ -136,6 +140,7 @@ describe('OAuth route failure logging', () => {
mockGetOAuthFailureMessage.mockClear();
mockRedirectToAuthFailure.mockClear();
mockPassportAuthenticate.mockClear();
mockOpenIDCallbackAuthenticatorOptions = undefined;
mockPassportAuthenticate.mockImplementation(() => (_req, _res, next) => next());
mockOpenIDCallbackMiddleware.mockImplementation((_req, _res, next) => next());
});
Expand Down
40 changes: 40 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# LibreChat e2e

The mock e2e profile is the safest default for generated tests. It starts LibreChat with `e2e/config/librechat.e2e.yaml`, points custom endpoints at the local mock LLM server, creates an authenticated e2e user, and avoids real provider credentials.

## Recording Tests

Use Playwright codegen when you want to turn an exploratory browser session into a draft test:

```sh
npm run e2e:record
```

That command builds the app, starts the mock LLM and LibreChat test server when needed, writes `e2e/storageState.json`, and opens Playwright codegen at `/c/new`. The npm script uses `http://localhost:3333` so it does not collide with a normal dev server on `3080`. Raw recordings are written to `e2e/recordings/` and ignored by git.

For a real local LibreChat config instead of the mock LLM profile:

```sh
npm run e2e:record:local
```

Useful direct options:

```sh
node e2e/setup/record.js --url=http://localhost:3080/c/new
node e2e/setup/record.js --profile=local --no-output
node e2e/setup/record.js --auth-only
node e2e/setup/record.js --output=e2e/recordings/settings-draft.spec.ts
```

## LLM-Assisted Loop

1. Start `npm run e2e:record`.
2. Let the LLM use Computer Use to operate the headed Playwright browser.
3. Stop codegen after the workflow is captured.
4. Move the useful parts from `e2e/recordings/` into a committed spec under `e2e/specs/mock/`.
5. Replace brittle generated selectors with role, label, text, or `data-testid` locators.
6. Add assertions that prove the behavior, not just the clicked path.
7. Run the finished spec with `npm run e2e:mock -- <spec name>`.

Generated recordings are a draft, not the final test. The committed version should use the shared helpers in `e2e/specs/mock/helpers.ts` where possible, wait on network or visible UI state instead of fixed sleeps, and keep test data deterministic.
26 changes: 26 additions & 0 deletions e2e/config/librechat.e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Credential-free e2e config template. e2e/playwright.config.mock.ts writes
# an ignored runtime copy and rewrites the mock LLM port from MOCK_LLM_PORT.
version: 1.3.11
cache: true

endpoints:
custom:
- name: 'Mock Provider A'
apiKey: 'e2e-mock-key-a'
baseURL: 'http://127.0.0.1:8889/v1'
Comment thread
danny-avila marked this conversation as resolved.
models:
default:
- 'mock-model-a'
fetch: false
titleConvo: false
modelDisplayLabel: 'Mock Provider A'

- name: 'Mock Provider B'
apiKey: 'e2e-mock-key-b'
baseURL: 'http://127.0.0.1:8889/v1'
models:
default:
- 'mock-model-b'
fetch: false
titleConvo: false
modelDisplayLabel: 'Mock Provider B'
Loading
Loading