mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-13 15:50:55 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b791be018 | |||
| 7c71038e1f |
+35
-171
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: pytest
|
||||
description: >
|
||||
Pytest testing patterns for Python.
|
||||
Trigger: When writing or refactoring pytest tests (fixtures, mocking, parametrize, markers). For Prowler-specific API/SDK testing conventions, also use prowler-test-api or prowler-test-sdk.
|
||||
description: "Trigger: When writing or refactoring pytest tests in Python, including fixtures, mocking, parametrization, async tests, and markers. Provides generic pytest structure before component-specific API or SDK rules."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -12,183 +10,49 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
---
|
||||
|
||||
## Basic Test Structure
|
||||
## Activation Contract
|
||||
|
||||
```python
|
||||
import pytest
|
||||
Use this skill for generic pytest structure and patterns; if the test touches Prowler API or SDK specifics, pair it with `prowler-test-api` or `prowler-test-sdk`.
|
||||
|
||||
class TestUserService:
|
||||
def test_create_user_success(self):
|
||||
user = create_user(name="John", email="john@test.com")
|
||||
assert user.name == "John"
|
||||
assert user.email == "john@test.com"
|
||||
## Hard Rules
|
||||
|
||||
def test_create_user_invalid_email_fails(self):
|
||||
with pytest.raises(ValueError, match="Invalid email"):
|
||||
create_user(name="John", email="invalid")
|
||||
```
|
||||
- Keep tests behavior-focused and name them after expected outcomes.
|
||||
- Extract reusable setup into fixtures instead of repeating inline construction.
|
||||
- Use `pytest.raises` for failure expectations and `@pytest.mark.parametrize` for matrix coverage.
|
||||
- Mock external boundaries, not the logic under test.
|
||||
- Register and use markers intentionally; do not invent silent marker names.
|
||||
- Prefer local references only; do not rely on external documentation links inside the skill.
|
||||
|
||||
## Fixtures
|
||||
## Decision Gates
|
||||
|
||||
```python
|
||||
import pytest
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Shared setup across tests? | Move it into a fixture or `conftest.py`. |
|
||||
| Same assertion logic over many inputs? | Use `@pytest.mark.parametrize`. |
|
||||
| Need to verify an exception? | Use `pytest.raises(..., match=...)`. |
|
||||
| Testing async behavior? | Use `@pytest.mark.asyncio` or the repo's async test pattern. |
|
||||
| Working in `api/` or `prowler/`? | Load the component-specific testing skill too. |
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
"""Create a test user."""
|
||||
return User(name="Test User", email="test@example.com")
|
||||
## Execution Steps
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client(client, user):
|
||||
"""Client with authenticated user."""
|
||||
client.force_login(user)
|
||||
return client
|
||||
1. Identify whether the test is generic pytest, API-specific, or SDK-specific.
|
||||
2. Read neighboring tests and `conftest.py` before adding new fixtures.
|
||||
3. Write focused test functions or test classes with clear outcome-based names.
|
||||
4. Promote repeated setup into fixtures and shared helpers only when duplication appears twice or more.
|
||||
5. Use parametrization, markers, and mocks deliberately to keep coverage broad but readable.
|
||||
6. Run the narrowest relevant pytest target and inspect failures before widening scope.
|
||||
7. Report the exact command used and any fixture or marker introduced.
|
||||
|
||||
# Fixture with teardown
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
path = Path("/tmp/test_file.txt")
|
||||
path.write_text("test content")
|
||||
yield path # Test runs here
|
||||
path.unlink() # Cleanup after test
|
||||
## Output Contract
|
||||
|
||||
# Fixture scopes
|
||||
@pytest.fixture(scope="module") # Once per module
|
||||
@pytest.fixture(scope="class") # Once per class
|
||||
@pytest.fixture(scope="session") # Once per test session
|
||||
```
|
||||
|
||||
## conftest.py
|
||||
|
||||
```python
|
||||
# tests/conftest.py - Shared fixtures
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
session = create_session()
|
||||
yield session
|
||||
session.rollback()
|
||||
|
||||
@pytest.fixture
|
||||
def api_client():
|
||||
return TestClient(app)
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
```python
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
class TestPaymentService:
|
||||
def test_process_payment_success(self):
|
||||
with patch("services.payment.stripe_client") as mock_stripe:
|
||||
mock_stripe.charge.return_value = {"id": "ch_123", "status": "succeeded"}
|
||||
|
||||
result = process_payment(amount=100)
|
||||
|
||||
assert result["status"] == "succeeded"
|
||||
mock_stripe.charge.assert_called_once_with(amount=100)
|
||||
|
||||
def test_process_payment_failure(self):
|
||||
with patch("services.payment.stripe_client") as mock_stripe:
|
||||
mock_stripe.charge.side_effect = PaymentError("Card declined")
|
||||
|
||||
with pytest.raises(PaymentError):
|
||||
process_payment(amount=100)
|
||||
|
||||
# MagicMock for complex objects
|
||||
def test_with_mock_object():
|
||||
mock_user = MagicMock()
|
||||
mock_user.id = "user-123"
|
||||
mock_user.name = "Test User"
|
||||
mock_user.is_active = True
|
||||
|
||||
result = get_user_info(mock_user)
|
||||
assert result["name"] == "Test User"
|
||||
```
|
||||
|
||||
## Parametrize
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
("hello", "HELLO"),
|
||||
("world", "WORLD"),
|
||||
("pytest", "PYTEST"),
|
||||
])
|
||||
def test_uppercase(input, expected):
|
||||
assert input.upper() == expected
|
||||
|
||||
@pytest.mark.parametrize("email,is_valid", [
|
||||
("user@example.com", True),
|
||||
("invalid-email", False),
|
||||
("", False),
|
||||
("user@.com", False),
|
||||
])
|
||||
def test_email_validation(email, is_valid):
|
||||
assert validate_email(email) == is_valid
|
||||
```
|
||||
|
||||
## Markers
|
||||
|
||||
```python
|
||||
# pytest.ini or pyproject.toml
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"slow: marks tests as slow",
|
||||
"integration: marks integration tests",
|
||||
]
|
||||
|
||||
# Usage
|
||||
@pytest.mark.slow
|
||||
def test_large_data_processing():
|
||||
...
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_database_connection():
|
||||
...
|
||||
|
||||
@pytest.mark.skip(reason="Not implemented yet")
|
||||
def test_future_feature():
|
||||
...
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
|
||||
def test_unix_specific():
|
||||
...
|
||||
|
||||
# Run specific markers
|
||||
# pytest -m "not slow"
|
||||
# pytest -m "integration"
|
||||
```
|
||||
|
||||
## Async Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
result = await async_fetch_data()
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pytest # Run all tests
|
||||
pytest -v # Verbose output
|
||||
pytest -x # Stop on first failure
|
||||
pytest -k "test_user" # Filter by name
|
||||
pytest -m "not slow" # Filter by marker
|
||||
pytest --cov=src # With coverage
|
||||
pytest -n auto # Parallel (pytest-xdist)
|
||||
pytest --tb=short # Short traceback
|
||||
```
|
||||
- State whether the change relied on fixtures, parametrization, mocking, markers, or async support.
|
||||
- Mention any component-specific skill paired with pytest.
|
||||
- Report the exact pytest command used for validation.
|
||||
- Call out any test isolation or fixture-scope decision that affects future contributors.
|
||||
|
||||
## References
|
||||
|
||||
For general pytest documentation, see:
|
||||
- **Official Docs**: https://docs.pytest.org/en/stable/
|
||||
|
||||
For Prowler SDK testing with provider-specific patterns (moto, MagicMock), see:
|
||||
- **Documentation**: [references/prowler-testing.md](references/prowler-testing.md)
|
||||
- [TDD skill](../tdd/SKILL.md)
|
||||
- [Prowler API testing skill](../prowler-test-api/SKILL.md)
|
||||
- [Prowler SDK testing skill](../prowler-test-sdk/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+33
-102
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: react-19
|
||||
description: >
|
||||
React 19 patterns with React Compiler.
|
||||
Trigger: When writing React 19 components/hooks in .tsx (React Compiler rules, hook patterns, refs as props). If using Next.js App Router/Server Actions, also use nextjs-15.
|
||||
description: "Trigger: When writing React 19 components, hooks, or `.tsx` files, especially with React Compiler, `use()`, actions, or ref-as-prop patterns. Applies React 19 runtime and composition rules."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -12,113 +10,46 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
---
|
||||
|
||||
## No Manual Memoization (REQUIRED)
|
||||
## Activation Contract
|
||||
|
||||
```typescript
|
||||
// ✅ React Compiler handles optimization automatically
|
||||
function Component({ items }) {
|
||||
const filtered = items.filter(x => x.active);
|
||||
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
|
||||
Use this skill when the change is inside React 19 component code and the agent must choose between Server Components, Client Components, compiler-friendly patterns, or modern hook APIs.
|
||||
|
||||
const handleClick = (id) => {
|
||||
console.log(id);
|
||||
};
|
||||
## Hard Rules
|
||||
|
||||
return <List items={sorted} onClick={handleClick} />;
|
||||
}
|
||||
- Do not add `useMemo` or `useCallback` for routine render-path optimization; React Compiler handles the common case.
|
||||
- Prefer Server Components by default; add `"use client"` only for client-only behavior.
|
||||
- Import named React APIs; do not use default `React` imports.
|
||||
- Use `ref` as a prop in React 19 instead of introducing `forwardRef` by habit.
|
||||
- If the task also involves App Router or Server Actions integration details, load `nextjs-15` too.
|
||||
|
||||
// ❌ NEVER: Manual memoization
|
||||
const filtered = useMemo(() => items.filter(x => x.active), [items]);
|
||||
const handleClick = useCallback((id) => console.log(id), []);
|
||||
```
|
||||
## Decision Gates
|
||||
|
||||
## Imports (REQUIRED)
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Does the component use state, effects, browser APIs, or event handlers? | Mark it as a Client Component with `"use client"`. |
|
||||
| Does the component only fetch or compose data for rendering? | Keep it as a Server Component. |
|
||||
| Are you reading a promise or conditional context? | Consider `use()` instead of older workarounds. |
|
||||
| Are you wiring form actions or pending state? | Prefer actions and `useActionState`. |
|
||||
| Are you about to add memoization for performance? | Stop and justify it; default to compiler-friendly plain code first. |
|
||||
|
||||
```typescript
|
||||
// ✅ ALWAYS: Named imports
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
## Execution Steps
|
||||
|
||||
// ❌ NEVER
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
```
|
||||
1. Identify whether the file should stay server-side or become client-side.
|
||||
2. Remove legacy React imports and manual memoization unless there is a proven exception.
|
||||
3. Keep render logic direct and compiler-friendly.
|
||||
4. Use `use()` for supported promise/context reads when it simplifies the flow.
|
||||
5. Use action-based form patterns for mutation flows when relevant.
|
||||
6. Pass refs as props in new React 19 component APIs.
|
||||
7. Validate that the final component model matches the feature's runtime needs.
|
||||
|
||||
## Server Components First
|
||||
## Output Contract
|
||||
|
||||
```typescript
|
||||
// ✅ Server Component (default) - no directive
|
||||
export default async function Page() {
|
||||
const data = await fetchData();
|
||||
return <ClientComponent data={data} />;
|
||||
}
|
||||
- State whether the component is server or client and why.
|
||||
- Call out any React 19 modernization applied, such as removing manual memoization, using `use()`, or replacing `forwardRef`.
|
||||
- Mention whether `nextjs-15` was also required.
|
||||
|
||||
// ✅ Client Component - only when needed
|
||||
"use client";
|
||||
export function Interactive() {
|
||||
const [state, setState] = useState(false);
|
||||
return <button onClick={() => setState(!state)}>Toggle</button>;
|
||||
}
|
||||
```
|
||||
## References
|
||||
|
||||
## When to use "use client"
|
||||
|
||||
- useState, useEffect, useRef, useContext
|
||||
- Event handlers (onClick, onChange)
|
||||
- Browser APIs (window, localStorage)
|
||||
|
||||
## use() Hook
|
||||
|
||||
```typescript
|
||||
import { use } from "react";
|
||||
|
||||
// Read promises (suspends until resolved)
|
||||
function Comments({ promise }) {
|
||||
const comments = use(promise);
|
||||
return comments.map(c => <div key={c.id}>{c.text}</div>);
|
||||
}
|
||||
|
||||
// Conditional context (not possible with useContext!)
|
||||
function Theme({ showTheme }) {
|
||||
if (showTheme) {
|
||||
const theme = use(ThemeContext);
|
||||
return <div style={{ color: theme.primary }}>Themed</div>;
|
||||
}
|
||||
return <div>Plain</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Actions & useActionState
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
async function submitForm(formData: FormData) {
|
||||
await saveToDatabase(formData);
|
||||
revalidatePath("/");
|
||||
}
|
||||
|
||||
// With pending state
|
||||
import { useActionState } from "react";
|
||||
|
||||
function Form() {
|
||||
const [state, action, isPending] = useActionState(submitForm, null);
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={isPending}>
|
||||
{isPending ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## ref as Prop (No forwardRef)
|
||||
|
||||
```typescript
|
||||
// ✅ React 19: ref is just a prop
|
||||
function Input({ ref, ...props }) {
|
||||
return <input ref={ref} {...props} />;
|
||||
}
|
||||
|
||||
// ❌ Old way (unnecessary now)
|
||||
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
|
||||
```
|
||||
- [Next.js 15 skill](../nextjs-15/SKILL.md)
|
||||
- [TypeScript skill](../typescript/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+36
-149
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: >
|
||||
Creates new AI agent skills following the Agent Skills spec.
|
||||
Trigger: When user asks to create a new skill, add agent instructions, or document patterns for AI.
|
||||
description: "Trigger: When user asks to create a new skill, add agent instructions, or document patterns for AI. Creates new AI agent skills following the Agent Skills spec."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -12,160 +10,49 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
---
|
||||
|
||||
## When to Create a Skill
|
||||
## Activation Contract
|
||||
|
||||
Create a skill when:
|
||||
- A pattern is used repeatedly and AI needs guidance
|
||||
- Project-specific conventions differ from generic best practices
|
||||
- Complex workflows need step-by-step instructions
|
||||
- Decision trees help AI choose the right approach
|
||||
Use this skill when the task is to create a new skill or reshape rough agent guidance into a reusable skill package.
|
||||
|
||||
**Don't create a skill when:**
|
||||
- Documentation already exists (create a reference instead)
|
||||
- Pattern is trivial or self-explanatory
|
||||
- It's a one-off task
|
||||
## Hard Rules
|
||||
|
||||
---
|
||||
- Create a skill only for reusable, non-trivial patterns.
|
||||
- Keep `description` on one quoted physical line with `Trigger:` first.
|
||||
- Use local references only; never point `references/` at web URLs.
|
||||
- Prefer short rules, decision tables, and minimal examples over tutorials.
|
||||
- Add `metadata.scope` and `metadata.auto_invoke` when the skill should surface in `AGENTS.md` auto-invoke tables.
|
||||
- Do not duplicate long docs inside the skill; point to local references instead.
|
||||
|
||||
## Skill Structure
|
||||
## Decision Gates
|
||||
|
||||
```
|
||||
skills/{skill-name}/
|
||||
├── SKILL.md # Required - main skill file
|
||||
├── assets/ # Optional - templates, schemas, examples
|
||||
│ ├── template.py
|
||||
│ └── schema.json
|
||||
└── references/ # Optional - links to local docs
|
||||
└── docs.md # Points to docs/developer-guide/*.mdx
|
||||
```
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Is the pattern already documented well enough? | Reuse or reference the existing doc instead of creating a new skill. |
|
||||
| Is the guidance specific to this repo or workflow? | Create a project-specific skill name such as `prowler-{component}` or `{action}-{target}`. |
|
||||
| Do you need templates, schemas, or example configs? | Put them in `assets/`. |
|
||||
| Do you need supporting documentation? | Link only local files from `references/`. |
|
||||
| Will the skill be auto-invoked from `AGENTS.md`? | Add or update `metadata.scope` and `metadata.auto_invoke`, then decide whether `skill-sync` must run. |
|
||||
|
||||
---
|
||||
## Execution Steps
|
||||
|
||||
## SKILL.md Template
|
||||
1. Confirm the skill does not already exist under `skills/`.
|
||||
2. Choose a reusable name that matches the repo naming conventions.
|
||||
3. Create `skills/{skill-name}/SKILL.md` and required support folders only if needed (`assets/`, `references/`).
|
||||
4. Write frontmatter with `name`, one-line quoted `description`, `license`, and metadata.
|
||||
5. Write the body in this order: Activation Contract, Hard Rules, Decision Gates, Execution Steps, Output Contract, References.
|
||||
6. Keep the body compact: operational instructions first, examples only when they unblock execution.
|
||||
7. If auto-invoke metadata changed, run the `skill-sync` workflow appropriate to the scope.
|
||||
8. Update any non-generated skill index entries the repository expects.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {skill-name}
|
||||
description: >
|
||||
{One-line description of what this skill does}.
|
||||
Trigger: {When the AI should load this skill}.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
---
|
||||
## Output Contract
|
||||
|
||||
## When to Use
|
||||
- Return the created or updated skill path(s).
|
||||
- State whether auto-invoke metadata changed and whether `skill-sync` was run, dry-run, or intentionally skipped.
|
||||
- Summarize the reusable pattern the skill captures in 1-3 bullets.
|
||||
- Call out any follow-up files the human should review, such as `AGENTS.md` or assets/templates.
|
||||
|
||||
{Bullet points of when to use this skill}
|
||||
## References
|
||||
|
||||
## Critical Patterns
|
||||
|
||||
{The most important rules - what AI MUST know}
|
||||
|
||||
## Code Examples
|
||||
|
||||
{Minimal, focused examples}
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
{Common commands}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **Templates**: See [assets/](assets/) for {description}
|
||||
- **Documentation**: See [references/](references/) for local docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Type | Pattern | Examples |
|
||||
|------|---------|----------|
|
||||
| Generic skill | `{technology}` | `pytest`, `playwright`, `typescript` |
|
||||
| Prowler-specific | `prowler-{component}` | `prowler-api`, `prowler-ui`, `prowler-sdk-check` |
|
||||
| Testing skill | `prowler-test-{component}` | `prowler-test-sdk`, `prowler-test-api` |
|
||||
| Workflow skill | `{action}-{target}` | `skill-creator`, `jira-task` |
|
||||
|
||||
---
|
||||
|
||||
## Decision: assets/ vs references/
|
||||
|
||||
```
|
||||
Need code templates? → assets/
|
||||
Need JSON schemas? → assets/
|
||||
Need example configs? → assets/
|
||||
Link to existing docs? → references/
|
||||
Link to external guides? → references/ (with local path)
|
||||
```
|
||||
|
||||
**Key Rule**: `references/` should point to LOCAL files (`docs/developer-guide/*.mdx`), not web URLs.
|
||||
|
||||
---
|
||||
|
||||
## Decision: Prowler-Specific vs Generic
|
||||
|
||||
```
|
||||
Patterns apply to ANY project? → Generic skill (e.g., pytest, typescript)
|
||||
Patterns are Prowler-specific? → prowler-{name} skill
|
||||
Generic skill needs Prowler info? → Add references/ pointing to Prowler docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontmatter Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Skill identifier (lowercase, hyphens) |
|
||||
| `description` | Yes | What + Trigger in one block |
|
||||
| `license` | Yes | Always `Apache-2.0` for Prowler |
|
||||
| `metadata.author` | Yes | `prowler-cloud` |
|
||||
| `metadata.version` | Yes | Semantic version as string |
|
||||
|
||||
---
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### DO
|
||||
- Start with the most critical patterns
|
||||
- Use tables for decision trees
|
||||
- Keep code examples minimal and focused
|
||||
- Include Commands section with copy-paste commands
|
||||
|
||||
### DON'T
|
||||
- Add Keywords section (agent searches frontmatter, not body)
|
||||
- Duplicate content from existing docs (reference instead)
|
||||
- Include lengthy explanations (link to docs)
|
||||
- Add troubleshooting sections (keep focused)
|
||||
- Use web URLs in references (use local paths)
|
||||
|
||||
---
|
||||
|
||||
## Registering the Skill
|
||||
|
||||
After creating the skill, add it to `AGENTS.md`:
|
||||
|
||||
```markdown
|
||||
| `{skill-name}` | {Description} | [SKILL.md](skills/{skill-name}/SKILL.md) |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Creating
|
||||
|
||||
- [ ] Skill doesn't already exist (check `skills/`)
|
||||
- [ ] Pattern is reusable (not one-off)
|
||||
- [ ] Name follows conventions
|
||||
- [ ] Frontmatter is complete (description includes trigger keywords)
|
||||
- [ ] Critical patterns are clear
|
||||
- [ ] Code examples are minimal
|
||||
- [ ] Commands section exists
|
||||
- [ ] Added to AGENTS.md
|
||||
|
||||
## Resources
|
||||
|
||||
- **Templates**: See [assets/](assets/) for SKILL.md template
|
||||
- [Template](assets/SKILL-TEMPLATE.md)
|
||||
- [Skills overview](../README.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+33
-96
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: skill-sync
|
||||
description: >
|
||||
Syncs skill metadata to AGENTS.md Auto-invoke sections.
|
||||
Trigger: When updating skill metadata (metadata.scope/metadata.auto_invoke), regenerating Auto-invoke tables, or running ./skills/skill-sync/assets/sync.sh (including --dry-run/--scope).
|
||||
description: "Trigger: When updating skill metadata (metadata.scope/metadata.auto_invoke), regenerating Auto-invoke tables, or running ./skills/skill-sync/assets/sync.sh. Syncs skill metadata to AGENTS.md Auto-invoke sections."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -15,107 +13,46 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
---
|
||||
|
||||
## Purpose
|
||||
## Activation Contract
|
||||
|
||||
Keeps AGENTS.md Auto-invoke sections in sync with skill metadata. When you create or modify a skill, run the sync script to automatically update all affected AGENTS.md files.
|
||||
Use this skill when a skill's `metadata.scope` or `metadata.auto_invoke` changes, when auto-invoke tables need regeneration, or when a skill is missing from `AGENTS.md` auto-invoke output.
|
||||
|
||||
## Required Skill Metadata
|
||||
## Hard Rules
|
||||
|
||||
Each skill that should appear in Auto-invoke sections needs these fields in `metadata`.
|
||||
- Treat `./skills/skill-sync/assets/sync.sh` as the source of truth for generated auto-invoke tables.
|
||||
- Do not hand-edit generated auto-invoke sections unless the workflow itself is being fixed.
|
||||
- Run `--dry-run` first when you only need verification or when metadata impact is uncertain.
|
||||
- Only `metadata.scope` and `metadata.auto_invoke` should drive sync decisions.
|
||||
- Keep scope values aligned to real targets: `root`, `ui`, `api`, `sdk`, `mcp_server`.
|
||||
|
||||
`auto_invoke` can be either a single string **or** a list of actions:
|
||||
## Decision Gates
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
scope: [ui] # Which AGENTS.md: ui, api, sdk, root
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Did `metadata.scope` or `metadata.auto_invoke` change? | Run `sync.sh` for real, or `--scope` if the blast radius is intentionally narrow. |
|
||||
| Did only body text or examples change? | Skip sync and say why; generated tables are unaffected. |
|
||||
| Are you checking expected output without modifying files? | Run `sync.sh --dry-run`. |
|
||||
| Is one surface affected? | Use `sync.sh --scope <scope>`. |
|
||||
| Is a skill missing from auto-invoke output? | Inspect its frontmatter first, then run `--dry-run` to confirm what the script sees. |
|
||||
|
||||
# Option A: single action
|
||||
auto_invoke: "Creating/modifying components"
|
||||
## Execution Steps
|
||||
|
||||
# Option B: multiple actions
|
||||
# auto_invoke:
|
||||
# - "Creating/modifying components"
|
||||
# - "Refactoring component folder placement"
|
||||
```
|
||||
1. Read the changed skill frontmatter and confirm `metadata.scope` and `metadata.auto_invoke` are present and well-formed.
|
||||
2. Decide whether the task needs a real sync, a dry-run, or a documented no-op.
|
||||
3. If validating only, run `./skills/skill-sync/assets/sync.sh --dry-run`.
|
||||
4. If updating one target, run `./skills/skill-sync/assets/sync.sh --scope <scope>`.
|
||||
5. If updating all affected targets, run `./skills/skill-sync/assets/sync.sh`.
|
||||
6. Verify the expected `AGENTS.md` surfaces changed only where metadata demanded it.
|
||||
|
||||
### Scope Values
|
||||
## Output Contract
|
||||
|
||||
| Scope | Updates |
|
||||
|-------|---------|
|
||||
| `root` | `AGENTS.md` (repo root) |
|
||||
| `ui` | `ui/AGENTS.md` |
|
||||
| `api` | `api/AGENTS.md` |
|
||||
| `sdk` | `prowler/AGENTS.md` |
|
||||
| `mcp_server` | `mcp_server/AGENTS.md` |
|
||||
- State whether sync was executed, dry-run only, or skipped as a no-op.
|
||||
- List the scope(s) evaluated and the `AGENTS.md` file(s) affected or intentionally untouched.
|
||||
- If the issue was missing auto-invoke output, explain the root cause in the skill metadata or script behavior.
|
||||
- Return the exact command used for verification or update.
|
||||
|
||||
Skills can have multiple scopes: `scope: [ui, api]`
|
||||
## References
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### After Creating/Modifying a Skill
|
||||
|
||||
```bash
|
||||
./skills/skill-sync/assets/sync.sh
|
||||
```
|
||||
|
||||
### What It Does
|
||||
|
||||
1. Reads all `skills/*/SKILL.md` files
|
||||
2. Extracts `metadata.scope` and `metadata.auto_invoke`
|
||||
3. Generates Auto-invoke tables for each AGENTS.md
|
||||
4. Updates the `### Auto-invoke Skills` section in each file
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Given this skill metadata:
|
||||
|
||||
```yaml
|
||||
# skills/prowler-ui/SKILL.md
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
scope: [ui]
|
||||
auto_invoke: "Creating/modifying React components"
|
||||
```
|
||||
|
||||
The sync script generates in `ui/AGENTS.md`:
|
||||
|
||||
```markdown
|
||||
### Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
| Action | Skill |
|
||||
|--------|-------|
|
||||
| Creating/modifying React components | `prowler-ui` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Sync all AGENTS.md files
|
||||
./skills/skill-sync/assets/sync.sh
|
||||
|
||||
# Dry run (show what would change)
|
||||
./skills/skill-sync/assets/sync.sh --dry-run
|
||||
|
||||
# Sync specific scope only
|
||||
./skills/skill-sync/assets/sync.sh --scope ui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist After Modifying Skills
|
||||
|
||||
- [ ] Added `metadata.scope` to new/modified skill
|
||||
- [ ] Added `metadata.auto_invoke` with action description
|
||||
- [ ] Ran `./skills/skill-sync/assets/sync.sh`
|
||||
- [ ] Verified AGENTS.md files updated correctly
|
||||
- [Sync script](assets/sync.sh)
|
||||
- [Sync script test helper](assets/sync_test.sh)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+32
-177
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: tailwind-4
|
||||
description: >
|
||||
Tailwind CSS 4 patterns and best practices.
|
||||
Trigger: When styling with Tailwind (className, variants, cn()), especially when dynamic styling or CSS variables are involved (no var() in className).
|
||||
description: "Trigger: When styling with Tailwind CSS 4, especially in `className`, variant composition, `cn()`, or dynamic-value decisions. Enforces Tailwind-first styling rules and escape hatches."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -12,188 +10,45 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
---
|
||||
|
||||
## Styling Decision Tree
|
||||
## Activation Contract
|
||||
|
||||
```
|
||||
Tailwind class exists? → className="..."
|
||||
Dynamic value? → style={{ width: `${x}%` }}
|
||||
Conditional styles? → cn("base", condition && "variant")
|
||||
Static only? → className="..." (no cn() needed)
|
||||
Library can't use class?→ style prop with var() constants
|
||||
```
|
||||
Use this skill when UI styling decisions involve Tailwind class composition, semantic theme usage, or choosing between `className`, `cn()`, and inline styles.
|
||||
|
||||
## Critical Rules
|
||||
## Hard Rules
|
||||
|
||||
### Never Use var() in className
|
||||
- Prefer Tailwind utility classes directly in `className` for static styling.
|
||||
- Do not put `var(...)` expressions inside `className`; use semantic Tailwind tokens or inline styles where needed.
|
||||
- Do not use hex colors in class strings; use theme or Tailwind palette classes.
|
||||
- Use `cn()` only when conditional or merge behavior is real.
|
||||
- Use inline `style` only for truly dynamic values or third-party APIs that cannot consume class names.
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER: var() in className
|
||||
<div className="bg-[var(--color-primary)]" />
|
||||
<div className="text-[var(--text-color)]" />
|
||||
## Decision Gates
|
||||
|
||||
// ✅ ALWAYS: Use Tailwind semantic classes
|
||||
<div className="bg-primary" />
|
||||
<div className="text-slate-400" />
|
||||
```
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Static styling only? | Use plain `className="..."`. |
|
||||
| Conditional or override-prone classes? | Use `cn(...)`. |
|
||||
| Dynamic numeric or percentage values? | Use the `style` prop. |
|
||||
| Third-party library prop cannot accept classes? | Pass CSS custom property values or inline style constants. |
|
||||
| Need a one-off dimension not in the design system? | Use an arbitrary value sparingly, but never for colors. |
|
||||
|
||||
### Never Use Hex Colors
|
||||
## Execution Steps
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER: Hex colors in className
|
||||
<p className="text-[#ffffff]" />
|
||||
<div className="bg-[#1e293b]" />
|
||||
1. Classify the styling need as static, conditional, dynamic, or third-party-only.
|
||||
2. Prefer semantic Tailwind utilities and theme tokens first.
|
||||
3. Introduce `cn()` only if merge logic or conditions justify it.
|
||||
4. Move dynamic measurements or library-only values into `style` constants.
|
||||
5. Replace color escape hatches with palette or theme classes.
|
||||
6. Review the final markup and remove unnecessary wrappers or styling indirection.
|
||||
|
||||
// ✅ ALWAYS: Use Tailwind color classes
|
||||
<p className="text-white" />
|
||||
<div className="bg-slate-800" />
|
||||
```
|
||||
## Output Contract
|
||||
|
||||
## The cn() Utility
|
||||
- State which styling path was chosen: plain `className`, `cn()`, or inline `style`.
|
||||
- Call out any removed anti-pattern such as `var(...)` in `className` or hex colors.
|
||||
- Mention any remaining escape hatch and why it was necessary.
|
||||
|
||||
```typescript
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
## References
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use cn()
|
||||
|
||||
```typescript
|
||||
// ✅ Conditional classes
|
||||
<div className={cn("base-class", isActive && "active-class")} />
|
||||
|
||||
// ✅ Merging with potential conflicts
|
||||
<button className={cn("px-4 py-2", className)} /> // className might override
|
||||
|
||||
// ✅ Multiple conditions
|
||||
<div className={cn(
|
||||
"rounded-lg border",
|
||||
variant === "primary" && "bg-blue-500 text-white",
|
||||
variant === "secondary" && "bg-gray-200 text-gray-800",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)} />
|
||||
```
|
||||
|
||||
### When NOT to Use cn()
|
||||
|
||||
```typescript
|
||||
// ❌ Static classes - unnecessary wrapper
|
||||
<div className={cn("flex items-center gap-2")} />
|
||||
|
||||
// ✅ Just use className directly
|
||||
<div className="flex items-center gap-2" />
|
||||
```
|
||||
|
||||
## Style Constants for Charts/Libraries
|
||||
|
||||
When libraries don't accept className (like Recharts):
|
||||
|
||||
```typescript
|
||||
// ✅ Constants with var() - ONLY for library props
|
||||
const CHART_COLORS = {
|
||||
primary: "var(--color-primary)",
|
||||
secondary: "var(--color-secondary)",
|
||||
text: "var(--color-text)",
|
||||
gridLine: "var(--color-border)",
|
||||
};
|
||||
|
||||
// Usage with Recharts (can't use className)
|
||||
<XAxis tick={{ fill: CHART_COLORS.text }} />
|
||||
<CartesianGrid stroke={CHART_COLORS.gridLine} />
|
||||
```
|
||||
|
||||
## Dynamic Values
|
||||
|
||||
```typescript
|
||||
// ✅ style prop for truly dynamic values
|
||||
<div style={{ width: `${percentage}%` }} />
|
||||
<div style={{ opacity: isVisible ? 1 : 0 }} />
|
||||
|
||||
// ✅ CSS custom properties for theming
|
||||
<div style={{ "--progress": `${value}%` } as React.CSSProperties} />
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox
|
||||
|
||||
```typescript
|
||||
<div className="flex items-center justify-between gap-4" />
|
||||
<div className="flex flex-col gap-2" />
|
||||
<div className="inline-flex items-center" />
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
```typescript
|
||||
<div className="grid grid-cols-3 gap-4" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" />
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
```typescript
|
||||
// Padding
|
||||
<div className="p-4" /> // All sides
|
||||
<div className="px-4 py-2" /> // Horizontal, vertical
|
||||
<div className="pt-4 pb-2" /> // Top, bottom
|
||||
|
||||
// Margin
|
||||
<div className="m-4" />
|
||||
<div className="mx-auto" /> // Center horizontally
|
||||
<div className="mt-8 mb-4" />
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
```typescript
|
||||
<h1 className="text-2xl font-bold text-white" />
|
||||
<p className="text-sm text-slate-400" />
|
||||
<span className="text-xs font-medium uppercase tracking-wide" />
|
||||
```
|
||||
|
||||
### Borders & Shadows
|
||||
|
||||
```typescript
|
||||
<div className="rounded-lg border border-slate-700" />
|
||||
<div className="rounded-full shadow-lg" />
|
||||
<div className="ring-2 ring-blue-500 ring-offset-2" />
|
||||
```
|
||||
|
||||
### States
|
||||
|
||||
```typescript
|
||||
<button className="hover:bg-blue-600 focus:ring-2 active:scale-95" />
|
||||
<input className="focus:border-blue-500 focus:outline-none" />
|
||||
<div className="group-hover:opacity-100" />
|
||||
```
|
||||
|
||||
### Responsive
|
||||
|
||||
```typescript
|
||||
<div className="w-full md:w-1/2 lg:w-1/3" />
|
||||
<div className="hidden md:block" />
|
||||
<div className="text-sm md:text-base lg:text-lg" />
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
```typescript
|
||||
<div className="bg-white dark:bg-slate-900" />
|
||||
<p className="text-gray-900 dark:text-white" />
|
||||
```
|
||||
|
||||
## Arbitrary Values (Escape Hatch)
|
||||
|
||||
```typescript
|
||||
// ✅ OK for one-off values not in design system
|
||||
<div className="w-[327px]" />
|
||||
<div className="top-[117px]" />
|
||||
<div className="grid-cols-[1fr_2fr_1fr]" />
|
||||
|
||||
// ❌ Don't use for colors - use theme instead
|
||||
<div className="bg-[#1e293b]" /> // NO
|
||||
```
|
||||
- [Prowler UI skill](../prowler-ui/SKILL.md)
|
||||
- [React 19 skill](../react-19/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+35
-344
@@ -1,9 +1,6 @@
|
||||
---
|
||||
name: tdd
|
||||
description: >
|
||||
Test-Driven Development workflow for ALL Prowler components (UI, SDK, API).
|
||||
Trigger: ALWAYS when implementing features, fixing bugs, or refactoring - regardless of component.
|
||||
This is a MANDATORY workflow, not optional.
|
||||
description: "Trigger: ALWAYS when implementing features, fixing bugs, refactoring, or modifying behavior in Prowler. Enforces the RED -> GREEN -> REFACTOR workflow across UI, API, and SDK work."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -18,354 +15,48 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
---
|
||||
|
||||
## TDD Cycle (MANDATORY)
|
||||
## Activation Contract
|
||||
|
||||
```
|
||||
+-----------------------------------------+
|
||||
| RED -> GREEN -> REFACTOR |
|
||||
| ^ | |
|
||||
| +------------------------+ |
|
||||
+-----------------------------------------+
|
||||
```
|
||||
Use this skill before changing production code whenever the task adds behavior, fixes a bug, or refactors existing logic.
|
||||
|
||||
**The question is NOT "should I write tests?" but "what tests do I need?"**
|
||||
## Hard Rules
|
||||
|
||||
---
|
||||
- Start with a failing test; no production change before RED is proven.
|
||||
- Run the smallest relevant test scope, not the whole suite, unless the refactor safety net requires broader coverage.
|
||||
- Add only enough code to pass the current failing test.
|
||||
- After GREEN, refactor with tests still passing.
|
||||
- Load the stack-specific testing skill when applicable: `vitest`, `prowler-test-ui`, `pytest`, `prowler-test-api`, or `prowler-test-sdk`.
|
||||
|
||||
## The Three Laws of TDD
|
||||
## Decision Gates
|
||||
|
||||
1. **No production code** until you have a failing test
|
||||
2. **No more test** than necessary to fail
|
||||
3. **No more code** than necessary to pass
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Working in `ui/`? | Use Vitest conventions and co-located `*.test.{ts,tsx}` files. |
|
||||
| Working in `api/`? | Use pytest + Django patterns and the API testing skill. |
|
||||
| Working in `prowler/`? | Use pytest + provider-specific SDK testing patterns. |
|
||||
| Refactoring without new behavior? | Capture current behavior first by running the closest existing tests before editing. |
|
||||
| No relevant test exists? | Create the narrowest new test that demonstrates the target behavior or bug. |
|
||||
|
||||
---
|
||||
## Execution Steps
|
||||
|
||||
## Detect Your Stack
|
||||
1. Identify the component and matching test runner.
|
||||
2. Read nearby tests first to match naming, fixtures, and assertion style.
|
||||
3. Write or extend one test that fails for the intended behavior.
|
||||
4. Run that focused test and confirm RED.
|
||||
5. Implement the minimum change to reach GREEN.
|
||||
6. Add triangulation cases when one test could be satisfied by a fake or hardcoded implementation.
|
||||
7. Refactor only after the behavior is protected by passing tests.
|
||||
8. Re-run the focused suite and report the exact validation command used.
|
||||
|
||||
Before starting, identify which component you're working on:
|
||||
## Output Contract
|
||||
|
||||
| Working in | Stack | Runner | Test pattern | Details |
|
||||
|------------|-------|--------|-------------|---------|
|
||||
| `ui/` | TypeScript / React | Vitest + RTL | `*.test.{ts,tsx}` (co-located) | See `vitest` skill |
|
||||
| `prowler/` | Python | pytest + moto | `*_test.py` (suffix) in `tests/` | See `prowler-test-sdk` skill |
|
||||
| `api/` | Python / Django | pytest + django | `test_*.py` (prefix) in `api/src/backend/**/tests/` | See `prowler-test-api` skill |
|
||||
- State the RED evidence: which test failed and why.
|
||||
- State the GREEN evidence: which command passed after the change.
|
||||
- Name the stack and test skill used.
|
||||
- Call out any blocker if RED or GREEN could not be executed exactly as intended.
|
||||
|
||||
---
|
||||
## References
|
||||
|
||||
## Phase 0: Assessment (ALWAYS FIRST)
|
||||
|
||||
Before writing ANY code:
|
||||
|
||||
### UI (`ui/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "*.test.tsx" ui/components/feature/
|
||||
|
||||
# 2. Check coverage
|
||||
pnpm test:coverage -- components/feature/
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### SDK (`prowler/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "*_test.py" tests/providers/aws/services/ec2/
|
||||
|
||||
# 2. Run specific test
|
||||
poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### API (`api/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "test_*.py" api/src/backend/api/tests/
|
||||
|
||||
# 2. Run specific test
|
||||
poetry run pytest api/src/backend/api/tests/test_models.py -v
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### Decision Tree (All Stacks)
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Does test file exist for this code? |
|
||||
+----------+-----------------------+-------+
|
||||
| NO | YES
|
||||
v v
|
||||
+------------------+ +------------------+
|
||||
| CREATE test file | | Check coverage |
|
||||
| -> Phase 1: RED | | for your change |
|
||||
+------------------+ +--------+---------+
|
||||
|
|
||||
+--------+--------+
|
||||
| Missing cases? |
|
||||
+---+---------+---+
|
||||
| YES | NO
|
||||
v v
|
||||
+-----------+ +-----------+
|
||||
| ADD tests | | Proceed |
|
||||
| Phase 1 | | Phase 2 |
|
||||
+-----------+ +-----------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: RED - Write Failing Tests
|
||||
|
||||
### For NEW Functionality
|
||||
|
||||
**UI (Vitest)**
|
||||
|
||||
```typescript
|
||||
describe("PriceCalculator", () => {
|
||||
it("should return 0 for quantities below threshold", () => {
|
||||
// Given
|
||||
const quantity = 3;
|
||||
|
||||
// When
|
||||
const result = calculateDiscount(quantity);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**SDK (pytest)**
|
||||
|
||||
```python
|
||||
class Test_ec2_ami_public:
|
||||
@mock_aws
|
||||
def test_no_public_amis(self):
|
||||
# Given - No AMIs exist
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch("prowler...ec2_service", new=EC2(aws_provider)):
|
||||
from prowler...ec2_ami_public import ec2_ami_public
|
||||
|
||||
# When
|
||||
check = ec2_ami_public()
|
||||
result = check.execute()
|
||||
|
||||
# Then
|
||||
assert len(result) == 0
|
||||
```
|
||||
|
||||
**API (pytest-django)**
|
||||
|
||||
```python
|
||||
@pytest.mark.django_db
|
||||
class TestResourceModel:
|
||||
def test_create_resource_with_tags(self, providers_fixture):
|
||||
# Given
|
||||
provider, *_ = providers_fixture
|
||||
tenant_id = provider.tenant_id
|
||||
|
||||
# When
|
||||
resource = Resource.objects.create(
|
||||
tenant_id=tenant_id, provider=provider,
|
||||
uid="arn:aws:ec2:us-east-1:123456789:instance/i-1234",
|
||||
name="test", region="us-east-1", service="ec2", type="instance",
|
||||
)
|
||||
|
||||
# Then
|
||||
assert resource.uid == "arn:aws:ec2:us-east-1:123456789:instance/i-1234"
|
||||
```
|
||||
|
||||
**Run -> MUST fail:** Test references code that doesn't exist yet.
|
||||
|
||||
### For BUG FIXES
|
||||
|
||||
Write a test that **reproduces the bug** first:
|
||||
|
||||
**UI:** `expect(() => render(<DatePicker value={null} />)).not.toThrow();`
|
||||
|
||||
**SDK:** `assert result[0].status == "FAIL" # Currently returns PASS incorrectly`
|
||||
|
||||
**API:** `assert response.status_code == 403 # Currently returns 200`
|
||||
|
||||
**Run -> Should FAIL (reproducing the bug)**
|
||||
|
||||
### For REFACTORING
|
||||
|
||||
Capture ALL current behavior BEFORE refactoring:
|
||||
|
||||
```
|
||||
# Any stack: run ALL existing tests, they should PASS
|
||||
# This is your safety net - if any fail after refactoring, you broke something
|
||||
```
|
||||
|
||||
**Run -> All should PASS (baseline)**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: GREEN - Minimum Code
|
||||
|
||||
Write the MINIMUM code to make the test pass. Hardcoding is valid for the first test.
|
||||
|
||||
**UI:**
|
||||
|
||||
```typescript
|
||||
// Test expects calculateDiscount(100, 10) === 10
|
||||
function calculateDiscount() {
|
||||
return 10; // FAKE IT - hardcoded is valid for first test
|
||||
}
|
||||
```
|
||||
|
||||
**Python (SDK/API):**
|
||||
|
||||
```python
|
||||
# Test expects check.execute() returns 0 results
|
||||
def execute(self):
|
||||
return [] # FAKE IT - hardcoded is valid for first test
|
||||
```
|
||||
|
||||
**This passes. But we're not done...**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Triangulation (CRITICAL)
|
||||
|
||||
**One test allows faking. Multiple tests FORCE real logic.**
|
||||
|
||||
Add tests with different inputs that break the hardcoded value:
|
||||
|
||||
| Scenario | Required? |
|
||||
|----------|-----------|
|
||||
| Happy path | YES |
|
||||
| Zero/empty values | YES |
|
||||
| Boundary values | YES |
|
||||
| Different valid inputs | YES (breaks fake) |
|
||||
| Error conditions | YES |
|
||||
|
||||
**UI:**
|
||||
|
||||
```typescript
|
||||
it("should calculate 10% discount", () => {
|
||||
expect(calculateDiscount(100, 10)).toBe(10);
|
||||
});
|
||||
|
||||
// ADD - breaks the fake:
|
||||
it("should calculate 15% on 200", () => {
|
||||
expect(calculateDiscount(200, 15)).toBe(30);
|
||||
});
|
||||
|
||||
it("should return 0 for 0% rate", () => {
|
||||
expect(calculateDiscount(100, 0)).toBe(0);
|
||||
});
|
||||
```
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
def test_single_public_ami(self):
|
||||
# Different input -> breaks hardcoded empty list
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_private_ami(self):
|
||||
assert result[0].status == "PASS"
|
||||
```
|
||||
|
||||
**Now fake BREAKS -> Real implementation required.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: REFACTOR
|
||||
|
||||
Tests GREEN -> Improve code quality WITHOUT changing behavior.
|
||||
|
||||
- Extract functions/methods
|
||||
- Improve naming
|
||||
- Add types/validation
|
||||
- Reduce duplication
|
||||
|
||||
**Run tests after EACH change -> Must stay GREEN**
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
+------------------------------------------------+
|
||||
| TDD WORKFLOW |
|
||||
+------------------------------------------------+
|
||||
| 0. ASSESS: What tests exist? What's missing? |
|
||||
| |
|
||||
| 1. RED: Write ONE failing test |
|
||||
| +-- Run -> Must fail with clear error |
|
||||
| |
|
||||
| 2. GREEN: Write MINIMUM code to pass |
|
||||
| +-- Fake It is valid for first test |
|
||||
| |
|
||||
| 3. TRIANGULATE: Add tests that break the fake |
|
||||
| +-- Different inputs, edge cases |
|
||||
| |
|
||||
| 4. REFACTOR: Improve with confidence |
|
||||
| +-- Tests stay green throughout |
|
||||
| |
|
||||
| 5. REPEAT: Next behavior/requirement |
|
||||
+------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (NEVER DO)
|
||||
|
||||
```
|
||||
# ANY language:
|
||||
|
||||
# 1. Code first, tests after
|
||||
def new_feature(): ... # Then writing tests = USELESS
|
||||
|
||||
# 2. Skip triangulation
|
||||
# Single test allows faking forever
|
||||
|
||||
# 3. Test implementation details
|
||||
assert component.state.is_loading == True # BAD - test behavior, not internals
|
||||
assert mock_service.call_count == 3 # BAD - brittle coupling
|
||||
|
||||
# 4. All tests at once before any code
|
||||
# Write ONE test, make it pass, THEN write the next
|
||||
|
||||
# 5. Giant test methods
|
||||
# Each test should verify ONE behavior
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands by Stack
|
||||
|
||||
### UI (`ui/`)
|
||||
|
||||
```bash
|
||||
pnpm test # Watch mode
|
||||
pnpm test:run # Single run (CI)
|
||||
pnpm test:coverage # Coverage report
|
||||
pnpm test ComponentName # Filter by name
|
||||
```
|
||||
|
||||
### SDK (`prowler/`)
|
||||
|
||||
```bash
|
||||
poetry run pytest tests/path/ -v # Run specific tests
|
||||
poetry run pytest tests/path/ -v -k "test_name" # Filter by name
|
||||
poetry run pytest -n auto tests/ # Parallel run
|
||||
poetry run pytest --cov=./prowler tests/ # Coverage
|
||||
```
|
||||
|
||||
### API (`api/`)
|
||||
|
||||
```bash
|
||||
poetry run pytest -x --tb=short # Run all (stop on first fail)
|
||||
poetry run pytest api/src/backend/api/tests/test_file.py # Specific file
|
||||
poetry run pytest -k "test_name" -v # Filter by name
|
||||
```
|
||||
- [Vitest skill](../vitest/SKILL.md)
|
||||
- [Pytest skill](../pytest/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+33
-120
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: typescript
|
||||
description: >
|
||||
TypeScript strict patterns and best practices.
|
||||
Trigger: When implementing or refactoring TypeScript in .ts/.tsx (types, interfaces, generics, const maps, type guards, removing any, tightening unknown).
|
||||
description: "Trigger: When implementing or refactoring TypeScript in `.ts` or `.tsx`, including types, interfaces, generics, type guards, const maps, and stricter unknown handling. Enforces strict TypeScript modeling patterns."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -12,131 +10,46 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
---
|
||||
|
||||
## Const Types Pattern (REQUIRED)
|
||||
## Activation Contract
|
||||
|
||||
```typescript
|
||||
// ✅ ALWAYS: Create const object first, then extract type
|
||||
const STATUS = {
|
||||
ACTIVE: "active",
|
||||
INACTIVE: "inactive",
|
||||
PENDING: "pending",
|
||||
} as const;
|
||||
Use this skill when the work changes TypeScript types or when runtime behavior depends on better compile-time modeling.
|
||||
|
||||
type Status = (typeof STATUS)[keyof typeof STATUS];
|
||||
## Hard Rules
|
||||
|
||||
// ❌ NEVER: Direct union types
|
||||
type Status = "active" | "inactive" | "pending";
|
||||
```
|
||||
- Prefer strict, expressive types over `any`; use `unknown`, generics, or narrow unions instead.
|
||||
- Model reusable literals from `as const` objects when values exist at runtime.
|
||||
- Keep interfaces flat; extract nested object shapes into named types.
|
||||
- Use discriminated unions when props or fields are only valid in coordinated sets.
|
||||
- Import types with `import type` when only the type is needed.
|
||||
|
||||
**Why?** Single source of truth, runtime values, autocomplete, easier refactoring.
|
||||
## Decision Gates
|
||||
|
||||
## Flat Interfaces (REQUIRED)
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Need both runtime values and a type union? | Create a const object and derive the type from it. |
|
||||
| Is a value shape deeply nested inline? | Extract dedicated named interfaces or types. |
|
||||
| Are multiple optional props semantically coupled? | Replace them with discriminated union branches. |
|
||||
| Is the input truly unknown? | Accept `unknown` and narrow with a type guard. |
|
||||
| Are you duplicating a mapped or transformed shape manually? | Reach for utility types before inventing parallel interfaces. |
|
||||
|
||||
```typescript
|
||||
// ✅ ALWAYS: One level depth, nested objects → dedicated interface
|
||||
interface UserAddress {
|
||||
street: string;
|
||||
city: string;
|
||||
}
|
||||
## Execution Steps
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
address: UserAddress; // Reference, not inline
|
||||
}
|
||||
1. Identify the domain shape that needs stronger typing.
|
||||
2. Replace `any` or weak optionals with precise unions, generics, or guards.
|
||||
3. Convert literal unions to const-derived types when runtime values matter.
|
||||
4. Flatten nested inline objects into named interfaces.
|
||||
5. Use utility types for projections, partials, and derived shapes.
|
||||
6. Re-check imports and convert type-only imports to `import type` where appropriate.
|
||||
7. Validate that invalid states are now rejected by the type system.
|
||||
|
||||
interface Admin extends User {
|
||||
permissions: string[];
|
||||
}
|
||||
## Output Contract
|
||||
|
||||
// ❌ NEVER: Inline nested objects
|
||||
interface User {
|
||||
address: { street: string; city: string }; // NO!
|
||||
}
|
||||
```
|
||||
- Summarize the type-system improvement made.
|
||||
- Call out any invalid state now prevented at compile time.
|
||||
- Mention the main pattern used: const-derived type, discriminated union, utility type, or type guard.
|
||||
|
||||
## Never Use `any`
|
||||
## References
|
||||
|
||||
```typescript
|
||||
// ✅ Use unknown for truly unknown types
|
||||
function parse(input: unknown): User {
|
||||
if (isUser(input)) return input;
|
||||
throw new Error("Invalid input");
|
||||
}
|
||||
|
||||
// ✅ Use generics for flexible types
|
||||
function first<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// ❌ NEVER
|
||||
function parse(input: any): any { }
|
||||
```
|
||||
|
||||
## Utility Types
|
||||
|
||||
```typescript
|
||||
Pick<User, "id" | "name"> // Select fields
|
||||
Omit<User, "id"> // Exclude fields
|
||||
Partial<User> // All optional
|
||||
Required<User> // All required
|
||||
Readonly<User> // All readonly
|
||||
Record<string, User> // Object type
|
||||
Extract<Union, "a" | "b"> // Extract from union
|
||||
Exclude<Union, "a"> // Exclude from union
|
||||
NonNullable<T | null> // Remove null/undefined
|
||||
ReturnType<typeof fn> // Function return type
|
||||
Parameters<typeof fn> // Function params tuple
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value &&
|
||||
"name" in value
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Coupled Optional Props (REQUIRED)
|
||||
|
||||
Do not model semantically coupled props as independent optionals — this allows invalid half-states that compile but break at runtime. Use discriminated unions with `never` to make invalid combinations impossible.
|
||||
|
||||
```typescript
|
||||
// ❌ BEFORE: Independent optionals — half-states allowed
|
||||
interface PaginationProps {
|
||||
onPageChange?: (page: number) => void;
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
}
|
||||
|
||||
// ✅ AFTER: Discriminated union — shape is all-or-nothing
|
||||
type ControlledPagination = {
|
||||
controlled: true;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
type UncontrolledPagination = {
|
||||
controlled: false;
|
||||
currentPage?: never;
|
||||
pageSize?: never;
|
||||
onPageChange?: never;
|
||||
};
|
||||
|
||||
type PaginationProps = ControlledPagination | UncontrolledPagination;
|
||||
```
|
||||
|
||||
**Key rule:** If two or more props are only meaningful together, they belong to the same discriminated union branch. Mixing them as independent optionals shifts correctness responsibility from the type system to runtime guards.
|
||||
|
||||
## Import Types
|
||||
|
||||
```typescript
|
||||
import type { User } from "./types";
|
||||
import { createUser, type Config } from "./utils";
|
||||
```
|
||||
- [React 19 skill](../react-19/SKILL.md)
|
||||
- [Zod 4 skill](../zod-4/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
+35
-175
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: vitest
|
||||
description: >
|
||||
Vitest unit testing patterns with React Testing Library.
|
||||
Trigger: When writing unit tests for React components, hooks, or utilities.
|
||||
description: "Trigger: When writing or refactoring Vitest tests for React components, hooks, or UI utilities. Defines unit and integration testing patterns with React Testing Library."
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
@@ -16,186 +14,48 @@ metadata:
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
---
|
||||
|
||||
> **For E2E tests**: Use `prowler-test-ui` skill (Playwright).
|
||||
> This skill covers **unit/integration tests** with Vitest + React Testing Library.
|
||||
## Activation Contract
|
||||
|
||||
## Test Structure (REQUIRED)
|
||||
Use this skill for UI unit and integration tests built with Vitest and React Testing Library; for browser E2E flows, switch to `prowler-test-ui` instead.
|
||||
|
||||
Use **Given/When/Then** (AAA) pattern with comments:
|
||||
## Hard Rules
|
||||
|
||||
```typescript
|
||||
it("should update user name when form is submitted", async () => {
|
||||
// Given - Arrange
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<UserForm onSubmit={onSubmit} />);
|
||||
- Structure tests with Given/When/Then intent.
|
||||
- Prefer behavior-oriented `describe` blocks grouped by condition, not by implementation method.
|
||||
- Query the screen by accessibility priority first: role, label, placeholder, text, then test id.
|
||||
- Use `userEvent` for interactions unless a lower-level event is explicitly required.
|
||||
- Keep async assertions focused: one expectation per `waitFor` block.
|
||||
- Restore mocks between tests.
|
||||
|
||||
// When - Act
|
||||
await user.type(screen.getByLabelText(/name/i), "John");
|
||||
await user.click(screen.getByRole("button", { name: /submit/i }));
|
||||
## Decision Gates
|
||||
|
||||
// Then - Assert
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
|
||||
});
|
||||
```
|
||||
| Question | Action |
|
||||
|---|---|
|
||||
| Testing a browser flow across pages? | Use `prowler-test-ui`, not Vitest. |
|
||||
| Need to interact like a user? | Use `userEvent.setup()` and await the interaction. |
|
||||
| Element appears later? | Use `findBy*` or `waitFor` appropriately. |
|
||||
| Need a selector? | Prefer accessible queries before `getByTestId`. |
|
||||
| Thinking about testing internals? | Stop and assert user-visible behavior instead. |
|
||||
|
||||
---
|
||||
## Execution Steps
|
||||
|
||||
## Describe Block Organization
|
||||
1. Confirm the test belongs in unit/integration scope, not Playwright.
|
||||
2. Read nearby tests to match file placement and helper patterns.
|
||||
3. Write or update the spec using AAA comments when clarity helps.
|
||||
4. Render through public component APIs and interact through accessible queries.
|
||||
5. Use `userEvent` for user actions and async queries for delayed UI.
|
||||
6. Isolate mocks and restore them after each test.
|
||||
7. Run only the relevant Vitest target and verify the expected behavior.
|
||||
|
||||
```typescript
|
||||
describe("ComponentName", () => {
|
||||
describe("when [condition]", () => {
|
||||
it("should [expected behavior]", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
## Output Contract
|
||||
|
||||
**Group by behavior, NOT by method.**
|
||||
- State whether the test covers a component, hook, or utility.
|
||||
- Report the main query and interaction patterns used.
|
||||
- Mention the exact Vitest command or filter used for validation.
|
||||
- Call out if E2E coverage was intentionally out of scope.
|
||||
|
||||
---
|
||||
## References
|
||||
|
||||
## Query Priority (REQUIRED)
|
||||
|
||||
| Priority | Query | Use Case |
|
||||
|----------|-------|----------|
|
||||
| 1 | `getByRole` | Buttons, inputs, headings |
|
||||
| 2 | `getByLabelText` | Form fields |
|
||||
| 3 | `getByPlaceholderText` | Inputs without label |
|
||||
| 4 | `getByText` | Static text |
|
||||
| 5 | `getByTestId` | Last resort only |
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD
|
||||
screen.getByRole("button", { name: /submit/i });
|
||||
screen.getByLabelText(/email/i);
|
||||
|
||||
// ❌ BAD
|
||||
container.querySelector(".btn-primary");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## userEvent over fireEvent (REQUIRED)
|
||||
|
||||
```typescript
|
||||
// ✅ ALWAYS use userEvent
|
||||
const user = userEvent.setup();
|
||||
await user.click(button);
|
||||
await user.type(input, "hello");
|
||||
|
||||
// ❌ NEVER use fireEvent for interactions
|
||||
fireEvent.click(button);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Testing Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ findBy for elements that appear async
|
||||
const element = await screen.findByText(/loaded/i);
|
||||
|
||||
// ✅ waitFor for assertions
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/success/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ✅ ONE assertion per waitFor
|
||||
await waitFor(() => expect(mockFn).toHaveBeenCalled());
|
||||
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());
|
||||
|
||||
// ❌ NEVER multiple assertions in waitFor
|
||||
await waitFor(() => {
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mocking
|
||||
|
||||
```typescript
|
||||
// Basic mock
|
||||
const handleClick = vi.fn();
|
||||
|
||||
// Mock with return value
|
||||
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });
|
||||
|
||||
// Always clean up
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
### vi.spyOn vs vi.mock
|
||||
|
||||
| Method | When to Use |
|
||||
|--------|-------------|
|
||||
| `vi.spyOn` | Observe without replacing (PREFERRED) |
|
||||
| `vi.mock` | Replace entire module (use sparingly) |
|
||||
|
||||
---
|
||||
|
||||
## Common Matchers
|
||||
|
||||
```typescript
|
||||
// Presence
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toBeVisible();
|
||||
|
||||
// State
|
||||
expect(button).toBeDisabled();
|
||||
expect(input).toHaveValue("text");
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
// Content
|
||||
expect(element).toHaveTextContent(/hello/i);
|
||||
expect(element).toHaveAttribute("href", "/home");
|
||||
|
||||
// Functions
|
||||
expect(fn).toHaveBeenCalledWith(arg1, arg2);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Test
|
||||
|
||||
```typescript
|
||||
// ❌ Internal state
|
||||
expect(component.state.isLoading).toBe(true);
|
||||
|
||||
// ❌ Third-party libraries
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
|
||||
// ❌ Static content (unless conditional)
|
||||
expect(screen.getByText("Welcome")).toBeInTheDocument();
|
||||
|
||||
// ✅ User-visible behavior
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
components/
|
||||
├── Button/
|
||||
│ ├── Button.tsx
|
||||
│ ├── Button.test.tsx # Co-located
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm test # Watch mode
|
||||
pnpm test:run # Single run
|
||||
pnpm test:coverage # With coverage
|
||||
pnpm test Button # Filter by name
|
||||
```
|
||||
- [TDD skill](../tdd/SKILL.md)
|
||||
- [Prowler UI E2E skill](../prowler-test-ui/SKILL.md)
|
||||
- [Repository agent rules](../../AGENTS.md)
|
||||
|
||||
Reference in New Issue
Block a user