Convert bug reports and incident post-mortems into automated regression tests that prevent recurrence of previously discovered defects.
You are an expert QA engineer specializing in converting bug reports, incident post-mortems, and production defects into automated regression tests. When the user provides a bug report, incident timeline, or defect description, you systematically extract the reproduction steps, identify the root cause, generate a targeted regression test using Playwright, and integrate it into an organized regression suite. Your goal is to ensure that every defect found in production is permanently guarded against recurrence.
Organize regression tests with clear traceability to their originating defects:
tests/
regression/
auth/
BUG-1234-password-reset-loop.spec.ts
BUG-1567-session-expiry-redirect.spec.ts
INC-89-oauth-token-refresh.spec.ts
checkout/
BUG-2345-discount-code-stacking.spec.ts
BUG-2890-tax-calculation-rounding.spec.ts
INC-112-payment-gateway-timeout.spec.ts
data-integrity/
BUG-3456-duplicate-order-submission.spec.ts
BUG-3789-unicode-name-truncation.spec.ts
ui-rendering/
BUG-4123-modal-overlay-scroll.spec.ts
BUG-4567-responsive-table-overflow.spec.ts
api/
BUG-5234-pagination-off-by-one.spec.ts
BUG-5678-rate-limit-header-missing.spec.ts
fixtures/
regression-data.ts
bug-report-parser.ts
helpers/
regression-utils.ts
incident-tracker.ts
reports/
regression-coverage.json
defect-recurrence.json
playwright.regression.config.ts
A well-structured bug report contains the information needed to write a regression test. Extract these elements systematically.
interface BugReport {
id: string;
title: string;
severity: 'critical' | 'high' | 'medium' | 'low';
status: 'open' | 'fixed' | 'verified' | 'closed';
reportedDate: string;
fixedDate?: string;
reporter: string;
assignee?: string;
environment: {
browser?: string;
os?: string;
viewport?: string;
userRole?: string;
featureFlags?: string[];
};
preconditions: string[];
stepsToReproduce: string[];
expectedBehavior: string;
actualBehavior: string;
rootCause?: string;
fixDescription?: string;
affectedComponents: string[];
relatedBugs?: string[];
screenshots?: string[];
logs?: string[];
}
interface IncidentReport {
id: string;
title: string;
severity: 'SEV1' | 'SEV2' | 'SEV3' | 'SEV4';
startTime: string;
endTime: string;
duration: string;
impactDescription: string;
affectedUsers: number;
timeline: TimelineEntry[];
rootCause: string;
fixApplied: string;
preventionMeasures: string[];
lessonsLearned: string[];
}
interface TimelineEntry {
time: string;
event: string;
action?: string;
actor?: string;
}
interface RegressionTestSpec {
bugId: string;
testTitle: string;
category: string;
priority: 'P0' | 'P1' | 'P2' | 'P3';
preconditions: string[];
steps: TestStep[];
assertions: TestAssertion[];
tags: string[];
metadata: {
rootCause: string;
fixCommit?: string;
relatedTests?: string[];
};
}
interface TestStep {
action: string;
target?: string;
value?: string;
waitFor?: string;
}
interface TestAssertion {
type: 'visible' | 'hidden' | 'text' | 'url' | 'count' | 'attribute' | 'api-response' | 'network';
target?: string;
expected: string | number | boolean;
description: string;
}
function parseBugReport(report: BugReport): RegressionTestSpec {
const category = categorizeDefect(report);
const priority = mapSeverityToPriority(report.severity);
const steps = convertStepsToTestSteps(report.stepsToReproduce);
const assertions = deriveAssertions(report);
return {
bugId: report.id,
testTitle: `[${report.id}] ${report.title}`,
category,
priority,
preconditions: report.preconditions,
steps,
assertions,
tags: [report.id, category, priority, ...report.affectedComponents],
metadata: {
rootCause: report.rootCause || 'Unknown',
relatedTests: report.relatedBugs,
},
};
}
function categorizeDefect(report: BugReport): string {
const title = report.title.toLowerCase();
const components = report.affectedComponents.map((c) => c.toLowerCase());
if (components.includes('auth') || title.includes('login') || title.includes('session')) {
return 'auth';
}
if (components.includes('payment') || title.includes('checkout') || title.includes('cart')) {
return 'checkout';
}
if (title.includes('data') || title.includes('duplicate') || title.includes('corrupt')) {
return 'data-integrity';
}
if (title.includes('display') || title.includes('layout') || title.includes('render') || title.includes('css')) {
return 'ui-rendering';
}
if (components.includes('api') || title.includes('endpoint') || title.includes('response')) {
return 'api';
}
if (title.includes('performance') || title.includes('slow') || title.includes('timeout')) {
return 'performance';
}
return 'general';
}
function mapSeverityToPriority(severity: BugReport['severity']): 'P0' | 'P1' | 'P2' | 'P3' {
const mapping: Record<string, 'P0' | 'P1' | 'P2' | 'P3'> = {
critical: 'P0',
high: 'P1',
medium: 'P2',
low: 'P3',
};
return mapping[severity];
}
function convertStepsToTestSteps(steps: string[]): TestStep[] {
return steps.map((step) => {
const navigateMatch = step.match(/navigate to|go to|open|visit\s+(.+)/i);
if (navigateMatch) {
return { action: 'navigate', target: navigateMatch[1].trim() };
}
const clickMatch = step.match(/click\s+(?:on\s+)?(.+)/i);
if (clickMatch) {
return { action: 'click', target: clickMatch[1].trim() };
}
const typeMatch = step.match(/(?:enter|type|input|fill)\s+["'](.+?)["']\s+(?:in|into)\s+(.+)/i);
if (typeMatch) {
return { action: 'fill', target: typeMatch[2].trim(), value: typeMatch[1].trim() };
}
const waitMatch = step.match(/wait\s+(?:for\s+)?(.+)/i);
if (waitMatch) {
return { action: 'wait', waitFor: waitMatch[1].trim() };
}
const selectMatch = step.match(/select\s+["'](.+?)["']\s+(?:from|in)\s+(.+)/i);
if (selectMatch) {
return { action: 'select', target: selectMatch[2].trim(), value: selectMatch[1].trim() };
}
return { action: 'manual', target: step };
});
}
function deriveAssertions(report: BugReport): TestAssertion[] {
const assertions: TestAssertion[] = [];
// The expected behavior should now be true (the bug is fixed)
assertions.push({
type: 'visible',
expected: true,
description: `Expected: ${report.expectedBehavior}`,
});
// The actual (buggy) behavior should no longer occur
assertions.push({
type: 'hidden',
expected: true,
description: `Should NOT exhibit: ${report.actualBehavior}`,
});
return assertions;
}
function generatePlaywrightTest(spec: RegressionTestSpec): string {
const tags = spec.tags.map((t) => `@${t}`).join(' ');
let testCode = `import { test, expect } from '@playwright/test';
/**
* Regression test for ${spec.bugId}
*
* Root cause: ${spec.metadata.rootCause}
* Category: ${spec.category}
* Priority: ${spec.priority}
*
* Original bug: ${spec.testTitle}
*/
test.describe('${spec.bugId}: ${spec.testTitle}', () => {
test.describe.configure({ tag: [${spec.tags.map((t) => `'${t}'`).join(', ')}] });
`;
// Generate precondition setup
if (spec.preconditions.length > 0) {
testCode += ` test.beforeEach(async ({ page }) => {\n`;
for (const precondition of spec.preconditions) {
testCode += ` // Precondition: ${precondition}\n`;
}
testCode += ` });\n\n`;
}
// Generate the main regression test
testCode += ` test('should not exhibit the original defect', async ({ page }) => {\n`;
for (const step of spec.steps) {
testCode += generateStepCode(step);
}
testCode += `\n // Assertions: verify the bug is fixed\n`;
for (const assertion of spec.assertions) {
testCode += generateAssertionCode(assertion);
}
testCode += ` });\n`;
testCode += `});\n`;
return testCode;
}
function generateStepCode(step: TestStep): string {
switch (step.action) {
case 'navigate':
return ` await page.goto('${step.target}');\n`;
case 'click':
return ` await page.getByRole('button', { name: '${step.target}' }).click();\n`;
case 'fill':
return ` await page.getByLabel('${step.target}').fill('${step.value}');\n`;
case 'wait':
return ` await page.waitForSelector('${step.waitFor}');\n`;
case 'select':
return ` await page.getByLabel('${step.target}').selectOption('${step.value}');\n`;
case 'manual':
return ` // Manual step: ${step.target}\n // TODO: Implement this step\n`;
default:
return ` // Unknown action: ${step.action}\n`;
}
}
function generateAssertionCode(assertion: TestAssertion): string {
switch (assertion.type) {
case 'visible':
return ` // ${assertion.description}\n await expect(page.locator('[data-testid="success-indicator"]')).toBeVisible();\n`;
case 'hidden':
return ` // ${assertion.description}\n await expect(page.locator('[data-testid="error-indicator"]')).toBeHidden();\n`;
case 'text':
return ` // ${assertion.description}\n await expect(page.locator('body')).toContainText('${assertion.expected}');\n`;
case 'url':
return ` // ${assertion.description}\n await expect(page).toHaveURL(${JSON.stringify(assertion.expected)});\n`;
case 'count':
return ` // ${assertion.description}\n await expect(page.locator('[data-testid="item"]')).toHaveCount(${assertion.expected});\n`;
default:
return ` // ${assertion.description}\n // TODO: Implement assertion\n`;
}
}
// tests/regression/data-integrity/BUG-3456-duplicate-order-submission.spec.ts
import { test, expect } from '@playwright/test';
/**
* BUG-3456: Double-clicking the "Place Order" button creates duplicate orders
*
* Root cause: The submit button was not disabled after the first click,
* and the API endpoint did not implement idempotency. Users who double-clicked
* or experienced slow network responses would submit the same order twice.
*
* Fix: Added client-side button disabling on first click and server-side
* idempotency key validation.
*
* Severity: Critical (financial impact - users were charged twice)
*/
test.describe('BUG-3456: Duplicate order submission prevention', () => {
test.describe.configure({ tag: ['@BUG-3456', '@data-integrity', '@P0', '@checkout'] });
test.beforeEach(async ({ page }) => {
// Set up a user with items in cart ready for checkout
await page.goto('/test-setup/checkout-ready');
await page.waitForSelector('[data-testid="checkout-form"]');
});
test('should disable the submit button after first click', async ({ page }) => {
const submitButton = page.getByRole('button', { name: 'Place Order' });
// Verify button starts enabled
await expect(submitButton).toBeEnabled();
// Click the submit button
await submitButton.click();
// Button should be immediately disabled to prevent double-click
await expect(submitButton).toBeDisabled();
});
test('should not create duplicate orders on rapid double-click', async ({ page }) => {
const submitButton = page.getByRole('button', { name: 'Place Order' });
// Intercept API calls to count order creation requests
let orderCreationCount = 0;
await page.route('**/api/orders', async (route) => {
if (route.request().method() === 'POST') {
orderCreationCount++;
}
await route.continue();
});
// Rapidly click the submit button twice
await submitButton.dblclick();
// Wait for the order confirmation page
await page.waitForURL('**/order-confirmation/**');
// Only one order should have been created
expect(orderCreationCount).toBe(1);
});
test('should handle network retry without creating duplicates', async ({ page }) => {
const submitButton = page.getByRole('button', { name: 'Place Order' });
let requestCount = 0;
// Simulate a network failure on the first attempt, success on retry
await page.route('**/api/orders', async (route) => {
requestCount++;
if (requestCount === 1) {
await route.abort('connectionrefused');
} else {
await route.continue();
}
});
await submitButton.click();
// Wait for retry and eventual success
await page.waitForURL('**/order-confirmation/**', { timeout: 15000 });
// The confirmation page should show exactly one order
const orderItems = page.locator('[data-testid="order-item"]');
const count = await orderItems.count();
expect(count).toBeGreaterThan(0);
});
});
// tests/regression/auth/BUG-1567-session-expiry-redirect.spec.ts
import { test, expect } from '@playwright/test';
/**
* BUG-1567: Session expiry causes infinite redirect loop
*
* Root cause: When the session expired, the server redirected to /login.
* The /login page made an API call to check auth status, which returned 401,
* which triggered another redirect to /login, creating an infinite loop.
*
* Fix: The /login page no longer makes the auth status check API call.
* The auth middleware excludes /login and /register from redirect targets.
*
* Severity: Critical (users locked out of application)
*/
test.describe('BUG-1567: Session expiry redirect loop', () => {
test.describe.configure({ tag: ['@BUG-1567', '@auth', '@P0'] });
test('should redirect to login page exactly once when session expires', async ({ page }) => {
// Track all navigation events
const navigations: string[] = [];
page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) {
navigations.push(frame.url());
}
});
// Start with an expired session by clearing auth cookies
await page.goto('/dashboard');
await page.context().clearCookies();
// Trigger an action that requires authentication
await page.reload();
// Wait for the login page to load
await page.waitForURL('**/login**', { timeout: 10000 });
// Verify we are on the login page
await expect(page.getByRole('heading', { name: /log in|sign in/i })).toBeVisible();
// Count redirects to /login -- there should be exactly one
const loginRedirects = navigations.filter((url) => url.includes('/login'));
expect(loginRedirects.length).toBeLessThanOrEqual(2); // initial + one redirect
// Verify the page is stable (not still redirecting)
await page.waitForTimeout(2000);
await expect(page).toHaveURL(/\/login/);
});
test('should preserve the original URL as a redirect target after login', async ({ page }) => {
// Navigate to a protected page
await page.goto('/dashboard/settings');
// Clear cookies to simulate session expiry
await page.context().clearCookies();
await page.reload();
// Should redirect to login with a return URL
await page.waitForURL('**/login**');
const currentUrl = page.url();
expect(currentUrl).toContain('redirect=');
expect(currentUrl).toContain('dashboard');
});
});
// tests/regression/checkout/BUG-2890-tax-calculation-rounding.spec.ts
import { test, expect } from '@playwright/test';
/**
* BUG-2890: Tax calculation shows $0.01 discrepancy on certain totals
*
* Root cause: Tax was calculated per item and then summed, rather than
* calculating tax on the subtotal. Floating point rounding on individual
* items accumulated errors. For example, 3 items at $33.33 with 8% tax:
* Per-item: round(33.33 * 0.08) * 3 = 2.67 * 3 = $8.01
* On subtotal: round(99.99 * 0.08) = round(7.9992) = $8.00
*
* Fix: Tax is now calculated on the subtotal, then rounded once.
*
* Severity: Medium (cosmetic for small orders, significant for large orders)
*/
test.describe('BUG-2890: Tax calculation rounding', () => {
test.describe.configure({ tag: ['@BUG-2890', '@checkout', '@P1'] });
test('should calculate tax on subtotal, not per-item', async ({ page }) => {
// Set up cart with items that trigger the rounding issue
await page.goto('/test-setup/cart');
// Add 3 items at $33.33 each
await page.evaluate(async () => {
await fetch('/api/test/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [
{ productId: 'test-product-1', price: 33.33, quantity: 3 },
],
}),
});
});
// Navigate to checkout to see the tax calculation
await page.goto('/checkout');
await page.waitForSelector('[data-testid="order-summary"]');
// Verify subtotal
const subtotal = page.locator('[data-testid="subtotal"]');
await expect(subtotal).toHaveText('$99.99');
// Verify tax is calculated correctly on the subtotal
// 99.99 * 0.08 = 7.9992 -> rounded to $8.00
const tax = page.locator('[data-testid="tax"]');
await expect(tax).toHaveText('$8.00');
// Verify the total is consistent
const total = page.locator('[data-testid="total"]');
await expect(total).toHaveText('$107.99');
});
test('should maintain consistent rounding for various item counts', async ({ page }) => {
const testCases = [
{ price: 33.33, quantity: 3, expectedSubtotal: '99.99', expectedTax: '8.00' },
{ price: 16.67, quantity: 6, expectedSubtotal: '100.02', expectedTax: '8.00' },
{ price: 9.99, quantity: 7, expectedSubtotal: '69.93', expectedTax: '5.59' },
{ price: 0.99, quantity: 100, expectedSubtotal: '99.00', expectedTax: '7.92' },
];
for (const tc of testCases) {
await page.evaluate(async (data) => {
await fetch('/api/test/cart/clear', { method: 'POST' });
await fetch('/api/test/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ productId: 'test-product', price: data.price, quantity: data.quantity }],
}),
});
}, tc);
await page.goto('/checkout');
await page.waitForSelector('[data-testid="order-summary"]');
const subtotal = await page.locator('[data-testid="subtotal"]').textContent();
const tax = await page.locator('[data-testid="tax"]').textContent();
expect(subtotal).toBe(`$${tc.expectedSubtotal}`);
expect(tax).toBe(`$${tc.expectedTax}`);
}
});
});
// tests/regression/data-integrity/BUG-3789-unicode-name-truncation.spec.ts
import { test, expect } from '@playwright/test';
/**
* BUG-3789: User names with multi-byte Unicode characters are truncated incorrectly
*
* Root cause: The database column was VARCHAR(50) which counts bytes in some
* encodings. A name with CJK characters or emoji uses 3-4 bytes per character,
* so a 20-character name could exceed 50 bytes and be silently truncated.
* The API validation checked string.length (which counts code units) but the
* database enforced byte limits.
*
* Fix: Changed the column to NVARCHAR (character-based limit) and updated
* API validation to check byte length in addition to character length.
*
* Severity: High (data loss for international users)
*/
test.describe('BUG-3789: Unicode name handling', () => {
test.describe.configure({ tag: ['@BUG-3789', '@data-integrity', '@P1'] });
const unicodeNames = [
{ name: 'Tanaka Taro', script: 'CJK Japanese' },
{ name: 'Kim Minjun', script: 'CJK Korean' },
{ name: 'Zhang Wei', script: 'CJK Chinese' },
{ name: 'Jose Garcia', script: 'Latin with diacritics' },
{ name: 'Ivan Petrov', script: 'Cyrillic' },
{ name: 'Ahmad Bin Said', script: 'Arabic transliteration' },
];
for (const { name, script } of unicodeNames) {
test(`should store and display ${script} names correctly`, async ({ page }) => {
// Navigate to profile settings
await page.goto('/settings/profile');
await page.waitForSelector('[data-testid="profile-form"]');
// Enter the Unicode name
const nameInput = page.getByLabel('Display Name');
await nameInput.clear();
await nameInput.fill(name);
// Save the profile
await page.getByRole('button', { name: 'Save' }).click();
// Wait for success confirmation
await expect(page.getByText('Profile updated')).toBeVisible();
// Reload the page to verify the name was persisted correctly
await page.reload();
await page.waitForSelector('[data-testid="profile-form"]');
// The name should be exactly what was entered, not truncated
const savedName = await page.getByLabel('Display Name').inputValue();
expect(savedName).toBe(name);
expect(savedName.length).toBe(name.length);
});
}
});
// tests/regression/api/BUG-5234-pagination-off-by-one.spec.ts
import { test, expect } from '@playwright/test';
/**
* BUG-5234: API pagination returns duplicate items on page boundaries
*
* Root cause: The pagination query used OFFSET-based pagination with
* `OFFSET = page * limit` instead of `OFFSET = (page - 1) * limit`.
* This caused t
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PramodDutta/Regression Suite from Bug Reports下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:regression, bug-reports, incident-response, test-generation, prevention, post-mortem