Sign inGet started

Provides Playwright end-to-end testing patterns including Page Object Model structure, configuration, CI/CD integration, artifact management, and strategies for fixing flaky tests.

Lift+25ppscreen tier · directional
90
Trustsecurity · quality · license · upkeep
Scan Saferisk 0/100
LicenseMITserved via MCP
Skill-matched tasks (generated, public + held-out) · glm-5.3-flash@cf
+25pp95% CI [0, 75]unverified · screen tier
baseline 75% → skill 100% pass
trigger rate 100% · 12 trials

A/B outputs — median trial per arm

Same task, same model: without the skill (left) vs with it (right).

e2e-testing--m1
baseline · 1.00
Here's a simple Playwright test using the Page Object Model:

**`pages/LoginPage.js`**
```javascript
class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.getByLabel('Username');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Submit' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

module.exports = { LoginPage };
```

**`tests/login.spec.js`**
```javascript
const { test, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/LoginPage');

test('successful login redirects to dashboard', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('myuser', 'mypassword');

  await expect(page).toHaveURL(/dashboard/);
});

test('invalid credentials show an error', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('myuser', 'wrongpassword');

  await expect(loginPage.errorMessage).toBeVisible();
});
```

**Key ideas:**

- **Page Object** — the `LoginPage` class owns the locators and actions for that page, so selectors live in one place. If the UI changes, you update only the page object.
- **Tests stay readable** — they describe *what* the user does, not *how* the DOM is structured.
- **Locators** — I used `getByLabel` and `getByRole`, which are the recommended user-facing locators in Playwright. Adjust to your actual markup (e.g., `page.locator('#username')` if there are no labels).
- **Assertions stay in the test** — a common convention is to keep `expect` calls in the spec file rather than inside the page object.

Run it with `npx playwright test`. If you're using TypeScript, the same structure works with typed fields (`private readonly page: Page`).
with skill · 1.00
Here's a simple Page Object Model setup for your login page.

## 1. Page Object — `tests/pages/LoginPage.ts`

```typescript
import { Page, Locator, expect } from '@playwright/test'

export class LoginPage {
  readonly page: Page
  readonly usernameInput: Locator
  readonly passwordInput: Locator
  readonly submitButton: Locator
  readonly errorMessage: Locator

  constructor(page: Page) {
    this.page = page
    this.usernameInput = page.locator('[data-testid="username"]')
    this.passwordInput = page.locator('[data-testid="password"]')
    this.submitButton = page.locator('[data-testid="submit-btn"]')
    this.errorMessage = page.locator('[data-testid="error-message"]')
  }

  async goto() {
    await this.page.goto('/login')
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username)
    await this.passwordInput.fill(password)
    await this.submitButton.click()
  }
}
```

## 2. Test — `tests/e2e/auth/login.spec.ts`

```typescript
import { test, expect } from '@playwright/test'
import { LoginPage } from '../../pages/LoginPage'

test.describe('Login', () => {
  let loginPage: LoginPage

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page)
    await loginPage.goto()
  })

  test('logs in with valid credentials', async ({ page }) => {
    await loginPage.login('testuser', 'correct-password')

    // Adjust the assertion to whatever shows after login, e.g. a dashboard
    await expect(page).toHaveURL(/dashboard/)
  })

  test('shows an error with invalid credentials', async () => {
    await loginPage.login('testuser', 'wrong-password')

    await expect(loginPage.errorMessage).toBeVisible()
    await expect(loginPage.errorMessage).toContainText(/invalid/i)
  })
})
```

## Key points for a beginner

- **Selectors:** I used `[data-testid="..."]` — add `data-testid` attributes to your inputs/button if you can. They're the most stable selectors (they don't break when styling or text changes). If you can't, use `page.getByLabel('Username')` or `page.getByRole('button', { name: 'Sign in' })` instead.
- **No manual waits:** Playwright's `fill()` and `click()` auto-wait for elements, so you don't need `waitForTimeout`.
- **POM benefit:** The test reads like a description of behavior; if your login page's HTML changes, you only fix `LoginPage.ts`, not every test.
- **Run it:** `npx playwright test tests/e2e/auth/login.spec.ts`

