From 37197cf6f17132e173a5036f309a51b8f1409f2a Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Wed, 25 Feb 2026 11:34:09 +0000 Subject: [PATCH 1/2] refactor(tests): add integration test infrastructure and review fixes Introduce vitest workspace with separate unit/integration projects, shared test helpers (factories, mock DB, mock personas), and a full integration test suite for credentialsRepository including encryption round-trip coverage. Review fixes: - OS-aware psql in ensure-services.sh for macOS compatibility - Explicit DATABASE_URL in setup.sh migrations (removes env race) - Build step added to CI integration-tests job - container_name in docker-compose.test.yml for easier debugging - Extract buildProgressMonitorConfig() to fix cognitive complexity lint Unit test cleanup: remove redundant mock boilerplate across 100+ test files by leveraging vitest workspace setup files and shared helpers. Co-Authored-By: Claude Opus 4.6 --- .cascade/ensure-services.sh | 25 ++ .cascade/env | 1 + .cascade/setup.sh | 32 ++- .github/workflows/ci.yml | 39 +++ docker-compose.test.yml | 17 ++ package.json | 11 +- src/backends/adapter.ts | 58 ++-- tests/helpers/factories.ts | 104 +++++++ tests/helpers/mockDb.ts | 89 ++++++ tests/helpers/mockPersonas.ts | 13 + .../db/credentialsRepository.test.ts | 259 ++++++++++++++++++ tests/integration/helpers/db.ts | 50 ++++ tests/integration/helpers/seed.ts | 117 ++++++++ tests/integration/setup.ts | 22 ++ tests/setup.ts | 12 +- tests/unit/agents/hooks.test.ts | 8 - tests/unit/agents/registry.test.ts | 4 - .../unit/agents/shared/builderFactory.test.ts | 1 - tests/unit/agents/shared/cleanup.test.ts | 1 - .../agents/shared/executionPipeline.test.ts | 1 - tests/unit/agents/shared/gadgets.test.ts | 4 - .../agents/shared/modelResolution.test.ts | 4 - tests/unit/agents/shared/runTracking.test.ts | 10 - tests/unit/agents/utils/agentLoop.test.ts | 1 - tests/unit/agents/utils/checklistSync.test.ts | 1 - tests/unit/agents/utils/logging.test.ts | 4 - tests/unit/agents/utils/setup.test.ts | 1 - tests/unit/api/access-control.test.ts | 24 +- tests/unit/api/auth/login.test.ts | 4 - tests/unit/api/auth/logout.test.ts | 4 - tests/unit/api/auth/session.test.ts | 4 - .../api/routers/_shared/projectAccess.test.ts | 1 - tests/unit/api/routers/agentConfigs.test.ts | 10 +- tests/unit/api/routers/auth.test.ts | 13 +- tests/unit/api/routers/credentials.test.ts | 10 +- tests/unit/api/routers/defaults.test.ts | 13 +- .../api/routers/integrationsDiscovery.test.ts | 10 +- tests/unit/api/routers/organization.test.ts | 13 +- tests/unit/api/routers/projects.test.ts | 10 +- tests/unit/api/routers/prompts.test.ts | 13 +- tests/unit/api/routers/runs.test.ts | 10 +- tests/unit/api/routers/webhookLogs.test.ts | 13 +- tests/unit/api/routers/webhooks.test.ts | 13 +- tests/unit/backends/accumulator.test.ts | 1 - tests/unit/backends/adapter.test.ts | 1 - tests/unit/backends/agent-profiles.test.ts | 4 - tests/unit/backends/claude-code-hooks.test.ts | 4 - tests/unit/backends/claude-code.test.ts | 4 - tests/unit/backends/githubPoster.test.ts | 4 - tests/unit/backends/llmist.test.ts | 4 - tests/unit/backends/pmPoster.test.ts | 1 - tests/unit/backends/postProcess.test.ts | 4 - tests/unit/backends/progress.test.ts | 1 - tests/unit/backends/progressModel.test.ts | 4 - tests/unit/backends/secretBuilder.test.ts | 1 - tests/unit/cli/credential-scoping.test.ts | 1 - tests/unit/cli/dashboard/base.test.ts | 4 - tests/unit/cli/dashboard/client.test.ts | 4 - tests/unit/cli/dashboard/config.test.ts | 1 - tests/unit/cli/file-input-flags.test.ts | 1 - tests/unit/config/compactionConfig.test.ts | 4 - tests/unit/config/hintConfig.test.ts | 1 - tests/unit/config/projects.test.ts | 21 -- tests/unit/config/provider.test.ts | 7 - tests/unit/config/statusUpdateConfig.test.ts | 4 - tests/unit/db/crypto.test.ts | 4 - .../db/repositories/configRepository.test.ts | 4 - .../credentialsRepository.test.ts | 46 +--- .../prWorkItemsRepository.test.ts | 29 +- .../runsRepository.dashboard.test.ts | 4 - .../repositories/settingsRepository.test.ts | 45 +-- .../db/repositories/usersRepository.test.ts | 2 - tests/unit/db/runsRepository.test.ts | 2 - tests/unit/db/webhookLogsRepository.test.ts | 2 - tests/unit/gadgets/fileInsertContent.test.ts | 1 - tests/unit/gadgets/fileRemoveContent.test.ts | 1 - tests/unit/gadgets/finish.test.ts | 4 - tests/unit/gadgets/github.test.ts | 4 - .../unit/gadgets/github/core/createPR.test.ts | 4 - tests/unit/gadgets/github/core/misc.test.ts | 4 - .../unit/gadgets/pm/core/addChecklist.test.ts | 4 - .../gadgets/pm/core/createWorkItem.test.ts | 4 - .../pm/core/deleteChecklistItem.test.ts | 4 - .../gadgets/pm/core/listWorkItems.test.ts | 4 - .../unit/gadgets/pm/core/postComment.test.ts | 1 - .../unit/gadgets/pm/core/readWorkItem.test.ts | 4 - .../pm/core/updateChecklistItem.test.ts | 4 - .../gadgets/pm/core/updateWorkItem.test.ts | 4 - .../unit/gadgets/session/core/finish.test.ts | 4 - .../gadgets/shared/diagnosticState.test.ts | 1 - tests/unit/gadgets/todo-storage.test.ts | 1 - tests/unit/gadgets/todo.test.ts | 8 - tests/unit/github/client.test.ts | 4 - tests/unit/github/personas.test.ts | 4 - tests/unit/jira/client.test.ts | 1 - tests/unit/pm/webhook-handler.test.ts | 1 - tests/unit/queue/retry-run-projectId.test.ts | 1 - tests/unit/router/ackMessageGenerator.test.ts | 4 - tests/unit/router/adapters/github.test.ts | 1 - tests/unit/router/adapters/jira.test.ts | 1 - tests/unit/router/adapters/trello.test.ts | 1 - tests/unit/router/trello.test.ts | 4 - tests/unit/router/webhook-processor.test.ts | 4 - tests/unit/sentry.test.ts | 2 - tests/unit/server.test.ts | 4 - tests/unit/server/webhookHandlers.test.ts | 13 - tests/unit/trello/client.test.ts | 12 - tests/unit/triggers/agent-execution.test.ts | 9 +- .../triggers/agent-result-handler.test.ts | 20 +- tests/unit/triggers/budget.test.ts | 14 +- tests/unit/triggers/builtins.test.ts | 4 - tests/unit/triggers/card-moved.test.ts | 35 +-- .../unit/triggers/check-suite-failure.test.ts | 36 +-- .../unit/triggers/check-suite-success.test.ts | 41 +-- tests/unit/triggers/debug-runner.test.ts | 23 +- tests/unit/triggers/debug-trigger.test.ts | 4 - .../github-pr-comment-mention.test.ts | 20 +- tests/unit/triggers/github-utils.test.ts | 1 - tests/unit/triggers/label-added.test.ts | 14 +- tests/unit/triggers/manual-runner.test.ts | 2 - tests/unit/triggers/pr-merged.test.ts | 11 +- tests/unit/triggers/pr-opened.test.ts | 34 +-- tests/unit/triggers/pr-ready-to-merge.test.ts | 11 +- .../unit/triggers/pr-review-submitted.test.ts | 25 +- tests/unit/triggers/review-requested.test.ts | 36 +-- tests/unit/utils/cascadeEnv.test.ts | 1 - tests/unit/utils/lifecycle.test.ts | 1 - tests/unit/utils/llmEnv.test.ts | 1 - tests/unit/utils/llmLogging.test.ts | 4 - tests/unit/utils/repo.test.ts | 1 - tests/unit/utils/safeOperation.test.ts | 4 - tests/unit/utils/squintDb.test.ts | 1 - tests/unit/utils/webhookLogger.test.ts | 1 - vitest.config.ts | 3 +- vitest.workspace.ts | 24 ++ web/src/components/projects/pm-wizard.tsx | 6 - 136 files changed, 951 insertions(+), 827 deletions(-) create mode 100644 docker-compose.test.yml create mode 100644 tests/helpers/factories.ts create mode 100644 tests/helpers/mockDb.ts create mode 100644 tests/helpers/mockPersonas.ts create mode 100644 tests/integration/db/credentialsRepository.test.ts create mode 100644 tests/integration/helpers/db.ts create mode 100644 tests/integration/helpers/seed.ts create mode 100644 tests/integration/setup.ts create mode 100644 vitest.workspace.ts diff --git a/.cascade/ensure-services.sh b/.cascade/ensure-services.sh index 32a2705d..21afbd0a 100755 --- a/.cascade/ensure-services.sh +++ b/.cascade/ensure-services.sh @@ -66,4 +66,29 @@ else fi fi +# Verify test database exists (needed for integration tests) +if pg_isready -q 2>/dev/null; then + # OS-aware psql command (macOS uses peer auth, Linux uses -U postgres) + case "$(uname -s)" in + Linux*) PSQL_CMD="psql -U postgres" ;; + *) PSQL_CMD="psql" ;; + esac + + if $PSQL_CMD -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw cascade_test; then + echo "Test database (cascade_test): exists" + else + echo "Test database (cascade_test): missing - creating..." + if [ "$(uname -s)" = "Linux" ]; then + $PSQL_CMD -c "CREATE DATABASE cascade_test;" 2>/dev/null || true + else + createdb cascade_test 2>/dev/null || true + fi + if $PSQL_CMD -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw cascade_test; then + echo "Test database (cascade_test): created" + else + echo "Test database (cascade_test): FAILED TO CREATE (integration tests will not work)" + fi + fi +fi + echo "=== All services running ===" diff --git a/.cascade/env b/.cascade/env index c1be4a9f..e4815a30 100644 --- a/.cascade/env +++ b/.cascade/env @@ -1,3 +1,4 @@ CI=true DATABASE_URL=postgresql://postgres:postgres@localhost:5432/cascade DATABASE_SSL=false +TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/cascade_test diff --git a/.cascade/setup.sh b/.cascade/setup.sh index 3f9dedb5..ea9c84e2 100755 --- a/.cascade/setup.sh +++ b/.cascade/setup.sh @@ -224,7 +224,7 @@ if pg_isready -q 2>/dev/null; then PSQL_CMD="sudo -u postgres psql" fi - # Create cascade database + # Create cascade database (development) if ! $PSQL_CMD -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw cascade; then log_info "Creating cascade database..." if [ "$OS" = "linux" ]; then @@ -236,6 +236,18 @@ if pg_isready -q 2>/dev/null; then log_info "Database cascade already exists" fi + # Create cascade_test database (integration tests) + if ! $PSQL_CMD -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw cascade_test; then + log_info "Creating cascade_test database..." + if [ "$OS" = "linux" ]; then + $PSQL_CMD -c "CREATE DATABASE cascade_test;" 2>/dev/null || true + else + createdb cascade_test 2>/dev/null || true + fi + else + log_info "Database cascade_test already exists" + fi + # On Linux, ensure postgres user has a known password for app connections if [ "$OS" = "linux" ]; then $PSQL_CMD -c "ALTER USER postgres WITH PASSWORD 'postgres';" 2>/dev/null || true @@ -266,9 +278,21 @@ echo "" echo "--- Database Migrations ---" if pg_isready -q 2>/dev/null; then - log_info "Running migrations..." - DATABASE_SSL=false npm run db:migrate 2>&1 || \ - log_warn "Migration failed - may need manual intervention" + if [ "$OS" = "linux" ]; then + DEV_DB_URL="postgresql://postgres:postgres@localhost:5432/cascade" + TEST_DB_URL="postgresql://postgres:postgres@localhost:5432/cascade_test" + else + DEV_DB_URL="postgresql://localhost:5432/cascade" + TEST_DB_URL="postgresql://localhost:5432/cascade_test" + fi + + log_info "Running migrations on cascade (dev)..." + DATABASE_URL="$DEV_DB_URL" DATABASE_SSL=false npm run db:migrate 2>&1 || \ + log_warn "Migration failed on cascade - may need manual intervention" + + log_info "Running migrations on cascade_test..." + DATABASE_URL="$TEST_DB_URL" DATABASE_SSL=false npm run db:migrate 2>&1 || \ + log_warn "Migration failed on cascade_test - may need manual intervention" else log_warn "PostgreSQL not ready, skipping migrations" fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a3afcf6..745cf9ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,45 @@ jobs: - name: Validate PR commits run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + integration-tests: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: cascade_test + POSTGRES_PASSWORD: cascade_test + POSTGRES_DB: cascade_test + ports: + - 5433:5432 + options: >- + --health-cmd "pg_isready -U cascade_test -d cascade_test" + --health-interval 2s + --health-timeout 5s + --health-retries 10 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build backend + run: npm run build + + - name: Run integration tests + run: npm run test:integration + env: + TEST_DATABASE_URL: postgresql://cascade_test:cascade_test@localhost:5433/cascade_test + docker-build-check: name: Validate Docker builds runs-on: ubuntu-latest diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..e7e1f1fd --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,17 @@ +services: + postgres-test: + container_name: cascade-postgres-test + image: postgres:16-alpine + ports: + - "5433:5432" + environment: + POSTGRES_USER: cascade_test + POSTGRES_PASSWORD: cascade_test + POSTGRES_DB: cascade_test + tmpfs: + - /var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cascade_test -d cascade_test"] + interval: 2s + timeout: 5s + retries: 10 diff --git a/package.json b/package.json index 266a76dd..e9928446 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,14 @@ "build": "tsc", "build:web": "cd web && npm run build", "start": "node dist/index.js", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", + "test": "vitest run --project unit", + "test:unit": "vitest run --project unit", + "test:integration": "vitest run --project integration", + "test:all": "vitest run", + "test:watch": "vitest --project unit", + "test:coverage": "vitest run --project unit --coverage", + "test:db:up": "docker compose -f docker-compose.test.yml up -d --wait", + "test:db:down": "docker compose -f docker-compose.test.yml down -v", "lint": "biome check .", "lint:fix": "biome check --write .", "typecheck": "tsc --noEmit", diff --git a/src/backends/adapter.ts b/src/backends/adapter.ts index b35d50c5..efe9af9d 100644 --- a/src/backends/adapter.ts +++ b/src/backends/adapter.ts @@ -139,6 +139,39 @@ async function buildBackendInput( }; } +/** + * Build progress-monitor config from pipeline inputs. + */ +function buildProgressMonitorConfig( + input: AgentInput & { config: CascadeConfig }, + agentType: string, + logWriter: LogWriter, + repoDir: string | null, + isGitHubAck: boolean, +) { + const { cardId } = input; + return { + logWriter, + agentType, + taskDescription: cardId ? `Work item ${cardId}` : 'Unknown task', + progressModel: input.config.defaults.progressModel, + intervalMinutes: input.config.defaults.progressIntervalMinutes, + customModels: CUSTOM_MODELS as ModelSpec[], + repoDir: repoDir ?? undefined, + trello: cardId ? { cardId } : undefined, + preSeededCommentId: isGitHubAck ? undefined : (input.ackCommentId as string | undefined), + ...(input.prNumber && input.repoFullName + ? { + github: { + owner: input.repoFullName.split('/')[0], + repo: input.repoFullName.split('/')[1], + headerMessage: input.ackMessage ?? '', + }, + } + : {}), + }; +} + export async function executeWithBackend( backend: AgentBackend, agentType: string, @@ -207,28 +240,9 @@ export async function executeWithBackend( recordInitialComment(input.ackCommentId as number); } - const monitor = createProgressMonitor({ - logWriter, - agentType, - taskDescription: cardId ? `Work item ${cardId}` : 'Unknown task', - progressModel: input.config.defaults.progressModel, - intervalMinutes: input.config.defaults.progressIntervalMinutes, - customModels: CUSTOM_MODELS as ModelSpec[], - repoDir: repoDir ?? undefined, - trello: cardId ? { cardId } : undefined, - // Only use preSeededCommentId for PM (Trello/JIRA) ack comments, not GitHub - preSeededCommentId: isGitHubAck ? undefined : (input.ackCommentId as string | undefined), - // Pass GitHub config so progress monitor can update the PR comment - ...(input.prNumber && input.repoFullName - ? { - github: { - owner: input.repoFullName.split('/')[0], - repo: input.repoFullName.split('/')[1], - headerMessage: input.ackMessage ?? '', - }, - } - : {}), - }); + const monitor = createProgressMonitor( + buildProgressMonitorConfig(input, agentType, logWriter, repoDir, isGitHubAck), + ); const backendInput: AgentBackendInput = { ...partialInput, diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts new file mode 100644 index 00000000..0f0587f7 --- /dev/null +++ b/tests/helpers/factories.ts @@ -0,0 +1,104 @@ +import type { TRPCContext, TRPCUser } from '../../src/api/trpc.js'; +import type { ProjectConfig, TriggerContext } from '../../src/types/index.js'; + +// --------------------------------------------------------------------------- +// Project factories +// --------------------------------------------------------------------------- + +/** + * Creates a mock Trello project config. Sensible defaults for trigger tests; + * pass overrides (shallow-merged) for test-specific customisation. + */ +export function createMockProject(overrides?: Partial): ProjectConfig { + return { + id: 'test', + orgId: 'org-1', + name: 'Test', + repo: 'owner/repo', + baseBranch: 'main', + branchPrefix: 'feature/', + pm: { type: 'trello' }, + trello: { + boardId: 'board123', + lists: { + splitting: 'splitting-list-id', + planning: 'planning-list-id', + todo: 'todo-list-id', + }, + labels: {}, + }, + ...overrides, + } as ProjectConfig; +} + +/** + * Creates a mock JIRA project config. + */ +export function createMockJiraProject(overrides?: Partial): ProjectConfig { + return { + id: 'jira-project', + orgId: 'org-1', + name: 'JIRA Project', + repo: 'owner/jira-repo', + baseBranch: 'main', + branchPrefix: 'feature/', + pm: { type: 'jira' }, + jira: { + projectKey: 'PROJ', + baseUrl: 'https://test.atlassian.net', + statuses: { splitting: 'Briefing' }, + labels: { + processing: 'my-processing', + processed: 'my-processed', + error: 'my-error', + readyToProcess: 'my-ready', + }, + }, + ...overrides, + } as ProjectConfig; +} + +// --------------------------------------------------------------------------- +// tRPC factories +// --------------------------------------------------------------------------- + +/** + * Creates a mock tRPC user. Defaults to an admin user. + */ +export function createMockUser(overrides?: Partial): TRPCUser { + return { + id: 'user-1', + orgId: 'org-1', + email: 'test@example.com', + name: 'Test User', + role: 'admin', + ...overrides, + }; +} + +/** + * Creates a mock tRPC context with an authenticated user. + */ +export function createMockContext(userOverrides?: Partial): TRPCContext { + const user = createMockUser(userOverrides); + return { + user, + effectiveOrgId: user.orgId, + }; +} + +// --------------------------------------------------------------------------- +// Trigger context factory +// --------------------------------------------------------------------------- + +/** + * Creates a mock trigger context for trigger handler tests. + */ +export function createTriggerContext(overrides?: Partial): TriggerContext { + return { + project: createMockProject(), + source: 'trello', + payload: {}, + ...overrides, + } as TriggerContext; +} diff --git a/tests/helpers/mockDb.ts b/tests/helpers/mockDb.ts new file mode 100644 index 00000000..fd216968 --- /dev/null +++ b/tests/helpers/mockDb.ts @@ -0,0 +1,89 @@ +import { vi } from 'vitest'; + +export type MockDbChain = Record>; + +export interface MockDbResult { + db: { + select: ReturnType; + insert: ReturnType; + update: ReturnType; + delete: ReturnType; + }; + chain: MockDbChain; +} + +/** + * Creates a mock Drizzle query chain that supports the common patterns: + * + * - `select().from().where()` / `select().from().innerJoin().where()` + * - `select().from().innerJoin().innerJoin().where()` (double join) + * - `insert().values().returning()` / `insert().values().onConflictDoUpdate()` + * - `update().set().where()` + * - `delete().where()` + * + * Options let you extend the chain for repo-specific needs. + */ +export function createMockDb( + opts: { + /** Add `.limit()` support on select chains */ + withLimit?: boolean; + /** Add nested `.innerJoin().innerJoin().where()` support */ + withDoubleJoin?: boolean; + /** Add `.onConflictDoUpdate()` on insert chains */ + withUpsert?: boolean; + /** Make the chain itself thenable (for queries without `.where()` terminal) */ + withThenable?: boolean; + } = {}, +): MockDbResult { + const chain: MockDbChain = {}; + + // Terminal methods that return results + chain.returning = vi.fn().mockResolvedValue([]); + + // Limit support — limit is the terminal when present, where is a chaining step + if (opts.withLimit) { + chain.limit = vi.fn().mockResolvedValue([]); + chain.where = vi.fn().mockReturnValue({ limit: chain.limit }); + } else { + chain.where = vi.fn().mockResolvedValue([]); + } + + // Chain methods - innerJoin + const innerJoinResult: Record = { where: chain.where }; + if (opts.withDoubleJoin) { + innerJoinResult.innerJoin = vi.fn().mockReturnValue({ where: chain.where }); + } + chain.innerJoin = vi.fn().mockReturnValue(innerJoinResult); + + // From + chain.from = vi.fn().mockReturnValue({ + where: chain.where, + innerJoin: chain.innerJoin, + }); + + // Update chain + chain.set = vi.fn().mockReturnValue({ where: chain.where }); + + // Insert chain + const valuesResult: Record = { returning: chain.returning }; + if (opts.withUpsert) { + chain.onConflictDoUpdate = vi.fn().mockReturnValue({ returning: chain.returning }); + valuesResult.onConflictDoUpdate = chain.onConflictDoUpdate; + } + chain.values = vi.fn().mockReturnValue(valuesResult); + + // Thenable support for queries without .where() terminal + if (opts.withThenable) { + // biome-ignore lint/suspicious/noThenProperty: intentional thenable mock for Drizzle query chains + chain.then = (resolve: (v: unknown) => unknown) => Promise.resolve([]).then(resolve); + } + + const db = { + select: vi.fn().mockReturnValue({ from: chain.from }), + insert: vi.fn().mockReturnValue({ values: chain.values }), + update: vi.fn().mockReturnValue({ set: chain.set }), + delete: vi.fn().mockReturnValue({ where: chain.where }), + }; + + return { db, chain }; +} diff --git a/tests/helpers/mockPersonas.ts b/tests/helpers/mockPersonas.ts new file mode 100644 index 00000000..f7409a30 --- /dev/null +++ b/tests/helpers/mockPersonas.ts @@ -0,0 +1,13 @@ +import type { PersonaIdentities } from '../../src/github/personas.js'; + +/** + * Standard mock persona identities used in trigger tests. + */ +export const mockPersonaIdentities: PersonaIdentities = { + implementer: 'cascade-impl', + reviewer: 'cascade-reviewer', +}; + +/** Convenience constants for readable assertions. */ +export const IMPLEMENTER_USERNAME = 'cascade-impl'; +export const REVIEWER_USERNAME = 'cascade-reviewer'; diff --git a/tests/integration/db/credentialsRepository.test.ts b/tests/integration/db/credentialsRepository.test.ts new file mode 100644 index 00000000..7d304f87 --- /dev/null +++ b/tests/integration/db/credentialsRepository.test.ts @@ -0,0 +1,259 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createCredential, + deleteCredential, + listOrgCredentials, + resolveAllIntegrationCredentials, + resolveAllOrgCredentials, + resolveIntegrationCredential, + resolveOrgCredential, + updateCredential, +} from '../../../src/db/repositories/credentialsRepository.js'; +import { truncateAll } from '../helpers/db.js'; +import { + seedCredential, + seedIntegration, + seedIntegrationCredential, + seedOrg, + seedProject, +} from '../helpers/seed.js'; + +describe('credentialsRepository (integration)', () => { + beforeEach(async () => { + await truncateAll(); + await seedOrg(); + await seedProject(); + }); + + // ========================================================================= + // CRUD + // ========================================================================= + + describe('createCredential', () => { + it('inserts a credential and returns the id', async () => { + const result = await createCredential({ + orgId: 'test-org', + name: 'My API Key', + envVarKey: 'MY_API_KEY', + value: 'secret-123', + }); + + expect(result.id).toBeGreaterThan(0); + }); + + it('defaults isDefault to false', async () => { + const { id } = await createCredential({ + orgId: 'test-org', + name: 'Key', + envVarKey: 'KEY', + value: 'val', + }); + + const creds = await listOrgCredentials('test-org'); + const cred = creds.find((c) => c.id === id); + expect(cred?.isDefault).toBe(false); + }); + }); + + describe('updateCredential', () => { + it('updates name and value', async () => { + const { id } = await createCredential({ + orgId: 'test-org', + name: 'Old Name', + envVarKey: 'UPD_KEY', + value: 'old-value', + }); + + await updateCredential(id, { name: 'New Name', value: 'new-value' }); + + const creds = await listOrgCredentials('test-org'); + const cred = creds.find((c) => c.id === id); + expect(cred?.name).toBe('New Name'); + expect(cred?.value).toBe('new-value'); + }); + }); + + describe('deleteCredential', () => { + it('removes the credential', async () => { + const { id } = await createCredential({ + orgId: 'test-org', + name: 'Temp', + envVarKey: 'TEMP', + value: 'tmp', + }); + + await deleteCredential(id); + + const creds = await listOrgCredentials('test-org'); + expect(creds.find((c) => c.id === id)).toBeUndefined(); + }); + }); + + describe('listOrgCredentials', () => { + it('returns all credentials for the org', async () => { + await createCredential({ orgId: 'test-org', name: 'A', envVarKey: 'A', value: 'a' }); + await createCredential({ orgId: 'test-org', name: 'B', envVarKey: 'B', value: 'b' }); + + const creds = await listOrgCredentials('test-org'); + expect(creds).toHaveLength(2); + expect(creds.map((c) => c.envVarKey).sort()).toEqual(['A', 'B']); + }); + + it('returns empty array for org with no credentials', async () => { + const creds = await listOrgCredentials('test-org'); + expect(creds).toEqual([]); + }); + }); + + // ========================================================================= + // Org-scoped credential resolution + // ========================================================================= + + describe('resolveOrgCredential', () => { + it('returns value for a default credential', async () => { + await createCredential({ + orgId: 'test-org', + name: 'OR Key', + envVarKey: 'OPENROUTER_API_KEY', + value: 'or-secret', + isDefault: true, + }); + + const result = await resolveOrgCredential('test-org', 'OPENROUTER_API_KEY'); + expect(result).toBe('or-secret'); + }); + + it('returns null for non-default credential', async () => { + await createCredential({ + orgId: 'test-org', + name: 'Non-default', + envVarKey: 'NON_DEFAULT', + value: 'val', + isDefault: false, + }); + + const result = await resolveOrgCredential('test-org', 'NON_DEFAULT'); + expect(result).toBeNull(); + }); + + it('returns null when credential does not exist', async () => { + const result = await resolveOrgCredential('test-org', 'MISSING_KEY'); + expect(result).toBeNull(); + }); + }); + + describe('resolveAllOrgCredentials', () => { + it('returns all default credentials as key-value map', async () => { + await createCredential({ + orgId: 'test-org', + name: 'K1', + envVarKey: 'KEY_1', + value: 'v1', + isDefault: true, + }); + await createCredential({ + orgId: 'test-org', + name: 'K2', + envVarKey: 'KEY_2', + value: 'v2', + isDefault: true, + }); + // Non-default — should be excluded + await createCredential({ + orgId: 'test-org', + name: 'K3', + envVarKey: 'KEY_3', + value: 'v3', + isDefault: false, + }); + + const result = await resolveAllOrgCredentials('test-org'); + expect(result).toEqual({ KEY_1: 'v1', KEY_2: 'v2' }); + }); + }); + + // ========================================================================= + // Integration credential resolution + // ========================================================================= + + describe('resolveIntegrationCredential', () => { + it('resolves a credential via integration link', async () => { + const cred = await seedCredential({ + envVarKey: 'TRELLO_API_KEY', + value: 'trello-key-secret', + }); + const integration = await seedIntegration({ category: 'pm', provider: 'trello' }); + await seedIntegrationCredential({ + integrationId: integration.id, + role: 'api_key', + credentialId: cred.id, + }); + + const result = await resolveIntegrationCredential('test-project', 'pm', 'api_key'); + expect(result).toBe('trello-key-secret'); + }); + + it('returns null when no link exists', async () => { + const result = await resolveIntegrationCredential('test-project', 'pm', 'api_key'); + expect(result).toBeNull(); + }); + }); + + describe('resolveAllIntegrationCredentials', () => { + it('resolves all credentials for a project', async () => { + const apiKeyCred = await seedCredential({ envVarKey: 'TRELLO_API_KEY', value: 'key1' }); + const tokenCred = await seedCredential({ + envVarKey: 'TRELLO_TOKEN', + value: 'token1', + name: 'Trello Token', + }); + const integration = await seedIntegration({ category: 'pm', provider: 'trello' }); + await seedIntegrationCredential({ + integrationId: integration.id, + role: 'api_key', + credentialId: apiKeyCred.id, + }); + await seedIntegrationCredential({ + integrationId: integration.id, + role: 'token', + credentialId: tokenCred.id, + }); + + const result = await resolveAllIntegrationCredentials('test-project'); + expect(result).toHaveLength(2); + expect(result).toEqual( + expect.arrayContaining([ + { category: 'pm', provider: 'trello', role: 'api_key', value: 'key1' }, + { category: 'pm', provider: 'trello', role: 'token', value: 'token1' }, + ]), + ); + }); + + it('returns empty array for project with no integrations', async () => { + const result = await resolveAllIntegrationCredentials('test-project'); + expect(result).toEqual([]); + }); + }); + + // ========================================================================= + // Encryption + // ========================================================================= + + describe('with encryption', () => { + it('round-trips through encrypt/decrypt transparently', async () => { + // 64-char hex = 32-byte AES-256 key + vi.stubEnv('CREDENTIAL_MASTER_KEY', 'a'.repeat(64)); + + const { id } = await createCredential({ + orgId: 'test-org', + name: 'Encrypted Key', + envVarKey: 'ENC_KEY', + value: 'plaintext-secret', + }); + + const creds = await listOrgCredentials('test-org'); + const cred = creds.find((c) => c.id === id); + expect(cred?.value).toBe('plaintext-secret'); // decrypted on read + }); + }); +}); diff --git a/tests/integration/helpers/db.ts b/tests/integration/helpers/db.ts new file mode 100644 index 00000000..a91f8e9a --- /dev/null +++ b/tests/integration/helpers/db.ts @@ -0,0 +1,50 @@ +import path from 'node:path'; +import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { closeDb, getDb } from '../../../src/db/client.js'; + +/** + * Runs Drizzle migrations against the test database. + * Uses the app's own getDb() which reads DATABASE_URL (set by integration/setup.ts). + */ +export async function runMigrations() { + const db = getDb(); + await migrate(db, { + migrationsFolder: path.resolve(import.meta.dirname, '../../../src/db/migrations'), + }); +} + +/** + * Truncates all application tables in dependency order. + * Call in `beforeEach` to isolate tests. + */ +export async function truncateAll() { + const db = getDb(); + // CASCADE handles FK dependencies; tables listed for explicitness + await db.execute(` + TRUNCATE TABLE + webhook_logs, + debug_analyses, + agent_run_llm_calls, + agent_run_logs, + agent_runs, + pr_work_items, + integration_credentials, + project_integrations, + agent_configs, + prompt_partials, + sessions, + users, + credentials, + projects, + cascade_defaults, + organizations + CASCADE + `); +} + +/** + * Closes the test database pool. Call in `afterAll`. + */ +export async function closeTestDb() { + await closeDb(); +} diff --git a/tests/integration/helpers/seed.ts b/tests/integration/helpers/seed.ts new file mode 100644 index 00000000..5c1c6a7f --- /dev/null +++ b/tests/integration/helpers/seed.ts @@ -0,0 +1,117 @@ +import { getDb } from '../../../src/db/client.js'; +import { + credentials, + integrationCredentials, + organizations, + projectIntegrations, + projects, +} from '../../../src/db/schema/index.js'; + +/** + * Seeds a test organization. + */ +export async function seedOrg(id = 'test-org', name = 'Test Org') { + const db = getDb(); + const [row] = await db.insert(organizations).values({ id, name }).returning(); + return row; +} + +/** + * Seeds a test project linked to an org. + */ +export async function seedProject( + overrides: { + id?: string; + orgId?: string; + name?: string; + repo?: string; + baseBranch?: string; + branchPrefix?: string; + } = {}, +) { + const db = getDb(); + const [row] = await db + .insert(projects) + .values({ + id: overrides.id ?? 'test-project', + orgId: overrides.orgId ?? 'test-org', + name: overrides.name ?? 'Test Project', + repo: overrides.repo ?? 'owner/repo', + baseBranch: overrides.baseBranch ?? 'main', + branchPrefix: overrides.branchPrefix ?? 'feature/', + }) + .returning(); + return row; +} + +/** + * Seeds a credential row. + */ +export async function seedCredential( + overrides: { + orgId?: string; + name?: string; + envVarKey?: string; + value?: string; + isDefault?: boolean; + } = {}, +) { + const db = getDb(); + const [row] = await db + .insert(credentials) + .values({ + orgId: overrides.orgId ?? 'test-org', + name: overrides.name ?? 'Test Key', + envVarKey: overrides.envVarKey ?? 'TEST_KEY', + value: overrides.value ?? 'test-value', + isDefault: overrides.isDefault ?? false, + }) + .returning(); + return row; +} + +/** + * Seeds a project integration (PM or SCM). + */ +export async function seedIntegration( + overrides: { + projectId?: string; + category?: string; + provider?: string; + config?: Record; + triggers?: Record; + } = {}, +) { + const db = getDb(); + const [row] = await db + .insert(projectIntegrations) + .values({ + projectId: overrides.projectId ?? 'test-project', + category: overrides.category ?? 'pm', + provider: overrides.provider ?? 'trello', + config: overrides.config ?? {}, + triggers: overrides.triggers ?? {}, + }) + .returning(); + return row; +} + +/** + * Seeds an integration credential link. + */ +export async function seedIntegrationCredential(overrides: { + integrationId: number; + role?: string; + credentialId: number; +}) { + const db = getDb(); + const [row] = await db + .insert(integrationCredentials) + .values({ + integrationId: overrides.integrationId, + role: overrides.role ?? 'api_key', + credentialId: overrides.credentialId, + }) + .returning(); + return row; +} diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts new file mode 100644 index 00000000..da27d1ae --- /dev/null +++ b/tests/integration/setup.ts @@ -0,0 +1,22 @@ +import { afterAll, beforeAll } from 'vitest'; +import { closeTestDb, runMigrations } from './helpers/db.js'; + +// Default: matches docker-compose.test.yml (port 5433, user cascade_test) +// Override via TEST_DATABASE_URL for: +// - .cascade/env: local PostgreSQL (port 5432, user postgres) +// - CI: GitHub Actions service container (port 5433, user cascade_test) +const TEST_DATABASE_URL = + process.env.TEST_DATABASE_URL ?? + 'postgresql://cascade_test:cascade_test@localhost:5433/cascade_test'; + +// Point the app's getDb() at the test database +process.env.DATABASE_URL = TEST_DATABASE_URL; +process.env.DATABASE_SSL = 'false'; + +beforeAll(async () => { + await runMigrations(); +}); + +afterAll(async () => { + await closeTestDb(); +}); diff --git a/tests/setup.ts b/tests/setup.ts index 47be3054..c3ce8832 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,6 +1,12 @@ -import { afterEach } from 'vitest'; -import { invalidateConfigCache } from '../src/config/provider.js'; +import { afterEach, beforeEach } from 'vitest'; +// Import configCache directly to avoid pulling in provider.js → credentialsRepository.js → client.js, +// which would pre-load real DB modules before test files can mock them. +import { configCache } from '../src/config/configCache.js'; + +beforeEach(() => { + configCache.invalidate(); +}); afterEach(() => { - invalidateConfigCache(); + configCache.invalidate(); }); diff --git a/tests/unit/agents/hooks.test.ts b/tests/unit/agents/hooks.test.ts index 56bc68c9..528b2231 100644 --- a/tests/unit/agents/hooks.test.ts +++ b/tests/unit/agents/hooks.test.ts @@ -25,10 +25,6 @@ describe('createObserverHooks - llmCallAccumulator', () => { getLogFiles: vi.fn().mockReturnValue([]), }; - beforeEach(() => { - vi.clearAllMocks(); - }); - it('accumulates LLM call metrics when accumulator is provided', async () => { const accumulator: AccumulatedLlmCall[] = []; const trackingContext = createTrackingContext(); @@ -171,10 +167,6 @@ describe('createObserverHooks - real-time DB logging', () => { getLogFiles: vi.fn().mockReturnValue([]), }; - beforeEach(() => { - vi.clearAllMocks(); - }); - it('calls storeLlmCall fire-and-forget when runId is set', async () => { const trackingContext = createTrackingContext(); const hooks = createObserverHooks({ diff --git a/tests/unit/agents/registry.test.ts b/tests/unit/agents/registry.test.ts index bf34f19f..962e31e8 100644 --- a/tests/unit/agents/registry.test.ts +++ b/tests/unit/agents/registry.test.ts @@ -78,10 +78,6 @@ function makeMockBackend(name: string, supportsAll = true): AgentBackend { }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('runAgent', () => { it('resolves backend name from config', async () => { const backend = makeMockBackend('llmist'); diff --git a/tests/unit/agents/shared/builderFactory.test.ts b/tests/unit/agents/shared/builderFactory.test.ts index 7ea267c9..065658f4 100644 --- a/tests/unit/agents/shared/builderFactory.test.ts +++ b/tests/unit/agents/shared/builderFactory.test.ts @@ -100,7 +100,6 @@ function createBaseOptions(overrides?: object) { } beforeEach(() => { - vi.clearAllMocks(); mockResolveSquintDbPath.mockReturnValue(null); // Reset all mock builder methods to return the builder instance diff --git a/tests/unit/agents/shared/cleanup.test.ts b/tests/unit/agents/shared/cleanup.test.ts index aee13ae8..37b01b78 100644 --- a/tests/unit/agents/shared/cleanup.test.ts +++ b/tests/unit/agents/shared/cleanup.test.ts @@ -47,7 +47,6 @@ describe('cleanupAgentResources', () => { const originalEnv = process.env.CASCADE_LOCAL_MODE; beforeEach(() => { - vi.clearAllMocks(); process.env.CASCADE_LOCAL_MODE = undefined; }); diff --git a/tests/unit/agents/shared/executionPipeline.test.ts b/tests/unit/agents/shared/executionPipeline.test.ts index 042d0978..8364f524 100644 --- a/tests/unit/agents/shared/executionPipeline.test.ts +++ b/tests/unit/agents/shared/executionPipeline.test.ts @@ -93,7 +93,6 @@ function setupMocks() { } beforeEach(() => { - vi.clearAllMocks(); process.env.CASCADE_LOCAL_MODE = ''; }); diff --git a/tests/unit/agents/shared/gadgets.test.ts b/tests/unit/agents/shared/gadgets.test.ts index 0a77d778..29591799 100644 --- a/tests/unit/agents/shared/gadgets.test.ts +++ b/tests/unit/agents/shared/gadgets.test.ts @@ -59,10 +59,6 @@ import { buildWorkItemGadgets, } from '../../../../src/agents/shared/gadgets.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - function names(gadgets: unknown[]): string[] { return gadgets.map((g) => (g as object).constructor.name); } diff --git a/tests/unit/agents/shared/modelResolution.test.ts b/tests/unit/agents/shared/modelResolution.test.ts index a9316172..e22da634 100644 --- a/tests/unit/agents/shared/modelResolution.test.ts +++ b/tests/unit/agents/shared/modelResolution.test.ts @@ -41,10 +41,6 @@ function makeConfig(overrides: Partial = {}): Cascade } describe('resolveModelConfig', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('prompt resolution chain', () => { it('uses .eta file when no custom prompts configured', async () => { const result = await resolveModelConfig({ diff --git a/tests/unit/agents/shared/runTracking.test.ts b/tests/unit/agents/shared/runTracking.test.ts index 226ff7a1..80d98132 100644 --- a/tests/unit/agents/shared/runTracking.test.ts +++ b/tests/unit/agents/shared/runTracking.test.ts @@ -63,10 +63,6 @@ const baseInput: RunTrackingInput = { }; describe('tryCreateRun', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('creates a run and returns the run ID', async () => { mockCreateRun.mockResolvedValue('run-abc'); @@ -97,10 +93,6 @@ describe('tryCreateRun', () => { }); describe('tryCompleteRun', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('calls completeRun with the given input', async () => { mockCompleteRun.mockResolvedValue(undefined); @@ -123,7 +115,6 @@ describe('tryCompleteRun', () => { describe('tryStoreRunLogs', () => { beforeEach(() => { - vi.clearAllMocks(); mockExistsSync.mockReturnValue(false); }); @@ -159,7 +150,6 @@ describe('tryStoreRunLogs', () => { describe('finalizeBackendRun', () => { beforeEach(() => { - vi.clearAllMocks(); mockExistsSync.mockReturnValue(false); }); diff --git a/tests/unit/agents/utils/agentLoop.test.ts b/tests/unit/agents/utils/agentLoop.test.ts index dd06b6c9..e529793b 100644 --- a/tests/unit/agents/utils/agentLoop.test.ts +++ b/tests/unit/agents/utils/agentLoop.test.ts @@ -90,7 +90,6 @@ function createMockAgent(events: object[]) { } beforeEach(() => { - vi.clearAllMocks(); mockConsumePendingSessionNotices.mockReturnValue(new Map()); mockConsumeLoopWarning.mockReturnValue(null); mockConsumeLoopAction.mockReturnValue(null); diff --git a/tests/unit/agents/utils/checklistSync.test.ts b/tests/unit/agents/utils/checklistSync.test.ts index 83cac063..fc6b5a40 100644 --- a/tests/unit/agents/utils/checklistSync.test.ts +++ b/tests/unit/agents/utils/checklistSync.test.ts @@ -25,7 +25,6 @@ import { loadTodos } from '../../../../src/gadgets/todo/storage.js'; const mockLoadTodos = vi.mocked(loadTodos); beforeEach(() => { - vi.clearAllMocks(); clearSyncedTodos(); }); diff --git a/tests/unit/agents/utils/logging.test.ts b/tests/unit/agents/utils/logging.test.ts index 35cb1c00..2f7bbd9d 100644 --- a/tests/unit/agents/utils/logging.test.ts +++ b/tests/unit/agents/utils/logging.test.ts @@ -14,10 +14,6 @@ import { logger } from '../../../../src/utils/logging.js'; const mockLogger = vi.mocked(logger); -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('createAgentLogger', () => { it('debug writes to both console logger and file logger', () => { const fileLogger = { write: vi.fn() }; diff --git a/tests/unit/agents/utils/setup.test.ts b/tests/unit/agents/utils/setup.test.ts index 576eff93..677adb32 100644 --- a/tests/unit/agents/utils/setup.test.ts +++ b/tests/unit/agents/utils/setup.test.ts @@ -24,7 +24,6 @@ const mockReadFileSync = vi.mocked(readFileSync); const mockRunCommand = vi.mocked(runCommand); beforeEach(() => { - vi.clearAllMocks(); Reflect.deleteProperty(process.env, 'LLMIST_LOG_LEVEL'); Reflect.deleteProperty(process.env, 'LOG_LEVEL'); }); diff --git a/tests/unit/api/access-control.test.ts b/tests/unit/api/access-control.test.ts index 1cc04466..90366f8a 100644 --- a/tests/unit/api/access-control.test.ts +++ b/tests/unit/api/access-control.test.ts @@ -116,36 +116,26 @@ import { protectedProcedure, router, } from '../../../src/api/trpc.js'; +import { createMockUser } from '../../helpers/factories.js'; // ========================================================================== // Shared test users // ========================================================================== -const adminUser: TRPCUser = { - id: 'user-1', - orgId: 'org-1', - email: 'admin@example.com', - name: 'Admin', - role: 'admin', -}; +const adminUser = createMockUser({ email: 'admin@example.com', name: 'Admin' }); -const memberUser: TRPCUser = { +const memberUser = createMockUser({ id: 'user-2', - orgId: 'org-1', email: 'member@example.com', name: 'Member', role: 'member', -}; +}); // ========================================================================== // Section 1: computeEffectiveOrgId // ========================================================================== describe('computeEffectiveOrgId', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('returns null when user is null', async () => { const result = await computeEffectiveOrgId(null, undefined); expect(result).toBeNull(); @@ -251,10 +241,6 @@ describe('Middleware edge cases', () => { // ========================================================================== describe('Auth router — role-based data exposure', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('member user gets no availableOrgs', async () => { const caller = authRouter.createCaller({ user: memberUser, effectiveOrgId: 'org-1' }); const result = await caller.me(); @@ -293,7 +279,6 @@ describe('Auth router — role-based data exposure', () => { describe('Router org-isolation with admin org-switching', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); @@ -392,7 +377,6 @@ describe('Router org-isolation with admin org-switching', () => { describe('Cross-org ownership checks', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/auth/login.test.ts b/tests/unit/api/auth/login.test.ts index a7c6fdc2..97a56459 100644 --- a/tests/unit/api/auth/login.test.ts +++ b/tests/unit/api/auth/login.test.ts @@ -42,10 +42,6 @@ const mockUser = { }; describe('loginHandler', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('returns 400 when email is missing', async () => { const app = createTestApp(); const res = await postLogin(app, { password: 'pass' }); diff --git a/tests/unit/api/auth/logout.test.ts b/tests/unit/api/auth/logout.test.ts index bd0ae680..59f61fcb 100644 --- a/tests/unit/api/auth/logout.test.ts +++ b/tests/unit/api/auth/logout.test.ts @@ -16,10 +16,6 @@ function createTestApp() { } describe('logoutHandler', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('deletes session and clears cookie when session cookie is present', async () => { mockDeleteSession.mockResolvedValue(undefined); const app = createTestApp(); diff --git a/tests/unit/api/auth/session.test.ts b/tests/unit/api/auth/session.test.ts index cb0360bf..33abe190 100644 --- a/tests/unit/api/auth/session.test.ts +++ b/tests/unit/api/auth/session.test.ts @@ -11,10 +11,6 @@ vi.mock('../../../../src/db/repositories/usersRepository.js', () => ({ import { resolveUserFromSession } from '../../../../src/api/auth/session.js'; describe('resolveUserFromSession', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('returns DashboardUser when token maps to valid session and user', async () => { const mockUser = { id: 'user-1', diff --git a/tests/unit/api/routers/_shared/projectAccess.test.ts b/tests/unit/api/routers/_shared/projectAccess.test.ts index 1d873501..dfea777b 100644 --- a/tests/unit/api/routers/_shared/projectAccess.test.ts +++ b/tests/unit/api/routers/_shared/projectAccess.test.ts @@ -19,7 +19,6 @@ import { verifyProjectOrgAccess } from '../../../../../src/api/routers/_shared/p describe('verifyProjectOrgAccess', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/routers/agentConfigs.test.ts b/tests/unit/api/routers/agentConfigs.test.ts index 7cd35fef..1e35c76b 100644 --- a/tests/unit/api/routers/agentConfigs.test.ts +++ b/tests/unit/api/routers/agentConfigs.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockListAgentConfigs = vi.fn(); const mockCreateAgentConfig = vi.fn(); @@ -36,17 +37,10 @@ function createCaller(ctx: TRPCContext) { return agentConfigsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('agentConfigsRouter', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/routers/auth.test.ts b/tests/unit/api/routers/auth.test.ts index 96f8ff27..92d835a4 100644 --- a/tests/unit/api/routers/auth.test.ts +++ b/tests/unit/api/routers/auth.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockListAllOrganizations = vi.fn(); @@ -15,19 +16,9 @@ function createCaller(ctx: TRPCContext) { } describe('authRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('me', () => { it('returns user data from context', async () => { - const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test User', - role: 'admin', - }; + const mockUser = createMockUser(); mockListAllOrganizations.mockResolvedValue([{ id: 'org-1', name: 'Org One' }]); const caller = createCaller({ user: mockUser, effectiveOrgId: mockUser.orgId }); diff --git a/tests/unit/api/routers/credentials.test.ts b/tests/unit/api/routers/credentials.test.ts index f9c29fe7..b924d860 100644 --- a/tests/unit/api/routers/credentials.test.ts +++ b/tests/unit/api/routers/credentials.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockListOrgCredentials = vi.fn(); const mockCreateCredential = vi.fn(); @@ -50,17 +51,10 @@ function createCaller(ctx: TRPCContext) { return credentialsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('credentialsRouter', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/routers/defaults.test.ts b/tests/unit/api/routers/defaults.test.ts index e749bd2a..2fbbb7be 100644 --- a/tests/unit/api/routers/defaults.test.ts +++ b/tests/unit/api/routers/defaults.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockGetCascadeDefaults = vi.fn(); const mockUpsertCascadeDefaults = vi.fn(); @@ -16,19 +17,9 @@ function createCaller(ctx: TRPCContext) { return defaultsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('defaultsRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('get', () => { it('returns cascade defaults for user orgId', async () => { const mockDefaults = { diff --git a/tests/unit/api/routers/integrationsDiscovery.test.ts b/tests/unit/api/routers/integrationsDiscovery.test.ts index 5827aef0..86a8fa83 100644 --- a/tests/unit/api/routers/integrationsDiscovery.test.ts +++ b/tests/unit/api/routers/integrationsDiscovery.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockDecryptCredential = vi.fn((value: string) => value); @@ -72,13 +73,7 @@ function createCaller(ctx: TRPCContext) { return integrationsDiscoveryRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); const trelloCredsInput = { apiKeyCredentialId: 1, tokenCredentialId: 2 }; const jiraCredsInput = { @@ -101,7 +96,6 @@ function setupDbCredentials(rows: Array<{ orgId: string; value: string }>) { describe('integrationsDiscoveryRouter', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/routers/organization.test.ts b/tests/unit/api/routers/organization.test.ts index 48cbc234..a84191a2 100644 --- a/tests/unit/api/routers/organization.test.ts +++ b/tests/unit/api/routers/organization.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockGetOrganization = vi.fn(); const mockUpdateOrganization = vi.fn(); @@ -18,19 +19,9 @@ function createCaller(ctx: TRPCContext) { return organizationRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('organizationRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('get', () => { it('returns organization for user orgId', async () => { const mockOrg = { id: 'org-1', name: 'My Org' }; diff --git a/tests/unit/api/routers/projects.test.ts b/tests/unit/api/routers/projects.test.ts index 7b641a1c..1384cef5 100644 --- a/tests/unit/api/routers/projects.test.ts +++ b/tests/unit/api/routers/projects.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; const mockListProjectsForOrg = vi.fn(); @@ -61,17 +62,10 @@ function createCaller(ctx: TRPCContext) { return projectsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('projectsRouter', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/api/routers/prompts.test.ts b/tests/unit/api/routers/prompts.test.ts index b2059d7d..84e2cf87 100644 --- a/tests/unit/api/routers/prompts.test.ts +++ b/tests/unit/api/routers/prompts.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; // Mock prompt functions const mockGetValidAgentTypes = vi.fn(); @@ -39,19 +40,9 @@ function createCaller(ctx: TRPCContext) { return promptsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); describe('promptsRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('agentTypes', () => { it('returns list of agent types', async () => { const types = ['splitting', 'planning', 'implementation']; diff --git a/tests/unit/api/routers/runs.test.ts b/tests/unit/api/routers/runs.test.ts index 4f9fbb66..a55d4230 100644 --- a/tests/unit/api/routers/runs.test.ts +++ b/tests/unit/api/routers/runs.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; // Mock repository functions const mockListRuns = vi.fn(); @@ -73,19 +74,12 @@ function createCaller(ctx: TRPCContext) { return runsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); const RUN_UUID = 'aaaaaaaa-1111-2222-3333-444444444444'; describe('runsRouter', () => { beforeEach(() => { - vi.clearAllMocks(); // Set up DB chain for getById org check mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); diff --git a/tests/unit/api/routers/webhookLogs.test.ts b/tests/unit/api/routers/webhookLogs.test.ts index df5b518e..ad134fff 100644 --- a/tests/unit/api/routers/webhookLogs.test.ts +++ b/tests/unit/api/routers/webhookLogs.test.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; // Mock repository functions const mockListWebhookLogs = vi.fn(); @@ -19,21 +20,11 @@ function createCaller(ctx: TRPCContext) { return webhookLogsRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); const LOG_UUID = 'aaaaaaaa-1111-2222-3333-444444444444'; describe('webhookLogsRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('list', () => { it('returns paginated webhook logs', async () => { const mockData = { diff --git a/tests/unit/api/routers/webhooks.test.ts b/tests/unit/api/routers/webhooks.test.ts index ada2e4d8..9c03c24b 100644 --- a/tests/unit/api/routers/webhooks.test.ts +++ b/tests/unit/api/routers/webhooks.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TRPCContext } from '../../../../src/api/trpc.js'; +import { createMockUser } from '../../../helpers/factories.js'; // --- Mock dependencies --- @@ -60,13 +61,7 @@ function createCaller(ctx: TRPCContext) { return webhooksRouter.createCaller(ctx); } -const mockUser = { - id: 'user-1', - orgId: 'org-1', - email: 'test@example.com', - name: 'Test', - role: 'admin', -}; +const mockUser = createMockUser(); const mockProject = { id: 'my-project', @@ -122,10 +117,6 @@ function setupProjectContext(opts?: { noTrello?: boolean; noGithub?: boolean }) } describe('webhooksRouter', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('list', () => { it('returns trello and github webhooks', async () => { setupProjectContext(); diff --git a/tests/unit/backends/accumulator.test.ts b/tests/unit/backends/accumulator.test.ts index 6786d01b..35e31a59 100644 --- a/tests/unit/backends/accumulator.test.ts +++ b/tests/unit/backends/accumulator.test.ts @@ -16,7 +16,6 @@ import { loadTodos } from '../../../src/gadgets/todo/storage.js'; const mockLoadTodos = vi.mocked(loadTodos); beforeEach(() => { - vi.clearAllMocks(); mockLoadTodos.mockReturnValue([]); }); diff --git a/tests/unit/backends/adapter.test.ts b/tests/unit/backends/adapter.test.ts index cca4b133..2dedc413 100644 --- a/tests/unit/backends/adapter.test.ts +++ b/tests/unit/backends/adapter.test.ts @@ -243,7 +243,6 @@ function setupMocks() { } beforeEach(() => { - vi.clearAllMocks(); process.env.CASCADE_LOCAL_MODE = ''; // Default runs repository mocks mockCreateRun.mockResolvedValue('run-uuid-123'); diff --git a/tests/unit/backends/agent-profiles.test.ts b/tests/unit/backends/agent-profiles.test.ts index 7fdae579..733c111f 100644 --- a/tests/unit/backends/agent-profiles.test.ts +++ b/tests/unit/backends/agent-profiles.test.ts @@ -137,10 +137,6 @@ const mockReadWorkItem = vi.mocked(readWorkItem); const mockGithub = vi.mocked(githubClient); -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('getAgentProfile', () => { describe('respond-to-ci profile', () => { let profile: AgentProfile; diff --git a/tests/unit/backends/claude-code-hooks.test.ts b/tests/unit/backends/claude-code-hooks.test.ts index 9266e64e..5fd0430a 100644 --- a/tests/unit/backends/claude-code-hooks.test.ts +++ b/tests/unit/backends/claude-code-hooks.test.ts @@ -47,10 +47,6 @@ function makeStopInput(): StopHookInput { }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('buildPreToolUseHooks', () => { it('blocks gh pr create commands', async () => { const logWriter = makeLogWriter(); diff --git a/tests/unit/backends/claude-code.test.ts b/tests/unit/backends/claude-code.test.ts index fbf9ac96..b8a012d5 100644 --- a/tests/unit/backends/claude-code.test.ts +++ b/tests/unit/backends/claude-code.test.ts @@ -86,10 +86,6 @@ function makeInput(overrides: Partial = {}): AgentBackendInpu }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('buildToolGuidance', () => { it('returns empty string for empty tools', () => { expect(buildToolGuidance([])).toBe(''); diff --git a/tests/unit/backends/githubPoster.test.ts b/tests/unit/backends/githubPoster.test.ts index 90ed6dcb..c25f2200 100644 --- a/tests/unit/backends/githubPoster.test.ts +++ b/tests/unit/backends/githubPoster.test.ts @@ -23,10 +23,6 @@ const mockGithubClient = vi.mocked(githubClient); const mockGetSessionState = vi.mocked(getSessionState); const mockFormatGitHubProgressComment = vi.mocked(formatGitHubProgressComment); -beforeEach(() => { - vi.clearAllMocks(); -}); - function makePoster() { return new GitHubProgressPoster({ owner: 'myorg', diff --git a/tests/unit/backends/llmist.test.ts b/tests/unit/backends/llmist.test.ts index d5d35c6f..49416fc8 100644 --- a/tests/unit/backends/llmist.test.ts +++ b/tests/unit/backends/llmist.test.ts @@ -110,10 +110,6 @@ function makeInput(agentType = 'implementation'): AgentBackendInput { }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('LlmistBackend', () => { it('has name "llmist"', () => { const backend = new LlmistBackend(); diff --git a/tests/unit/backends/pmPoster.test.ts b/tests/unit/backends/pmPoster.test.ts index 86f880f8..9d2e6b74 100644 --- a/tests/unit/backends/pmPoster.test.ts +++ b/tests/unit/backends/pmPoster.test.ts @@ -27,7 +27,6 @@ const mockPMProvider = { }; beforeEach(() => { - vi.clearAllMocks(); // Default: state file exists mockReadProgressCommentId.mockReturnValue({ workItemId: 'card1', commentId: 'comment1' }); }); diff --git a/tests/unit/backends/postProcess.test.ts b/tests/unit/backends/postProcess.test.ts index c0d52b6d..263c3d28 100644 --- a/tests/unit/backends/postProcess.test.ts +++ b/tests/unit/backends/postProcess.test.ts @@ -48,10 +48,6 @@ function makeInput(overrides?: Partial): AgentInput & { project: } as AgentInput & { project: ProjectConfig }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('postProcessResult', () => { describe('PR validation for implementation agents', () => { it('marks as failed when implementation agent succeeds without prUrl', () => { diff --git a/tests/unit/backends/progress.test.ts b/tests/unit/backends/progress.test.ts index 5592606d..190ad471 100644 --- a/tests/unit/backends/progress.test.ts +++ b/tests/unit/backends/progress.test.ts @@ -73,7 +73,6 @@ const mockCallProgressModel = vi.mocked(callProgressModel); const mockSyncChecklist = vi.mocked(syncCompletedTodosToChecklist); beforeEach(() => { - vi.clearAllMocks(); vi.useFakeTimers(); mockLoadTodos.mockReturnValue([]); mockGetPMProvider.mockReturnValue(null); diff --git a/tests/unit/backends/progressModel.test.ts b/tests/unit/backends/progressModel.test.ts index d3724db8..c6b90b9f 100644 --- a/tests/unit/backends/progressModel.test.ts +++ b/tests/unit/backends/progressModel.test.ts @@ -27,10 +27,6 @@ function makeContext(overrides: Partial = {}): ProgressContext }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('callProgressModel', () => { it('returns text output from LLM on success', async () => { mockTextComplete.mockResolvedValue( diff --git a/tests/unit/backends/secretBuilder.test.ts b/tests/unit/backends/secretBuilder.test.ts index 33edb1d6..81a26c22 100644 --- a/tests/unit/backends/secretBuilder.test.ts +++ b/tests/unit/backends/secretBuilder.test.ts @@ -48,7 +48,6 @@ function makeProfile(overrides?: Partial): AgentProfile { } beforeEach(() => { - vi.clearAllMocks(); mockGetAllProjectCredentials.mockResolvedValue({}); }); diff --git a/tests/unit/cli/credential-scoping.test.ts b/tests/unit/cli/credential-scoping.test.ts index 2a7b8600..11e8f115 100644 --- a/tests/unit/cli/credential-scoping.test.ts +++ b/tests/unit/cli/credential-scoping.test.ts @@ -28,7 +28,6 @@ describe('CredentialScopedCommand', () => { const originalEnv = process.env; beforeEach(() => { - vi.clearAllMocks(); process.env = { ...originalEnv }; process.env.GITHUB_TOKEN = undefined; process.env.TRELLO_API_KEY = undefined; diff --git a/tests/unit/cli/dashboard/base.test.ts b/tests/unit/cli/dashboard/base.test.ts index 0cd0fff1..61a2c3ce 100644 --- a/tests/unit/cli/dashboard/base.test.ts +++ b/tests/unit/cli/dashboard/base.test.ts @@ -94,10 +94,6 @@ describe('extractBaseFlags', () => { }); describe('DashboardCommand', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('config loading', () => { it('errors when not logged in (no config)', async () => { mockLoadConfig.mockReturnValue(null); diff --git a/tests/unit/cli/dashboard/client.test.ts b/tests/unit/cli/dashboard/client.test.ts index ff558e8c..c0c356b8 100644 --- a/tests/unit/cli/dashboard/client.test.ts +++ b/tests/unit/cli/dashboard/client.test.ts @@ -9,10 +9,6 @@ import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createDashboardClient } from '../../../../src/cli/dashboard/_shared/client.js'; describe('createDashboardClient', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('creates a tRPC client with links', () => { const config = { serverUrl: 'http://localhost:3000', sessionToken: 'my-token' }; diff --git a/tests/unit/cli/dashboard/config.test.ts b/tests/unit/cli/dashboard/config.test.ts index cc0612c0..11760d94 100644 --- a/tests/unit/cli/dashboard/config.test.ts +++ b/tests/unit/cli/dashboard/config.test.ts @@ -22,7 +22,6 @@ describe('config', () => { const originalEnv = process.env; beforeEach(() => { - vi.clearAllMocks(); process.env = { ...originalEnv }; process.env.CASCADE_SERVER_URL = undefined; process.env.CASCADE_SESSION_TOKEN = undefined; diff --git a/tests/unit/cli/file-input-flags.test.ts b/tests/unit/cli/file-input-flags.test.ts index f8d6cff8..c9a936eb 100644 --- a/tests/unit/cli/file-input-flags.test.ts +++ b/tests/unit/cli/file-input-flags.test.ts @@ -58,7 +58,6 @@ let tmpDir: string; const mockConfig = { runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }) }; beforeEach(() => { - vi.clearAllMocks(); tmpDir = mkdtempSync(join(tmpdir(), 'cascade-cli-test-')); }); diff --git a/tests/unit/config/compactionConfig.test.ts b/tests/unit/config/compactionConfig.test.ts index fc368a68..6f489108 100644 --- a/tests/unit/config/compactionConfig.test.ts +++ b/tests/unit/config/compactionConfig.test.ts @@ -20,10 +20,6 @@ import { clearReadTracking } from '../../../src/gadgets/readTracking.js'; import { logger } from '../../../src/utils/logging.js'; describe('config/compactionConfig', () => { - afterEach(() => { - vi.clearAllMocks(); - }); - describe('getCompactionConfig', () => { it('returns implementation agent config with lower threshold', () => { const config = getCompactionConfig('implementation'); diff --git a/tests/unit/config/hintConfig.test.ts b/tests/unit/config/hintConfig.test.ts index 00f71074..58b9d011 100644 --- a/tests/unit/config/hintConfig.test.ts +++ b/tests/unit/config/hintConfig.test.ts @@ -37,7 +37,6 @@ function getMessage(agentType: string | undefined, iteration = 3, maxIterations describe('getIterationTrailingMessage', () => { afterEach(() => { clearDiagnosticState(); - vi.clearAllMocks(); mockLoadTodos.mockReturnValue([]); mockFormatTodoList.mockReturnValue(''); mockExecSync.mockReturnValue(''); diff --git a/tests/unit/config/projects.test.ts b/tests/unit/config/projects.test.ts index b1f493f7..5ad09d6a 100644 --- a/tests/unit/config/projects.test.ts +++ b/tests/unit/config/projects.test.ts @@ -82,15 +82,6 @@ describe('config provider', () => { projects: [mockProject1, mockProject2], }; - beforeEach(() => { - vi.clearAllMocks(); - invalidateConfigCache(); - }); - - afterEach(() => { - invalidateConfigCache(); - }); - describe('loadConfig', () => { it('loads config from database', async () => { vi.mocked(loadConfigFromDb).mockResolvedValue(mockConfig); @@ -175,10 +166,6 @@ describe('config provider', () => { beforeEach(() => { vi.stubEnv('TRELLO_API_KEY', ''); }); - afterEach(() => { - vi.unstubAllEnvs(); - }); - it('resolves credential from DB', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('db-secret-value'); @@ -200,10 +187,6 @@ describe('config provider', () => { beforeEach(() => { vi.stubEnv('GITHUB_TOKEN_IMPLEMENTER', ''); }); - afterEach(() => { - vi.unstubAllEnvs(); - }); - it('returns credential value when found', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('secret-value'); @@ -249,10 +232,6 @@ describe('config provider', () => { beforeEach(() => { vi.stubEnv('GITHUB_TOKEN_IMPLEMENTER', ''); }); - afterEach(() => { - vi.unstubAllEnvs(); - }); - it('returns implementer token when available', async () => { vi.mocked(resolveIntegrationCredential).mockResolvedValue('implementer-token'); diff --git a/tests/unit/config/provider.test.ts b/tests/unit/config/provider.test.ts index efd4986c..3b2d00a9 100644 --- a/tests/unit/config/provider.test.ts +++ b/tests/unit/config/provider.test.ts @@ -111,18 +111,11 @@ describe('config/provider', () => { envKeysToClean.push(key); } - beforeEach(() => { - invalidateConfigCache(); - vi.clearAllMocks(); - }); - afterEach(() => { for (const key of envKeysToClean) { delete process.env[key]; } envKeysToClean.length = 0; - invalidateConfigCache(); - vi.clearAllMocks(); }); describe('loadConfig', () => { diff --git a/tests/unit/config/statusUpdateConfig.test.ts b/tests/unit/config/statusUpdateConfig.test.ts index 3d041f00..0845ecf9 100644 --- a/tests/unit/config/statusUpdateConfig.test.ts +++ b/tests/unit/config/statusUpdateConfig.test.ts @@ -16,10 +16,6 @@ vi.mock('../../../src/gadgets/todo/storage.js', () => ({ import { formatTodoList, loadTodos } from '../../../src/gadgets/todo/storage.js'; describe('config/statusUpdateConfig', () => { - afterEach(() => { - vi.clearAllMocks(); - }); - describe('getStatusUpdateConfig', () => { it('returns enabled config for non-debug agents', () => { const agentTypes = ['implementation', 'splitting', 'planning', 'review']; diff --git a/tests/unit/db/crypto.test.ts b/tests/unit/db/crypto.test.ts index c2506f5f..054322d9 100644 --- a/tests/unit/db/crypto.test.ts +++ b/tests/unit/db/crypto.test.ts @@ -15,10 +15,6 @@ describe('crypto', () => { vi.stubEnv('CREDENTIAL_MASTER_KEY', TEST_KEY); }); - afterEach(() => { - vi.unstubAllEnvs(); - }); - describe('isEncryptionEnabled', () => { it('returns true when CREDENTIAL_MASTER_KEY is set', () => { expect(isEncryptionEnabled()).toBe(true); diff --git a/tests/unit/db/repositories/configRepository.test.ts b/tests/unit/db/repositories/configRepository.test.ts index e26547f2..e574d677 100644 --- a/tests/unit/db/repositories/configRepository.test.ts +++ b/tests/unit/db/repositories/configRepository.test.ts @@ -163,10 +163,6 @@ function createSequentialMockDb(results: QueryResult[]) { } describe('configRepository', () => { - afterEach(() => { - vi.clearAllMocks(); - }); - describe('loadConfigFromDb', () => { it('loads config with Trello integration from project_integrations', async () => { // loadConfigFromDb Promise.all order: defaults, projects, agentConfigs, integrations diff --git a/tests/unit/db/repositories/credentialsRepository.test.ts b/tests/unit/db/repositories/credentialsRepository.test.ts index 3d20d8ff..f735970f 100644 --- a/tests/unit/db/repositories/credentialsRepository.test.ts +++ b/tests/unit/db/repositories/credentialsRepository.test.ts @@ -1,5 +1,6 @@ import { randomBytes } from 'node:crypto'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createMockDb } from '../../../helpers/mockDb.js'; // Mock the DB client vi.mock('../../../../src/db/client.js', () => ({ @@ -18,55 +19,14 @@ import { updateCredential, } from '../../../../src/db/repositories/credentialsRepository.js'; -/** - * Creates a mock Drizzle query chain that supports the common patterns: - * select().from().innerJoin().where(), select().from().innerJoin().innerJoin().where(), - * insert().values().returning(), update().set().where(), delete().from().where() - */ -function createMockDb() { - const chain: Record> = {}; - - // Terminal methods that return results - chain.where = vi.fn().mockResolvedValue([]); - chain.returning = vi.fn().mockResolvedValue([]); - - // Chain methods - chain.innerJoin = vi.fn().mockReturnValue({ - where: chain.where, - innerJoin: vi.fn().mockReturnValue({ where: chain.where }), - }); - chain.from = vi.fn().mockReturnValue({ - where: chain.where, - innerJoin: chain.innerJoin, - }); - chain.set = vi.fn().mockReturnValue({ where: chain.where }); - chain.values = vi.fn().mockReturnValue({ - returning: chain.returning, - }); - - const db = { - select: vi.fn().mockReturnValue({ from: chain.from }), - insert: vi.fn().mockReturnValue({ values: chain.values }), - update: vi.fn().mockReturnValue({ set: chain.set }), - delete: vi.fn().mockReturnValue({ where: chain.where }), - }; - - return { db, chain }; -} - describe('credentialsRepository', () => { let mockDb: ReturnType; beforeEach(() => { - mockDb = createMockDb(); + mockDb = createMockDb({ withDoubleJoin: true }); vi.mocked(getDb).mockReturnValue(mockDb.db as never); }); - afterEach(() => { - vi.unstubAllEnvs(); - vi.clearAllMocks(); - }); - describe('resolveIntegrationCredential', () => { it('returns decrypted value when found', async () => { mockDb.chain.where.mockResolvedValueOnce([{ value: 'trello-api-key', orgId: 'org1' }]); diff --git a/tests/unit/db/repositories/prWorkItemsRepository.test.ts b/tests/unit/db/repositories/prWorkItemsRepository.test.ts index 46c5419c..670a0eec 100644 --- a/tests/unit/db/repositories/prWorkItemsRepository.test.ts +++ b/tests/unit/db/repositories/prWorkItemsRepository.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createMockDb } from '../../../helpers/mockDb.js'; vi.mock('../../../../src/db/client.js', () => ({ getDb: vi.fn(), @@ -19,38 +20,14 @@ import { lookupWorkItemForPR, } from '../../../../src/db/repositories/prWorkItemsRepository.js'; -function createMockDb() { - const chain: Record> = {}; - - chain.limit = vi.fn().mockResolvedValue([]); - chain.where = vi.fn().mockReturnValue({ limit: chain.limit }); - chain.from = vi.fn().mockReturnValue({ where: chain.where }); - - chain.onConflictDoUpdate = vi.fn().mockResolvedValue(undefined); - chain.values = vi.fn().mockReturnValue({ - onConflictDoUpdate: chain.onConflictDoUpdate, - }); - - const db = { - select: vi.fn().mockReturnValue({ from: chain.from }), - insert: vi.fn().mockReturnValue({ values: chain.values }), - }; - - return { db, chain }; -} - describe('prWorkItemsRepository', () => { let mockDb: ReturnType; beforeEach(() => { - mockDb = createMockDb(); + mockDb = createMockDb({ withLimit: true, withUpsert: true }); vi.mocked(getDb).mockReturnValue(mockDb.db as never); }); - afterEach(() => { - vi.clearAllMocks(); - }); - // ========================================================================== // linkPRToWorkItem // ========================================================================== diff --git a/tests/unit/db/repositories/runsRepository.dashboard.test.ts b/tests/unit/db/repositories/runsRepository.dashboard.test.ts index acefaef0..9472806f 100644 --- a/tests/unit/db/repositories/runsRepository.dashboard.test.ts +++ b/tests/unit/db/repositories/runsRepository.dashboard.test.ts @@ -69,10 +69,6 @@ function createChain(resolveValue: unknown = []) { } describe('runsRepository - dashboard queries', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('listRuns', () => { it('returns data and total count', async () => { const dataChain = createChain([{ id: 'run-1', agentType: 'impl' }]); diff --git a/tests/unit/db/repositories/settingsRepository.test.ts b/tests/unit/db/repositories/settingsRepository.test.ts index 5d48073a..e922fdb8 100644 --- a/tests/unit/db/repositories/settingsRepository.test.ts +++ b/tests/unit/db/repositories/settingsRepository.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createMockDb } from '../../../helpers/mockDb.js'; vi.mock('../../../../src/db/client.js', () => ({ getDb: vi.fn(), @@ -24,54 +25,14 @@ import { upsertProjectIntegration, } from '../../../../src/db/repositories/settingsRepository.js'; -function createMockDb() { - const chain: Record> = {}; - - chain.where = vi.fn().mockResolvedValue([]); - chain.returning = vi.fn().mockResolvedValue([]); - chain.limit = vi.fn().mockReturnValue(chain); - - chain.innerJoin = vi.fn().mockReturnValue({ where: chain.where }); - chain.from = vi.fn().mockReturnValue({ - where: chain.where, - innerJoin: chain.innerJoin, - limit: chain.limit, - }); - chain.set = vi.fn().mockReturnValue({ where: chain.where }); - chain.onConflictDoUpdate = vi.fn().mockReturnValue({ - returning: chain.returning, - }); - chain.values = vi.fn().mockReturnValue({ - onConflictDoUpdate: chain.onConflictDoUpdate, - returning: chain.returning, - }); - - // Make chain itself thenable for queries without .where() terminal - // biome-ignore lint/suspicious/noThenProperty: intentional thenable mock for Drizzle query chains - chain.then = (resolve: (v: unknown) => unknown) => Promise.resolve([]).then(resolve); - - const db = { - select: vi.fn().mockReturnValue({ from: chain.from }), - insert: vi.fn().mockReturnValue({ values: chain.values }), - update: vi.fn().mockReturnValue({ set: chain.set }), - delete: vi.fn().mockReturnValue({ where: chain.where }), - }; - - return { db, chain }; -} - describe('settingsRepository', () => { let mockDb: ReturnType; beforeEach(() => { - mockDb = createMockDb(); + mockDb = createMockDb({ withUpsert: true, withThenable: true }); vi.mocked(getDb).mockReturnValue(mockDb.db as never); }); - afterEach(() => { - vi.clearAllMocks(); - }); - // ============================================================================ // Organizations // ============================================================================ diff --git a/tests/unit/db/repositories/usersRepository.test.ts b/tests/unit/db/repositories/usersRepository.test.ts index b2beac1e..dc9dceb7 100644 --- a/tests/unit/db/repositories/usersRepository.test.ts +++ b/tests/unit/db/repositories/usersRepository.test.ts @@ -44,8 +44,6 @@ import { describe('usersRepository', () => { beforeEach(() => { - vi.clearAllMocks(); - mockInsert.mockReturnValue({ values: mockValues }); mockValues.mockReturnValue({ returning: mockReturning }); mockSelect.mockReturnValue({ from: mockFrom }); diff --git a/tests/unit/db/runsRepository.test.ts b/tests/unit/db/runsRepository.test.ts index e699c367..d8e88f18 100644 --- a/tests/unit/db/runsRepository.test.ts +++ b/tests/unit/db/runsRepository.test.ts @@ -52,8 +52,6 @@ import { describe('runsRepository', () => { beforeEach(() => { - vi.clearAllMocks(); - // Set up chained mock returns mockInsert.mockReturnValue({ values: mockValues }); mockValues.mockReturnValue({ returning: mockReturning }); diff --git a/tests/unit/db/webhookLogsRepository.test.ts b/tests/unit/db/webhookLogsRepository.test.ts index d058edb4..290d5f1b 100644 --- a/tests/unit/db/webhookLogsRepository.test.ts +++ b/tests/unit/db/webhookLogsRepository.test.ts @@ -48,8 +48,6 @@ import { describe('webhookLogsRepository', () => { beforeEach(() => { - vi.clearAllMocks(); - // Set up chained mock returns mockInsert.mockReturnValue({ values: mockValues }); mockValues.mockReturnValue({ returning: mockReturning }); diff --git a/tests/unit/gadgets/fileInsertContent.test.ts b/tests/unit/gadgets/fileInsertContent.test.ts index 01d21bb7..b9fbdad4 100644 --- a/tests/unit/gadgets/fileInsertContent.test.ts +++ b/tests/unit/gadgets/fileInsertContent.test.ts @@ -52,7 +52,6 @@ beforeEach(() => { afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); - vi.clearAllMocks(); }); function createFile(name: string, content: string): string { diff --git a/tests/unit/gadgets/fileRemoveContent.test.ts b/tests/unit/gadgets/fileRemoveContent.test.ts index e71c17f8..f8846cf9 100644 --- a/tests/unit/gadgets/fileRemoveContent.test.ts +++ b/tests/unit/gadgets/fileRemoveContent.test.ts @@ -52,7 +52,6 @@ beforeEach(() => { afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); - vi.clearAllMocks(); }); function createFile(name: string, content: string): string { diff --git a/tests/unit/gadgets/finish.test.ts b/tests/unit/gadgets/finish.test.ts index 66d1fbc9..a3e571c3 100644 --- a/tests/unit/gadgets/finish.test.ts +++ b/tests/unit/gadgets/finish.test.ts @@ -22,10 +22,6 @@ vi.mock('../../../src/github/client.js', () => ({ })); describe('Finish gadget', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('has exclusive set to prevent parallel execution with other gadgets', () => { initSessionState('unknown'); const gadget = new Finish(); diff --git a/tests/unit/gadgets/github.test.ts b/tests/unit/gadgets/github.test.ts index 3f7d5f98..9132eaa0 100644 --- a/tests/unit/gadgets/github.test.ts +++ b/tests/unit/gadgets/github.test.ts @@ -54,10 +54,6 @@ function mockRunCommand( describe('GitHub Gadgets', () => { describe('CreatePR', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('is a valid llmist Gadget class', () => { const gadget = new CreatePR(); expect(gadget).toBeDefined(); diff --git a/tests/unit/gadgets/github/core/createPR.test.ts b/tests/unit/gadgets/github/core/createPR.test.ts index b0d331a4..edb73a48 100644 --- a/tests/unit/gadgets/github/core/createPR.test.ts +++ b/tests/unit/gadgets/github/core/createPR.test.ts @@ -37,10 +37,6 @@ function mockGitCommands( }); } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('detectOwnerRepo (tested through createPR)', () => { it('parses HTTPS URL', async () => { mockRunCommand.mockImplementation(async (_cmd, args) => { diff --git a/tests/unit/gadgets/github/core/misc.test.ts b/tests/unit/gadgets/github/core/misc.test.ts index 9fdb264b..72178c8e 100644 --- a/tests/unit/gadgets/github/core/misc.test.ts +++ b/tests/unit/gadgets/github/core/misc.test.ts @@ -28,10 +28,6 @@ import { githubClient } from '../../../../../src/github/client.js'; const mockGithub = vi.mocked(githubClient); -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('getPRDetails', () => { it('formats PR with number, title, state, branches, URL', async () => { mockGithub.getPR.mockResolvedValue({ diff --git a/tests/unit/gadgets/pm/core/addChecklist.test.ts b/tests/unit/gadgets/pm/core/addChecklist.test.ts index 8e1131d5..2b9793d0 100644 --- a/tests/unit/gadgets/pm/core/addChecklist.test.ts +++ b/tests/unit/gadgets/pm/core/addChecklist.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { addChecklist } from '../../../../../src/gadgets/pm/core/addChecklist.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('addChecklist', () => { it('creates checklist and adds string items', async () => { mockProvider.createChecklist.mockResolvedValue({ diff --git a/tests/unit/gadgets/pm/core/createWorkItem.test.ts b/tests/unit/gadgets/pm/core/createWorkItem.test.ts index ccf831a0..4196cea8 100644 --- a/tests/unit/gadgets/pm/core/createWorkItem.test.ts +++ b/tests/unit/gadgets/pm/core/createWorkItem.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { createWorkItem } from '../../../../../src/gadgets/pm/core/createWorkItem.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('createWorkItem', () => { it('creates a work item and returns success message', async () => { mockProvider.createWorkItem.mockResolvedValue({ diff --git a/tests/unit/gadgets/pm/core/deleteChecklistItem.test.ts b/tests/unit/gadgets/pm/core/deleteChecklistItem.test.ts index 976ca898..cd3c84f9 100644 --- a/tests/unit/gadgets/pm/core/deleteChecklistItem.test.ts +++ b/tests/unit/gadgets/pm/core/deleteChecklistItem.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { deleteChecklistItem } from '../../../../../src/gadgets/pm/core/deleteChecklistItem.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('deleteChecklistItem', () => { it('deletes a checklist item and returns success message', async () => { mockProvider.deleteChecklistItem.mockResolvedValue(undefined); diff --git a/tests/unit/gadgets/pm/core/listWorkItems.test.ts b/tests/unit/gadgets/pm/core/listWorkItems.test.ts index 00328e94..33bc5f1f 100644 --- a/tests/unit/gadgets/pm/core/listWorkItems.test.ts +++ b/tests/unit/gadgets/pm/core/listWorkItems.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { listWorkItems } from '../../../../../src/gadgets/pm/core/listWorkItems.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('listWorkItems', () => { it('returns "No work items found." when list is empty', async () => { mockProvider.listWorkItems.mockResolvedValue([]); diff --git a/tests/unit/gadgets/pm/core/postComment.test.ts b/tests/unit/gadgets/pm/core/postComment.test.ts index 7cf2fa62..10c737e5 100644 --- a/tests/unit/gadgets/pm/core/postComment.test.ts +++ b/tests/unit/gadgets/pm/core/postComment.test.ts @@ -23,7 +23,6 @@ const mockReadProgressCommentId = vi.mocked(readProgressCommentId); const mockClearProgressCommentId = vi.mocked(clearProgressCommentId); beforeEach(() => { - vi.clearAllMocks(); mockReadProgressCommentId.mockReturnValue(null); }); diff --git a/tests/unit/gadgets/pm/core/readWorkItem.test.ts b/tests/unit/gadgets/pm/core/readWorkItem.test.ts index aa1ac666..5d502ad9 100644 --- a/tests/unit/gadgets/pm/core/readWorkItem.test.ts +++ b/tests/unit/gadgets/pm/core/readWorkItem.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { readWorkItem } from '../../../../../src/gadgets/pm/core/readWorkItem.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('readWorkItem', () => { const baseItem = { id: 'item1', diff --git a/tests/unit/gadgets/pm/core/updateChecklistItem.test.ts b/tests/unit/gadgets/pm/core/updateChecklistItem.test.ts index 032025c6..7065812e 100644 --- a/tests/unit/gadgets/pm/core/updateChecklistItem.test.ts +++ b/tests/unit/gadgets/pm/core/updateChecklistItem.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { updateChecklistItem } from '../../../../../src/gadgets/pm/core/updateChecklistItem.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('updateChecklistItem', () => { it('marks a checklist item as complete', async () => { mockProvider.updateChecklistItem.mockResolvedValue(undefined); diff --git a/tests/unit/gadgets/pm/core/updateWorkItem.test.ts b/tests/unit/gadgets/pm/core/updateWorkItem.test.ts index 9a263c89..21291f25 100644 --- a/tests/unit/gadgets/pm/core/updateWorkItem.test.ts +++ b/tests/unit/gadgets/pm/core/updateWorkItem.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../../../src/pm/index.js', () => ({ import { updateWorkItem } from '../../../../../src/gadgets/pm/core/updateWorkItem.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('updateWorkItem', () => { it('returns early message when nothing to update', async () => { const result = await updateWorkItem({ workItemId: 'item1' }); diff --git a/tests/unit/gadgets/session/core/finish.test.ts b/tests/unit/gadgets/session/core/finish.test.ts index f51126c7..0ba207c2 100644 --- a/tests/unit/gadgets/session/core/finish.test.ts +++ b/tests/unit/gadgets/session/core/finish.test.ts @@ -22,10 +22,6 @@ import { githubClient } from '../../../../../src/github/client.js'; const mockGithub = vi.mocked(githubClient); -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('hasUncommittedChanges', () => { it('returns true when git status has output', () => { mockExecSync.mockReturnValue('M src/file.ts'); diff --git a/tests/unit/gadgets/shared/diagnosticState.test.ts b/tests/unit/gadgets/shared/diagnosticState.test.ts index 138f66c3..70f4d292 100644 --- a/tests/unit/gadgets/shared/diagnosticState.test.ts +++ b/tests/unit/gadgets/shared/diagnosticState.test.ts @@ -22,7 +22,6 @@ const mockShouldRunDiagnostics = vi.mocked(shouldRunDiagnostics); afterEach(() => { clearDiagnosticState(); - vi.clearAllMocks(); }); describe('updateDiagnosticState', () => { diff --git a/tests/unit/gadgets/todo-storage.test.ts b/tests/unit/gadgets/todo-storage.test.ts index 7bb6b8c0..e0538751 100644 --- a/tests/unit/gadgets/todo-storage.test.ts +++ b/tests/unit/gadgets/todo-storage.test.ts @@ -24,7 +24,6 @@ import { describe('todo storage', () => { beforeEach(() => { - vi.clearAllMocks(); // Reset session state by re-initializing vi.mocked(existsSync).mockReturnValue(true); }); diff --git a/tests/unit/gadgets/todo.test.ts b/tests/unit/gadgets/todo.test.ts index 6757bfe2..1e709da1 100644 --- a/tests/unit/gadgets/todo.test.ts +++ b/tests/unit/gadgets/todo.test.ts @@ -63,10 +63,6 @@ describe('TodoUpsert', () => { (storage as unknown as { _resetTodos: () => void })._resetTodos(); }); - afterEach(() => { - vi.clearAllMocks(); - }); - describe('gadget metadata', () => { it('has correct name', () => { expect(gadget.name).toBe('TodoUpsert'); @@ -203,10 +199,6 @@ describe('TodoUpdateStatus', () => { (storage as unknown as { _resetTodos: () => void })._resetTodos(); }); - afterEach(() => { - vi.clearAllMocks(); - }); - describe('gadget metadata', () => { it('has correct name', () => { expect(gadget.name).toBe('TodoUpdateStatus'); diff --git a/tests/unit/github/client.test.ts b/tests/unit/github/client.test.ts index ad922a4b..9de950f8 100644 --- a/tests/unit/github/client.test.ts +++ b/tests/unit/github/client.test.ts @@ -71,10 +71,6 @@ import { import { Octokit } from '@octokit/rest'; describe('githubClient', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('getClient throws without scope', () => { it('throws when no withGitHubToken scope is active', async () => { await expect(githubClient.getPR('owner', 'repo', 1)).rejects.toThrow( diff --git a/tests/unit/github/personas.test.ts b/tests/unit/github/personas.test.ts index 61f6e66e..7e669060 100644 --- a/tests/unit/github/personas.test.ts +++ b/tests/unit/github/personas.test.ts @@ -30,10 +30,6 @@ import { import type { PersonaIdentities } from '../../../src/github/personas.js'; describe('personas', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - // ======================================================================== // getPersonaForAgentType // ======================================================================== diff --git a/tests/unit/jira/client.test.ts b/tests/unit/jira/client.test.ts index 758b8e15..88fe2dd9 100644 --- a/tests/unit/jira/client.test.ts +++ b/tests/unit/jira/client.test.ts @@ -109,7 +109,6 @@ describe('jiraClient', () => { // Note: We don't call vi.restoreAllMocks() here because it would reset // the Version3Client mock implementation from vi.mock(), breaking subsequent tests. // Instead we clear only the fetch spy manually. - vi.clearAllMocks(); }); describe('getCloudId', () => { diff --git a/tests/unit/pm/webhook-handler.test.ts b/tests/unit/pm/webhook-handler.test.ts index 1b4740e3..13aa3386 100644 --- a/tests/unit/pm/webhook-handler.test.ts +++ b/tests/unit/pm/webhook-handler.test.ts @@ -131,7 +131,6 @@ function createMockRegistry(result?: object | null) { } beforeEach(() => { - vi.clearAllMocks(); mockIsCurrentlyProcessing.mockReturnValue(false); mockIsCardActive.mockReturnValue(false); mockEnqueueWebhook.mockReturnValue(true); diff --git a/tests/unit/queue/retry-run-projectId.test.ts b/tests/unit/queue/retry-run-projectId.test.ts index 55f24385..6bda8650 100644 --- a/tests/unit/queue/retry-run-projectId.test.ts +++ b/tests/unit/queue/retry-run-projectId.test.ts @@ -79,7 +79,6 @@ const RUN_UUID = 'aaaaaaaa-1111-2222-3333-444444444444'; describe('retry-run job submission with projectId', () => { beforeEach(() => { - vi.clearAllMocks(); mockDbSelect.mockReturnValue({ from: mockDbFrom }); mockDbFrom.mockReturnValue({ where: mockDbWhere }); }); diff --git a/tests/unit/router/ackMessageGenerator.test.ts b/tests/unit/router/ackMessageGenerator.test.ts index da10d716..d9d8613a 100644 --- a/tests/unit/router/ackMessageGenerator.test.ts +++ b/tests/unit/router/ackMessageGenerator.test.ts @@ -51,10 +51,6 @@ import { generateAckMessage, } from '../../../src/router/ackMessageGenerator.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - // --------------------------------------------------------------------------- // Context extractors // --------------------------------------------------------------------------- diff --git a/tests/unit/router/adapters/github.test.ts b/tests/unit/router/adapters/github.test.ts index ade58d55..576c24d8 100644 --- a/tests/unit/router/adapters/github.test.ts +++ b/tests/unit/router/adapters/github.test.ts @@ -94,7 +94,6 @@ const mockTriggerRegistry = { } as unknown as TriggerRegistry; beforeEach(() => { - vi.clearAllMocks(); vi.mocked(loadProjectConfig).mockResolvedValue({ projects: [mockProject], fullProjects: [{ id: 'p1', repo: 'owner/repo' } as never], diff --git a/tests/unit/router/adapters/jira.test.ts b/tests/unit/router/adapters/jira.test.ts index 056a240a..2a847339 100644 --- a/tests/unit/router/adapters/jira.test.ts +++ b/tests/unit/router/adapters/jira.test.ts @@ -60,7 +60,6 @@ const mockTriggerRegistry = { } as unknown as TriggerRegistry; beforeEach(() => { - vi.clearAllMocks(); vi.mocked(loadProjectConfig).mockResolvedValue({ projects: [mockProject], fullProjects: [{ id: 'p1' } as never], diff --git a/tests/unit/router/adapters/trello.test.ts b/tests/unit/router/adapters/trello.test.ts index beb4048e..6eb9f7b0 100644 --- a/tests/unit/router/adapters/trello.test.ts +++ b/tests/unit/router/adapters/trello.test.ts @@ -69,7 +69,6 @@ const mockTriggerRegistry = { } as unknown as TriggerRegistry; beforeEach(() => { - vi.clearAllMocks(); vi.mocked(loadProjectConfig).mockResolvedValue({ projects: [mockProject], fullProjects: [{ id: 'p1' } as never], diff --git a/tests/unit/router/trello.test.ts b/tests/unit/router/trello.test.ts index d69c324f..01a6335d 100644 --- a/tests/unit/router/trello.test.ts +++ b/tests/unit/router/trello.test.ts @@ -39,10 +39,6 @@ const mockProject: RouterProjectConfig = { }, }; -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('isAgentLogFilename', () => { it('matches valid agent log filenames', () => { expect(isAgentLogFilename('implementation-2026-01-02T16-30-24-339Z.zip')).toBe(true); diff --git a/tests/unit/router/webhook-processor.test.ts b/tests/unit/router/webhook-processor.test.ts index 55f02e93..eb8e2252 100644 --- a/tests/unit/router/webhook-processor.test.ts +++ b/tests/unit/router/webhook-processor.test.ts @@ -58,10 +58,6 @@ function makeMockAdapter(overrides: Partial = {}): Router }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('processRouterWebhook', () => { it('returns shouldProcess false when parseWebhook returns null', async () => { const adapter = makeMockAdapter({ diff --git a/tests/unit/sentry.test.ts b/tests/unit/sentry.test.ts index 57e8a250..a371d538 100644 --- a/tests/unit/sentry.test.ts +++ b/tests/unit/sentry.test.ts @@ -23,7 +23,6 @@ describe('sentry wrappers', () => { let sentry: typeof import('../../src/sentry.js'); beforeEach(async () => { - vi.clearAllMocks(); vi.resetModules(); // biome-ignore lint/performance/noDelete: process.env requires delete to truly unset delete process.env.SENTRY_DSN; @@ -59,7 +58,6 @@ describe('sentry wrappers', () => { let sentry: typeof import('../../src/sentry.js'); beforeEach(async () => { - vi.clearAllMocks(); vi.resetModules(); for (const k of Object.keys(mockScope)) mockScope[k as keyof typeof mockScope].mockClear(); process.env.SENTRY_DSN = 'https://fake@sentry.io/123'; diff --git a/tests/unit/server.test.ts b/tests/unit/server.test.ts index 32b133c9..f750515e 100644 --- a/tests/unit/server.test.ts +++ b/tests/unit/server.test.ts @@ -113,10 +113,6 @@ async function postJson( } describe('createServer', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('Trello webhook', () => { it('calls sendAcknowledgeReaction for commentCard events', async () => { vi.useFakeTimers(); diff --git a/tests/unit/server/webhookHandlers.test.ts b/tests/unit/server/webhookHandlers.test.ts index 7a117fb3..9c6725c7 100644 --- a/tests/unit/server/webhookHandlers.test.ts +++ b/tests/unit/server/webhookHandlers.test.ts @@ -83,7 +83,6 @@ async function postJson( describe('createWebhookHandler', () => { beforeEach(() => { - vi.clearAllMocks(); mockIsCurrentlyProcessing.mockReturnValue(false); mockCanAcceptWebhook.mockReturnValue(true); }); @@ -460,10 +459,6 @@ describe('buildTrelloReactionSender', () => { ], }; - beforeEach(() => { - vi.clearAllMocks(); - }); - it('sends reaction for commentCard events', async () => { vi.useFakeTimers(); const sender = buildTrelloReactionSender(config); @@ -491,10 +486,6 @@ describe('buildTrelloReactionSender', () => { }); describe('buildGitHubReactionSender', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('sends reaction for issue_comment events', async () => { vi.useFakeTimers(); const mockProject = { id: 'proj-1' } as never; @@ -550,10 +541,6 @@ describe('buildJiraReactionSender', () => { ], }; - beforeEach(() => { - vi.clearAllMocks(); - }); - it('sends reaction for comment_created events', async () => { vi.useFakeTimers(); const sender = buildJiraReactionSender(config); diff --git a/tests/unit/trello/client.test.ts b/tests/unit/trello/client.test.ts index dfb34005..09b3a0a0 100644 --- a/tests/unit/trello/client.test.ts +++ b/tests/unit/trello/client.test.ts @@ -51,18 +51,6 @@ import { describe('trelloClient', () => { const creds = { apiKey: 'test-key', token: 'test-token' }; - beforeEach(() => { - // Reset individual mock functions without clearing implementations - for (const fn of Object.values(mockCards)) fn.mockReset(); - for (const fn of Object.values(mockChecklists)) fn.mockReset(); - for (const fn of Object.values(mockLists)) fn.mockReset(); - }); - - afterEach(() => { - // Don't call restoreAllMocks() as it would clear the Version3Client mock impl - vi.clearAllMocks(); - }); - // ===== trelloFetch helper ===== describe('trelloFetch (via public methods)', () => { diff --git a/tests/unit/triggers/agent-execution.test.ts b/tests/unit/triggers/agent-execution.test.ts index 5d5305b9..649c634c 100644 --- a/tests/unit/triggers/agent-execution.test.ts +++ b/tests/unit/triggers/agent-execution.test.ts @@ -47,22 +47,20 @@ import { triggerDebugAnalysis } from '../../../src/triggers/shared/debug-runner. import { shouldTriggerDebug } from '../../../src/triggers/shared/debug-trigger.js'; import type { TriggerResult } from '../../../src/triggers/types.js'; import type { AgentResult, CascadeConfig, ProjectConfig } from '../../../src/types/index.js'; +import { createMockProject } from '../../helpers/factories.js'; // ── Fixtures ────────────────────────────────────────────────────────────────── -const mockProject: ProjectConfig = { +const mockProject: ProjectConfig = createMockProject({ id: 'test-project', name: 'Test Project', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', trello: { boardId: 'board123', lists: {}, labels: {}, customFields: { cost: 'cf-cost-123' }, }, -}; +}); const mockConfig: CascadeConfig = { defaults: { @@ -98,7 +96,6 @@ const mockLifecycle = { // ── Setup ───────────────────────────────────────────────────────────────────── beforeEach(() => { - vi.clearAllMocks(); vi.mocked(createPMProvider).mockReturnValue({} as ReturnType); vi.mocked(resolveProjectPMConfig).mockReturnValue({ labels: {}, statuses: {} }); vi.mocked(PMLifecycleManager).mockImplementation(() => mockLifecycle as never); diff --git a/tests/unit/triggers/agent-result-handler.test.ts b/tests/unit/triggers/agent-result-handler.test.ts index 9abda72e..63dd0560 100644 --- a/tests/unit/triggers/agent-result-handler.test.ts +++ b/tests/unit/triggers/agent-result-handler.test.ts @@ -12,6 +12,7 @@ import type { PMProvider } from '../../../src/pm/index.js'; import { getPMProvider } from '../../../src/pm/index.js'; import { handleAgentResultArtifacts } from '../../../src/triggers/shared/agent-result-handler.js'; import type { AgentResult, ProjectConfig } from '../../../src/types/index.js'; +import { createMockJiraProject, createMockProject } from '../../helpers/factories.js'; const mockPMProvider = { getCustomFieldNumber: vi.fn(), @@ -20,39 +21,28 @@ const mockPMProvider = { vi.mocked(getPMProvider).mockReturnValue(mockPMProvider as unknown as PMProvider); -const mockTrelloProject: ProjectConfig = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', +const mockTrelloProject: ProjectConfig = createMockProject({ trello: { boardId: 'board123', lists: {}, labels: {}, customFields: { cost: 'cf-cost-123' }, }, -}; +}); -const mockJiraProject: ProjectConfig = { +const mockJiraProject: ProjectConfig = createMockJiraProject({ id: 'test', name: 'Test', repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', pm: { type: 'jira' }, jira: { host: 'example.atlassian.net', projectKey: 'TEST', customFields: { cost: 'cf-jira-cost-456' }, }, -}; +}); describe('handleAgentResultArtifacts', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('updates cost custom field with accumulation', async () => { mockPMProvider.getCustomFieldNumber.mockResolvedValue(2.5); diff --git a/tests/unit/triggers/budget.test.ts b/tests/unit/triggers/budget.test.ts index 17c0d4b6..74e75cb1 100644 --- a/tests/unit/triggers/budget.test.ts +++ b/tests/unit/triggers/budget.test.ts @@ -8,23 +8,19 @@ import type { PMProvider } from '../../../src/pm/index.js'; import { getPMProvider } from '../../../src/pm/index.js'; import { checkBudgetExceeded, resolveCardBudget } from '../../../src/triggers/shared/budget.js'; import type { CascadeConfig, ProjectConfig } from '../../../src/types/index.js'; +import { createMockProject } from '../../helpers/factories.js'; const mockPMProvider = { getCustomFieldNumber: vi.fn() }; vi.mocked(getPMProvider).mockReturnValue(mockPMProvider as unknown as PMProvider); -const baseProject: ProjectConfig = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', +const baseProject: ProjectConfig = createMockProject({ trello: { boardId: 'board123', lists: {}, labels: {}, customFields: { cost: 'cf-cost-123' }, }, -}; +}); const baseConfig: CascadeConfig = { defaults: { @@ -69,10 +65,6 @@ describe('resolveCardBudget', () => { }); describe('checkBudgetExceeded', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('returns null when no cost field configured', async () => { const project = { ...baseProject, diff --git a/tests/unit/triggers/builtins.test.ts b/tests/unit/triggers/builtins.test.ts index b75fab79..2f6bcd77 100644 --- a/tests/unit/triggers/builtins.test.ts +++ b/tests/unit/triggers/builtins.test.ts @@ -72,10 +72,6 @@ function createMockRegistry(): { register: ReturnType; handlers: o }; } -beforeEach(() => { - vi.clearAllMocks(); -}); - describe('registerBuiltInTriggers', () => { it('registers all expected trigger handlers', () => { const registry = createMockRegistry(); diff --git a/tests/unit/triggers/card-moved.test.ts b/tests/unit/triggers/card-moved.test.ts index 03a46d73..a171d1c6 100644 --- a/tests/unit/triggers/card-moved.test.ts +++ b/tests/unit/triggers/card-moved.test.ts @@ -45,26 +45,12 @@ import { CardMovedToTodoTrigger, } from '../../../src/triggers/trello/card-moved.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; describe('CardMovedToSplittingTrigger', () => { const trigger = CardMovedToSplittingTrigger; - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; + const mockProject = createMockProject(); it('matches when card moved to splitting list', () => { const ctx: TriggerContext = { @@ -197,22 +183,7 @@ describe('CardMovedToSplittingTrigger', () => { describe('CardMovedToTodoTrigger', () => { const trigger = CardMovedToTodoTrigger; - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; + const mockProject = createMockProject(); it('matches when card moved to todo list', () => { const ctx: TriggerContext = { diff --git a/tests/unit/triggers/check-suite-failure.test.ts b/tests/unit/triggers/check-suite-failure.test.ts index a77b9ff2..f2dae961 100644 --- a/tests/unit/triggers/check-suite-failure.test.ts +++ b/tests/unit/triggers/check-suite-failure.test.ts @@ -4,6 +4,8 @@ import { resetFixAttempts, } from '../../../src/triggers/github/check-suite-failure.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; +import { mockPersonaIdentities } from '../../helpers/mockPersonas.js'; vi.mock('../../../src/github/client.js', () => ({ githubClient: { @@ -23,22 +25,7 @@ import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRep describe('CheckSuiteFailureTrigger', () => { const trigger = new CheckSuiteFailureTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; + const mockProject = createMockProject(); const makeFailurePayload = (overrides: Record = {}) => ({ action: 'completed', @@ -55,7 +42,6 @@ describe('CheckSuiteFailureTrigger', () => { }); beforeEach(() => { - vi.clearAllMocks(); resetFixAttempts(42); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); @@ -160,7 +146,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -197,7 +183,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -223,7 +209,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -277,7 +263,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -312,7 +298,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -345,7 +331,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; const result = await trigger.handle(ctx); @@ -375,7 +361,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; // First 3 attempts should succeed @@ -417,7 +403,7 @@ describe('CheckSuiteFailureTrigger', () => { project: mockProject, source: 'github', payload: makeFailurePayload(), - personaIdentities: { implementer: 'cascade-impl', reviewer: 'cascade-reviewer' }, + personaIdentities: mockPersonaIdentities, }; // Use up 3 attempts diff --git a/tests/unit/triggers/check-suite-success.test.ts b/tests/unit/triggers/check-suite-success.test.ts index d8c32553..ce670064 100644 --- a/tests/unit/triggers/check-suite-success.test.ts +++ b/tests/unit/triggers/check-suite-success.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CheckSuiteSuccessTrigger } from '../../../src/triggers/github/check-suite-success.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; +import { mockPersonaIdentities } from '../../helpers/mockPersonas.js'; vi.mock('../../../src/github/client.js', () => ({ githubClient: { @@ -21,27 +23,7 @@ import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRep describe('CheckSuiteSuccessTrigger', () => { const trigger = new CheckSuiteSuccessTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; - - const mockPersonaIdentities = { - implementer: 'cascade-impl', - reviewer: 'cascade-reviewer', - }; + const mockProject = createMockProject(); const makeCheckSuitePayload = (overrides: Record = {}) => ({ action: 'completed', @@ -58,7 +40,6 @@ describe('CheckSuiteSuccessTrigger', () => { }); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); @@ -538,34 +519,31 @@ describe('CheckSuiteSuccessTrigger', () => { describe('reviewTrigger mode-aware behavior', () => { /** Project with only externalPrs enabled */ - const mockProjectExternalOnly = { - ...mockProject, + const mockProjectExternalOnly = createMockProject({ github: { triggers: { reviewTrigger: { ownPrsOnly: false, externalPrs: true, onReviewRequested: false }, }, }, - }; + }); /** Project with both ownPrsOnly and externalPrs enabled */ - const mockProjectBothModes = { - ...mockProject, + const mockProjectBothModes = createMockProject({ github: { triggers: { reviewTrigger: { ownPrsOnly: true, externalPrs: true, onReviewRequested: false }, }, }, - }; + }); /** Project with all modes disabled */ - const mockProjectNoModes = { - ...mockProject, + const mockProjectNoModes = createMockProject({ github: { triggers: { reviewTrigger: { ownPrsOnly: false, externalPrs: false, onReviewRequested: false }, }, }, - }; + }); it('does not match when all modes are disabled', () => { const ctx: TriggerContext = { @@ -677,7 +655,6 @@ describe('CheckSuiteSuccessTrigger', () => { expect(implResult).not.toBeNull(); // External PR - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); setupMocks('external-contributor'); const extCtx: TriggerContext = { diff --git a/tests/unit/triggers/debug-runner.test.ts b/tests/unit/triggers/debug-runner.test.ts index b7106c00..c94f7a31 100644 --- a/tests/unit/triggers/debug-runner.test.ts +++ b/tests/unit/triggers/debug-runner.test.ts @@ -49,26 +49,15 @@ import { } from '../../../src/triggers/shared/debug-status.js'; const mockPMProvider = { addComment: vi.fn() }; -import type { CascadeConfig, ProjectConfig } from '../../../src/types/index.js'; - -const mockProject = { - id: 'test-project', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board-1', - lists: { splitting: 'l1', planning: 'l2', todo: 'l3' }, - labels: {}, - }, -} as unknown as ProjectConfig; +import type { CascadeConfig } from '../../../src/types/index.js'; +import { createMockProject } from '../../helpers/factories.js'; + +const mockProject = createMockProject({ id: 'test-project' }); const mockConfig = {} as CascadeConfig; describe('triggerDebugAnalysis', () => { beforeEach(() => { - vi.clearAllMocks(); vi.mocked(getPMProvider).mockReturnValue(mockPMProvider as unknown as PMProvider); }); @@ -315,10 +304,6 @@ describe('triggerDebugAnalysis', () => { }); describe('parseDebugOutput (via triggerDebugAnalysis)', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('parses all structured sections from markdown', async () => { vi.mocked(getRunById).mockResolvedValue({ id: 'run-1', diff --git a/tests/unit/triggers/debug-trigger.test.ts b/tests/unit/triggers/debug-trigger.test.ts index 31794325..c9f31866 100644 --- a/tests/unit/triggers/debug-trigger.test.ts +++ b/tests/unit/triggers/debug-trigger.test.ts @@ -19,10 +19,6 @@ import { import { shouldTriggerDebug } from '../../../src/triggers/shared/debug-trigger.js'; describe('shouldTriggerDebug', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it('returns null when runId is undefined', async () => { const result = await shouldTriggerDebug(undefined); expect(result).toBeNull(); diff --git a/tests/unit/triggers/github-pr-comment-mention.test.ts b/tests/unit/triggers/github-pr-comment-mention.test.ts index 7c876b96..2e703052 100644 --- a/tests/unit/triggers/github-pr-comment-mention.test.ts +++ b/tests/unit/triggers/github-pr-comment-mention.test.ts @@ -32,31 +32,27 @@ vi.mock('../../../src/db/repositories/prWorkItemsRepository.js', () => ({ import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRepository.js'; import { PRCommentMentionTrigger } from '../../../src/triggers/github/pr-comment-mention.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; +import { + IMPLEMENTER_USERNAME, + REVIEWER_USERNAME, + mockPersonaIdentities, +} from '../../helpers/mockPersonas.js'; -const IMPLEMENTER_USERNAME = 'cascade-impl'; -const REVIEWER_USERNAME = 'cascade-reviewer'; const HUMAN_USERNAME = 'alice-human'; const CARD_SHORT_ID = 'abc123card'; const PR_BODY_WITH_CARD = `Fixes https://trello.com/c/${CARD_SHORT_ID}/my-card`; const PR_BODY_NO_CARD = 'This PR has no Trello card link'; -const mockProject = { +const mockProject = createMockProject({ id: 'test-project', name: 'Test Project', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', trello: { boardId: 'board-123', lists: { splitting: 'b', planning: 'p', todo: 't' }, labels: {}, }, -} as TriggerContext['project']; - -const mockPersonaIdentities = { - implementer: IMPLEMENTER_USERNAME, - reviewer: REVIEWER_USERNAME, -}; +}); /** Build an issue_comment.created payload (PR conversation comment) */ function buildIssueCommentPayload( diff --git a/tests/unit/triggers/github-utils.test.ts b/tests/unit/triggers/github-utils.test.ts index bf63adad..38b6bb9e 100644 --- a/tests/unit/triggers/github-utils.test.ts +++ b/tests/unit/triggers/github-utils.test.ts @@ -181,7 +181,6 @@ describe('requireWorkItemId', () => { describe('resolveWorkItemId', () => { beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); diff --git a/tests/unit/triggers/label-added.test.ts b/tests/unit/triggers/label-added.test.ts index 5821fa86..d8f7be02 100644 --- a/tests/unit/triggers/label-added.test.ts +++ b/tests/unit/triggers/label-added.test.ts @@ -1,5 +1,6 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; // Mocks required for PM integration registration (pm/index.js side-effect) vi.mock('../../../src/config/provider.js', () => ({ @@ -38,12 +39,7 @@ describe('ReadyToProcessLabelTrigger', () => { const trigger = new ReadyToProcessLabelTrigger(); const mockGetCard = vi.mocked(trelloClient.getCard); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', + const mockProject = createMockProject({ trello: { boardId: 'board123', lists: { @@ -55,10 +51,6 @@ describe('ReadyToProcessLabelTrigger', () => { readyToProcess: 'ready-label-id', }, }, - }; - - beforeEach(() => { - vi.clearAllMocks(); }); describe('matches', () => { diff --git a/tests/unit/triggers/manual-runner.test.ts b/tests/unit/triggers/manual-runner.test.ts index b3264843..ea9aa26f 100644 --- a/tests/unit/triggers/manual-runner.test.ts +++ b/tests/unit/triggers/manual-runner.test.ts @@ -61,7 +61,6 @@ const mockConfig = {} as CascadeConfig; describe('triggerManualRun', () => { beforeEach(() => { - vi.clearAllMocks(); clearTriggerTracking(); }); @@ -223,7 +222,6 @@ describe('triggerManualRun', () => { describe('triggerRetryRun', () => { beforeEach(() => { - vi.clearAllMocks(); clearTriggerTracking(); }); diff --git a/tests/unit/triggers/pr-merged.test.ts b/tests/unit/triggers/pr-merged.test.ts index f7f8e9d0..af904875 100644 --- a/tests/unit/triggers/pr-merged.test.ts +++ b/tests/unit/triggers/pr-merged.test.ts @@ -52,6 +52,7 @@ import '../../../src/pm/index.js'; import { PRMergedTrigger } from '../../../src/triggers/github/pr-merged.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRepository.js'; import { githubClient } from '../../../src/github/client.js'; @@ -59,12 +60,7 @@ import { githubClient } from '../../../src/github/client.js'; describe('PRMergedTrigger', () => { const trigger = new PRMergedTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', + const mockProject = createMockProject({ trello: { boardId: 'board123', lists: { @@ -75,10 +71,9 @@ describe('PRMergedTrigger', () => { }, labels: {}, }, - }; + }); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); diff --git a/tests/unit/triggers/pr-opened.test.ts b/tests/unit/triggers/pr-opened.test.ts index 212d7219..4ed09605 100644 --- a/tests/unit/triggers/pr-opened.test.ts +++ b/tests/unit/triggers/pr-opened.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PROpenedTrigger } from '../../../src/triggers/github/pr-opened.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; vi.mock('../../../src/db/repositories/prWorkItemsRepository.js', () => ({ lookupWorkItemForPR: vi.fn(), @@ -10,49 +11,30 @@ import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRep describe('PROpenedTrigger', () => { const trigger = new PROpenedTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; + const mockProject = createMockProject(); /** Project with prOpened + externalPrs enabled (most common config for external PR review) */ - const mockProjectWithPrOpenedEnabled = { - ...mockProject, + const mockProjectWithPrOpenedEnabled = createMockProject({ github: { triggers: { prOpened: true, reviewTrigger: { externalPrs: true } }, }, - }; + }); /** Project with prOpened + ownPrsOnly (fires on implementer-authored PRs) */ - const mockProjectWithOwnPrsOnly = { - ...mockProject, + const mockProjectWithOwnPrsOnly = createMockProject({ github: { triggers: { prOpened: true, reviewTrigger: { ownPrsOnly: true } }, }, - }; + }); /** Project with prOpened + both modes (fires on all PRs) */ - const mockProjectWithBothModes = { - ...mockProject, + const mockProjectWithBothModes = createMockProject({ github: { triggers: { prOpened: true, reviewTrigger: { ownPrsOnly: true, externalPrs: true } }, }, - }; + }); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); diff --git a/tests/unit/triggers/pr-ready-to-merge.test.ts b/tests/unit/triggers/pr-ready-to-merge.test.ts index d47736a5..0dbc403a 100644 --- a/tests/unit/triggers/pr-ready-to-merge.test.ts +++ b/tests/unit/triggers/pr-ready-to-merge.test.ts @@ -53,6 +53,7 @@ import '../../../src/pm/index.js'; import { PRReadyToMergeTrigger } from '../../../src/triggers/github/pr-ready-to-merge.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRepository.js'; import { githubClient } from '../../../src/github/client.js'; @@ -60,12 +61,7 @@ import { githubClient } from '../../../src/github/client.js'; describe('PRReadyToMergeTrigger', () => { const trigger = new PRReadyToMergeTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', + const mockProject = createMockProject({ trello: { boardId: 'board123', lists: { @@ -76,10 +72,9 @@ describe('PRReadyToMergeTrigger', () => { }, labels: {}, }, - }; + }); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); diff --git a/tests/unit/triggers/pr-review-submitted.test.ts b/tests/unit/triggers/pr-review-submitted.test.ts index fa2a6a06..fb2301e6 100644 --- a/tests/unit/triggers/pr-review-submitted.test.ts +++ b/tests/unit/triggers/pr-review-submitted.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PRReviewSubmittedTrigger } from '../../../src/triggers/github/pr-review-submitted.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; +import { mockPersonaIdentities } from '../../helpers/mockPersonas.js'; vi.mock('../../../src/db/repositories/prWorkItemsRepository.js', () => ({ lookupWorkItemForPR: vi.fn(), @@ -11,31 +13,10 @@ describe('PRReviewSubmittedTrigger', () => { const trigger = new PRReviewSubmittedTrigger(); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - }; - - const mockPersonaIdentities = { - implementer: 'cascade-impl', - reviewer: 'cascade-reviewer', - }; + const mockProject = createMockProject(); const makeReviewPayload = (overrides: Record = {}) => ({ action: 'submitted', diff --git a/tests/unit/triggers/review-requested.test.ts b/tests/unit/triggers/review-requested.test.ts index b01b38a3..0cc1d745 100644 --- a/tests/unit/triggers/review-requested.test.ts +++ b/tests/unit/triggers/review-requested.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ReviewRequestedTrigger } from '../../../src/triggers/github/review-requested.js'; import type { TriggerContext } from '../../../src/triggers/types.js'; +import { createMockProject } from '../../helpers/factories.js'; +import { mockPersonaIdentities } from '../../helpers/mockPersonas.js'; vi.mock('../../../src/db/repositories/prWorkItemsRepository.js', () => ({ lookupWorkItemForPR: vi.fn(), @@ -10,49 +12,25 @@ import { lookupWorkItemForPR } from '../../../src/db/repositories/prWorkItemsRep describe('ReviewRequestedTrigger', () => { const trigger = new ReviewRequestedTrigger(); - const mockProject = { - id: 'test', - name: 'Test', - repo: 'owner/repo', - baseBranch: 'main', - branchPrefix: 'feature/', - trello: { - boardId: 'board123', - lists: { - splitting: 'splitting-list-id', - planning: 'planning-list-id', - todo: 'todo-list-id', - }, - labels: {}, - }, - // Review-requested is opt-in, default disabled - }; + const mockProject = createMockProject(); /** Project with reviewRequested trigger explicitly enabled (legacy style) */ - const mockProjectWithReviewRequested = { - ...mockProject, + const mockProjectWithReviewRequested = createMockProject({ github: { triggers: { reviewRequested: true }, }, - }; + }); /** Project with new structured reviewTrigger.onReviewRequested enabled */ - const mockProjectWithOnReviewRequested = { - ...mockProject, + const mockProjectWithOnReviewRequested = createMockProject({ github: { triggers: { reviewTrigger: { ownPrsOnly: false, externalPrs: false, onReviewRequested: true }, }, }, - }; - - const mockPersonaIdentities = { - implementer: 'cascade-impl', - reviewer: 'cascade-reviewer', - }; + }); beforeEach(() => { - vi.clearAllMocks(); vi.mocked(lookupWorkItemForPR).mockResolvedValue(null); }); diff --git a/tests/unit/utils/cascadeEnv.test.ts b/tests/unit/utils/cascadeEnv.test.ts index 1fe3f387..cd0a9d7d 100644 --- a/tests/unit/utils/cascadeEnv.test.ts +++ b/tests/unit/utils/cascadeEnv.test.ts @@ -22,7 +22,6 @@ describe('cascadeEnv', () => { const originalEnv = process.env; beforeEach(() => { - vi.clearAllMocks(); process.env = { ...originalEnv }; }); diff --git a/tests/unit/utils/lifecycle.test.ts b/tests/unit/utils/lifecycle.test.ts index 37e50675..20b1bedd 100644 --- a/tests/unit/utils/lifecycle.test.ts +++ b/tests/unit/utils/lifecycle.test.ts @@ -26,7 +26,6 @@ const mockFlush = vi.mocked(flush); describe('lifecycle', () => { beforeEach(() => { - vi.clearAllMocks(); vi.useFakeTimers(); vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); }); diff --git a/tests/unit/utils/llmEnv.test.ts b/tests/unit/utils/llmEnv.test.ts index 9126a45e..7ccdec69 100644 --- a/tests/unit/utils/llmEnv.test.ts +++ b/tests/unit/utils/llmEnv.test.ts @@ -19,7 +19,6 @@ import { injectLlmApiKeys } from '../../../src/utils/llmEnv.js'; const mockGetOrgCredential = vi.mocked(getOrgCredential); beforeEach(() => { - vi.clearAllMocks(); // Clean up the env var before each test Reflect.deleteProperty(process.env, 'OPENROUTER_API_KEY'); }); diff --git a/tests/unit/utils/llmLogging.test.ts b/tests/unit/utils/llmLogging.test.ts index 3da19850..971e13da 100644 --- a/tests/unit/utils/llmLogging.test.ts +++ b/tests/unit/utils/llmLogging.test.ts @@ -23,10 +23,6 @@ import { } from '../../../src/utils/llmLogging.js'; describe('llmLogging', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('formatCallNumber', () => { it('pads single digit', () => { expect(formatCallNumber(1)).toBe('0001'); diff --git a/tests/unit/utils/repo.test.ts b/tests/unit/utils/repo.test.ts index a773e6bf..0bd327e2 100644 --- a/tests/unit/utils/repo.test.ts +++ b/tests/unit/utils/repo.test.ts @@ -72,7 +72,6 @@ describe('repo utils', () => { const originalEnv = process.env; beforeEach(() => { - vi.clearAllMocks(); process.env = { ...originalEnv }; }); diff --git a/tests/unit/utils/safeOperation.test.ts b/tests/unit/utils/safeOperation.test.ts index 1d0490a7..d9390840 100644 --- a/tests/unit/utils/safeOperation.test.ts +++ b/tests/unit/utils/safeOperation.test.ts @@ -10,10 +10,6 @@ vi.mock('../../../src/utils/logging.js', () => ({ import { logger } from '../../../src/utils/logging.js'; describe('safeOperation', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe('safeOperation', () => { it('returns result on success', async () => { const result = await safeOperation(() => Promise.resolve('hello'), { diff --git a/tests/unit/utils/squintDb.test.ts b/tests/unit/utils/squintDb.test.ts index 118b624f..88773abd 100644 --- a/tests/unit/utils/squintDb.test.ts +++ b/tests/unit/utils/squintDb.test.ts @@ -27,7 +27,6 @@ describe('squintDb', () => { const originalEnv = process.env; beforeEach(() => { - vi.clearAllMocks(); process.env = { ...originalEnv }; }); diff --git a/tests/unit/utils/webhookLogger.test.ts b/tests/unit/utils/webhookLogger.test.ts index 0fa03d78..3e837e1e 100644 --- a/tests/unit/utils/webhookLogger.test.ts +++ b/tests/unit/utils/webhookLogger.test.ts @@ -29,7 +29,6 @@ const sampleInput: WebhookLogInput = { }; beforeEach(() => { - vi.clearAllMocks(); vi.useFakeTimers(); mockInsertWebhookLog.mockResolvedValue(undefined); mockPruneWebhookLogs.mockResolvedValue(undefined); diff --git a/vitest.config.ts b/vitest.config.ts index 72e00e78..16ebd3e2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,8 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + clearMocks: true, + unstubEnvs: true, coverage: { provider: 'v8', reporter: ['text', 'lcov', 'html'], diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 00000000..cfd71e4f --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,24 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace([ + { + extends: './vitest.config.ts', + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + setupFiles: ['./tests/setup.ts'], + }, + }, + { + extends: './vitest.config.ts', + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + setupFiles: ['./tests/integration/setup.ts'], + testTimeout: 30_000, + hookTimeout: 30_000, + pool: 'forks', + poolOptions: { forks: { singleFork: true } }, + }, + }, +]); diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index e03ca409..1ad15db1 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -1661,12 +1661,6 @@ export function PMWizard({ Create Webhook -

- Callback URL:{' '} - - {callbackBaseUrl}/{state.provider === 'trello' ? 'trello' : 'jira'}/webhook - -

{createWebhookMutation.isError && (

{createWebhookMutation.error.message}

)} From f8cce4effd87bd3e2f0a522e9bfc51a3ae875df5 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Wed, 25 Feb 2026 11:43:05 +0000 Subject: [PATCH 2/2] fix(ci): run integration tests on PRs Remove the `if: github.event_name == 'push'` guard from the integration-tests job so it also runs on pull_request events. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 745cf9ad..321e0347 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,6 @@ jobs: integration-tests: runs-on: ubuntu-latest - if: github.event_name == 'push' services: postgres: