# Tester Subagent Reference

## Critical Rule: Linux Only (ADR-014)

**ALL tests MUST run in the dev container.** Windows test results are unreliable.

| Result                    | Interpretation       |
| ------------------------- | -------------------- |
| Pass Windows / Fail Linux | BROKEN - must fix    |
| Fail Windows / Pass Linux | PASSING - acceptable |

---

## Test Commands

### From Windows Host (via Podman)

```bash
# Unit tests (~2900 tests) - pipe to file for AI processing
podman exec -it flyer-crawler-dev npm run test:unit 2>&1 | tee test-results.txt

# Integration tests (requires DB/Redis)
podman exec -it flyer-crawler-dev npm run test:integration

# E2E tests (requires all services)
podman exec -it flyer-crawler-dev npm run test:e2e

# Specific test file
podman exec -it flyer-crawler-dev npm test -- --run src/hooks/useAuth.test.tsx

# Type checking (CRITICAL before commit)
podman exec -it flyer-crawler-dev npm run type-check

# Coverage report
podman exec -it flyer-crawler-dev npm run test:coverage
```

### Inside Dev Container

```bash
npm test                    # All tests
npm run test:unit           # Unit tests only
npm run test:integration    # Integration tests
npm run test:e2e            # E2E tests
npm run type-check          # TypeScript check
```

---

## Test File Locations

| Test Type   | Location                                      | Config                         |
| ----------- | --------------------------------------------- | ------------------------------ |
| Unit        | `src/**/*.test.ts`, `src/**/*.test.tsx`       | `vite.config.ts`               |
| Integration | `src/tests/integration/*.integration.test.ts` | `vitest.config.integration.ts` |
| E2E         | `src/tests/e2e/*.e2e.test.ts`                 | `vitest.config.e2e.ts`         |
| Setup       | `src/tests/setup/*.ts`                        | -                              |
| Helpers     | `src/tests/utils/*.ts`                        | -                              |

---

## Test Helpers

### Location: `src/tests/utils/`

| Helper                  | Purpose                                                         | Import                              |
| ----------------------- | --------------------------------------------------------------- | ----------------------------------- |
| `testHelpers.ts`        | `createAndLoginUser()`, `getTestBaseUrl()`, `getFlyerBaseUrl()` | `../tests/utils/testHelpers`        |
| `cleanup.ts`            | `cleanupDb({ userIds, flyerIds })`                              | `../tests/utils/cleanup`            |
| `mockFactories.ts`      | `createMockStore()`, `createMockAddress()`, `createMockFlyer()` | `../tests/utils/mockFactories`      |
| `storeHelpers.ts`       | `createStoreWithLocation()`, `cleanupStoreLocations()`          | `../tests/utils/storeHelpers`       |
| `poll.ts`               | `poll(fn, predicate, options)` - wait for async conditions      | `../tests/utils/poll`               |
| `mockLogger.ts`         | Mock pino logger for tests                                      | `../tests/utils/mockLogger`         |
| `createTestApp.ts`      | Create Express app instance for route tests                     | `../tests/utils/createTestApp`      |
| `createMockRequest.ts`  | Create mock Express request objects                             | `../tests/utils/createMockRequest`  |
| `cleanupFiles.ts`       | Clean up test file uploads                                      | `../tests/utils/cleanupFiles`       |
| `websocketTestUtils.ts` | WebSocket testing utilities                                     | `../tests/utils/websocketTestUtils` |

### Usage Examples

```typescript
// Create authenticated user for tests
import { createAndLoginUser, TEST_PASSWORD } from '../tests/utils/testHelpers';
const { user, token } = await createAndLoginUser({
  email: `test-${Date.now()}@example.com`,
  request: request(app), // For integration tests
  role: 'admin', // Optional: make admin
});

// Cleanup after tests
import { cleanupDb } from '../tests/utils/cleanup';
afterEach(async () => {
  await cleanupDb({ userIds: [user.user.user_id] });
});

// Wait for async operation
import { poll } from '../tests/utils/poll';
await poll(
  () => db.userRepo.findUserByEmail(email, logger),
  (user) => !!user,
  { timeout: 5000, interval: 500, description: 'user to be findable' },
);

// Create mock data
import { createMockStore, createMockFlyer } from '../tests/utils/mockFactories';
const mockStore = createMockStore({ name: 'Test Store' });
const mockFlyer = createMockFlyer({ store_id: mockStore.store_id });
```

---

## Test Setup Files