Adjust the URL assertions and error text to match your app. Want me to show how to add a fixture so logged-in state is reusable across other tests?
e2e-testing--m2
baseline · 0.82
Here's a solid `playwright.config.ts` for CI with parallelism and failure artifacts:

```ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,          // run tests within files in parallel
  workers: process.env.CI ? 2 : undefined, // limit workers in CI; local uses defaults
  forbidOnly: !!process.env.CI, // fail if test.only sneaks into CI
  retries: process.env.CI ? 2 : 0,

  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
    trace: 'retain-on-failure', // trace saved only for failed tests
    screenshot: 'only-on-failure',
    video: 'retain-on-failure', // optional
    actionTimeout: 10_000,
    navigationTimeout: 15_000,
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    // { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    // { name: 'webkit',  use: { ...devices['Desktop Safari'] } },
  ],

  reporter: process.env.CI
    ? [['html', { open: 'never' }], ['junit', { outputFile: 'results/junit.xml' }]]
    : [['list']],
});
```

Key points:

- **Parallelism**: `fullyParallel: true` parallelizes tests *within* a file too. If your tests share state (same user, DB records), keep `fullyParallel: false` and rely on per-file workers, or use `test.describe.configure({ mode: 'parallel' })` selectively.
- **CI workers**: More workers isn't always better — start with 2–4 and scale based on your app's capacity under load.
- **Retries**: `retries: 2` in CI helps with flakiness; failed retries still produce artifacts.
- **Artifacts**: `trace: 'retain-on-failure'` is the sweet spot — traces are expensive to keep for every test. View them with `npx playwright show-trace test-results/.../trace.zip`.
- **CI integration**: In GitHub Actions, upload `test-results/` and `playwright-report/` with `actions/upload-artifact` using `if: always()` so artifacts survive failed runs. Playwright's official action (`microsoft/playwright-github-action`) or `npx playwright install --with-deps` in your setup step handles browsers.

If you're on GitHub Actions, also set `workers: process.env.CI ? 4 : undefined` only if your runners have enough cores — Playwright defaults to ~50% of CPU cores otherwise.
with skill · 1.00
Here's a `playwright.config.ts` tuned for CI with parallelism and failure artifacts:

```typescript
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e',

  // Parallelism
  fullyParallel: true,          // run tests within files in parallel too
  workers: process.env.CI ? 4 : undefined, // or omit to use all cores

  // CI safety
  forbidOnly: !!process.env.CI, // fail if test.only is committed
  retries: process.env.CI ? 2 : 0,

  reporter: [
    ['html', { outputFolder: 'playwright-report', open: 'never' }],
    ['junit', { outputFile: 'playwright-results.xml' }], // for CI test reporting
  ],

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',

    // Artifacts on failure
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',   // or 'on-first-retry' if you rely on retries
    video: 'retain-on-failure',

    actionTimeout: 10_000,
    navigationTimeout: 30_000,
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    // add firefox/webkit/mobile as needed
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
})
```

Key points:

- **Parallelism:** `fullyParallel: true` parallelizes tests *within* files, not just across files. If your tests share state (e.g., a shared DB user), either set `fullyParallel: false` and use `test.describe.configure({ mode: 'parallel' })` per file, or make each test fully independent (unique users/data per test).
- **Trace choice:** `trace: 'retain-on-failure'` always captures traces for failures; `'on-first-retry'` is lighter and pairs well with `retries: 2` — pick one based on whether you debug via retries or want traces on every failure.
- **CI upload:** Make sure artifacts survive the run, e.g. in GitHub Actions:

```yaml
- run: npx playwright test
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: |
      playwright-report/
      test-results/
    retention-days: 30
```

`test-results/` is where screenshots, videos, and traces land on failure — open traces with `npx playwright show-trace test-results/.../trace.zip`.
e2e-testing--m3
baseline · 0.82
# Making Playwright Tests More Reliable

## Core principle: use auto-waiting, not manual waits

**Avoid** `waitForTimeout()`, `sleep()`, or arbitrary delays — they're the #1 cause of flakiness (too short = fails, too long = slow).

