CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# Multiforum Development Guide
> Use **Node.js 26** for development (`nvm use`). Node.js ≥ 24.13.0 remains
> supported while Vercel deploys on Node 24; older majors break the tooling.
## Build & Run Commands
- `pnpm run graphql:check` - Validate production GraphQL documents against the pinned schema with no outstanding errors
- `pnpm run graphql:test` - Test GraphQL extraction, source locations, and baseline enforcement
- `pnpm run schema:check` - Verify the pinned backend schema and report its source commit
- `pnpm run schema:update /path/to/backend-artifact` - Update the schema pin; see [contract workflow](docs/graphql-contract-campaign.md)
- `pnpm run compile` - Generate schema and operation types offline from the pinned contract
- `pnpm run operations:check` - Verify typed-document freshness and production consumer contracts; see the [migration guide](docs/graphql-contract-campaign.md)
- `pnpm run dev` - Start development server
- `pnpm run build` - Build for production
- `pnpm run test:playwright` - Run Playwright tests
- `pnpm run build:playwright:mocked` - Build the mocked app once for fast parallel Playwright runs
- `pnpm run test:playwright:mocked:build` - Run the mocked Playwright suite against the prebuilt app
- `pnpm run test:unit` - Run unit tests with Vitest
- `pnpm run test:playwright:mocked -- tests/playwright/mocked/discussions/createEditDeleteDiscussions.spec.ts` - Run a specific mocked Playwright test
- `pnpm run tsc` - Run TypeScript type checking
- `pnpm exec eslint --fix path/to/file.vue` - Fix linting issues
## Playwright Testing (E2E)
- Use `installMockAuth()` to handle authentication for tests that need it
- Most Playwright tests use mocked GraphQL responses and require no backend.
- Cross-repository workflows that need to prove browser, API, and persistence
behavior use the opt-in stateful suites under `tests/playwright/stateful`.
Run the admin moderation suite with a backend checkout available via
`PLAYWRIGHT_BACKEND_CWD`: start Neo4j with `pnpm run playwright:neo4j:up`,
run `pnpm run test:playwright:stateful:admin`, then stop it with
`pnpm run playwright:neo4j:down`.
### Playwright Test Optimizations
- **Replace arbitrary timeouts with network waits**:
```javascript
// Before: Fixed timeout that might be too short or too long
await page.getByRole('button', { name: 'Save' }).click();
// After: Wait for the actual network request to complete
await page.waitForResponse((response) => response.url().includes('/graphql'));
```
- **Validate network responses**: Add status code checks to ensure operations completed successfully
```javascript
cy.wait('@graphqlRequest').its('response.statusCode').should('eq', 200);
```
- **Intercept specific GraphQL operations** by pattern-matching on the request body:
```javascript
// Match specific GraphQL operations
cy.intercept('POST', '**/graphql', (req) => {
if (req.body.query.includes('createDiscussion')) {
req.alias = 'createDiscussionRequest';
}
});
// Later in the test
cy.wait('@createDiscussionRequest');
```
## Vitest Testing (Unit)
- Unit tests live in `tests/unit` or colocated next to the file under test (`Foo.vue` → `Foo.spec.ts`)
- Run all unit tests with `pnpm run test:unit`
- Run specific tests with `pnpm run test:unit -- --run tests/unit/path/to/test.spec.ts`
- Use exactly one `expect(...)` assertion per `it(...)` test case
- If you need multiple checks, split into multiple `it(...)` blocks or combine into a single structured `expect(...)`
- Prefer `it.each(...)` tables when it helps keep tests DRY
- Keep `vi.mock(...)` at the module top level — Vitest hoists it, so calling it inside `beforeEach`/`describe` is deprecated. If the factory references a local variable, prefix its name with `mock` (e.g. `mockThemeRef`)
- Test thin page wrappers via `shallowMount` + `findComponent(Child).props(...)`; stub slot wrappers (`NuxtLayout`, `FormRow`) to render their slot, and `vi.stubGlobal('definePageMeta', vi.fn())` for pages that call it. See [CONTRIBUTING.md](./CONTRIBUTING.md#frontend-testing-patterns)
### Unit Test Best Practices
- **Test Real Code, Not Mocks**: Ensure tests verify actual application code rather than reimplementing logic in test files
- Extract component logic into utility files when appropriate for better testability
- Import and test the actual functions from your codebase rather than creating test-only implementations
- **Component Testing**:
- Mount components to test actual validation logic, error messages, and UI state
- Mock only external dependencies and services, not the core logic being tested
- Verify that error/validation messages appear in the component when expected
- **Meaningful Tests**: Tests should validate application behavior, not trivial language features
- Avoid tests that only check basic JavaScript functionality
- Focus on edge cases, expected validations, and user interactions
- **Testable Code**: Refactor components to make logic more testable
- Move complex formatting/validation logic to separate utility files
- Use dependency injection to make components easier to test
- **Test Structure**: Organize tests to mirror the structure of the code being tested
- Group related tests with descriptive names
- Test both success and failure paths
#### Unit Test Gotchas
Hard-won pitfalls (full detail + code in [CONTRIBUTING.md](./CONTRIBUTING.md#frontend-testing-patterns)):
- **Mount the real component, never a hand-written stand-in.** A spec that defines `const FooTest = { template: '...' }` and mounts that adds **zero** coverage to `Foo.vue`. Mock the component's dependencies instead of reimplementing it.
- **Mock Apollo at module level.** `mountWithDefaults` does not wire Apollo. Mock `@vue/apollo-composable`; to run a mutation's `onDone` handlers, make `mutate()` synchronously fire the registered `onDone` callbacks, and return an empty `useQuery` result.
- **Seed hoisted `vi.fn` mocks with the full return shape.** `vi.fn(() => ({ valid: true }))` narrows the type, so a later `mockReturnValue({ valid: false, message })` fails CI's `vue-tsc` even when the test passes locally. Seed `{ valid: true, message: '' }`.
- **`@/utils` and `@/utils/index` are the same module** — if you mock one, mock both with every needed export.
- **Teleported UI** (`<Teleport to="body">` tooltips/modals) renders into `document.body`, not the wrapper — query the document.
- **Mocked GraphQL entities need `__typename`** or Apollo normalization hands the component only the key field.
## Pre-commit Workflow
- The pre-commit hook is intentionally fast and runs only `pnpm exec lint-staged`
- Full linting, type checking, unit tests, and Playwright checks run in CI
- To skip pre-commit hooks temporarily: `git commit --no-verify`
## Code Style Guidelines
- **TypeScript**: Use strict typing whenever possible, proper interfaces in `types/` directory
- **Typed operations**: Import documents and exact selected result types from `@/__generated__/operations`; let Apollo infer variables/results. Prefer operation or fragment result subsets over full schema entities for migrated paths. See [the migration guide](docs/graphql-contract-campaign.md#phase-3-typed-operation-pilot).
- **GraphQL results and variables**: Import documents and selected result types from `@/__generated__/operations`; let Apollo infer variables/results. Schema entities from `@/__generated__/graphql` describe the complete schema, not a query's selections. Use schema input types/enums for genuine inputs, and selected operation/fragment types for props and caches. Do not invent unselected fields to satisfy a schema entity.
- **Avoid `any`**: `@typescript-eslint/no-explicit-any` is an **error** in application source (relaxed only in `*.spec.ts`). Only use `any` for genuinely untyped third-party interop, with a targeted `// eslint-disable-next-line` + reason — never for GraphQL data. See the `apollo-data` skill.
- **Type Checking**: Use `pnpm run tsc` (which runs `vue-tsc --noEmit`) for proper Vue component type checking
### Common TypeScript Patterns and Fixes
- **GraphQL fixtures and cache writes**: Use the exact generated operation or fragment selection, including selected nullable fields. Never fabricate connection fields merely to satisfy a full schema entity. Preserve Apollo normalization keys and test cache writes against the real selected document.
- **Vue Router Type Safety**: Use proper route object structure for navigation
```typescript
// Avoid: path property may not be compatible with router types
routeAndClose({ path: '/account_settings' });
// Prefer: use name-based routing
routeAndClose({ name: 'account_settings' });
```
- **Error Type Handling**: GraphQL error objects may have inferred types as `never` in some contexts
```typescript
// Cast to access message property when TypeScript can't infer the error type
:text="(getCommentError as any)?.message || 'Error loading comment'"
```
- **Form Value Types**: Ensure form default values match the expected type interface completely
```typescript
// If CreateEditChannelFormValues includes eventsEnabled and feedbackEnabled
const defaultValues = {
// ... other properties
eventsEnabled: true, // Don't forget these!
feedbackEnabled: true,
};
```
- **Nuxt Page Meta**: Use proper placement and TypeScript handling
```typescript
// Place at top of script setup block with TypeScript ignore
// @ts-ignore - definePageMeta is auto-imported by Nuxt
definePageMeta({
middleware: 'some-middleware',
});
```
- **GraphQL Query Imports**: When refactoring queries, check that exports exist in the target file
```typescript
// Before using GET_MOD_SUSPENSION, verify it exists in the queries file
// If not available, find similar queries like GET_SUSPENDED_MODS_BY_CHANNEL
```
- **Optional Chaining vs Null Coalescing**: Use appropriate patterns for component props
```typescript
// For optional props that might be undefined
:old-version="activeRevision.oldVersionData || {}"
:new-version="activeRevision.newVersionData || {}"
```
- **Function Parameters**: For functions with more than one parameter, use a typed object instead of positional arguments
```typescript
// Avoid:
function updateUser(id: string, name: string, email: string) { ... }
// Prefer:
type UpdateUserParams = {
id: string;
name: string;
email: string;
};
function updateUser(params: UpdateUserParams) { ... }
```
- **Vue Components**: Use script setup API with TypeScript and properly typed props/emits
- **RequireAuth slot pattern**: For auth-gated controls that should look identical in both auth states, extract a shared presentational child component.
Use a thin functional wrapper for the authenticated slot when the real behavior needs to be attached, and render the shared child directly in the unauthenticated slot unless a placebo wrapper is genuinely useful.
- **Error Handling**: Use try/catch with specific error types, validate GraphQL responses
- **Apollo state (useQuery & useMutation)**: Always leverage the hook's built-in refs — `useMutation`'s `loading` / `error` / `onDone` and `useQuery`'s `result` / `loading` / `error` / `onResult` — instead of recreating that state in hand-written `ref`s. Bind UI disabled/loading/error states directly to the hook's refs, and put side effects in `onDone` / `onResult` / `onError`. Do not hand-roll `mutate()`/`refetch()` in try/catch to mirror state the hook already exposes. See the `apollo-data` skill and [docs/code-review-checklist.md](./docs/code-review-checklist.md).
- **Naming**: camelCase for variables/functions, PascalCase for components/interfaces
- **Imports**: Group imports by type (Vue, libraries, local components, utils)
- **Testing**: Each feature should have Playwright coverage, with shared seed data and cleanup helpers
- **CSS**: Use Tailwind utility classes, dark mode compatible with `dark:` prefix
- **Accessibility**: build to WCAG 2.2 AA. The `accessibility` skill carries the full
contract, AA thresholds, and Vue patterns (invoke it for any UI/component work);
`pnpm run lint:a11y` and the axe Playwright helper are the detection layers. Baseline:
4.5:1 contrast for normal text; accessible names (`<label>` / `aria-label` /
`aria-labelledby`) on every input and interactive control.
- **Composables**: Extract reusable logic into composables under `composables/` directory
- **Reactivity and Watchers**:
- Avoid unnecessary watchers. Use Vue's built-in reactivity system (props, computed, refs) whenever possible
- Use watchers only when absolutely necessary (e.g., for router param changes or external API calls)
- When a component needs to react to prop changes, handle this through the component lifecycle or computed properties, not watchers
- For individual item state that doesn't need to be shared, use local `ref` variables instead of Pinia store state
## Playwright Testing Guidelines
- Prefer explicit URLs and route helpers over hard-coded relative navigation
- Wait on requests or UI state instead of arbitrary sleeps
- Use `installMockAuth()` for browser-level auth setup and `resetStatefulBackendData()` for shared test data
- Replace UI-based authentication (`loginUser()`) with this programmatic pattern
- This pattern is faster and more reliable than UI-based authentication
### Dynamic Test IDs in Modals
Modals like `BrokenRulesModal.vue` use dynamic test IDs based on content type:
- For comments: `report-comment-input`
- For discussions: `report-discussion-input`
- For events: `report-event-input`
Always use the correct testid matching the content type being moderated.
## Permission System
The canonical reference for moderation permissions and suspension behavior is:
- [docs/moderation-architecture.md](./docs/moderation-architecture.md)
Keep `CLAUDE.md` concise and treat the moderation architecture doc as the single source of truth for:
- user vs moderator permission levels
- permission precedence and fallback order
- suspension lifecycle and expiry cleanup
- issue-linked moderation workflow
- server-scope vs channel-scope moderation model
Practical reminders:
- moderator menus in headers should show standard actions like "Give Feedback" and "Report" whenever the resolved permissions allow them
- the "Moderation Actions" section should appear for any actor with at least one moderation permission
- UI components should check specific permission flags rather than inferring from moderator status alone
- tests should verify that unprivileged users do not see moderation actions and that suspended moderators cannot use them
### Suspension System
The application enforces suspensions at both channel and server levels. Suspensions can be time-limited or indefinite.
#### How Suspensions Work
1. **Suspension Creation**: When a moderator suspends a user (e.g., via "Archive and Suspend" on a discussion), a `Suspension` node is created with:
- `suspendedUntil` - Expiration date (for time-limited suspensions)
- `suspendedIndefinitely` - Flag for permanent suspensions
- Link to the related moderation issue
2. **Suspension Detection**: The backend checks for active suspensions when users attempt actions:
- A suspension is active if `suspendedIndefinitely` is true OR `suspendedUntil` is in the future
- Expired suspensions are automatically cleaned up (disconnected from channel relationships)
3. **Permission Enforcement**:
- **Channel-level**: Suspended users use `SuspendedRole` permissions (typically very restricted)
- **Server-level**: Users with any active suspension use `DefaultSuspendedRole` for server actions (e.g., creating new forums)
4. **User Notifications**: When a suspended user is blocked from an action, they receive an in-app notification explaining:
- Which channel they're suspended in
- What action was blocked
- Reference to the related moderation issue
#### Unsuspension
Users can be unsuspended through the moderation issue interface, which removes the suspension relationship from the channel.
### Testing Moderator Permissions
When testing moderator permissions:
- Make sure the test user has the expected permission level
- Check that appropriate UI elements appear based on permission level
- Verify that unprivileged users don't see moderation options
- Test that suspended moderators can't access moderation features
## SSR and Hydration
### Preventing Hydration Mismatches
Hydration errors occur when the server-rendered HTML doesn't match what the client expects to render. Common causes:
1. **`v-if` conditions with async data**: If a condition depends on data that may differ between server and client (e.g., GraphQL query results, auth state), wrap the content in `<ClientOnly>`.
2. **Slot content with client-dependent conditions**: When passing slot content that has `v-if` conditions depending on async data, wrap the content in `<ClientOnly>` **at the source** (where the slot content is defined), not in the child component receiving the slot.
```vue
<!-- WRONG: ClientOnly in the child component doesn't help -->
<!-- components/Child.vue -->
<template>
<ClientOnly>
<slot />
</ClientOnly>
</template>
<!-- CORRECT: ClientOnly wraps the content at the source -->
<!-- components/Parent.vue -->
<template>
<Child>
<ClientOnly>
<MyComponent v-if="asyncData" />
</ClientOnly>
</Child>
</template>
```
3. **Auth-dependent UI**: Components that show different content based on authentication state should use `<ClientOnly>` since auth state is client-side only.
### Key Files with ClientOnly Wrappers
- `components/discussion/detail/DiscussionDetailContent.vue` - Comment form wrappers
- `components/discussion/detail/DiscussionCommentsWrapper.vue` - Subscribe button
## Project Structure
- Components in `components/` directory with subdirectories for features
- Pages in `pages/` directory matching route structure
- GraphQL queries/mutations in `graphQLData/` by domain
- Types in `types/` directory
- Utility functions in `utils/` directory (including permission utilities)
## Data Model Notes
- **Downloads**: There is no separate Download type. Downloads are discussions with the `hasDownload` field set to true. They use the Discussion type and are displayed with a different frontend skin in download-specific components.
## Working with Claude
- **Incremental Changes**: Make small, focused changes rather than large sweeping changes
- Work on one test file at a time rather than multiple tests at once
- Fix specific issues incrementally rather than rewriting multiple files
- **Step-by-Step Approach**: If multiple files need changes, address them one at a time
- First understand the issue, then propose a targeted solution
- Get confirmation before proceeding to the next file
- **Show Your Work**: Explain reasoning behind changes, especially for complex fixes
- When troubleshooting, describe the issue and how the solution addresses it
- Provide context for why a particular approach was chosen