@keber/qa-framework 1.10.0 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # -----------------------------------------------------------------------
2
+ # Integration test environment variables
3
+ # -----------------------------------------------------------------------
4
+ # Copy this file to .env and fill in real values.
5
+ # NEVER commit .env to source control.
6
+ # -----------------------------------------------------------------------
7
+
8
+ # -----------------------------------------------------------------------
9
+ # Application under test
10
+ # -----------------------------------------------------------------------
11
+ QA_BASE_URL=https://your-app.qa.example.com
12
+
13
+ # -----------------------------------------------------------------------
14
+ # Auth - choose one strategy
15
+ # -----------------------------------------------------------------------
16
+
17
+ # Strategy 1: Static bearer token (preferred for pure API services)
18
+ # QA_API_TOKEN=your-api-token-here
19
+
20
+ # Strategy 2: Browser login (for apps with cookie/session auth)
21
+ # QA_USER_EMAIL=qa-user@example.com
22
+ # QA_USER_PASSWORD=CHANGE_ME
23
+
24
+ # -----------------------------------------------------------------------
25
+ # Login form selectors (used only when QA_API_TOKEN is not set)
26
+ # -----------------------------------------------------------------------
27
+ # QA_LOGIN_PATH=/login
28
+ # QA_LOGIN_EMAIL_SELECTOR=input[type="email"]
29
+ # QA_LOGIN_PASSWORD_SELECTOR=input[type="password"]
30
+ # QA_LOGIN_SUBMIT_SELECTOR=button[type="submit"]
31
+ # QA_LOGIN_SUCCESS_SELECTOR=.dashboard
32
+
33
+ # -----------------------------------------------------------------------
34
+ # Azure DevOps integration (optional)
35
+ # -----------------------------------------------------------------------
36
+ # ADO_ORG=your-org
37
+ # ADO_PROJECT=YourProject
38
+ # ADO_PLAN_ID=00000
39
+ # ADO_PAT=your-pat-here
40
+ # SYSTEM_ACCESSTOKEN= # Set automatically in Azure Pipelines
@@ -0,0 +1,77 @@
1
+ # API Integration Tests
2
+
3
+ Playwright-based API test project for `qa/07-automation/integration/`.
4
+
5
+ Tests run without a browser. All tests use Playwright's `APIRequestContext`
6
+ to call HTTP endpoints directly. Auth state is established once by
7
+ `global-setup.ts` and reused across the test run.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 18+
12
+ - `@playwright/test` (installed via `npm install` in this directory)
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ cd qa/07-automation/integration
18
+ npm install
19
+ cp .env.example .env
20
+ # Edit .env and fill in QA_BASE_URL and auth credentials
21
+ ```
22
+
23
+ Add the following to your root `.gitignore`:
24
+
25
+ ```
26
+ qa/07-automation/integration/.env
27
+ qa/07-automation/integration/.auth/
28
+ qa/07-automation/integration/test-results/
29
+ qa/07-automation/integration/playwright-report/
30
+ ```
31
+
32
+ ## Environment variables
33
+
34
+ | Variable | Required | Description |
35
+ |---|---|---|
36
+ | `QA_BASE_URL` | Yes | Base URL of the API under test |
37
+ | `QA_API_TOKEN` | One of | Static bearer token for token-based APIs |
38
+ | `QA_USER_EMAIL` | One of | Email/username for browser-login APIs |
39
+ | `QA_USER_PASSWORD` | One of | Password for browser-login APIs |
40
+
41
+ See `.env.example` for all supported variables.
42
+
43
+ ## Running tests
44
+
45
+ ```bash
46
+ # All tests
47
+ npm test
48
+
49
+ # CI mode (retries enabled)
50
+ npm run test:ci
51
+
52
+ # Open HTML report
53
+ npm run report
54
+ ```
55
+
56
+ ## Writing tests
57
+
58
+ Import from `@playwright/test` and use the `request` fixture:
59
+
60
+ ```typescript
61
+ import { test, expect } from '@playwright/test';
62
+
63
+ test('GET /users returns 200', async ({ request }) => {
64
+ const response = await request.get('/users');
65
+ expect(response.status()).toBe(200);
66
+ });
67
+ ```
68
+
69
+ For token-based APIs that require an `Authorization` header, pass it per request:
70
+
71
+ ```typescript
72
+ const response = await request.get('/api/protected', {
73
+ headers: { Authorization: `Bearer ${process.env.QA_API_TOKEN}` },
74
+ });
75
+ ```
76
+
77
+ See `tests/example.spec.ts` for a starter test to replace.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * global-setup.ts - Integration (API-only) project
3
+ *
4
+ * Runs once before all test files to establish auth state.
5
+ *
6
+ * Two strategies, tried in order:
7
+ * 1. Token-based: QA_API_TOKEN is set -> write a minimal storageState with the
8
+ * token in localStorage so the `request` fixture picks it up automatically.
9
+ * 2. Browser login: QA_USER_EMAIL + QA_USER_PASSWORD are set -> launch Chromium,
10
+ * perform the login flow, and save the resulting storageState to
11
+ * .auth/api-session.json.
12
+ *
13
+ * Playwright's `storageState` mechanism handles cookies and localStorage for
14
+ * APIRequestContext automatically when `use.storageState` is configured.
15
+ * For token-only APIs that require an Authorization header, set the header
16
+ * directly in each test or extend the `request` fixture in fixtures/base.ts.
17
+ */
18
+
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ import { chromium } from '@playwright/test';
22
+ import * as dotenv from 'dotenv';
23
+
24
+ dotenv.config();
25
+
26
+ const AUTH_FILE = path.join(__dirname, '.auth', 'api-session.json');
27
+
28
+ export default async function globalSetup(): Promise<void> {
29
+ fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
30
+
31
+ if (process.env.QA_API_TOKEN) {
32
+ await setupWithToken(process.env.QA_API_TOKEN, process.env.QA_BASE_URL!);
33
+ return;
34
+ }
35
+
36
+ await setupWithBrowserLogin();
37
+ }
38
+
39
+ /**
40
+ * Write a minimal storageState that carries the bearer token in localStorage.
41
+ * Replace the localStorage key/value format if your app uses a different scheme
42
+ * (e.g. a cookie, or a different localStorage key name).
43
+ */
44
+ async function setupWithToken(token: string, baseURL: string): Promise<void> {
45
+ const origin = baseURL.replace(/\/$/, '');
46
+
47
+ const state = {
48
+ cookies: [],
49
+ origins: [
50
+ {
51
+ origin,
52
+ localStorage: [
53
+ { name: 'qa_api_token', value: token },
54
+ ],
55
+ },
56
+ ],
57
+ };
58
+
59
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2), 'utf-8');
60
+ console.log('[integration-setup] Token-based auth written to api-session.json');
61
+ }
62
+
63
+ /**
64
+ * Perform a browser login and save the resulting cookies + localStorage to disk.
65
+ *
66
+ * Configure login form selectors via env vars (see .env.example) or update the
67
+ * defaults below to match your application's login page.
68
+ */
69
+ async function setupWithBrowserLogin(): Promise<void> {
70
+ const baseURL = process.env.QA_BASE_URL!.replace(/\/$/, '');
71
+ const email = process.env.QA_USER_EMAIL!;
72
+ const password = process.env.QA_USER_PASSWORD!;
73
+ const loginPath = process.env.QA_LOGIN_PATH ?? '/login';
74
+ const emailSelector = process.env.QA_LOGIN_EMAIL_SELECTOR ?? 'input[type="email"]';
75
+ const passwordSelector = process.env.QA_LOGIN_PASSWORD_SELECTOR ?? 'input[type="password"]';
76
+ const submitSelector = process.env.QA_LOGIN_SUBMIT_SELECTOR ?? 'button[type="submit"]';
77
+ const successSelector = process.env.QA_LOGIN_SUCCESS_SELECTOR ?? '.dashboard, .main-content, [data-testid="app-shell"]';
78
+
79
+ console.log('[integration-setup] Performing browser login...');
80
+
81
+ const browser = await chromium.launch();
82
+ const context = await browser.newContext();
83
+ const page = await context.newPage();
84
+
85
+ try {
86
+ await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'load', timeout: 60_000 });
87
+
88
+ await page.locator(emailSelector).waitFor({ state: 'visible', timeout: 30_000 });
89
+ await page.locator(emailSelector).fill(email);
90
+ await page.locator(passwordSelector).fill(password);
91
+ await page.locator(submitSelector).click();
92
+ await page.waitForSelector(successSelector, { timeout: 60_000 });
93
+
94
+ await context.storageState({ path: AUTH_FILE });
95
+ console.log(`[integration-setup] Auth saved: ${AUTH_FILE}`);
96
+ } finally {
97
+ await browser.close();
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "qa-integration-tests",
3
+ "version": "1.0.0",
4
+ "description": "API integration tests - no browser required",
5
+ "private": true,
6
+ "scripts": {
7
+ "test": "playwright test",
8
+ "test:ci": "CI=true playwright test",
9
+ "report": "playwright show-report"
10
+ },
11
+ "devDependencies": {
12
+ "@playwright/test": "^1.44.0",
13
+ "dotenv": "^16.4.0"
14
+ }
15
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * playwright.config.ts - API integration test project (no browser)
3
+ *
4
+ * All tests use APIRequestContext directly. No browser projects are defined.
5
+ * Auth is handled by global-setup.ts, which writes .auth/api-session.json.
6
+ *
7
+ * Required env vars:
8
+ * QA_BASE_URL - Base URL of the API under test
9
+ *
10
+ * Auth (at least one of the following):
11
+ * QA_API_TOKEN - Bearer token (skips browser login in global-setup)
12
+ * QA_USER_EMAIL + QA_USER_PASSWORD - Credentials for browser-based login
13
+ */
14
+
15
+ import { defineConfig } from '@playwright/test';
16
+ import * as dotenv from 'dotenv';
17
+
18
+ dotenv.config();
19
+
20
+ const required = ['QA_BASE_URL'];
21
+ for (const key of required) {
22
+ if (!process.env[key]) {
23
+ throw new Error(`[integration] Missing required env var: ${key}. Check your .env file.`);
24
+ }
25
+ }
26
+
27
+ const hasToken = !!process.env.QA_API_TOKEN;
28
+ const hasCredentials = !!(process.env.QA_USER_EMAIL && process.env.QA_USER_PASSWORD);
29
+
30
+ if (!hasToken && !hasCredentials) {
31
+ throw new Error(
32
+ '[integration] No auth configured. Set QA_API_TOKEN, or both QA_USER_EMAIL and QA_USER_PASSWORD.',
33
+ );
34
+ }
35
+
36
+ // Optional: Azure DevOps reporter configuration
37
+ // To enable: set env vars ADO_ORG, ADO_PROJECT, ADO_PAT, ADO_PLAN_ID and CI=true,
38
+ // then uncomment the reporter entry in the `reporter` array below.
39
+ // Install: npm install @alex_neo/playwright-azure-reporter --save-dev
40
+ // const adoReporterConfig = {
41
+ // orgUrl: `https://dev.azure.com/${process.env.ADO_ORG}`,
42
+ // token: process.env.SYSTEM_ACCESSTOKEN ?? process.env.ADO_PAT,
43
+ // planId: Number(process.env.ADO_PLAN_ID),
44
+ // projectName: process.env.ADO_PROJECT!,
45
+ // testRunTitle: `[Auto API] ${new Date().toISOString().slice(0, 10)}`,
46
+ // publishTestResultsMode: 'testRun' as const,
47
+ // uploadAttachments: false,
48
+ // isDisabled: !process.env.CI,
49
+ // autoMarkTestCasesAsAutomated: {
50
+ // enabled: true,
51
+ // updateAutomatedTestName: true,
52
+ // updateAutomatedTestStorage: true,
53
+ // },
54
+ // };
55
+
56
+ export default defineConfig({
57
+ testDir: './tests',
58
+ fullyParallel: false,
59
+ workers: 1,
60
+ timeout: 30_000,
61
+ retries: process.env.CI ? 1 : 0,
62
+
63
+ globalSetup: './global-setup.ts',
64
+
65
+ reporter: [
66
+ ['list'],
67
+ ['html', { open: 'never', outputFolder: 'playwright-report' }],
68
+ // Uncomment for ADO:
69
+ // ['@alex_neo/playwright-azure-reporter', adoReporterConfig],
70
+ ],
71
+
72
+ use: {
73
+ baseURL: process.env.QA_BASE_URL,
74
+ storageState: '.auth/api-session.json',
75
+ },
76
+
77
+ outputDir: 'test-results/',
78
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * example.spec.ts - API integration test example
3
+ *
4
+ * This file shows the basic pattern for API tests using Playwright's
5
+ * APIRequestContext. Replace this with your real tests.
6
+ *
7
+ * The `request` fixture is pre-configured with:
8
+ * - baseURL from QA_BASE_URL
9
+ * - storageState from .auth/api-session.json (set by global-setup.ts)
10
+ *
11
+ * For token-based APIs that require an Authorization header, add it
12
+ * explicitly per request or extend the fixture in a fixtures/base.ts file.
13
+ */
14
+
15
+ import { test, expect } from '@playwright/test';
16
+
17
+ // Replace with the actual health/status endpoint of your API.
18
+ const HEALTH_ENDPOINT = '/health';
19
+
20
+ test.describe('API smoke tests', () => {
21
+
22
+ test('GET health endpoint returns 200', async ({ request }) => {
23
+ const response = await request.get(HEALTH_ENDPOINT);
24
+
25
+ expect(response.status()).toBe(200);
26
+ });
27
+
28
+ // Example of a test with an Authorization header when QA_API_TOKEN is set.
29
+ // Uncomment and adapt when your API uses Bearer token auth.
30
+ //
31
+ // test('GET protected resource returns 200 with bearer token', async ({ request }) => {
32
+ // const response = await request.get('/api/me', {
33
+ // headers: {
34
+ // Authorization: `Bearer ${process.env.QA_API_TOKEN}`,
35
+ // },
36
+ // });
37
+ //
38
+ // expect(response.status()).toBe(200);
39
+ // const body = await response.json();
40
+ // expect(body).toHaveProperty('id');
41
+ // });
42
+
43
+ });
@@ -0,0 +1,36 @@
1
+ # CI Pipeline Findings
2
+
3
+ > Document CI pipeline issues, root causes, and applied fixes as they are discovered.
4
+ > This file is loaded by the agent when diagnosing CI failures or reviewing pipeline changes.
5
+
6
+ ## Applied Fixes
7
+
8
+ Document each fix that was applied to the pipeline or test setup. Use a table or prose - whatever fits the change.
9
+
10
+ | Problem | Root cause | Fix applied |
11
+ |---|---|---|
12
+ | _Add pipeline issue here_ | _Root cause_ | _What was changed and where_ |
13
+
14
+ > Examples: auth setup defaults, missing reporter packages, artifact name collisions,
15
+ > browser binary not installed in a CI stage, grep flag escaping issues.
16
+
17
+ ## Known Limitations
18
+
19
+ Document constraints that are accepted but not fixed (timeout budgets, worker counts, retry behavior).
20
+
21
+ > Examples:
22
+ > - Full suite exceeds the CI timeout with 1 worker. Workaround: run only P0/P1 tags by default.
23
+ > - `retries: 2` counts each attempt separately, so reported execution count exceeds test count.
24
+ > - Attachment upload warnings on `PublishTestResults` when multiple failures share filenames.
25
+
26
+ ## Failure Triage Categories
27
+
28
+ Use these categories when classifying CI failures. Add project-specific categories as needed.
29
+
30
+ | Category | Description | Action |
31
+ |---|---|---|
32
+ | TIMEOUT | Test exceeded its time budget | Adjust `timeout` in the spec or in `playwright.config.ts` |
33
+ | SESSION | Auth session expired mid-run | Implement session-refresh fixture or reduce suite duration |
34
+ | FLAKY | Intermittent failure, passes on retry | Acceptable if retries cover it; investigate if failure rate > 20% |
35
+ | APP-DEFECT | Application does not meet the spec | Open defect file; mark test with `test.fail()` |
36
+ | TEST-CODE | Selector or assertion is wrong in CI | Fix the spec file |
@@ -0,0 +1,70 @@
1
+ # Data Volatility Strategies
2
+
3
+ > The QA environment is typically shared between automated tests and manual testers.
4
+ > Data (record counts, available filter values, document states) changes constantly,
5
+ > which invalidates hardcoded values in specs. Document the chosen strategy here.
6
+
7
+ ## Problem
8
+
9
+ List the types of data that are volatile in this project's QA environment.
10
+
11
+ | Data type | Example of fragile assertion | Tests affected |
12
+ |---|---|---|
13
+ | Filter option values | `hasText: 'specific-value'` | _Add count here_ |
14
+ | Exact row counts | `toBe(33)` | _Add count here_ |
15
+ | Record names used as search inputs | `fill('specific-record-name')` | _Add count here_ |
16
+ | Process or transaction IDs | `TEST_ID = '608'` | _Add count here_ |
17
+
18
+ ## Strategy 1: Shared Constants with Dynamic Discovery
19
+
20
+ Maintain a `fixtures/test-data-constants.ts` file with well-named constants. Populate them
21
+ from a discovery step (global-setup or a `beforeAll` fixture) that reads the actual environment.
22
+
23
+ ```typescript
24
+ // fixtures/test-data-constants.ts
25
+ export const QA_ITEM_CURRENT = 'current-item-name'; // most recent available
26
+ export const QA_ITEM_OLDEST = 'oldest-item-name'; // oldest available
27
+ export const QA_ITEM_INVALID = 'nonexistent-item'; // guaranteed absent
28
+ ```
29
+
30
+ **Tradeoff:** eliminates hardcoded filter values centrally, but discovery adds startup time
31
+ and blocks the suite if it fails.
32
+
33
+ ## Strategy 2: Resilient Assertions (Patterns and Ranges)
34
+
35
+ Replace exact-value assertions with pattern or range checks.
36
+
37
+ ```typescript
38
+ // Instead of:
39
+ expect(count).toBe(33);
40
+ await expect(counter).toHaveText('1-5 of 5');
41
+
42
+ // Use:
43
+ expect(count).toBeGreaterThan(0);
44
+ await expect(counter).toHaveText(/\d+-\d+ of \d+/);
45
+ ```
46
+
47
+ **Tradeoff:** no extra infrastructure, each test is self-contained, but precision is lower.
48
+ Useful for count assertions; less useful for filter-value selection.
49
+
50
+ ## Strategy 3: Snapshot Delta Validation
51
+
52
+ Capture the initial state before applying a filter, then assert that the filtered state
53
+ differs from the initial state in the expected direction (fewer rows, etc.).
54
+
55
+ **Tradeoff:** maximum resilience, but tests become longer and harder to trace. Use only for
56
+ critical filter tests where behavior (not data) must be validated.
57
+
58
+ ## Chosen Approach
59
+
60
+ > Document which strategy (or combination) was chosen for this project, and which test
61
+ > categories each strategy applies to.
62
+
63
+ | Test category | Strategy | Status |
64
+ |---|---|---|
65
+ | Filter option values | Strategy 1 or 2 | _Pending / Implemented_ |
66
+ | Row count assertions | Strategy 2 | _Pending / Implemented_ |
67
+ | Search input values | Strategy 2 or 3 | _Pending / Implemented_ |
68
+
69
+ > Tests that are blocked on a data strategy can be marked `test.skip()` with a
70
+ > `PENDING-DATA-STRATEGY` annotation until the strategy is implemented.
@@ -0,0 +1,54 @@
1
+ # E2E Stabilization Patterns
2
+
3
+ > A running log of flakiness patterns found in this project and the techniques used to fix them.
4
+ > Load this file when investigating intermittent test failures or refactoring fragile specs.
5
+
6
+ ## Crash-Recovery Pattern
7
+
8
+ When the application server crashes intermittently (e.g. unhandled error page with a Reload link),
9
+ a bare `waitFor` will timeout. Use `Promise.race` to detect the error state and recover:
10
+
11
+ ```typescript
12
+ const outcome = await Promise.race([
13
+ targetElement.waitFor({ state: 'visible', timeout: 60_000 }).then(() => 'ready' as const),
14
+ page.getByText('An unhandled error has occurred.').waitFor({ state: 'visible', timeout: 60_000 }).then(() => 'crash' as const),
15
+ ]).catch(() => 'timeout' as const);
16
+
17
+ if (outcome === 'crash') {
18
+ await page.getByRole('link', { name: 'Reload' }).click();
19
+ await targetElement.waitFor({ state: 'visible', timeout: 60_000 });
20
+ }
21
+ ```
22
+
23
+ > Document which spec files use this pattern and under what conditions it fires.
24
+
25
+ ## Strict-Mode Selector Conflicts
26
+
27
+ `getByText()` and similar matchers can match multiple elements, triggering strict-mode violations.
28
+ Scope selectors to the most specific container available.
29
+
30
+ > Document conflicts found in this project here.
31
+ > Example: a pagination counter regex that also matches a card element.
32
+ > Fix: use a scoped `locator('.specific-class')` instead of a broad text match.
33
+
34
+ ## Known Flaky Failure Types
35
+
36
+ Document categories of instability specific to this project. A row per pattern is enough.
37
+
38
+ | ID | Root cause | Fix | Affected tests |
39
+ |---|---|---|---|
40
+ | _Add ID_ | _Describe the underlying cause_ | _Technique used to fix_ | _Spec files or TC IDs_ |
41
+
42
+ > Examples: framework hydration delays, input binding issues requiring `pressSequentially`,
43
+ > elements that are in the DOM but not yet rendered, session expiry during long runs.
44
+
45
+ ## Reference Timeouts
46
+
47
+ Fill in the timeout values used in this project once they are stabilized.
48
+
49
+ | Setting | Value | Location |
50
+ |---|---|---|
51
+ | Default test timeout | - | `playwright.config.ts` |
52
+ | `actionTimeout` | - | `playwright.config.ts` |
53
+ | `navigationTimeout` | - | `playwright.config.ts` |
54
+ | Long-running spec timeout | - | Per-spec `describe.configure` |
@@ -24,7 +24,15 @@ This project uses `@keber/qa-framework` v{{VERSION}} for spec-driven automated t
24
24
  1. Generate the output in `UTF-8` encoding without BOM (`UTF-8`, no signature).
25
25
  2. Ensure the raw output is `UTF-8` encoded with no `EF BB BF` bytes at the beginning.
26
26
 
27
- {{ADO_SECTION}}
27
+ ## Azure DevOps integration
28
+
29
+ Before any ADO operation, check if `.github/skills/ado-qa/` exists or if
30
+ `integrations.ado.enabled` is `true` in `qa/qa-framework.config.json`. If either
31
+ condition is met, load `.github/skills/qa-ado-integration/SKILL.md` and use it for
32
+ all ADO interactions (work items, test plans, test cases, bugs, etc.).
33
+
34
+ > To enable Azure DevOps integration run: `npm install github:keber/ado-qa`
35
+
28
36
  ## QA Pipeline
29
37
 
30
38
  Stages must run in order. Never start a stage unless its prerequisite is met.
@@ -2,6 +2,7 @@
2
2
 
3
3
  > Framework: `@keber/qa-framework` v{VERSION}
4
4
  > Language: {LANGUAGE} | Base URL: `{QA_BASE_URL}`
5
+ > ADO Project: `{ADO_ORG} / {ADO_PROJECT}` _(optional — remove line if not using Azure DevOps)_
5
6
 
6
7
  ---
7
8
 
@@ -47,12 +48,32 @@ npx playwright test --last-failed
47
48
 
48
49
  ---
49
50
 
51
+ ## Last Run
52
+
53
+ | Suite | Date | Pass | Fail | Skip | CI Link |
54
+ |---|---|---|---|---|---|
55
+ | E2E | — | — | — | — | — |
56
+ | API | — | — | — | — | — |
57
+
58
+ > Update this table after each significant run. The CI Link can be an ADO TestRun URL, a
59
+ > GitHub Actions run URL, or a path to a local report file.
60
+
50
61
  ## Last Execution
51
62
 
52
63
  | Date | Suite | Pass | Skip | Fail | Duration | Report |
53
64
  |---|---|---|---|---|---|---|
54
65
  | — | — | — | — | — | — | — |
55
66
 
67
+ ## Flaky Tests
68
+
69
+ > Mark known flaky tests with this inline annotation directly in the table or spec file.
70
+ > Format: `[flaky] <test-id-or-name> - <root cause> - last seen <YYYY-MM-DD>`
71
+ >
72
+ > Example:
73
+ > `[flaky] TC-MOD-012 - timing issue on slow CI agents - last seen 2026-01-15`
74
+ >
75
+ > Remove the annotation once the test has been stable for 2+ consecutive CI runs.
76
+
56
77
  ---
57
78
 
58
79
  ## Sprint History