## Clicking a button that appears after an API call

Playwright's locators auto-wait for elements to be visible, stable, and enabled, so often this just works:

```js
await page.getByRole('button', { name: 'Submit' }).click();
```

If the button is rendered conditionally *after* the API resolves, prefer waiting on the actual condition rather than the network:

```js
// Wait for the UI state that results from the API call
await page.getByRole('button', { name: 'Submit' }).waitFor({ state: 'visible' });
await page.getByRole('button', { name: 'Submit' }).click();
```

Or use `expect` with auto-retrying assertions (recommended):

```js
const button = page.getByRole('button', { name: 'Submit' });
await expect(button).toBeVisible();
await button.click();
```

## Waiting on network responses (when you truly need it)

```js
const responsePromise = page.waitForResponse(resp =>
  resp.url().includes('/api/submit') && resp.status() === 200
);
await page.getByRole('button', { name: 'Submit' }).click();
await responsePromise;
```

Set this up **before** triggering the action to avoid race conditions.

## Other key strategies

1. **Mock/stub the API** with `page.route()` so tests don't depend on real backend latency:
   ```js
   await page.route('**/api/user', route =>
     route.fulfill({ json: { name: 'Test User' } }));
   ```

2. **Prefer role/text-based locators** (`getByRole`, `getByText`, `getByLabel`) over brittle CSS/XPath selectors. Add `data-testid` attributes where semantic locators aren't possible.

3. **Use web-first assertions** — `expect(locator).toBeVisible()` retries automatically; `locator.isVisible()` does not.

4. **Avoid test interdependence** — each test should set up its own state. Use `storageState` for auth instead of logging in via UI in every test.

5. **Configure retries and workers** in `playwright.config.ts`:
   ```js
   retries: process.env.CI ? 2 : 0,
   workers: process.env.CI ? 2 : undefined,
   ```

6. **Trace on failure** for debugging CI-only flakiness:
   ```js
   use: { trace: 'on-first-retry' }
   ```

7. **Disable animations** which cause "element not stable" errors:
   ```css
   *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }
   ```

8. **Expect no race between assertion and navigation** — for clicks that trigger navigation, use `await page.waitForURL(...)` or Playwright's auto-waiting on `click()`.

The general rule: **wait for observable state (elements, assertions), never for time.**
with skill · 1.00
Here are the most effective strategies, using your example (clicking a button that appears after an API call):

## 1. Rely on auto-waiting locators, not `waitForTimeout`

Playwright locators auto-wait for the element to exist, be visible, and be actionable:

```typescript
// Bad: arbitrary sleep — slow when fast, flaky when slow
await page.waitForTimeout(3000);
await page.click('#submit');

// Good: auto-waits until the button is actionable
await page.locator('[data-testid="submit-btn"]').click();
```

## 2. Wait for the specific network response, not a timer

If the button appears after an API call, tie the wait to that call:

```typescript
const responsePromise = page.waitForResponse(
  resp => resp.url().includes('/api/data') && resp.status() === 200
);
await page.locator('[data-testid="load-btn"]').click();
await responsePromise;

// Now the button is guaranteed to be rendered from real data
await page.locator('[data-testid="confirm-btn"]').click();
```

## 3. Use web-first assertions

`expect(locator).toBeVisible()` retries automatically until the timeout — never use `isVisible()` in an `if` or plain `expect` on a boolean:

```typescript
await expect(page.locator('[data-testid="confirm-btn"]')).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="confirm-btn"]').click();
```

## 4. Prefer `waitForLoadState`/`waitForURL` over sleeps after navigation

```typescript
await page.waitForURL('**/dashboard');
// or
await page.waitForLoadState('networkidle'); // use sparingly — can hang with polling/websockets
```

## 5. Mock flaky/slow APIs

For CI stability, intercept the API so tests don't depend on backend latency:

```typescript
await page.route('**/api/data', route =>
  route.fulfill({ status: 200, json: { items: [/* deterministic data */] } })
);
```

## 6. Config-level hardening