| File                                          | Purpose                                  |
| --------------------------------------------- | ---------------------------------------- |
| `src/tests/setup/tests-setup-unit.ts`         | Unit test setup (mocks, DOM environment) |
| `src/tests/setup/tests-setup-integration.ts`  | Integration test setup (DB connections)  |
| `src/tests/setup/global-setup.ts`             | Global setup for unit tests              |
| `src/tests/setup/integration-global-setup.ts` | Global setup for integration tests       |
| `src/tests/setup/e2e-global-setup.ts`         | Global setup for E2E tests               |
| `src/tests/setup/mockHooks.ts`                | React hook mocking utilities             |
| `src/tests/setup/mockUI.ts`                   | UI component mocking                     |
| `src/tests/setup/globalApiMock.ts`            | API mocking setup                        |

---

## Known Integration Test Issues

### 1. Vitest globalSetup Context Isolation

**Problem**: globalSetup runs in separate Node.js context. Singletons/mocks don't share instances.

**Affected**: BullMQ worker mocks

**Solution**: Use `.todo()`, test-only API endpoints, or Redis-based flags.

### 2. Cleanup Queue Race Condition

**Problem**: Cleanup worker processes jobs before test verification.

**Solution**:

```typescript
const { cleanupQueue } = await import('../../services/queues.server');
await cleanupQueue.drain();
await cleanupQueue.pause();
// ... run test ...
await cleanupQueue.resume();
```

### 3. Cache Stale After Direct SQL

**Problem**: Direct `pool.query()` bypasses cache invalidation.

**Solution**:

```typescript
await pool.query('INSERT INTO flyers ...');
await cacheService.invalidateFlyers(); // Add this
```

### 4. File Upload Filename Collisions

**Problem**: Multer predictable filenames cause race conditions.

**Solution**:

```typescript
const filename = `test-${Date.now()}-${Math.round(Math.random() * 1e9)}.jpg`;
```

### 5. Response Format Mismatches

**Problem**: API response structure changes (`data.jobId` vs `data.job.id`).

**Solution**: Log response bodies, update assertions to match actual format.

---

## Test Patterns

### Unit Test Pattern

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';

describe('MyService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should do something', () => {
    // Arrange
    const input = { data: 'test' };

    // Act
    const result = myService.process(input);

    // Assert
    expect(result).toBe('expected');
  });
});
```

### Integration Test Pattern

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';

describe('API Integration', () => {
  let user, token;

  beforeEach(async () => {
    const result = await createAndLoginUser({ request: request(app) });
    user = result.user;
    token = result.token;
  });

  afterEach(async () => {
    await cleanupDb({ userIds: [user.user.user_id] });
  });

  it('GET /api/resource returns data', async () => {
    const res = await request(app).get('/api/resource').set('Authorization', `Bearer ${token}`);

    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
  });
});
```

### Route Test Pattern

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { createTestApp } from '../tests/utils/createTestApp';

vi.mock('../services/db/flyer.db', () => ({
  getFlyerById: vi.fn(),
}));

describe('Flyer Routes', () => {
  let app;

  beforeEach(() => {
    vi.clearAllMocks();
    app = createTestApp();
  });

  it('GET /api/flyers/:id returns flyer', async () => {
    const mockFlyer = { id: '123', name: 'Test' };
    vi.mocked(flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);

    const res = await request(app).get('/api/flyers/123');

    expect(res.status).toBe(200);
    expect(res.body.data).toEqual(mockFlyer);
  });
});
```

---

## Mocking Patterns

### Mock Modules

```typescript
// At top of test file
vi.mock('../services/db/flyer.db', () => ({
  getFlyerById: vi.fn(),
  listFlyers: vi.fn(),
}));

// In test
import * as flyerDb from '../services/db/flyer.db';
vi.mocked(flyerDb.getFlyerById).mockResolvedValue(mockFlyer);
```

### Mock React Query

```typescript
vi.mock('@tanstack/react-query', async () => {
  const actual = await vi.importActual('@tanstack/react-query');
  return {
    ...actual,
    useQuery: vi.fn().mockReturnValue({
      data: mockData,
      isLoading: false,
      error: null,
    }),
  };
});
```

### Mock Pino Logger

```typescript
import { createMockLogger } from '../tests/utils/mockLogger';
const mockLogger = createMockLogger();
```

---

## Testing React Components

```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>{children}</BrowserRouter>
    </QueryClientProvider>
  );
};

it('renders component', () => {
  render(<MyComponent />, { wrapper: createWrapper() });
  expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
```

---

## Test Coverage

```bash
# Generate coverage report
podman exec -it flyer-crawler-dev npm run test:coverage

# View HTML report
# Coverage reports generated in coverage/ directory
```

---

## Debugging Tests

```bash
# Verbose output
npm test -- --reporter=verbose

# Run single test with debugging
DEBUG=* npm test -- --run src/path/to/test.test.ts

# Vitest UI (interactive)
npm run test:ui
```
