docs: enhance TDD guidelines in CLAUDE.md and INSTRUCTIONS.md
This commit is contained in:
44
CLAUDE.md
44
CLAUDE.md
@@ -6,6 +6,29 @@ right knowledge.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Default Workflow: Test-Driven Development (TDD)
|
||||||
|
|
||||||
|
> **Every implementation step in this project follows TDD. This is non-negotiable.**
|
||||||
|
|
||||||
|
The cycle for every step is:
|
||||||
|
|
||||||
|
| Phase | Action | Gate |
|
||||||
|
|------------|-------------------------------------------------------------------------|------------------------------------|
|
||||||
|
| **Red** | Write test file(s) for the step. Run the test suite. | New tests must **fail**. |
|
||||||
|
| **Green** | Write the minimum implementation to make all tests pass. | All tests must **pass**. |
|
||||||
|
| **Refactor** | Clean up the implementation. | Tests must stay **green**. |
|
||||||
|
| **Done** | Mark step ✅ only when the full build is green. | `./gradlew build` / `npm run build && npm run test` |
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Never write implementation code before the test file exists and the tests fail.
|
||||||
|
- Never mark a step Done unless the full test suite passes.
|
||||||
|
- Test method names (Kotlin/JUnit): `should_[expectedBehavior]_when_[condition]`.
|
||||||
|
- Backend mocking: **MockK** only (not Mockito).
|
||||||
|
- Backend integration tests: `@SpringBootTest` with **H2 in-memory** database.
|
||||||
|
- Frontend tests: **Vitest** + **React Testing Library**, mocked Axios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
- **Type:** Monorepo (backend + frontend in the same repository)
|
- **Type:** Monorepo (backend + frontend in the same repository)
|
||||||
@@ -361,17 +384,32 @@ docker compose down
|
|||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
|
> **This project follows strict TDD.** For every implementation step, tests are written
|
||||||
|
> first (Red), then the implementation is added until all tests pass (Green), then code is
|
||||||
|
> cleaned up (Refactor). Never write implementation code before the tests exist and fail.
|
||||||
|
|
||||||
|
### TDD Workflow (apply to every step)
|
||||||
|
1. **Red** — Write the test file(s). Run `./gradlew test` (backend) or `npm run test`
|
||||||
|
(frontend) and confirm the new tests **fail** (compile errors are acceptable at this stage).
|
||||||
|
2. **Green** — Write the minimum implementation needed to make all tests pass.
|
||||||
|
3. **Refactor** — Clean up the implementation while keeping tests green.
|
||||||
|
4. A step is only ✅ Done when `./gradlew build` (backend) or
|
||||||
|
`npm run build && npm run test` (frontend) is fully green.
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- Every service class must have a corresponding unit test class.
|
- Every service class must have a corresponding unit test class **written before the service**.
|
||||||
- Use **MockK** for mocking (not Mockito).
|
- Use **MockK** for mocking (not Mockito).
|
||||||
- Integration tests use `@SpringBootTest` and an **H2 in-memory** database.
|
- Integration tests use `@SpringBootTest` and an **H2 in-memory** database.
|
||||||
- Test method names follow: `should_[expectedBehavior]_when_[condition]`.
|
- Test method names follow: `should_[expectedBehavior]_when_[condition]`.
|
||||||
- Minimum 80% code coverage for service classes.
|
- Minimum 80% code coverage for service classes.
|
||||||
|
- Test files live in `src/test/kotlin/` mirroring the `src/main/kotlin/` package structure.
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- Every page component must have at least a smoke test (renders without crashing).
|
- Every page component must have at least a smoke test (renders without crashing),
|
||||||
|
**written before the component**.
|
||||||
- API layer functions must be tested with mocked Axios responses.
|
- API layer functions must be tested with mocked Axios responses.
|
||||||
- Use **Vitest** as the test runner and **React Testing Library** for component tests.
|
- Use **Vitest** as the test runner and **React Testing Library** for component tests.
|
||||||
|
- Test files live in `src/__tests__/` mirroring the `src/` structure.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -458,6 +496,8 @@ BODY:
|
|||||||
- Branch naming: `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`
|
- Branch naming: `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`
|
||||||
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `chore:`, `docs:`, `test:`
|
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `chore:`, `docs:`, `test:`
|
||||||
- Scope your commits: `feat(backend):`, `feat(frontend):`, `chore(docker):`
|
- Scope your commits: `feat(backend):`, `feat(frontend):`, `chore(docker):`
|
||||||
|
- **TDD commit order per step:** first `test(<scope>): add failing tests for <step>`, then
|
||||||
|
`feat(<scope>): implement <step> — all tests passing`.
|
||||||
- PRs require all CI checks to pass before merging.
|
- PRs require all CI checks to pass before merging.
|
||||||
- Never commit directly to `main`.
|
- Never commit directly to `main`.
|
||||||
|
|
||||||
|
|||||||
474
INSTRUCTIONS.md
474
INSTRUCTIONS.md
@@ -27,9 +27,16 @@ employee is an AI-powered entity that:
|
|||||||
|
|
||||||
- Follow steps **in order** — each step builds on the previous one.
|
- Follow steps **in order** — each step builds on the previous one.
|
||||||
- At the start of each AI session, share the contents of `CLAUDE.md` for context.
|
- At the start of each AI session, share the contents of `CLAUDE.md` for context.
|
||||||
|
- **Each step follows TDD: write the tests first, then implement until all tests pass.**
|
||||||
- After completing each step, mark it ✅ **Done** and note any decisions made.
|
- After completing each step, mark it ✅ **Done** and note any decisions made.
|
||||||
- If anything changes (new library, schema change, etc.), update `CLAUDE.md` too.
|
- If anything changes (new library, schema change, etc.), update `CLAUDE.md` too.
|
||||||
|
|
||||||
|
### TDD Workflow Per Step
|
||||||
|
1. **Red** — AI writes the test file(s) for the step. `./gradlew test` or `npm run test` must fail (or show the new tests failing).
|
||||||
|
2. **Green** — AI writes the implementation until all tests pass.
|
||||||
|
3. **Refactor** — Clean up while keeping tests green.
|
||||||
|
4. Mark the step ✅ Done only when `./gradlew build` (backend) or `npm run build && npm run test` (frontend) is fully green.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Progress Tracker
|
## Progress Tracker
|
||||||
@@ -48,10 +55,11 @@ employee is an AI-powered entity that:
|
|||||||
| 9 | REST Controllers & DTOs | ⬜ Pending |
|
| 9 | REST Controllers & DTOs | ⬜ Pending |
|
||||||
| 10 | Authentication (JWT login) | ⬜ Pending |
|
| 10 | Authentication (JWT login) | ⬜ Pending |
|
||||||
| 11 | React Frontend | ⬜ Pending |
|
| 11 | React Frontend | ⬜ Pending |
|
||||||
| 12 | Unit & Integration Tests | ⬜ Pending |
|
| 12 | Docker Compose (dev + prod) | ⬜ Pending |
|
||||||
| 13 | Docker Compose (dev + prod) | ⬜ Pending |
|
| 13 | All-in-one Docker image | ⬜ Pending |
|
||||||
| 14 | All-in-one Docker image | ⬜ Pending |
|
| 14 | CI/CD — GitHub Actions + Docker Hub | ⬜ Pending |
|
||||||
| 15 | CI/CD — GitHub Actions + Docker Hub | ⬜ Pending |
|
|
||||||
|
> ⚠️ **Steps 2–11 each follow TDD.** The AI writes failing tests first, then implements until green. See "TDD Workflow Per Step" above.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -219,14 +227,35 @@ A record of every AI generation + email send attempt.
|
|||||||
| `error_message` | String | Nullable — stores failure reason if FAILED |
|
| `error_message` | String | Nullable — stores failure reason if FAILED |
|
||||||
| `dispatched_at` | LocalDateTime | When the dispatch was triggered |
|
| `dispatched_at` | LocalDateTime | When the dispatch was triggered |
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create the two JPA entities: `VirtualEntity` and `DispatchLog`.
|
|
||||||
> Place them in the `model/` package. Use UUIDs as primary keys and proper JPA annotations.
|
**Test file:** `src/test/kotlin/com/condado/newsletter/model/EntityMappingTest.kt`
|
||||||
> `DispatchLog` has a `@ManyToOne` relationship to `VirtualEntity`."
|
|
||||||
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_persistVirtualEntity_when_allFieldsProvided
|
||||||
|
// should_enforceUniqueEmail_when_duplicateEmailInserted
|
||||||
|
// should_persistDispatchLog_when_linkedToVirtualEntity
|
||||||
|
// should_setCreatedAtAutomatically_when_virtualEntitySaved
|
||||||
|
// should_defaultActiveToTrue_when_virtualEntityCreated
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write an integration test class `EntityMappingTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/model/`. Use `@DataJpaTest` with H2. Write tests
|
||||||
|
> that verify: VirtualEntity persists all fields, email is unique, DispatchLog links to
|
||||||
|
> VirtualEntity, createdAt is auto-set, active defaults to true. Do NOT create the entities
|
||||||
|
> yet — tests should fail to compile."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Now create the two JPA entities `VirtualEntity` and `DispatchLog` in `model/`. Use UUIDs
|
||||||
|
> as primary keys and proper JPA annotations. `DispatchLog` has a `@ManyToOne` to
|
||||||
|
> `VirtualEntity`. Make the tests in `EntityMappingTest` pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `EntityMappingTest.kt` exists with all 5 tests.
|
||||||
- [ ] Both entity files exist in `src/main/kotlin/com/condado/newsletter/model/`.
|
- [ ] Both entity files exist in `src/main/kotlin/com/condado/newsletter/model/`.
|
||||||
- [ ] `./gradlew build` compiles cleanly.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] Tables are auto-created by Hibernate on startup (`ddl-auto: create-drop` in dev).
|
- [ ] Tables are auto-created by Hibernate on startup (`ddl-auto: create-drop` in dev).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -238,15 +267,34 @@ A record of every AI generation + email send attempt.
|
|||||||
| Repository | Entity | Custom queries needed |
|
| Repository | Entity | Custom queries needed |
|
||||||
|--------------------------|-----------------|----------------------------------------------------------|
|
|--------------------------|-----------------|----------------------------------------------------------|
|
||||||
| `VirtualEntityRepository`| `VirtualEntity` | `findAllByActiveTrue()`, `findByEmail()` |
|
| `VirtualEntityRepository`| `VirtualEntity` | `findAllByActiveTrue()`, `findByEmail()` |
|
||||||
| `DispatchLogRepository` | `DispatchLog` | `findAllByEntityId()`, `findTopByEntityIdOrderByDispatchedAtDesc()` |
|
| `DispatchLogRepository` | `DispatchLog` | `findAllByVirtualEntity()`, `findTopByVirtualEntityOrderByDispatchedAtDesc()` |
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create the two JPA repositories in the `repository/` package.
|
|
||||||
> Each must extend `JpaRepository` and include the custom query methods listed in the instructions."
|
**Test file:** `src/test/kotlin/com/condado/newsletter/repository/RepositoryTest.kt`
|
||||||
|
|
||||||
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_returnOnlyActiveEntities_when_findAllByActiveTrueCalled
|
||||||
|
// should_findEntityByEmail_when_emailExists
|
||||||
|
// should_returnEmptyOptional_when_emailNotFound
|
||||||
|
// should_returnAllLogsForEntity_when_findAllByVirtualEntityCalled
|
||||||
|
// should_returnMostRecentLog_when_findTopByVirtualEntityOrderByDispatchedAtDescCalled
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a `@DataJpaTest` class `RepositoryTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/repository/`. Test all custom query methods for
|
||||||
|
> both repositories using H2. Do NOT create the repositories yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `VirtualEntityRepository` and `DispatchLogRepository` in `repository/`, each
|
||||||
|
> extending `JpaRepository`, with the custom query methods needed to make `RepositoryTest` pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `RepositoryTest.kt` exists with all 5 tests.
|
||||||
- [ ] Both repository files exist in `src/main/kotlin/com/condado/newsletter/repository/`.
|
- [ ] Both repository files exist in `src/main/kotlin/com/condado/newsletter/repository/`.
|
||||||
- [ ] `./gradlew build` compiles cleanly.
|
- [ ] `./gradlew test` is green.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -277,16 +325,33 @@ data class EmailContext(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create the `EmailReaderService` class in `service/`. It must
|
|
||||||
> connect to an IMAP server using env vars (`IMAP_HOST`, `IMAP_PORT`, `MAIL_USERNAME`,
|
**Test file:** `src/test/kotlin/com/condado/newsletter/service/EmailReaderServiceTest.kt`
|
||||||
> `MAIL_PASSWORD`, `IMAP_INBOX_FOLDER`), fetch emails from the last N days, and return them
|
|
||||||
> as a list of `EmailContext` data objects sorted chronologically. Use Jakarta Mail."
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_returnEmailsSortedChronologically_when_multipleEmailsFetched
|
||||||
|
// should_returnEmptyList_when_imapConnectionFails
|
||||||
|
// should_filterEmailsOlderThanContextWindow_when_windowIs3Days
|
||||||
|
// should_stripHtml_when_emailBodyContainsHtmlTags
|
||||||
|
```
|
||||||
|
|
||||||
|
The IMAP `Session`/`Store` must be injected or overridable so tests can mock it with MockK.
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a MockK unit test class `EmailReaderServiceTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/service/`. Mock the Jakarta Mail `Store` and
|
||||||
|
> `Folder`. Write the 4 tests listed. Do NOT create the service yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `EmailContext` data class and `EmailReaderService` in `service/`. Use Jakarta Mail
|
||||||
|
> for IMAP. The Store must be injectable/mockable. Make all `EmailReaderServiceTest` tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
- [ ] `EmailReaderService.kt` exists in `service/`.
|
- [ ] `EmailReaderServiceTest.kt` exists with all 4 tests.
|
||||||
- [ ] `EmailContext.kt` data class exists.
|
- [ ] `EmailReaderService.kt` and `EmailContext.kt` exist in their packages.
|
||||||
- [ ] Service reads real emails when tested against a live IMAP account.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] Returns empty list (not exception) on connection failure.
|
- [ ] Returns empty list (not exception) on connection failure.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -308,15 +373,37 @@ It must be followed exactly, with the entity fields and email context filled in
|
|||||||
fun buildPrompt(entity: VirtualEntity, emailContext: List<EmailContext>): String
|
fun buildPrompt(entity: VirtualEntity, emailContext: List<EmailContext>): String
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create `PromptBuilderService` in `service/`. It must implement
|
|
||||||
> `buildPrompt(entity, emailContext)` following the prompt template defined in CLAUDE.md exactly.
|
**Test file:** `src/test/kotlin/com/condado/newsletter/service/PromptBuilderServiceTest.kt`
|
||||||
> This is the only class allowed to build prompt strings."
|
|
||||||
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_containEntityName_when_buildPromptCalled
|
||||||
|
// should_containEntityJobTitle_when_buildPromptCalled
|
||||||
|
// should_containEntityPersonality_when_buildPromptCalled
|
||||||
|
// should_containContextWindowDays_when_buildPromptCalled
|
||||||
|
// should_containEachEmailSenderAndSubject_when_emailContextProvided
|
||||||
|
// should_containFormatInstruction_when_buildPromptCalled // verifies SUBJECT:/BODY: instruction
|
||||||
|
// should_returnPromptWithNoEmails_when_emailContextIsEmpty
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a pure unit test class `PromptBuilderServiceTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/service/`. No mocks needed — just build a
|
||||||
|
> `VirtualEntity` and `List<EmailContext>` and assert the output string. Write all 7 tests.
|
||||||
|
> Do NOT create the service yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `PromptBuilderService` in `service/`. It must implement `buildPrompt(entity,
|
||||||
|
> emailContext)` following the prompt template in CLAUDE.md exactly. Make all
|
||||||
|
> `PromptBuilderServiceTest` tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `PromptBuilderServiceTest.kt` exists with all 7 tests.
|
||||||
- [ ] `PromptBuilderService.kt` exists in `service/`.
|
- [ ] `PromptBuilderService.kt` exists in `service/`.
|
||||||
- [ ] Output prompt matches the template from `CLAUDE.md` with fields correctly substituted.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] Unit test verifies the prompt structure.
|
- [ ] Output prompt matches the template from `CLAUDE.md` with all fields correctly substituted.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -331,8 +418,7 @@ fun buildPrompt(entity: VirtualEntity, emailContext: List<EmailContext>): String
|
|||||||
- Use the model configured in `OPENAI_MODEL` env var (default: `gpt-4o`).
|
- Use the model configured in `OPENAI_MODEL` env var (default: `gpt-4o`).
|
||||||
- Send the prompt as a `user` message.
|
- Send the prompt as a `user` message.
|
||||||
- Return the AI's response as a plain `String`.
|
- Return the AI's response as a plain `String`.
|
||||||
- Parse the response to extract `subject` and `body` — the AI should be instructed to return
|
- Parse the response to extract `subject` and `body` — the AI is instructed to return:
|
||||||
them in a structured format:
|
|
||||||
```
|
```
|
||||||
SUBJECT: <generated subject>
|
SUBJECT: <generated subject>
|
||||||
BODY:
|
BODY:
|
||||||
@@ -342,15 +428,34 @@ fun buildPrompt(entity: VirtualEntity, emailContext: List<EmailContext>): String
|
|||||||
|
|
||||||
**HTTP Client:** Use Spring's `RestClient` (Spring Boot 3.2+) — do NOT use WebClient or Feign.
|
**HTTP Client:** Use Spring's `RestClient` (Spring Boot 3.2+) — do NOT use WebClient or Feign.
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create `AiService` in `service/`. It must call the OpenAI
|
|
||||||
> Chat Completions API using Spring's `RestClient`, using the `OPENAI_API_KEY` and `OPENAI_MODEL`
|
**Test file:** `src/test/kotlin/com/condado/newsletter/service/AiServiceTest.kt`
|
||||||
> env vars. Return the AI's text response. Also implement a `parseResponse(raw: String)` method
|
|
||||||
> that extracts the subject and body from a response formatted as `SUBJECT: ...\nBODY:\n...`."
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_returnAiResponseText_when_apiCallSucceeds
|
||||||
|
// should_throwAiServiceException_when_apiReturnsError
|
||||||
|
// should_extractSubjectAndBody_when_responseIsWellFormatted
|
||||||
|
// should_throwParseException_when_responseIsMissingSubjectLine
|
||||||
|
// should_throwParseException_when_responseIsMissingBodySection
|
||||||
|
```
|
||||||
|
|
||||||
|
Mock `RestClient` with MockK. `parseResponse()` can be tested without mocking.
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a MockK unit test class `AiServiceTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/service/`. Mock Spring's `RestClient` chain.
|
||||||
|
> Write all 5 tests listed. Do NOT create the service yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `AiService` in `service/` using Spring `RestClient` for the OpenAI API. Implement
|
||||||
|
> `parseResponse(raw: String)` that extracts SUBJECT and BODY. Make all `AiServiceTest` tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `AiServiceTest.kt` exists with all 5 tests.
|
||||||
- [ ] `AiService.kt` exists in `service/`.
|
- [ ] `AiService.kt` exists in `service/`.
|
||||||
- [ ] Calling the service with a real API key returns a valid AI-generated email.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] `parseResponse()` correctly extracts subject and body.
|
- [ ] `parseResponse()` correctly extracts subject and body.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -367,21 +472,37 @@ fun send(from: String, to: List<String>, subject: String, body: String)
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `from` is the `VirtualEntity.email` (the fictional employee's address).
|
- `from` is the `VirtualEntity.email` (the fictional employee's address).
|
||||||
- `to` is the list of all real participants' emails (loaded from config or DB — TBD in Step 9).
|
- `to` is the list of all real participants' emails (from `app.recipients` config).
|
||||||
- `body` may be plain text or simple HTML — send as both `text/plain` and `text/html` (multipart).
|
- `body` may be plain text or simple HTML — send as both `text/plain` and `text/html` (multipart).
|
||||||
- Log every send attempt.
|
- Log every send attempt.
|
||||||
|
|
||||||
**Note on recipients:** For now, the list of recipients (the real friends' emails) will be stored
|
### TDD — Write tests first
|
||||||
as a comma-separated string in `application.yml` under `app.recipients`. This can be made dynamic later.
|
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
**Test file:** `src/test/kotlin/com/condado/newsletter/service/EmailSenderServiceTest.kt`
|
||||||
> "Using the CLAUDE.md context, create `EmailSenderService` in `service/`. It must use
|
|
||||||
> `JavaMailSender` to send emails. The `from` address comes from the entity, the `to` list
|
Tests to write (all should **fail** before implementation):
|
||||||
> comes from `app.recipients` in config, and the body is sent as both plain text and HTML."
|
```kotlin
|
||||||
|
// should_callJavaMailSenderWithCorrectFromAddress_when_sendCalled
|
||||||
|
// should_sendToAllRecipients_when_multipleRecipientsConfigured
|
||||||
|
// should_sendMultipartMessage_when_sendCalled // verifies both text/plain and text/html parts
|
||||||
|
// should_logSendAttempt_when_sendCalled
|
||||||
|
```
|
||||||
|
|
||||||
|
Mock `JavaMailSender` and `MimeMessage` with MockK.
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a MockK unit test class `EmailSenderServiceTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/service/`. Mock `JavaMailSender`. Write all 4 tests.
|
||||||
|
> Do NOT create the service yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `EmailSenderService` in `service/` using `JavaMailSender`. Send emails as multipart
|
||||||
|
> (text/plain + text/html). Make all `EmailSenderServiceTest` tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `EmailSenderServiceTest.kt` exists with all 4 tests.
|
||||||
- [ ] `EmailSenderService.kt` exists in `service/`.
|
- [ ] `EmailSenderService.kt` exists in `service/`.
|
||||||
- [ ] Email is received in a real inbox when tested with valid SMTP credentials.
|
- [ ] `./gradlew test` is green.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -392,29 +513,47 @@ as a comma-separated string in `application.yml` under `app.recipients`. This ca
|
|||||||
**Class:** `EntityScheduler` in `scheduler/` package.
|
**Class:** `EntityScheduler` in `scheduler/` package.
|
||||||
|
|
||||||
**Approach:**
|
**Approach:**
|
||||||
- Spring's `@Scheduled` with a fixed-rate tick (every 60 seconds) checks which entities are due.
|
- Use `SchedulingConfigurer` to register a cron task per active entity on startup.
|
||||||
- Use a `ScheduledTaskRegistrar` (or dynamic scheduling) to register a task per entity using its
|
- A `@Scheduled(fixedRate = 60_000)` refresh method re-reads entities and re-registers tasks
|
||||||
`schedule_cron` expression.
|
when entities are added/updated.
|
||||||
- When triggered, orchestrate the full pipeline:
|
- When triggered, orchestrate the full pipeline:
|
||||||
1. Read emails via `EmailReaderService` (using `entity.contextWindowDays`).
|
1. Read emails via `EmailReaderService` (using `entity.contextWindowDays`).
|
||||||
2. Build prompt via `PromptBuilderService`.
|
2. Build prompt via `PromptBuilderService`.
|
||||||
3. Call AI via `AiService`.
|
3. Call AI via `AiService`.
|
||||||
4. Parse AI response (subject + body).
|
4. Parse AI response (subject + body).
|
||||||
5. Send email via `EmailSenderService`.
|
5. Send email via `EmailSenderService`.
|
||||||
6. Save a `DispatchLog` record with status SENT or FAILED.
|
6. Save a `DispatchLog` with status SENT.
|
||||||
- If the pipeline fails at any step, save a `DispatchLog` with status FAILED and the error message.
|
- If the pipeline fails at any step, save a `DispatchLog` with status FAILED and the error message.
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create `EntityScheduler` in `scheduler/`. It must dynamically
|
|
||||||
> schedule a cron job per active `VirtualEntity` using `SchedulingConfigurer`. When triggered,
|
**Test file:** `src/test/kotlin/com/condado/newsletter/scheduler/EntitySchedulerTest.kt`
|
||||||
> it must run the full pipeline: read emails → build prompt → call AI → send email → save
|
|
||||||
> `DispatchLog`. Handle failures gracefully and always write a `DispatchLog`."
|
Tests to write (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_runFullPipeline_when_entityIsTriggered
|
||||||
|
// should_saveDispatchLogWithStatusSent_when_pipelineSucceeds
|
||||||
|
// should_saveDispatchLogWithStatusFailed_when_aiServiceThrows
|
||||||
|
// should_saveDispatchLogWithStatusFailed_when_emailSenderThrows
|
||||||
|
// should_notTrigger_when_entityIsInactive
|
||||||
|
```
|
||||||
|
|
||||||
|
Mock all five services with MockK.
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write a MockK unit test class `EntitySchedulerTest` in
|
||||||
|
> `src/test/kotlin/com/condado/newsletter/scheduler/`. Mock all five service dependencies.
|
||||||
|
> Write all 5 tests listed. Do NOT create the scheduler yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `EntityScheduler` in `scheduler/` using `SchedulingConfigurer`. Orchestrate the full
|
||||||
|
> pipeline and always persist a `DispatchLog`. Make all `EntitySchedulerTest` tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] `EntitySchedulerTest.kt` exists with all 5 tests.
|
||||||
- [ ] `EntityScheduler.kt` exists in `scheduler/`.
|
- [ ] `EntityScheduler.kt` exists in `scheduler/`.
|
||||||
- [ ] `@EnableScheduling` is present on the main app or a config class.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] An active entity triggers at its scheduled time and a `DispatchLog` record is created.
|
- [ ] `@EnableScheduling` is on `CondadoApplication`.
|
||||||
- [ ] End-to-end test: entity fires → email arrives in inbox.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -426,7 +565,7 @@ as a comma-separated string in `application.yml` under `app.recipients`. This ca
|
|||||||
|
|
||||||
### `VirtualEntityController` — `/api/v1/virtual-entities`
|
### `VirtualEntityController` — `/api/v1/virtual-entities`
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|---------------|--------------------------------------|
|
|--------|-------------------|--------------------------------------|
|
||||||
| POST | `/` | Create a new virtual entity |
|
| POST | `/` | Create a new virtual entity |
|
||||||
| GET | `/` | List all virtual entities |
|
| GET | `/` | List all virtual entities |
|
||||||
| GET | `/{id}` | Get one entity by ID |
|
| GET | `/{id}` | Get one entity by ID |
|
||||||
@@ -440,21 +579,52 @@ as a comma-separated string in `application.yml` under `app.recipients`. This ca
|
|||||||
| GET | `/` | List all dispatch logs |
|
| GET | `/` | List all dispatch logs |
|
||||||
| GET | `/entity/{id}` | List logs for a specific entity |
|
| GET | `/entity/{id}` | List logs for a specific entity |
|
||||||
|
|
||||||
**DTOs to create (in `dto/` package):**
|
**DTOs (in `dto/` package):**
|
||||||
- `VirtualEntityCreateDto` — name, email, jobTitle, personality, scheduleCron, contextWindowDays
|
- `VirtualEntityCreateDto` — name, email, jobTitle, personality, scheduleCron, contextWindowDays
|
||||||
- `VirtualEntityUpdateDto` — same fields, all optional
|
- `VirtualEntityUpdateDto` — same fields, all optional
|
||||||
- `VirtualEntityResponseDto` — full entity fields + id + createdAt
|
- `VirtualEntityResponseDto` — full entity fields + id + createdAt
|
||||||
- `DispatchLogResponseDto` — all DispatchLog fields
|
- `DispatchLogResponseDto` — all DispatchLog fields
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, create `VirtualEntityController` and `DispatchLogController` in
|
|
||||||
> `controller/`, and all DTOs in `dto/`. Controllers return `ResponseEntity<T>`. The `/trigger`
|
**Test files:**
|
||||||
> endpoint manually runs the entity pipeline. DTOs use validation annotations."
|
- `src/test/kotlin/com/condado/newsletter/controller/VirtualEntityControllerTest.kt`
|
||||||
|
- `src/test/kotlin/com/condado/newsletter/controller/DispatchLogControllerTest.kt`
|
||||||
|
|
||||||
|
Tests to write — `VirtualEntityControllerTest` (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_return201AndBody_when_postWithValidPayload
|
||||||
|
// should_return400_when_postWithMissingRequiredField
|
||||||
|
// should_return200AndList_when_getAllEntities
|
||||||
|
// should_return200AndEntity_when_getById
|
||||||
|
// should_return404_when_getByIdNotFound
|
||||||
|
// should_return200_when_putWithValidPayload
|
||||||
|
// should_return200AndDeactivated_when_delete
|
||||||
|
// should_return200_when_triggerEndpointCalled
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests to write — `DispatchLogControllerTest`:
|
||||||
|
```kotlin
|
||||||
|
// should_return200AndAllLogs_when_getAllLogs
|
||||||
|
// should_return200AndFilteredLogs_when_getByEntityId
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `@SpringBootTest` + `MockMvc` + H2. Mock `EntityScheduler` so trigger tests don't run the pipeline.
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write `@SpringBootTest` integration test classes
|
||||||
|
> `VirtualEntityControllerTest` and `DispatchLogControllerTest` using MockMvc and H2. Write
|
||||||
|
> all tests listed. Do NOT create the controllers or DTOs yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `VirtualEntityController`, `DispatchLogController`, and all DTOs in their packages.
|
||||||
|
> Controllers return `ResponseEntity<T>`. Make all controller tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] Both test files exist with all tests listed above.
|
||||||
- [ ] All controller and DTO files exist.
|
- [ ] All controller and DTO files exist.
|
||||||
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] Swagger UI shows all endpoints.
|
- [ ] Swagger UI shows all endpoints.
|
||||||
- [ ] CRUD via Swagger UI or Postman works end-to-end.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -464,34 +634,59 @@ as a comma-separated string in `application.yml` under `app.recipients`. This ca
|
|||||||
|
|
||||||
**Approach:**
|
**Approach:**
|
||||||
- `POST /api/auth/login` accepts `{ "password": "..." }`, validates against `APP_PASSWORD` env var.
|
- `POST /api/auth/login` accepts `{ "password": "..." }`, validates against `APP_PASSWORD` env var.
|
||||||
- On success, generates a JWT (signed with `JWT_SECRET`, expiry from `JWT_EXPIRATION_MS`) and
|
- On success, generates a JWT signed with `JWT_SECRET` and sets it as an `httpOnly` cookie.
|
||||||
sets it as an `httpOnly` cookie in the response.
|
- `JwtAuthFilter` (`OncePerRequestFilter`) validates the cookie on every protected request.
|
||||||
- Spring Security `JwtAuthFilter` (extends `OncePerRequestFilter`) validates the cookie on every
|
- Public paths: `POST /api/auth/login`, `GET /api/auth/me`, `/swagger-ui/**`, `/v3/api-docs/**`.
|
||||||
protected request.
|
|
||||||
- Public paths: `POST /api/auth/login`, `/swagger-ui.html`, `/v3/api-docs/**`.
|
|
||||||
- There is **no user table** — the password lives only in the environment variable.
|
- There is **no user table** — the password lives only in the environment variable.
|
||||||
|
|
||||||
**Classes to create:**
|
**Classes to create:**
|
||||||
- `AuthController` — `POST /api/auth/login` endpoint
|
- `AuthController` — `POST /api/auth/login`, `GET /api/auth/me`, `POST /api/auth/logout`
|
||||||
- `AuthService` — validates password, generates JWT
|
- `AuthService` — validates password, generates JWT
|
||||||
- `JwtService` — signs and validates JWT tokens
|
- `JwtService` — signs and validates JWT tokens
|
||||||
- `JwtAuthFilter` — reads cookie, validates JWT, sets `SecurityContext`
|
- `JwtAuthFilter` — reads cookie, validates JWT, sets `SecurityContext`
|
||||||
- `SecurityConfig` — Spring Security HTTP config (permit login + swagger, protect everything else)
|
- `SecurityConfig` — Spring Security HTTP config
|
||||||
|
|
||||||
**DTOs:**
|
**DTOs:**
|
||||||
- `LoginRequest` — `{ "password": String }`
|
- `LoginRequest` — `{ "password": String }`
|
||||||
- `AuthResponse` — `{ "message": String }` (cookie is set on the response; no token in body)
|
- `AuthResponse` — `{ "message": String }`
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, implement JWT authentication for the single-admin model.
|
|
||||||
> Create `AuthController`, `AuthService`, `JwtService`, `JwtAuthFilter`, and `SecurityConfig`.
|
**Test files:**
|
||||||
> `POST /api/auth/login` validates against `APP_PASSWORD` env var and returns a JWT in an
|
- `src/test/kotlin/com/condado/newsletter/service/AuthServiceTest.kt`
|
||||||
> httpOnly cookie. All other endpoints require the JWT cookie. Swagger UI is public."
|
- `src/test/kotlin/com/condado/newsletter/controller/AuthControllerTest.kt`
|
||||||
|
|
||||||
|
Tests to write — `AuthServiceTest` (all should **fail** before implementation):
|
||||||
|
```kotlin
|
||||||
|
// should_returnJwtToken_when_correctPasswordProvided
|
||||||
|
// should_throwUnauthorizedException_when_wrongPasswordProvided
|
||||||
|
// should_returnValidClaims_when_jwtTokenParsed
|
||||||
|
// should_returnFalse_when_expiredTokenValidated
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests to write — `AuthControllerTest`:
|
||||||
|
```kotlin
|
||||||
|
// should_return200AndSetCookie_when_correctPasswordPosted
|
||||||
|
// should_return401_when_wrongPasswordPosted
|
||||||
|
// should_return200_when_getMeWithValidCookie
|
||||||
|
// should_return401_when_getMeWithNoCookie
|
||||||
|
// should_return401_when_protectedEndpointAccessedWithoutCookie
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write MockK unit tests for `AuthService` and a `@SpringBootTest`
|
||||||
|
> integration test `AuthControllerTest` using MockMvc and H2. Write all tests listed.
|
||||||
|
> Do NOT create the auth classes yet."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Create `AuthController`, `AuthService`, `JwtService`, `JwtAuthFilter`, and `SecurityConfig`.
|
||||||
|
> `POST /api/auth/login` validates against `APP_PASSWORD`, returns JWT in an httpOnly cookie.
|
||||||
|
> Make all auth tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
- [ ] `POST /api/auth/login` with correct password sets an `httpOnly` JWT cookie and returns `200`.
|
- [ ] Both test files exist with all tests listed above.
|
||||||
- [ ] `POST /api/auth/login` with wrong password returns `401`.
|
- [ ] All auth class files exist.
|
||||||
- [ ] All `/api/v1/**` endpoints return `401` without a valid JWT cookie.
|
- [ ] `./gradlew test` is green.
|
||||||
- [ ] Swagger UI remains accessible without auth.
|
- [ ] Swagger UI remains accessible without auth.
|
||||||
- [ ] Password and JWT secret are never hardcoded.
|
- [ ] Password and JWT secret are never hardcoded.
|
||||||
|
|
||||||
@@ -539,61 +734,88 @@ router/
|
|||||||
- Login form submits to `POST /api/auth/login` — on success React Query invalidates and
|
- Login form submits to `POST /api/auth/login` — on success React Query invalidates and
|
||||||
React Router navigates to `/`.
|
React Router navigates to `/`.
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
### TDD — Write tests first
|
||||||
> "Using the CLAUDE.md context, build the React frontend. Create all four pages (Login,
|
|
||||||
> Dashboard, Entities, Logs) with React Query for data fetching, shadcn/ui for components,
|
**Test files:**
|
||||||
> and React Router for navigation. Implement `ProtectedRoute` using `GET /api/auth/me`.
|
- `src/__tests__/api/authApi.test.ts`
|
||||||
> All API calls must go through the `src/api/` layer."
|
- `src/__tests__/api/entitiesApi.test.ts`
|
||||||
|
- `src/__tests__/api/logsApi.test.ts`
|
||||||
|
- `src/__tests__/pages/LoginPage.test.tsx`
|
||||||
|
- `src/__tests__/pages/EntitiesPage.test.tsx`
|
||||||
|
- `src/__tests__/pages/DashboardPage.test.tsx`
|
||||||
|
- `src/__tests__/pages/LogsPage.test.tsx`
|
||||||
|
- `src/__tests__/components/ProtectedRoute.test.tsx`
|
||||||
|
|
||||||
|
Tests to write — **API layer** (all should **fail** before implementation):
|
||||||
|
```typescript
|
||||||
|
// authApi.test.ts
|
||||||
|
// should_callLoginEndpoint_when_loginCalled
|
||||||
|
// should_callLogoutEndpoint_when_logoutCalled
|
||||||
|
// should_callMeEndpoint_when_getMeCalled
|
||||||
|
|
||||||
|
// entitiesApi.test.ts
|
||||||
|
// should_callGetEndpoint_when_getAllEntitiesCalled
|
||||||
|
// should_callPostEndpoint_when_createEntityCalled
|
||||||
|
// should_callPutEndpoint_when_updateEntityCalled
|
||||||
|
// should_callDeleteEndpoint_when_deleteEntityCalled
|
||||||
|
// should_callTriggerEndpoint_when_triggerEntityCalled
|
||||||
|
|
||||||
|
// logsApi.test.ts
|
||||||
|
// should_callGetAllLogsEndpoint_when_getAllLogsCalled
|
||||||
|
// should_callGetByEntityEndpoint_when_getLogsByEntityCalled
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests to write — **Pages & Components** (all should **fail** before implementation):
|
||||||
|
```typescript
|
||||||
|
// LoginPage.test.tsx
|
||||||
|
// should_renderLoginForm_when_pageLoads
|
||||||
|
// should_callLoginApi_when_formSubmitted
|
||||||
|
// should_showErrorMessage_when_loginFails
|
||||||
|
// should_redirectToDashboard_when_loginSucceeds
|
||||||
|
|
||||||
|
// EntitiesPage.test.tsx
|
||||||
|
// should_renderEntityList_when_entitiesLoaded
|
||||||
|
// should_openCreateDialog_when_addButtonClicked
|
||||||
|
// should_callDeleteApi_when_deleteConfirmed
|
||||||
|
|
||||||
|
// DashboardPage.test.tsx
|
||||||
|
// should_renderEntityCount_when_pageLoads
|
||||||
|
// should_renderRecentLogs_when_pageLoads
|
||||||
|
|
||||||
|
// LogsPage.test.tsx
|
||||||
|
// should_renderLogTable_when_logsLoaded
|
||||||
|
// should_filterLogsByEntity_when_filterSelected
|
||||||
|
|
||||||
|
// ProtectedRoute.test.tsx
|
||||||
|
// should_renderChildren_when_sessionIsValid
|
||||||
|
// should_redirectToLogin_when_sessionIsInvalid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prompt — Phase 1 (tests):**
|
||||||
|
> "Using the CLAUDE.md context, write Vitest + React Testing Library test files for the
|
||||||
|
> frontend. Create tests for all three API modules (`authApi`, `entitiesApi`, `logsApi`) —
|
||||||
|
> mock Axios and assert that each function calls the correct endpoint. Create smoke tests for
|
||||||
|
> all four pages and for `ProtectedRoute`. Mock React Query and the API layer. Write all tests
|
||||||
|
> listed above. Do NOT create any implementation files yet — tests should fail."
|
||||||
|
|
||||||
|
**Prompt — Phase 2 (implementation):**
|
||||||
|
> "Using the CLAUDE.md context, implement the full React frontend. Create all API modules in
|
||||||
|
> `src/api/`, all components in `src/components/`, all pages in `src/pages/`, and the router
|
||||||
|
> in `src/router/index.tsx`. Use React Query for all server state, shadcn/ui for UI components,
|
||||||
|
> and React Router for navigation. Make all frontend tests pass."
|
||||||
|
|
||||||
**Done when:**
|
**Done when:**
|
||||||
|
- [ ] All test files exist with all tests listed above.
|
||||||
|
- [ ] `npm run test` is green (all tests pass).
|
||||||
- [ ] `npm run build` succeeds with no TypeScript errors.
|
- [ ] `npm run build` succeeds with no TypeScript errors.
|
||||||
- [ ] `npm run dev` serves the app and login flow works end-to-end.
|
- [ ] `npm run dev` serves the app and login flow works end-to-end.
|
||||||
- [ ] Unauthenticated users are redirected to `/login`.
|
- [ ] Unauthenticated users are redirected to `/login`.
|
||||||
- [ ] Entities can be created, edited, toggled active, and deleted via the UI.
|
- [ ] Entities can be created, edited, toggled active, and deleted via the UI.
|
||||||
- [ ] Dispatch logs are visible and filterable by entity.
|
- [ ] Dispatch logs are visible and filterable by entity.
|
||||||
- [ ] All pages have at least a smoke test (`npm run test` passes).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 12 — Unit & Integration Tests
|
## Step 12 — Docker Compose (Dev + Prod)
|
||||||
|
|
||||||
**Goal:** Test every service class and one integration test per controller.
|
|
||||||
|
|
||||||
### Backend tests
|
|
||||||
|
|
||||||
| Test Class | Type | Covers |
|
|
||||||
|---------------------------------|-------------|----------------------------------------------|
|
|
||||||
| `EmailReaderServiceTest` | Unit | IMAP fetch, empty list on error |
|
|
||||||
| `PromptBuilderServiceTest` | Unit | Prompt matches template, fields substituted |
|
|
||||||
| `AiServiceTest` | Unit | API call mocked, `parseResponse` parsing |
|
|
||||||
| `EmailSenderServiceTest` | Unit | `JavaMailSender` called with correct args |
|
|
||||||
| `AuthServiceTest` | Unit | Correct/incorrect password, JWT generation |
|
|
||||||
| `VirtualEntityControllerTest` | Integration | CRUD endpoints, trigger endpoint |
|
|
||||||
| `DispatchLogControllerTest` | Integration | List all, list by entity |
|
|
||||||
|
|
||||||
### Frontend tests
|
|
||||||
|
|
||||||
| Test File | Covers |
|
|
||||||
|---------------------------------|--------------------------------------------------------|
|
|
||||||
| `LoginPage.test.tsx` | Renders, submits form, shows error on wrong password |
|
|
||||||
| `EntitiesPage.test.tsx` | Lists entities, opens create dialog, handles delete |
|
|
||||||
| `authApi.test.ts` | `login()` calls correct endpoint with correct payload |
|
|
||||||
| `entitiesApi.test.ts` | CRUD functions call correct endpoints |
|
|
||||||
|
|
||||||
**Prompt to use with AI:**
|
|
||||||
> "Using the CLAUDE.md context, generate unit tests for all backend service classes using MockK,
|
|
||||||
> integration tests for controllers using `@SpringBootTest` with H2, and frontend component and
|
|
||||||
> API layer tests using Vitest + React Testing Library. Follow naming convention
|
|
||||||
> `should_[expectedBehavior]_when_[condition]` for backend tests."
|
|
||||||
|
|
||||||
**Done when:**
|
|
||||||
- [ ] `./gradlew test` passes all backend tests green.
|
|
||||||
- [ ] Backend service class coverage ≥ 80%.
|
|
||||||
- [ ] `npm run test` passes all frontend tests green.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 13 — Docker Compose (Dev + Prod)
|
|
||||||
|
|
||||||
**Goal:** Containerize both services and wire them together for local dev and production.
|
**Goal:** Containerize both services and wire them together for local dev and production.
|
||||||
|
|
||||||
@@ -629,7 +851,7 @@ condado-news-letter/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 14 — All-in-one Docker Image
|
## Step 13 — All-in-one Docker Image
|
||||||
|
|
||||||
**Goal:** Build a single Docker image that runs the entire stack (Nginx + Spring Boot + PostgreSQL)
|
**Goal:** Build a single Docker image that runs the entire stack (Nginx + Spring Boot + PostgreSQL)
|
||||||
under Supervisor, deployable with a single `docker run` command.
|
under Supervisor, deployable with a single `docker run` command.
|
||||||
@@ -710,7 +932,7 @@ docker run -d \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 15 — CI/CD (GitHub Actions + Docker Hub)
|
## Step 14 — CI/CD (GitHub Actions + Docker Hub)
|
||||||
|
|
||||||
**Goal:** Automate testing on every PR and publish the all-in-one image to Docker Hub on every
|
**Goal:** Automate testing on every PR and publish the all-in-one image to Docker Hub on every
|
||||||
merge to `main`.
|
merge to `main`.
|
||||||
|
|||||||
Reference in New Issue
Block a user