```typescript
export default defineConfig({
  retries: process.env.CI ? 2 : 0,        // retry only in CI
  workers: process.env.CI ? 1 : undefined, // reduce parallelism if resources are constrained
  use: {
    trace: 'on-first-retry',               // debug flaky failures via trace viewer
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 10_000,
  },
});
```

## 7. Find and fix the flaky tests themselves

```bash
npx playwright test tests/search.spec.ts --repeat-each=10
```

Run suspects repeatedly locally to reproduce, then quarantine genuinely broken ones with `test.fixme(true, 'Issue #123')` while fixing the root cause.

**Key principle:** every wait should be tied to a *condition* (element visible, URL changed, response received), never a duration. Traces from retries (`trace: 'on-first-retry'`) are your best tool for diagnosing the remaining flaky cases — open them with `npx playwright show-trace trace.zip`.
e2e-testing--m4
baseline · 0.67
# Structuring a Large Playwright Suite

## Directory layout

```
tests/
  pages/                  # Page Objects
    base-page.ts
    login-page.ts
    checkout-page.ts
  components/             # Reusable UI fragments (header, modal, nav)
    header.component.ts
  fixtures/               # Custom fixtures extending base test
    test.ts
  utils/                  # Helpers: api clients, data builders, auth
    api-client.ts
    auth.ts
  data/                   # Test data, JSON fixtures
  e2e/                    # Test specs grouped by feature
    auth/
      login.spec.ts
    checkout/
      checkout.spec.ts
playwright.config.ts
```

## Page Object best practices

**1. One class per page, extend a shared base:**

```ts
// pages/base-page.ts
import { Page } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}

  async goto(path: string) {
    await this.page.goto(path);
  }
}
```

```ts
// pages/login-page.ts
import { Locator, Page, expect } from '@playwright/test';
import { BasePage } from './base-page';

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
    await expect(this.page).toHaveURL(/dashboard/);
  }
}
```

Key rules:

- **Locators as class fields**, resolved lazily — never store `page.locator(...)` results from `waitFor` or reuse stale element handles.
- **Prefer user-facing locators**: `getByRole`, `getByLabel`, `getByText`, `getByTestId`. Avoid brittle CSS/XPath selectors.
- **Expose behavior, not internals.** Tests call `loginPage.login(...)`, not `fill()` + `click()` sequences. Don't put assertions in page objects except where they define "readiness" (e.g., waiting for navigation).
- **No test data or branching logic in page objects** — pass data in as parameters.
- **Extract shared components** (header, nav, modals) into separate component classes that pages compose, so you don't duplicate locators across 30 page objects.

**2. Use custom fixtures instead of instantiating pages in every test:**

```ts
// fixtures/test.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

type Fixtures = { loginPage: LoginPage };

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});
export { expect } from '@playwright/test';
```

```ts
// e2e/auth/login.spec.ts
import { test, expect } from '../../fixtures/test';

test('user can log in', async ({ loginPage }) => {
  await loginPage.goto('/login');
  await loginPage.login('user@example.com', 'secret');
  await expect(loginPage.page.getByText('Welcome')).toBeVisible();
});
```

**3. Speed up with storage state.** Log in once in a setup project, save `storageState`, and reuse it in dependent projects so most tests skip the login UI:

```ts
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /auth\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});
```

**4. Parallelism & sharding.** Set `fullyParallel: true`, make tests independent (no shared state/ordering), and shard in CI.

## GitHub Actions integration

```yaml
name: E2E Tests
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      - run: npx playwright test --shard=${{ matrix.shard }}/4

      # Always upload artifacts, even on failure
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report-shard-${{ matrix.shard }}
          path: playwright-report/
          retention-days: 7

      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: test-results-shard-${{ matrix.shard }}
          path: test-results/
          retention-days: 7
          if-no-files-found: ignore
```

Config essentials:

```ts
// playwright.config.ts
export default defineConfig({
  reporter: process.env.CI
    ? [['html', { open: 'never' }], ['github']]
    : [['html', { open: 'on-failure' }]],
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'retain-on-failure',   // enables trace viewer debugging
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
});
```

Notes:

