AI Agent Guidelines
This document provides instructions for AI agents working on the Reveni codebase.
General Guidelines
General Rules
- NO COMMENTS: NEVER add comments to the code. Code should be self-documenting and clear without them.
- NO PROPTYPES: NEVER add
propTypesto components. We are moving away from them. - STYLES IMPORT: Styles MUST always be imported using the following pattern:
import * as Styles from './styles'. - USE REVENI-UI TEXT COMPONENTS: NEVER create custom styled text components (e.g.,
styled.p,styled.span,styled.h1) for displaying text. Use theTextcomponent fromreveni-uiinstead, which already handles font sizes, colors, and variants from the design system.
// ✅ GOOD
import { Text } from 'reveni-ui'
<Text version="v4" type="titleL">Some text</Text>
// ❌ BAD
export const CustomText = styled.p`
font-size: ${props => props.theme.v4.texts.bodyMRegular.fontSize};
color: ${props => props.theme.v4.colors.black};
margin: 0;
`
- NO TRANSIENT PROPS PREFIX: NEVER use the
$prefix for styled-components props (e.g., useprops.activeinstead ofprops.$active). This is handled separately in this project. - USE DESIGN TOKENS: ALWAYS use
reveni-uitheme tokens for colors, padding, gap, margins, font sizes, border radius, etc. NEVER hardcode values like#333,16px, or8px. Access them viaprops.theme.v4.
// ✅ GOOD
export const Container = styled.div`
gap: ${props => props.theme.v4.spacing.sp4};
background-color: ${props => props.theme.v4.colors.grey.tone50};
padding: ${props => props.theme.v4.spacing.sp4} ${props => props.theme.v4.spacing.sp8}
${props => props.theme.v4.spacing.sp8} ${props => props.theme.v4.spacing.sp8};
`
// ❌ BAD
export const Container = styled.div`
gap: 4px;
background-color: #f5f5f5;
padding: 4px 8px 8px 8px;
`
- NAMING CONVENTIONS: Always use
camelCasefor naming variables, constants, and functions. - COMPONENT TESTING: Every time a new component is created, its corresponding tests MUST be implemented.
- CONSTANTS: If a component has constants (with no logic), they MUST be extracted to a
constants.jsfile. - ONE COMPONENT PER FILE: Each
.jsxfile MUST contain only one component. NEVER define multiple components in the same file. - NO SPREAD PROPS IN JSX: NEVER use spread operators (
{...obj}) to pass props to JSX components. Always pass each prop explicitly by name.
// ✅ GOOD
<Component
icon={getIcon(item)}
style={getStyle(item)}
/>
// ❌ BAD
<Component
{...(item.icon && { icon: item.icon })}
{...(item.error && { style: errorStyle })}
/>
- NO INLINE FUNCTIONS IN JSX: NEVER define arrow functions directly in JSX props. Extract them into named handler functions declared in the component body (or in a custom hook) and pass the reference. The only exception is inside iterators (e.g.,
.map()) where you need to pass the current item as an argument.
// ✅ GOOD
const handleClose = () => setRuleToDelete(null)
return <Modal onClose={handleClose} />
// ✅ GOOD — exception: inline inside a .map() to pass the item
{items.map(item => (
<Item key={item.id} onClick={() => handleSelect(item)} />
))}
// ❌ BAD
return <Modal onClose={() => setRuleToDelete(null)} />
- ARROW FUNCTION BODIES: If the arrow function body fits on the same line, you can use the implicit return (no braces, no
return). If it doesn't fit on the same line and needs to wrap, you MUST use{}with an explicitreturn. NEVER use implicit return that wraps to the next line.
// ✅ GOOD — fits on the same line, implicit return
const double = x => x * 2
const getLabel = item => item.label
// ✅ GOOD — doesn't fit on the same line, braces + explicit return
const getFilteredItems = (items, isEnabled) => {
return items.filter(item => item.active && isEnabled)
}
// ❌ BAD — implicit return wrapping to the next line
const getFilteredItems = (items, isEnabled) =>
items.filter(item => item.active && isEnabled)
- BLANK LINES BETWEEN LOGICAL BLOCKS: ALWAYS separate distinct logical blocks within a function with blank lines. Group related statements together (e.g., consecutive
ifstatements stay together) and add a blank line between different groups (e.g., declarations vs return, guards vs main logic).
// ✅ GOOD — ifs grouped together, blank line before return
export const getInitialFilter = (selectedProductTypes, selectedCategories, tags) => {
if (selectedProductTypes?.length > 0) return returnReasonsFilteringTypes.productType
if (selectedCategories?.length > 0) return returnReasonsFilteringTypes.category
if (tags?.length > 0) return returnReasonsFilteringTypes.productTags
return allProducts
}
// ❌ BAD — everything crammed without separation
export const getInitialFilter = (selectedProductTypes, selectedCategories, tags) => {
if (selectedProductTypes?.length > 0) return returnReasonsFilteringTypes.productType
if (selectedCategories?.length > 0) return returnReasonsFilteringTypes.category
if (tags?.length > 0) return returnReasonsFilteringTypes.productTags
return allProducts
}
// ✅ GOOD — blank line separating declarations from return
const a = () => {
const b = '1'
const c = '2'
return b + c
}
// ❌ BAD
const a = () => {
const b = '1'
const c = '2'
return b + c
}
// ✅ GOOD — declarations, guards, and main logic clearly separated
const getLabel = (items, isEnabled) => {
const count = items.length
const hasItems = count > 0
if (!isEnabled) return defaultLabel
if (!hasItems) return emptyLabel
return `${count} items`
}
// ❌ BAD
const getLabel = (items, isEnabled) => {
const count = items.length
const hasItems = count > 0
if (!isEnabled) return defaultLabel
if (!hasItems) return emptyLabel
return `${count} items`
}
Frontend Focus
- NEVER look at backend code to perform the tasks you're given. You must rely solely on the documentation, frontend code, and provided information.
- If you need to inspect a component from the
reveni-uidesign system (e.g., its props, styles, or implementation), you can find it in../reveni-ui. - If you add or change any literal in the codebase, you MUST also add or update its corresponding translation (e.g., in
src/i18n/translations/). Before adding a new translation key, check if an existing key already covers that text to avoid duplicates. - Tests MUST always use
Shouldin the description and use thetestfunction instead ofit(e.g., usetest('Should render...', () => {})instead ofit('should render...', () => {})). - Test Execution: Tests are launched by executing
yarn test:agent. You can pass the path to the specific test file or directory you want to run (e.g.,yarn test:agent src/components/MyComponent/MyComponent.test.js).
Component Architecture
- Components MUST NOT contain complex business logic or state management directly.
- If a component requires state or complex logic, it MUST be extracted into a custom hook.
- EARLY RETURNS: Use early returns to handle loading states, error states, and empty/no-results states BEFORE the main
return. NEVER nest these as ternaries inside the main JSX. Each special state should be its ownifblock with areturn. The finalreturnshould only contain the happy-path rendering.
// ✅ GOOD
const MyComponent = ({ loading, items }) => {
if (loading) {
return <Skeleton />
}
if (!items?.length) {
return <EmptyState />
}
return (
<Styles.Container>
{items.map(item => (
<Item key={item.id} item={item} />
))}
</Styles.Container>
)
}
// ❌ BAD
const MyComponent = ({ loading, items }) => {
return (
<Styles.Container>
{loading ? (
<Skeleton />
) : items?.length > 0 ? (
items.map(item => <Item key={item.id} item={item} />)
) : (
<EmptyState />
)}
</Styles.Container>
)
}
- The standard folder structure for a component should be:
hooks/: Directory for custom hooks.helpers.js: Pure functions and logic helpers.helpers.test.js: Unit tests for helpers.[ComponentName].jsx: The functional component itself.[ComponentName].test.jsx: Unit tests for the component.constants.js: Constants used by the component.
- These files and folders are not mandatory for every component but should be created when needed following this structure.
Data Fetching & Transformers
When a custom hook calls the API (typically through useFetch), the data that goes to the backend and the data that comes from it MUST be mapped through transformer functions. The goal is to have a single, explicit place that documents the shape of the payloads the frontend actually uses.
Rules
- DEDICATED FILE: Every time there is an API request, create a
transformers.jsfile in the same directory where the request is made (usually inside the custom hook folder). Do not mix transformers with unrelated helpers. - RESPONSE NAMING: Exported functions that transform the data coming from the API MUST be named with the prefix
transformResponse, followed by the entity. Example:transformResponsePolicyRules. - REQUEST NAMING: Exported functions that transform the data being sent to the API MUST be named with the prefix
transformRequest, followed by the action/entity. Example:transformRequestCreatePolicyRule,transformRequestUpdatePolicyRule. - USE WITH
useFetch: Pass transformers touseFetchvia thetransformRequest/transformResponseoptions. Memoize them (declare them outside the component or wrap inuseCallback) so thefetchData/fetchDataCancellableidentity stays stable across renders. - MAP, DON'T TRANSFORM: The default intent is mapping — explicitly picking the fields the frontend uses — not reshaping data. Extra transformations are allowed when genuinely needed (e.g., parsing a date, flattening a structure the UI consumes differently), but do not invent work.
- DO NOT RENAME FIELDS: Transformers MUST NOT rename fields from snake_case to camelCase or any other convention. Keep the original field names from the API. The transformer only picks which fields to keep, it does not rename them.
- ONLY WHAT THE FRONT USES: Map only the fields consumed by the frontend. If a field is not used, omit it — do not forward it "just in case". This keeps the contract between back and front explicit.
- NESTED OBJECTS / ARRAYS: For nested structures, compose transformers using internal
mapfunctions (e.g.,transformResponsePolicyRulesiterates with.map(mapPolicyRule)). Do not inline deep mappings inside a single function when a child mapper already exists or can be extracted. - MINIMAL LOGIC: Transformers MUST NOT contain business logic, data processing, or imports from helpers. They should only pick and forward fields. If data needs processing (filtering, normalizing, computing), do it in the hook or in helpers — not in the transformer.
- NAMING CONVENTION: Only exported transformer functions use the
transformResponse/transformRequestprefix. Internal helper functions withintransformers.js(e.g., mapping a nested object) MUST use themapprefix instead (e.g.,mapDeliveryMethod,mapCondition). - TESTS ARE MANDATORY: Every transformer MUST have its own unit tests in a
transformers.test.jsfile next to it. Test the mapping shape, the fields that must be present, and the fields that must be omitted.
Example structure
hooks/
usePolicyRule/
usePolicyRule.js
usePolicyRule.test.js
transformers.js
transformers.test.js
Example — response transformer
// transformers.js
const mapPolicyRuleCondition = condition => ({
id: condition.id,
type: condition.type,
value: condition.value,
})
const mapPolicyRule = rule => ({
id: rule.id,
name: rule.name,
conditions: rule.conditions.map(mapPolicyRuleCondition),
})
export const transformResponsePolicyRules = rules => rules.map(mapPolicyRule)
Example — request transformer
// transformers.js
export const transformRequestCreatePolicyRule = rule => ({
name: rule.name,
conditions: rule.conditions.map(condition => ({
type: condition.type,
value: condition.value,
})),
})
Example — wiring into useFetch
import useFetch from 'hooks/useFetch'
import { transformResponsePolicyRules, transformRequestCreatePolicyRule } from './transformers'
const usePolicyRule = id => {
const { data, fetchData } = useFetch(getPolicyRuleService, {
doInitialRequest: true,
params: { id },
transformResponse: transformResponsePolicyRules,
})
const { fetchData: createPolicyRule } = useFetch(createPolicyRuleService, {
doInitialRequest: false,
transformRequest: transformRequestCreatePolicyRule,
transformResponse: transformResponsePolicyRules,
})
return { policyRules: data, createPolicyRule }
}
Final Checks
After completing all changes, you MUST run the following checks in this order before committing:
- Run linter: Execute
yarn linterto check for linting errors. This is mandatory before every commit. - Run formatter (if linter fails): If there are linting errors, execute
yarn formatto automatically fix formatting issues, then runyarn linteragain to verify. - Run tests: Execute
yarn test:agentto ensure all tests pass. If you modified specific files, you can run tests for those files only.
These checks ensure code quality and prevent issues before committing.
Pull Requests
- When creating a pull request, you MUST use the template located at
.github/pull_request_template.mdas the body of the PR. - Fill in the description section with a summary of the changes.
- Mark the applicable checklist items according to what the PR includes.
- You MUST always add the following users as reviewers:
sergio-reveni,gianmarco-reveniandmiguel-reveni. Do NOT add the PR author as a reviewer since GitHub does not allow it. - Bump version: Before creating the PR, you MUST bump the version in
package.jsonusingnpm version <patch|minor|major> --no-git-tag-version. Choose the correct semver increment based on the type of change:- patch (e.g.
5.330.0→5.330.1): Bug fixes, hotfixes, small corrections that don't add new functionality. - minor (e.g.
5.330.0→5.331.0): New features, new components, new endpoints, or enhancements to existing functionality. - major (e.g.
5.330.0→6.0.0): Breaking changes, large-scale refactors, or incompatible API changes. - If you are unsure which type of bump applies, you MUST ask the user before bumping.
- patch (e.g.
Testing Best Practices
When writing unit tests for React components, follow these rules:
1. Explicit Rendering
DO NOT extract component rendering to a renderComponent or similar helper function at the top level of the test file.
DO perform an explicit render call inside each test block.
DO NOT pass the theme prop to BaseTest. It is already handled internally.
Rationale: It makes it easier to see exactly which props are being passed to the component for each test case without having to look up a helper function.
// ✅ GOOD
test('Should call handleChange when clicked', async () => {
render(
<BaseTest>
<MyComponent onChange={mockOnChange} />
</BaseTest>
)
// ... test logic
})
// ❌ BAD
const renderComponent = props =>
render(
<BaseTest>
<MyComponent {...props} />
</BaseTest>
)
test('Should call handleChange when clicked', async () => {
renderComponent({ onChange: mockOnChange })
// ... test logic
})
2. Robust Selectors
- Use
screen.getByRolefor interactive elements (buttons, checkboxes, etc.) to ensure accessibility and robustness. - Use
screen.getByTextorscreen.getByDisplayValuewhen specific text or values are the focus of the test. - Use regex for case-insensitive matching when appropriate:
screen.getByText(/save/i).
3. User Interactions
- Prefer
@testing-library/user-eventoverfireEventfor simulating user interactions, as it better mimics real browser behavior. - Always
awaituser interactions. - DO NOT use
userEvent.setup(). UseuserEventmethods directly (e.g.,await userEvent.click(element)).
// ✅ GOOD
import userEvent from '@testing-library/user-event'
test('Should call handler when clicked', async () => {
render(<MyComponent onClick={mockHandler} />)
await userEvent.click(screen.getByRole('button'))
expect(mockHandler).toHaveBeenCalled()
})
// ❌ BAD
import userEvent from '@testing-library/user-event'
test('Should call handler when clicked', async () => {
const user = userEvent.setup()
render(<MyComponent onClick={mockHandler} />)
await user.click(screen.getByRole('button'))
expect(mockHandler).toHaveBeenCalled()
})
4. Test Behavior, Not Implementation
DO NOT write tests that only verify a function was called. DO write tests that verify the observable outcome of a user action: what renders, what values are returned, what the user sees.
A test that only checks "this function was called" covers lines and passes thresholds, but it doesn't prove anything works correctly. If the implementation changes internally but the behavior stays the same, the test should still pass.
Rule of thumb: assertions should answer "what happened from the user's perspective?", not "which internal function was invoked?".
// ❌ BAD — tests implementation (only checks the function was called)
test('Should call handleSubmit when button is clicked', async () => {
const handleSubmit = jest.fn()
render(
<BaseTest>
<MyForm onSubmit={handleSubmit} />
</BaseTest>
)
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(handleSubmit).toHaveBeenCalled()
})
// ✅ GOOD — tests behavior (verifies outcome and how the function was called)
test('Should submit the form with the entered values when button is clicked', async () => {
const handleSubmit = jest.fn()
render(
<BaseTest>
<MyForm onSubmit={handleSubmit} />
</BaseTest>
)
await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Reveni')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(handleSubmit).toHaveBeenCalledTimes(1)
expect(handleSubmit).toHaveBeenCalledWith({ name: 'Reveni' })
})
When asserting on mock functions, always verify:
- How many times it was called (
toHaveBeenCalledTimes). - With what arguments it was called (
toHaveBeenCalledWith). - What the user sees after the action (e.g., a success message, a list update, a modal closing).
5. No Snapshot Tests for Pages or Composed Components
DO NOT write snapshot tests (toMatchSnapshot) for pages or components that are mostly compositions of already-tested child components. Snapshots of large component trees are brittle, break on any minor change in a child, and provide no meaningful coverage beyond what the child tests already guarantee.
Snapshot tests are only acceptable for small, leaf-level components with stable output (e.g., an icon, a badge).
// ❌ BAD — snapshot of an entire page whose children are already tested
test('Should match snapshot', () => {
const { container } = render(
<BaseTest>
<DashboardPage />
</BaseTest>
)
expect(container).toMatchSnapshot()
})
// ✅ GOOD — test the page's own logic: what it renders conditionally, how it reacts to state
test('Should render the empty state when there are no items', () => {
render(
<BaseTest>
<DashboardPage items={[]} />
</BaseTest>
)
expect(screen.getByText(/no items/i)).toBeInTheDocument()
})
6. Mocking
- Mock functions using
jest.fn(). - Use
jest.clearAllMocks()inbeforeEachto ensure test independence.