test-agent-skill
Complete TDD workflow for creating comprehensive failing test suites across all architectural layers (entities, use cases, services, APIs, E2E) using Vitest, Playwright, and Testing Library. Mandatory Context7 consultation for latest testing patterns.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: test-agent-skill
description: Complete TDD workflow for creating comprehensive failing test suites across all architectural layers (entities, use cases, services, APIs, E2E) using Vitest, Playwright, and Testing Library. Mandatory Context7 consultation for latest testing patterns.
---
# Test Agent Technical Skill
**Purpose**: Guide Test Agent through creating comprehensive failing test suites that serve as the living specification for features before any implementation begins.
**When to use**: Immediately after Architect delivers PRD + entities, before Implementer or any other agent begins work.
---
## 🎯 Core Mission
You create the **RED phase of TDD**: comprehensive test suites that FAIL appropriately, defining exactly what must be implemented. Your tests are the immutable specification—once created, they cannot be modified.
**Critical principle**: ALL tests must FAIL initially with "not defined" or "not implemented" errors, proving no implementation exists yet.
---
## 📋 6-PHASE WORKFLOW
### PHASE 0: Pre-Testing Research (CRITICAL - MANDATORY)
**⚠️ DO NOT SKIP THIS PHASE**
Before creating ANY test files, complete this research to understand requirements and verify latest patterns.
#### Step 0.1: Read and Understand Requirements
```bash
# 1. Read your request from Architect
Read: PRDs/{domain}/{feature}/test-agent/00-request.md
# 2. Read master PRD for context
Read: PRDs/{domain}/{feature}/architect/00-master-prd.md
# 3. Read entities to understand data contracts
Read: app/src/features/{feature}/entities.ts
```
**Extract from PRD**:
- ✅ All user stories and acceptance criteria
- ✅ Data contracts and validation rules
- ✅ API specifications (endpoints, request/response formats)
- ✅ Business rules and edge cases
- ✅ Security requirements (RLS policies, authorization)
- ✅ Performance requirements
#### Step 0.2: Consult Context7 for Latest Patterns (MANDATORY)
**⚠️ CRITICAL**: Always verify latest testing patterns before creating tests.
```typescript
// Query 1: Vitest mocking patterns
await context7.get_library_docs({
context7CompatibleLibraryID: "/vitest-dev/vitest",
topic: "mocking vi.mock vi.spyOn best practices",
tokens: 2000
})
// Query 2: Playwright E2E patterns
await context7.get_library_docs({
context7CompatibleLibraryID: "/microsoft/playwright",
topic: "user flow testing accessibility keyboard navigation",
tokens: 2500
})
// Query 3: Zod validation testing
await context7.get_library_docs({
context7CompatibleLibraryID: "/colinhacks/zod",
topic: "safeParse testing error validation",
tokens: 2000
})
```
**Reference files** (if Context7 unavailable):
- `references/vitest-patterns.md` - Mocking strategies
- `references/playwright-e2e-patterns.md` - E2E best practices
- `references/zod-testing-patterns.md` - Schema validation
#### Step 0.3: Plan Test Coverage
Create mental map of ALL tests needed:
```markdown
**Entities Layer**:
- [ ] Schema validation for all fields
- [ ] Edge cases (empty, null, undefined, wrong types)
- [ ] Refinement validations (custom rules)
- [ ] Create/Update/Query schema tests
**Use Cases Layer** (for EACH use case):
- [ ] Happy path
- [ ] Business rule validations
- [ ] Authorization checks
- [ ] Error handling
- [ ] Edge cases and boundaries
**Services Layer**:
- [ ] CRUD operations
- [ ] Query filters and pagination
- [ ] Data transformations (snake_case ↔ camelCase)
- [ ] Database error handling
**API Layer** (for EACH endpoint):
- [ ] Authentication
- [ ] Authorization
- [ ] Request validation
- [ ] Response formatting
- [ ] HTTP status codes
- [ ] Error responses
**E2E Layer**:
- [ ] Complete CRUD workflows
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Keyboard navigation
- [ ] Loading/error states
```
**Checkpoint**: Do NOT proceed to Phase 1 until you have:
- ✅ Read all requirement documents
- ✅ Consulted Context7 for latest patterns
- ✅ Mapped complete test coverage
---
### PHASE 1: Create Entity Tests
**File**: `app/src/features/{feature}/entities.test.ts`
**Purpose**: Validate Zod schemas correctly enforce data contracts.
#### Step 1.1: Use Template
Start with `assets/entity-test-template.ts` as boilerplate.
#### Step 1.2: Test Main Schema
```typescript
import { describe, it, expect } from 'vitest'
import { EntitySchema } from './entities'
describe('EntitySchema', () => {
describe('valid data', () => {
it('accepts valid complete entity', () => {
const validEntity = {
id: '550e8400-e29b-41d4-a716-446655440000',
field1: 'Valid Value',
userId: '550e8400-e29b-41d4-a716-446655440001',
organizationId: '550e8400-e29b-41d4-a716-446655440002',
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-02T00:00:00Z'),
}
const result = EntitySchema.safeParse(validEntity)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data).toEqual(validEntity)
}
})
})
describe('invalid data', () => {
it('rejects entity with invalid uuid', () => {
const invalidEntity = {
id: 'not-a-uuid',
// ... other fields
}
const result = EntitySchema.safeParse(invalidEntity)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].validation).toBe('uuid')
}
})
// Add tests for EACH validation rule from PRD
})
})
```
**CRITICAL**: Use `.safeParse()` NEVER `.parse()` in tests.
#### Step 1.3: Test Derived Schemas
```typescript
describe('EntityCreateSchema', () => {
it('accepts data without auto-generated fields', () => {
const createData = {
field1: 'Valid Value',
userId: 'user-id',
organizationId: 'org-id',
// id, createdAt, updatedAt omitted
}
const result = EntityCreateSchema.safeParse(createData)
expect(result.success).toBe(true)
})
it('rejects data with id field', () => {
const createData = {
id: 'should-not-be-here',
field1: 'Valid Value',
// ...
}
const result = EntityCreateSchema.safeParse(createData)
expect(result.success).toBe(false)
})
})
describe('EntityUpdateSchema', () => {
it('accepts partial data', () => {
const updateData = { field1: 'Updated' }
const result = EntityUpdateSchema.safeParse(updateData)
expect(result.success).toBe(true)
})
})
```
#### Step 1.4: Entity Test Checklist
- [ ] ✅ All schemas tested (main, Create, Update, Query)
- [ ] ✅ Valid data tests
- [ ] ✅ Invalid data tests (for EACH validation rule)
- [ ] ✅ Missing required field tests
- [ ] ✅ Min/max length tests
- [ ] ✅ Enum value tests
- [ ] ✅ UUID validation tests
- [ ] ✅ Refinement tests (custom validations)
- [ ] ✅ Uses .safeParse() everywhere
- [ ] ✅ Tests both success and error branches
**Reference**: See `references/zod-testing-patterns.md` for complete patterns.
---
### PHASE 2: Create Use Case Tests
**Files**: `app/src/features/{feature}/use-cases/{action}.test.ts`
**Purpose**: Define expected business logic behavior (will FAIL: functions not defined).
#### Step 2.1: Use Template
Start with `assets/use-case-test-template.ts`.
#### Step 2.2: Configure Mocks
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createEntity } from './createEntity'
import type { EntityService } from '../services/entity.service'
// Mock the service module
vi.mock('../services/entity.service')
describe('createEntity', () => {
let mockService: jest.Mocked<EntityService>
beforeEach(() => {
mockService = {
create: vi.fn(),
getById: vi.fn(),
list: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
}
})
// Tests go here
})
```
**Reference**: See `references/vitest-patterns.md` for mocking strategies.
#### Step 2.3: Test Structure (for EACH use case)
```typescript
describe('createEntity', () => {
describe('happy path', () => {
it('creates entity with valid data', async () => {
// Arrange
const createData = { field1: 'Test' }
const expectedEntity = { id: 'uuid', ...createData }
mockService.create.mockResolvedValue(expectedEntity)
// Act
const result = await createEntity(createData, mockService)
// Assert
expect(result).toEqual(expectedEntity)
expect(mockService.create).toHaveBeenCalledWith(createData)
expect(mockService.create).toHaveBeenCalledTimes(1)
})
})
describe('validation', () => {
it('rejects invalid data', async () => {
const invalidData = { field1: '' } // Empty (invalid)
await expect(
createEntity(invalidData, mockService)
).rejects.toThrow('Validation failed')
expect(mockService.create).not.toHaveBeenCalled()
})
})
describe('authorization', () => {
it('allows creation in own organization', async () => {
// Test authorization logic
})
it('rejects creation in unauthorized organization', async () => {
// Test authorization rejection
})
})
describe('business rules', () => {
it('enforces unique constraint', async () => {
// Test business rule validation
})
})
describe('error handling', () => {
it('handles database errors', async () => {
mockService.create.mockRejectedValue(new Error('DB error'))
await expect(
createEntity(createData, mockService)
).rejects.toThrow('Failed to create')
})
})
describe('edge cases', () => {
it('handles unicode characters', async () => {
// Test unicode support
})
})
})
```
#### Step 2.4: Use Case Test Checklist (for EACH use case)
- [ ] ✅ Happy path with valid data
- [ ] ✅ Input validation (all validation rules from PRD)
- [ ] ✅ Authorization checks (user/org permissions)
- [ ] ✅ Business rules (from PRD)
- [ ] ✅ Error handling (database, network, unexpected)
- [ ] ✅ Edge cases (unicode, max lengths, boundaries)
- [ ] ✅ Service mock configured
- [ ] ✅ Service calls verified
- [ ] ✅ Service NOT called when validation fails
**Reference**: See `references/test-structure-guide.md` for Arrange-Act-Assert pattern.
---
### PHASE 2.5: Create CASL Ability Tests (IF Authorization Required)
**File**: `app/src/features/{feature}/abilities/defineAbility.test.ts`
**Purpose**: Define authorization behavior for permission-based features (will FAIL: defineAbilitiesFor not defined).
**When to include**: If PRD specifies permission checks, role-based access, or Owner/Super Admin rules.
#### Step 2.5.1: Test Structure Template
```typescript
import { describe, it, expect } from 'vitest';
import { defineAbilitiesFor } from './defineAbility';
import type { User, Workspace, Permission } from '../entities';
describe('defineAbilitiesFor', () => {
// RED phase: This should fail initially
it('should not be defined yet (RED phase)', () => {
expect(defineAbilitiesFor).toBeUndefined();
});
// These tests will fail until Implementer creates the function
describe('Owner bypass', () => {
it('Owner can manage all resources', () => {
const owner: User = {
id: 'owner-id',
email: 'owner@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'owner-id',
};
const ability = defineAbilitiesFor({
user: owner,
workspace,
permissions: [],
});
expect(ability.can('manage', 'all')).toBe(true);
expect(ability.can('delete', 'Board')).toBe(true);
expect(ability.can('create', 'Card')).toBe(true);
});
});
describe('Super Admin', () => {
it('Super Admin can manage most resources', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id', // Part of org where user is Super Admin
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('manage', 'all')).toBe(true);
expect(ability.can('read', 'Board')).toBe(true);
expect(ability.can('update', 'Card')).toBe(true);
});
it('Super Admin cannot delete Organization', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id',
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('delete', 'Organization')).toBe(false);
});
it('Super Admin cannot remove Owner permissions', () => {
const superAdmin: User = {
id: 'admin-id',
email: 'admin@test.com',
superAdminOrgs: ['org-id'],
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
parent_id: 'org-id',
};
const ability = defineAbilitiesFor({
user: superAdmin,
workspace,
permissions: [],
});
expect(ability.can('remove', 'Permission', { role: 'owner' })).toBe(false);
});
});
describe('Permission-based access', () => {
it('User with boards.create permission can create boards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'boards.create', user_id: 'user-id' },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('create', 'Board')).toBe(true);
});
it('User with boards.update permission can update boards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'boards.update', user_id: 'user-id' },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('update', 'Board')).toBe(true);
expect(ability.can('delete', 'Board')).toBe(false); // No delete permission
});
it('User without permission cannot access resource', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = []; // No permissions
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('create', 'Board')).toBe(false);
expect(ability.can('read', 'Board')).toBe(false);
expect(ability.can('update', 'Board')).toBe(false);
expect(ability.can('delete', 'Board')).toBe(false);
});
});
describe('Conditional permissions', () => {
it('User can only update own cards', () => {
const user: User = {
id: 'user-id',
email: 'user@test.com',
};
const workspace: Workspace = {
id: 'workspace-id',
owner_id: 'different-owner-id',
};
const permissions: Permission[] = [
{ id: '1', full_name: 'cards.update', user_id: 'user-id', conditions: { user_id: 'user-id' } },
];
const ability = defineAbilitiesFor({ user, workspace, permissions });
expect(ability.can('update', 'Card', { user_id: 'user-id' })).toBe(true);
expect(ability.can('update', 'Card', { user_id: 'other-user-id' })).toBe(false);
});
});
});
```
#### Step 2.5.2: E2E Tests for CASL Visibility
**File**: `e2e/{feature}/authorization.spec.ts`
**Purpose**: Verify `<Can>` component correctly hides/shows UI based on abilities.
```typescript
import { test, expect } from '@playwright/test';
test.describe('Board authorization', () => {
test('user without delete permission cannot see delete button', async ({ page }) => {
// Login as user WITHOUT boards.delete permission
await page.goto('/login');
await page.fill('[name="email"]', 'viewer@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// Delete button should NOT exist in DOM
const deleteButton = page.getByRole('button', { name: /delete/i });
await expect(deleteButton).not.toBeVisible();
});
test('user with delete permission sees delete button', async ({ page }) => {
// Login as user WITH boards.delete permission
await page.goto('/login');
await page.fill('[name="email"]', 'editor@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// Delete button SHOULD be visible
const deleteButton = page.getByRole('button', { name: /delete/i });
await expect(deleteButton).toBeVisible();
});
test('Owner sees all action buttons', async ({ page }) => {
// Login as workspace Owner
await page.goto('/login');
await page.fill('[name="email"]', 'owner@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Navigate to boards page
await page.goto('/boards/123');
// All buttons should be visible
await expect(page.getByRole('button', { name: /create/i })).toBeVisible();
await expect(page.getByRole('button', { name: /edit/i })).toBeVisible();
await expect(page.getByRole('button', { name: /delete/i })).toBeVisible();
await expect(page.getByRole('button', { name: /settings/i })).toBeVisible();
});
test('user without access is redirected or sees empty state', async ({ page }) => {
// Login as user from different organization
await page.goto('/login');
await page.fill('[name="email"]', 'outsider@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Try to navigate to boards page
await page.goto('/boards/123');
// Should redirect to 403 or show "No access" message
await expect(page).toHaveURL(/\/(403|forbidden|no-access)/);
// OR
await expect(page.getByText(/you don't have access/i)).toBeVisible();
});
});
```
#### Step 2.5.3: CASL Test Checklist
- [ ] ✅ defineAbilitiesFor() initially undefined (RED phase)
- [ ] ✅ Owner can manage 'all' resources
- [ ] ✅ Super Admin can manage 'all' with restrictions
- [ ] ✅ Super Admin cannot delete Organization
- [ ] ✅ Super Admin cannot remove Owner permissions
- [ ] ✅ User with permission can perform action
- [ ] ✅ User without permission cannot perform action
- [ ] ✅ Conditional permissions tested (if applicable)
- [ ] ✅ Field-level permissions tested (if applicable)
- [ ] ✅ E2E tests verify <Can> component visibility
- [ ] ✅ E2E tests cover all user roles (Owner, Super Admin, Editor, Viewer)
- [ ] ✅ E2E tests verify unauthorized access handling
**Critical Rules**:
- ❌ DON'T test CASL library itself (trust it works)
- ✅ DO test your defineAbilitiesFor() logic
- ✅ DO test that abilities match PRD requirements
- ✅ DO test Owner/Super Admin special cases
- ✅ DO verify E2E visibility matches abilities
---
### PHASE 3: Create Service Tests
**Files**: `app/src/features/{feature}/services/{entity}.service.test.ts`
**Purpose**: Define data access behavior (will FAIL: service not defined).
#### Step 3.1: Use Template
Start with `assets/service-test-template.ts`.
#### Step 3.2: Mock Supabase Client
```typescript
import { describe, it, expec