- `if: ${{ !cancelled() }}` ensures artifacts upload even when tests fail (unlike `always()` it respects job cancellation).
- The `github` reporter annotates failed tests directly on PRs.
- `test-results/` contains traces, screenshots, and videos for failed tests; `playwright-report/` is the HTML report. Download artifacts and open `trace.zip` via `npx playwright show-trace trace.zip`.
- For large suites, also consider `merge-reports` to combine shard reports into one HTML report, and caching browsers (`actions/cache` keyed on the Playwright version) if `--with
with skill · 0.74
# Structuring a Large Playwright Suite

## Directory Layout

```
tests/
├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── register.spec.ts
│   ├── features/
│   │   ├── browse.spec.ts
│   │   └── search.spec.ts
│   └── api/
│       └── endpoints.spec.ts
├── pages/          # Page objects
├── fixtures/       # Custom fixtures, test data
└── playwright.config.ts
```

## Page Object Model

Keep page objects as thin wrappers around locators + actions — no assertions inside them:

```typescript
import { Page, Locator } from '@playwright/test'

export class ItemsPage {
  readonly page: Page
  readonly searchInput: Locator
  readonly itemCards: Locator
  readonly createButton: Locator

  constructor(page: Page) {
    this.page = page
    this.searchInput = page.locator('[data-testid="search-input"]')
    this.itemCards = page.locator('[data-testid="item-card"]')
    this.createButton = page.locator('[data-testid="create-btn"]')
  }

  async goto() {
    await this.page.goto('/items')
  }

  async search(query: string) {
    await this.searchInput.fill(query)
    await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
  }

  async getItemCount() {
    return await this.itemCards.count()
  }
}
```

Key rules for maintainability at scale:

- **Use `data-testid` selectors** — resilient to CSS/JS refactors.
- **Locators as class fields** — created once in the constructor, auto-waiting, no `waitForTimeout`.
- **Wait for specific conditions** (`waitForResponse`, visibility) instead of arbitrary timeouts.
- **No assertions in page objects** — assertions belong in specs so failures read clearly.
- **Compose, don't inherit** — share common behavior via small helper modules or custom fixtures rather than deep class hierarchies.

## Test Files

```typescript
import { test, expect } from '@playwright/test'
import { ItemsPage } from '../../pages/ItemsPage'

test.describe('Item Search', () => {
  let itemsPage: ItemsPage

  test.beforeEach(async ({ page }) => {
    itemsPage = new ItemsPage(page)
    await itemsPage.goto()
  })

  test('should search by keyword', async () => {
    await itemsPage.search('test')
    expect(await itemsPage.getItemCount()).toBeGreaterThan(0)
    await expect(itemsPage.itemCards.first()).toContainText(/test/i)
  })
})
```

One spec file per feature/flow, grouped by domain folder. Use custom fixtures (`tests/fixtures/auth.ts`) for login state so specs don't repeat auth boilerplate.

## Playwright Config

```typescript
export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'playwright-results.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})
```

## GitHub Actions Workflow

```yaml
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
        env:
          BASE_URL: ${{ vars.STAGING_URL }}
      - uses: actions/upload-artifact@v4
        if: always()          # upload even when tests fail
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
```

Notes:

- `if: always()` is essential — otherwise failed runs upload nothing, which is exactly when you need the report.
- `trace: 'on-first-retry'` + `retries: 2` in CI gives you a trace zip for every flaky failure; open it with `npx playwright show-trace`.
- For large suites, consider `--shard` across multiple CI jobs to cut wall-clock time, and upload per-shard artifacts.

## Flakiness Hygiene

- Diagnose flaky tests with `--repeat-each=10` and `--retries=3` locally.
- Quarantine known-flaky tests with `test.fixme(...)` referencing an issue number, rather than letting them erode trust in the suite.
- Prefer auto-waiting locators and explicit condition waits over `waitForTimeout`.
Limitations
  • Agent noise: single-model results vary run to run; the confidence interval reflects task-level variance.
  • Screen tier (1 trial/arm) is a directional gate, not a verified claim.
  • Results are for the SkillzBench Reference Agent on glm-5.3-flash@cf and do not necessarily transfer to other agents or models.
  • Task coverage: Skill-matched tasks (generated, public + held-out) only; a skill can shine elsewhere.