Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ if [ "$BRANCH" = "main" ]; then
exit 1
fi

npm run lint
# Run lint-staged (includes linting and formatting on changed files)
npx lint-staged

# Run typecheck on the entire project
npm run typecheck

# Run tests with coverage to catch any coverage threshold violations
npm run test:unit -- --coverage
23 changes: 23 additions & 0 deletions .opencode/docs/DEVELOPMENT_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
- **Pure Logic**: Keep timer logic in `TimerStateMachine` and verify with unit tests in `src/__tests__/`.
- **Debugging**: Use `Shift+D` for fast-forwarding timers for quick testing of transitions.

## Testing Guidelines - AI-First DAMP Principles

### Test Structure
- **DAMP over DRY**: Tests should be Descriptive And Meaningful, not overly DRY. Favor readability over code reuse.
- **AAA Pattern**: Structure tests with clear Arrange/Act/Assert phases using comments when helpful.
- **Factory Functions**: Use centralized factories from `src/__tests__/utils/factories.ts` for creating test data:
- `createMockState()` - For TimerState objects
- `createMockConfig()` - For TimerConfig objects
- `createMockSettings()` - For Settings objects
- `createQuickTestConfig()` - For unit tests with short durations

### Mock Management
- **Centralized Setup**: Use utilities from `src/__tests__/utils/mocks.ts` for common mocking patterns.
- **Event Helpers**: Use `createKeyEvent()`, `createMouseEvent()`, and `createResizeEvent()` for input testing.
- **Audio Mocks**: Use `createMockChildProcess()` for audio player tests.
- **Top-Level Mocks**: Keep `jest.mock()` calls at the top level of test files for proper hoisting.

### AI-First Considerations
- **Consistent Patterns**: Follow established patterns so AI tools can easily extend tests.
- **Clear Intent**: Make test purpose obvious through naming and structure.
- **Minimal Boilerplate**: Use utilities to reduce repetitive setup code.
- **Type Safety**: Ensure all test helpers are properly typed.

## Debugging Workflow

- When investigating CI/CD failures, always start by checking the output of the latest GitHub Actions run using the `gh` CLI tool before attempting to reproduce the issue locally. For example: `gh run view --log`.
44 changes: 44 additions & 0 deletions _plans/active/refactor-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Test Refactoring Strategy & Implementation Guide

## Objective
Refactor the test suite in `src/__tests__/` to align with AI-first and DAMP (Descriptive and Meaningful Phrases) principles. The goal is to reduce file size, abstract repetitive setup/mocking logic, and preserve the readability of the "Act" and "Assert" phases.

## Phase 1: Establish the Testing Utility Layer
Instead of repeating `jest.mock()` or setting up default state objects in every file, we will create a centralized utility layer.

### 1. Create `src/__tests__/utils/factories.ts`
**Goal:** Abstract the creation of standard data objects.
**Implementation Steps:**
* Create a `createMockState(overrides?: Partial<State>)` function to generate default state machine objects used by `stateMachine.test.ts` and `app.test.ts`[cite: 3].
* Create a `createMockConfig(overrides?: Partial<Config>)` function to generate configurations used by `config.test.ts`[cite: 3].

### 2. Create `src/__tests__/utils/mocks.ts`
**Goal:** Abstract repetitive side-effect mocks (audio, console, timers).
**Implementation Steps:**
* Move `jest.mock()` calls for external dependencies (e.g., terminal UI renderers, file system access) here.
* Create helper functions like `setupAudioMocks()` to wrap the setup of dummy audio players used in `audio.test.ts` and `player.test.ts`[cite: 3].
* Create `setupInputMocks()` to simulate stdin/mouse events for `input.test.ts` and `mouse.test.ts`[cite: 3].

## Phase 2: Refactor Core Test Suites

### 1. Refactor Logic & State (`stateMachine.test.ts`, `app.test.ts`)
* **Action:** Replace manual state initialization in `beforeEach` or inside test blocks with the `createMockState()` factory.
* **Validation:** Ensure each test block clearly shows the *Arrange* (using the factory), *Act* (triggering a transition), and *Assert* (checking the new state).

### 2. Refactor Side Effects (`audio.test.ts`, `player.test.ts`, `update.test.ts`)
* **Action:** Import and invoke `setupAudioMocks()` or `setupNetworkMocks()` at the top of the file.
* **Validation:** Remove all inline `jest.spyOn()` and `jest.mock()` boilerplate from these files[cite: 3] unless a test requires a highly specific, one-off override.

### 3. Refactor UI & Input (`ui.test.ts`, `input.test.ts`, `mouse.test.ts`)
* **Action:** Utilize the new input mocks to simulate keystrokes or clicks.
* **Validation:** These files[cite: 3] should read like user stories (e.g., "When user presses space, timer pauses"), hiding the messy stream manipulation behind the utility layer.

## Phase 3: Update AI Context Guidelines
To ensure AI tools (like GitHub Copilot or Gemini) maintain this new standard, update the `.opencode/docs/DEVELOPMENT_GUIDELINES.md`[cite: 3] file.

**Add the following section to `DEVELOPMENT_GUIDELINES.md`:**
```markdown
### Writing Tests (AI-First DAMP Principles)
* **Do not duplicate setup logic:** Always use the factories and mocks located in `src/__tests__/utils/`.
* **Keep tests DAMP:** The logic inside `it()` or `test()` blocks must clearly show the Arrange, Act, and Assert steps.
* **Abstract Mocks, not Assertions:** Hide `jest.mock()` and boilerplate configuration in the utils folder, but keep assertions explicitly visible in the test file.
16 changes: 14 additions & 2 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverage: !!process.env.CI || process.argv.includes('--coverage'),
collectCoverageFrom: ['src/**/*.ts', '!src/cli.ts'],
coverageThreshold: {
global: {
branches: 73.5,
functions: 90,
lines: 80,
statements: 80
},
// Per-file thresholds to catch individual files with poor coverage
'./src/**/!(*.test|*.spec|cli).ts': {
branches: 60,
functions: 60,
lines: 60,
statements: 60
}
},
transform: {
'^.+\\.tsx?$': ['ts-jest', { useESM: true }]
'^.+\\.tsx?$': ['ts-jest', { useESM: true }],
'^.+\\.js$': ['ts-jest', { useESM: true }]
},
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
}
},
transformIgnorePatterns: [
'node_modules/(?!(env-paths|is-safe-filename)/)'
]
};
Loading
Loading