Skip to content

Fix finalize and tutor conflict reason recovery - #563

Merged
The-Obstacle-Is-The-Way merged 10 commits into
devfrom
fix/finalize-tutor-conflict-reasons
Jul 6, 2026
Merged

Fix finalize and tutor conflict reason recovery#563
The-Obstacle-Is-The-Way merged 10 commits into
devfrom
fix/finalize-tutor-conflict-reasons

Conversation

@The-Obstacle-Is-The-Way

@The-Obstacle-Is-The-Way The-Obstacle-Is-The-Way commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • drop provably stale exam-expiry final draft flushes while keeping server-side grace-window grading intact
  • add structured AlreadyEnded reasons for ended-session submit/get-next conflicts and recover tutor load/submit conflicts to summary
  • map double-finalize already-answered losers to AlreadyEnded only after a fresh post-transaction session re-read
  • keep the stale-flush client threshold in the app layer to preserve Clean Architecture boundaries

Verification

  • pnpm typecheck && pnpm lint && pnpm test --run && pnpm test:browser && pnpm test:integration && pnpm build && pnpm test:e2e
  • pre-push hook: pnpm typecheck && pnpm test --run

Summary by CodeRabbit

  • Bug Fixes

    • Improved exam timer expiration to support async retry behavior, including re-invoking expiration handling when the first finalize attempt fails.
    • Enhanced “final draft flush” logic by dropping provably stale late flushes (with warnings) so finalization succeeds.
    • Strengthened “session already ended” conflict recovery across load/submission/tutor flows, consistently transitioning back to the summary view.
  • Tests

    • Added/expanded coverage for timer retry, stale flush dropping, ended-session recovery (including deduping), and structured conflict reason/message assertions.
    • Updated expectations for new boolean return values in review and end-session flows.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
naltrexone-university Ready Ready Preview, Comment Jul 6, 2026 5:57am

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@The-Obstacle-Is-The-Way, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2208b3ea-06a1-4eb3-95fd-23c9d3c851ae

📥 Commits

Reviewing files that changed from the base of the PR and between 0923cab and 683e2cd.

📒 Files selected for processing (5)
  • app/(app)/app/practice/[sessionId]/components/exam-review-view.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/components/exam-review-view.tsx
  • app/(app)/app/practice/[sessionId]/components/practice-session-page-view.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts

Walkthrough

This PR changes exam timer expiry and practice-session conflict handling to support retryable async callbacks, ended-session recovery through question-flow and page-model flows, boolean finalize contracts, stale final-draft dropping, and shared conflict messaging.

Changes

Timer retry, conflict recovery, and finalize flow

Layer / File(s) Summary
Exam timer retry semantics
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts, app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
onExpire can now resolve to a boolean or promise, and the hook clears its fired-deadline guard on handled-false or rejection so expiry can be retried.
Ended-session conflict recovery in question-flow-actions
app/(app)/app/practice/shared/question-flow-actions.ts, app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
Adds EndedSessionConflictRecovery, exports ended-session conflict helpers, and routes load/submit flows through recovery handling when AlreadyEnded conflicts are returned.
Question-flow and page-logic recovery threading
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts, app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
Threads recoverEndedSessionConflict through question loading and answer submission, and changes endSession to return Promise<boolean> with boolean outcomes across guards and conflict recovery.
Page-model finalization and session recovery
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts, app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-*.browser.spec.tsx
Adds provably-stale draft detection, boolean finalization, and ended-session summary recovery with deduplication; browser tests cover grace-window resume, retry, and conflict recovery.
Review-stage finalize return contract
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts, app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts, app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
finalizeSession and onFinalizeReview now return Promise<boolean>, and the non-exam path awaits the review-stage finalize call before returning.
FinalizeExamAnswersUseCase stale flush and double-finalize handling
src/application/use-cases/finalize-exam-answers.ts, src/application/use-cases/finalize-exam-answers.test.ts, lib/container/use-cases.ts
Adds a warn logger, uses the shared grace-window constant, remaps some double-finalize conflicts after re-read, and drops stale post-grace final-draft flushes instead of throwing.
Shared conflict messages and use-case error shape
src/application/errors/*, src/adapters/repositories/drizzle-attempt-repository.*, src/application/test-helpers/fakes/fake-attempt-repository.ts, src/application/use-cases/get-next-question.ts, src/application/use-cases/submit-answer.ts, related tests
Introduces AttemptConflictMessages, reuses it in repositories/fakes, and switches ended-session use cases/tests to structured practiceSessionAlreadyEndedError() / details.reason assertions.
Shared grace-window constant
src/domain/services/time-constants.ts, src/domain/services/index.ts, src/domain/services/time-constants.test.ts
Adds EXAM_FINAL_DRAFT_FLUSH_GRACE_MS, re-exports it, and verifies the constant value.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant useExamTimer
  participant PageModel
  participant QuestionFlow
  participant RecoverSummary
  useExamTimer->>PageModel: onExpire()
  PageModel->>QuestionFlow: recoverEndedSessionConflict
  QuestionFlow->>RecoverSummary: handle AlreadyEnded conflict
  RecoverSummary-->>QuestionFlow: handled true/false
  QuestionFlow-->>PageModel: ready or error state
  PageModel-->>useExamTimer: boolean return for retry
Loading
sequenceDiagram
  participant FinalizeExamAnswersUseCase
  participant Repository
  participant Logger
  FinalizeExamAnswersUseCase->>Repository: writeTransaction(...)
  Repository-->>FinalizeExamAnswersUseCase: already-answered conflict
  FinalizeExamAnswersUseCase->>Repository: re-fetch session
  FinalizeExamAnswersUseCase->>Logger: warn dropped stale flush
  FinalizeExamAnswersUseCase-->>Repository: return unchanged session
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the core change set: finalize-flow fixes and tutor conflict reason recovery.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/finalize-tutor-conflict-reasons

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
src/application/use-cases/submit-answer-tutor.test.ts (1)

213-217: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Same hardcoded-message duplication as the exam test counterpart.

Same concern as submit-answer-exam.test.ts — asserting against the literal string instead of PracticeSessionConflictMessages.AlreadyEnded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/use-cases/submit-answer-tutor.test.ts` around lines 213 -
217, The tutor submit-answer test is duplicating the hardcoded conflict message
string, which should match the shared constant used elsewhere. Update the
assertion in submitAnswerTutor test coverage to reference
PracticeSessionConflictMessages.AlreadyEnded instead of the literal message,
keeping the conflict code and details check intact. Use the existing
submit-answer-exam.test.ts pattern and the related symbols
PracticeSessionConflictMessages and PracticeSessionConflictReasons.AlreadyEnded
to locate the affected assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx:
- Around line 101-121: Add a test in use-exam-timer.browser.spec.tsx that covers
the onExpire rejection path in TimerProbe/useExamTimer, since the current case
only verifies handled === false. Create a scenario where onExpire rejects once
and assert the timer retries the expired deadline on the next tick, exercising
the branch that clears firedDeadlineMsRef in the .catch handler and confirming
onExpire is called again after the rejection.

In `@app/`(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts:
- Around line 47-51: The final-flush grace window is hardcoded in
use-practice-session-page-model via STALE_FINAL_DRAFT_FLUSH_AFTER_DEADLINE_MS,
which can drift from the server’s finalize grace-window invariant. Replace the
client-only constant with a shared exported value used by both client and
server, or add a parity test that asserts they stay equal, and update any
references so the final draft flush timing always matches the authoritative
server window.

In
`@app/`(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts:
- Around line 9-14: The test file is using vi.hoisted() unnecessarily for
fixture IDs even though no vi.mock() factory reads them. Replace the hoisted
fixture block in question-flow-actions-ended-session-conflict.test.ts with plain
top-level const declarations for fixtureChoice1Id, fixtureQuestion1Id, and
fixtureQuestionOldId so the setup stays simple and matches the existing test
usage.

In `@app/`(app)/app/practice/shared/question-flow-actions.ts:
- Around line 134-142: The recovery-check logic is duplicated in both
runLoadQuestionFlow and runSubmitAnswerFlow, so extract the repeated “ended
session conflict” handling into a small shared helper such as
tryRecoverEndedSessionConflict that accepts res, recoverEndedSessionConflict,
and canCommit. Move the exact await/handled/canCommit/early-return flow into
that helper, then replace both inline blocks with calls to it so the behavior
stays identical and cannot drift between the two paths.
- Around line 138-140: The ended-session recovery flow in question-flow-actions
should be deduplicated so it cannot run twice when entered from both load and
submit before shared state is updated. Update recoverEndedSessionConflict to use
the same in-flight guard pattern as finalizeExpiredExam, or cache the active
recovery promise, and ensure recoverEndedSessionSummary only fetches/applies
once per active recovery.

In `@src/application/use-cases/finalize-exam-answers.ts`:
- Around line 70-79: The double-finalize conflict handling in
isAttemptAlreadyAnsweredConflict still relies on a duplicated message string,
which can drift from the repository/fake and break the AlreadyEnded mapping.
Centralize the signal by exporting and reusing one shared constant or replacing
the message check with a structured reason/value used by finalize-exam-answers,
the attempt repository, and the fake so the conflict is identified consistently.

In `@src/application/use-cases/submit-answer-exam.test.ts`:
- Around line 130-134: The test assertion for the rejected practice session
conflict uses a hardcoded message string, which can drift from the source of
truth. Update the expectation in submit-answer-exam.test.ts to assert the
message against PracticeSessionConflictMessages.AlreadyEnded instead of typing
the literal string, keeping the existing code and details checks unchanged.

---

Duplicate comments:
In `@src/application/use-cases/submit-answer-tutor.test.ts`:
- Around line 213-217: The tutor submit-answer test is duplicating the hardcoded
conflict message string, which should match the shared constant used elsewhere.
Update the assertion in submitAnswerTutor test coverage to reference
PracticeSessionConflictMessages.AlreadyEnded instead of the literal message,
keeping the conflict code and details check intact. Use the existing
submit-answer-exam.test.ts pattern and the related symbols
PracticeSessionConflictMessages and PracticeSessionConflictReasons.AlreadyEnded
to locate the affected assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bb4ed117-7bd7-4630-bf74-aa422e3ab320

📥 Commits

Reviewing files that changed from the base of the PR and between c9e91b4 and 44e71d0.

📒 Files selected for processing (20)
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • lib/container/use-cases.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/submit-answer.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: test
🧰 Additional context used
📓 Path-based instructions (24)
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use dynamic imports in beforeAll (or beforeEach only when mock/module-reset order requires it) to load test modules; do not import heavy modules inside individual it() blocks
Do not add per-test timeout overrides (it(..., 10_000) or { timeout: 15_000 }); global Vitest policy is configured in vitest.config.ts, vitest.browser.config.ts, and vitest.integration.config.ts
Use fake repository/gateway classes from src/application/test-helpers/fakes/ (e.g., FakeAttemptRepository, FakeAuthGateway) instead of vi.mock() for our own code; only use vi.mock() for external packages like Drizzle, Clerk, or Stripe SDK
Use vi.fn() inline objects only for external dependencies (Drizzle db, Clerk, Stripe SDK) that cannot be dependency-injected; use { spy: true } for sealed ESM namespaces like server-action controllers
Never use vi.mock() for our own code in tests; use fake classes from src/application/test-helpers/fakes/ instead (e.g., FakeQuestionRepository, FakeAttemptRepository)

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Use constructor dependency injection in all classes; do not import singletons directly (e.g., use constructor(private db: DrizzleDb) instead of import { db } from './db')
Use ApplicationError with typed codes for all custom errors; do not use bare throw new Error('message')
Do not use magic numbers; use constants/configs instead
Dependencies must point inward only; domain layer has ZERO external imports, application layer depends only on domain, adapters depend on application
Bare catch {} blocks allowed only for intentionally suppressed secondary failures (telemetry, logging, rollback, parse fallback); add a short nearby comment and use shared application/adapters logger-safety helpers when 3+ files need the same wrapper

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Wire dependencies at entry points via lib/container.ts, lib/container/**, and lib/controller-helpers.ts, not ad hoc imports inside use cases

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Keep modules small, prefer pure functions in lib/; avoid non-null assertions (!) and unused imports/variables (Biome errors); prefer importing via @/... alias

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • lib/container/use-cases.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome as the source of truth for code style: 2-space indents, single quotes, semicolons required, trailing commas; run pnpm lint:fix to auto-fix

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • lib/container/use-cases.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use plain Vitest for *.test.ts files without environment directive

Unit tests (logic) should be colocated with source files as *.test.ts and run with pnpm test

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts

⚙️ CodeRabbit configuration file

**/*.test.ts: TESTING CONVENTIONS (Vitest, NOT Jest):

  1. Use fakes via dependency injection, NOT vi.mock() for our own code
  2. vi.mock() is ONLY acceptable for external packages (Clerk, Next.js, server-only)
  3. vi.fn() inside fake objects for spying is CORRECT
  4. One concept per test, use Arrange-Act-Assert pattern
  5. Test behavior, not implementation details
  6. Use factories: createQuestion(), createChoice() from test-helpers/
  7. Do NOT require dynamic import()/beforeAll loading in pure TypeScript .test.ts suites.
    That React 19 compatibility rule is scoped to **/
    .test.tsx component tests.
  8. FIXTURE IDs (see .claude/rules/fixture-integrity.md): in fakes-only
    application/domain/use-case tests, readable string IDs such as 'q1',
    'user-1', or 'attempt-1' are INTENTIONAL and correct — do NOT suggest
    replacing them with UUIDs or crypto.randomUUID(). UUID-shaped fixtures are
    required ONLY where an ID crosses a zUuid/Drizzle uuid() boundary, which is
    covered in adapter/controller tests (those legitimately assert UUID shape and
    the 'not-a-uuid' rejection path). Flagging readable fake-backed IDs in
    fakes-only tests is a known recurring false positive in this repo.

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{test.ts,test.tsx,spec.ts,spec.tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test.ts,test.tsx,spec.ts,spec.tsx}: Use FakeXxxRepository from src/application/test-helpers/fakes/ instead of mocks in tests (fakes over mocks)
Write the test first before implementation (TDD mandatory)
Apply process.env snapshot/restore and vi.stubEnv cleanup with combined ordering in test files

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ApplicationError with typed codes for all error handling

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Application layer (src/application/) must depend only on domain layer. Implement use cases and define port interfaces.

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/fixture-integrity.md)

**/*.{test,spec}.{ts,tsx}: Test fixtures must match production validators when they cross a real boundary. Application-owned IDs are UUID-shaped at the boundary: use zUuid = z.guid() in controller schemas and Drizzle uuid() columns in db/schema.ts. Use UUID-valid values for fixtures that model controller input/output DTOs, adapter repository rows, mocked SQL rows, mapper rows, app-auth userId values, E2E-helper app DB rows, and shared factory/fake-generated defaults.
Prefer UUID-emitting factories for ID fields. Otherwise use named, role-bearing variables like const userId = crypto.randomUUID() and const questionId = crypto.randomUUID() to make entity relationships clear in tests.
FIX UUID values that cross zUuid or Drizzle uuid() boundaries. LEAVE values that are not application-owned UUID fixtures: provider IDs (cus_, sub_, evt_, price_, Clerk, Svix), slugs, labels, HTML ids, data-testid, React-only keys, intentionally invalid negative-validation fixtures, readable fake-backed test keys, and fake-repository behavior-test keys.
Preserve entity relationships in test fixtures by reusing the same named UUID variable everywhere the same entity is referenced. For example, use the same questionId variable when creating a question, its choices, and attempts that reference it.
Capture generated IDs in test assertions and error strings. Do not assert old deterministic placeholders after switching a factory or fake to generated IDs.
Do not add as any, as unknown as, @ts-ignore, widened DTO types, or relaxed expectations to make a fixture fit. Use a production-shaped fixture instead.
Use vi.hoisted() for fixture values only when the value is read inside a vi.mock(path, () => ...) factory body. Do not use vi.hoisted() for UUID values passed to vi.mocked(controllerFn).mockResolvedValue(...), mockResolvedValueOnce(...), or mockImplementation(...) in normal beforeEach/it scope after a { spy: true } controller mock. Do not consistency-hoist ...

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts}

📄 CodeRabbit inference engine (.claude/rules/testing.md)

**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts}: Use Vitest exclusively for testing. Do NOT use Jest APIs or jest.mock().
Import testing utilities from 'vitest': import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
Use existing fakes from src/application/test-helpers/fakes/ instead of vi.mock() for internal code. Consult src/application/test-helpers/fakes/index.ts for available fakes.
vi.mock() is ONLY acceptable for external SDKs (@clerk/nextjs, next/link, server-only) and Browser Mode sealed ESM modules (vi.mock(path, { spy: true }) for controller modules)
Tests that mutate process.env MUST snapshot/restore via tests/shared/process-env.ts and follow cleanup ordering rules from .claude/rules/test-isolation.md
Use factories createQuestion(), createChoice() from src/domain/test-helpers/ instead of manually constructing test objects
Use descriptive test names that describe the expected behavior (e.g., 'returns isCorrect=false when incorrect choice selected')

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts,helpers.ts}

📄 CodeRabbit inference engine (.claude/rules/testing.md)

Tests and test helpers that create boundary-shaped fixtures MUST keep application-owned IDs valid at controller/DB boundaries and MUST leave provider IDs, fake-backed semantic keys, UI tokens, and intentional-invalid fixtures alone per .claude/rules/fixture-integrity.md

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
**

⚙️ CodeRabbit configuration file

**: # AGENTS.md

Repository guidelines for AI coding agents (Codex CLI, Claude Code, Cursor, GitHub Copilot, etc.) working with this codebase.

This is the single source of truth for all agents. Claude Code also reads CLAUDE.md (slim, Claude-specific supplements) and .claude/rules/ (path-scoped rules). Keep universal project rules here; keep Claude-only or path-scoped guidance in those Claude-specific files so instructions do not drift or conflict.


⚠️ CRITICAL: React 19 + Vitest Testing Requirements

READ THIS FIRST. Tests will fail in git hooks/CI without these requirements.

For ALL .test.tsx files:

// `@vitest-environment` jsdom   ← MUST be first line
import { renderToStaticMarkup } from 'react-dom/server';
import { beforeAll, describe, expect, it } from 'vitest';

let MyComponent: typeof import('./MyComponent').default;

beforeAll(async () => {
  MyComponent = (await import('./MyComponent')).default;
});

// Use renderToStaticMarkup for render-output tests
describe('MyComponent', () => {
  it('renders output', () => {
    const html = renderToStaticMarkup(<MyComponent />);
    expect(html).toContain('Expected text');
  });
});

Why:

  • @testing-library/react has a known bug with React 19 + Vitest — no fix coming
  • Git hooks and CI load production builds where act() is undefined
  • renderToStaticMarkup is a stable first-party React API that works everywhere

Import placement + timeout policy (DEBT-225):

  • Keep dynamic imports, but load them in beforeAll (or beforeEach only when mock/module-reset order requires it)
  • Do not import heavy modules inside individual it() blocks
  • Do not add per-test timeout overrides (it(..., 10_000) or { timeout: 15_000 })
  • Global Vitest policy is configured in vitest.config.ts, vitest.browser.config.ts, and vitest.integration.config.ts

DO NOT USE for jsdom component t...

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-tutor.test.ts
  • lib/container/use-cases.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
src/application/**

⚙️ CodeRabbit configuration file

src/application/**: This is the APPLICATION layer (use cases + ports).
Dependencies flow INWARD only (can import from domain/).
Use cases orchestrate, they don't implement business rules.
Ports (interfaces) define contracts for adapters.
Tests should use fakes from src/application/test-helpers/fakes/, NOT vi.mock().

Files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
{app,lib,db}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Outermost layer (app/, lib/, db/) contains Next.js framework code and may depend on inner layers.

Files:

  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • lib/container/use-cases.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
**/{app,components,lib,hooks}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Use @/... alias for all imports instead of relative paths

Files:

  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • lib/container/use-cases.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
app/**

⚙️ CodeRabbit configuration file

app/**: Next.js App Router with Server Components and Server Actions.
Server Actions must be in files with 'use server' directive.
Client components must have 'use client' directive.
Use composition root pattern - wire dependencies at entry points.

Files:

  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
  • app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
**/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Extract custom hooks into hooks/ subdirectories rather than keeping them in component files

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
**/*.browser.spec.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use vitest-browser-react in *.browser.spec.tsx files for interactive and async hook tests, run with pnpm test:browser

Use vitest-browser-react for *.browser.spec.tsx files and run with pnpm test:browser

**/*.browser.spec.tsx: Use *.browser.spec.tsx for testing hooks with async state transitions (useEffect, useState after await)
Use *.browser.spec.tsx for interactive UI testing (clicks, forms, user events)
Use *.browser.spec.tsx for tests that need real DOM behavior (not jsdom simulation)
Import render from 'vitest-browser-react' and expect, test from 'vitest' in browser mode tests
Use expect.element() with built-in retry-ability instead of manual polling for DOM assertions in browser mode tests
Do not use // @vitest-environment jsdom directive in browser mode test files
Mock server-action controllers with vi.mock() using { spy: true } in browser mode tests to handle Node-only code that cannot execute in Chromium
Keep hook inputs and callback refs stable in browser mode tests to avoid infinite React effect re-triggers
Use useMemo/useCallback in test wrapper components if needed to maintain stable references in browser mode tests

Hook tests (async/interactive) should be colocated with source files as *.browser.spec.tsx and run with pnpm test:browser

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx

⚙️ CodeRabbit configuration file

**/*.browser.spec.tsx: Browser mode tests use vitest-browser-react and run with pnpm test:browser.
Use real browser interactions and async expectations.
Do not suggest @testing-library/react or jsdom-only interaction patterns here.

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
{app,components}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

{app,components}/**/*.tsx: Never use raw hex colors (#fff, #121212) or palette colors (bg-zinc-400, text-slate-300) in .tsx UI code except documented third-party API seams; always use semantic tokens (bg-primary, text-foreground, border-border, etc.)
Use the canonical focus-ring pattern focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]; never hand-roll variants
All interactive click targets MUST use the <Button> component; raw <button> is allowed only inside components/ui/ primitives and app-shell disclosure toggles per Pattern Registry I-6
Use the canonical muted/layer-2 opacity scale (/20, /40, /50, /60) for bg-muted-class fills; documented foreground-ramp arbitrary values allowed only in Pattern Registry contexts; undocumented arbitrary values forbidden
Semantic tokens handle light/dark mode automatically; component-specific dark: overrides allowed only when they appear in pattern-registry.md or contrast-policy.md; duplicated dark overrides across 2+ components must promote to shared primitive or lib/shared-styles.ts

{app,components}/**/*.tsx: Use <Button> for ALL interactive click targets; raw <button> only inside components/ui/ and app-shell disclosure toggles per Pattern Registry I-6
Use single canonical focus ring: focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]
Use semantic tokens only in UI code; never raw hex or palette colors in .tsx files except documented third-party API seams
Use muted/layer-2 opacity scale: /20//40//50//60 from pattern-registry.md; documented foreground-ramp arbitrary values allowed only in documented Pattern Registry contexts; undocumented arbitrary values forbidden

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
app/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

For browser visual verification of /app/* pages requiring Clerk auth, use agent-browser --profile /tmp/clerk-profile with prior human login via --headed mode; close daemon with agent-browser close before re-opening with --profile

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
**/{app,components}/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

**/{app,components}/**/*.{tsx,jsx}: Use the canonical focus ring pattern: focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] for interactive elements OR use the <Button> component which has the ring built in
Use canonical opacity values from pattern-registry.md when using layer fills: /20 (tint), /40 (subtle hover), /50 (standard hover), /60 (exception-only), never use /80 or /100 for hover, and documented foreground-ramp values only in exact Pattern Registry contexts
All interactive click targets MUST use the <Button> component. Raw <button> elements are allowed only inside components/ui/ primitives and documented app-shell disclosure toggles (components/mobile-nav.tsx Pattern Registry I-6 exception)
Component-specific dark: CSS overrides are allowed ONLY when documented in pattern-registry.md or contrast-policy.md. If the same dark override appears in 2+ components, promote it into a shared primitive or constant in lib/shared-styles.ts
Import routes from lib/routes.ts using the ROUTES constant. NEVER hard-code route strings like '/app/dashboard'
Keep components small, targeting less than 300 lines per file
Use components/ui/ shadcn primitives and don't reinvent custom UI primitives
Use Server Components by default; add 'use client' directive only when necessary for client-side interactivity

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
**/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Use semantic tokens (e.g., bg-primary, text-foreground, text-muted-foreground, border-border) NEVER use raw hex colors or palette colors (e.g., bg-zinc-400, text-slate-300) in .tsx UI code except documented third-party API seams like Clerk appearance.variables

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
**/lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Place core utilities (auth, Stripe, env, DB) in the lib/ directory

Files:

  • lib/container/use-cases.ts
🧠 Learnings (8)
📚 Learning: 2026-03-07T17:42:02.738Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 178
File: components/marketing/marketing-home.test.tsx:33-35
Timestamp: 2026-03-07T17:42:02.738Z
Learning: In test files across the repository, do not flag or suggest extracting duplicated local helpers like getClassTokens(className: string): Set<string> unless there is a repo-wide test-helper cleanup ticket. Only consider extraction when a dedicated, cross-cutting refactor scope is approved. Do not perform partial extractions scoped to a single feature ticket, as per maintainer policy.

Applied to files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-03-21T18:11:54.932Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 243
File: components/marketing/marketing-layout.test.tsx:44-64
Timestamp: 2026-03-21T18:11:54.932Z
Learning: In Node.js environments that support runtime TZ changes (Node >= 13 on POSIX; Node >= 16.2 on Windows), mutating `process.env.TZ` inside a test case is a valid way to simulate a non-UTC runtime. This should reliably affect subsequent `Date` calls within that test, so review comments should not flag `process.env.TZ` mid-test mutations as ineffective when the test is explicitly asserting UTC-based date logic.

Applied to files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-04-25T21:05:17.378Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 287
File: src/application/use-cases/save-exam-draft-answer.ts:8-8
Timestamp: 2026-04-25T21:05:17.378Z
Learning: Within `src/application/use-cases/`, sibling use-case modules should use same-directory relative imports (e.g., `./save-exam-draft-answer`, `./practice-session-summary`, `./submit-answer`) for intra-use-case references. Treat this as the established local convention and do not recommend converting these sibling relative imports to the `@/...` path alias.

Applied to files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/get-next-question.ts
  • src/application/use-cases/submit-answer.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-06-19T15:08:41.673Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 464
File: src/application/use-cases/discard-practice-session.test.ts:1-1
Timestamp: 2026-06-19T15:08:41.673Z
Learning: In this repo, the dynamic `import()` + `beforeAll` module-loading convention is for React component tests (`**/*.test.tsx`). For pure TypeScript tests (`**/*.test.ts`, e.g. use-case/unit tests), use static top-level imports by convention and do NOT flag static imports as a violation of the dynamic-import convention.

Applied to files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-06-30T01:19:56.106Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 540
File: src/application/use-cases/set-bookmark.test.ts:11-12
Timestamp: 2026-06-30T01:19:56.106Z
Learning: In application-layer use-case tests that run fully behind fake repositories (i.e., they do not cross a real adapter/schema/database boundary), fixtures may use readable fake-backed semantic IDs (e.g., `user-1`, `q1`) and should not be flagged for missing UUID conversion. Only require UUID-valid application-owned IDs at real boundaries—for example, controller/request validation using `zUuid` or persistence/column typing via Drizzle `uuid()`—where inputs are validated or stored in real adapters. For bookmarks, ensure UUID-boundary coverage exists in `src/adapters/controllers/bookmark-controller.test.ts`, not in `src/application/use-cases/set-bookmark.test.ts`.

Applied to files:

  • src/application/use-cases/get-next-question-explicit-question.test.ts
  • src/application/use-cases/get-next-question-navigation.test.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-03-03T17:31:31.300Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 165
File: app/(app)/app/practice/shared/question-flow-actions.ts:18-31
Timestamp: 2026-03-03T17:31:31.300Z
Learning: In app-layer orchestration helpers under app/practice/shared, throw plain Error for programmer misuse (e.g., internal invariants like assertRequestSequencingHooks) and reserve ApplicationError with typed codes for domain/business failures. This guideline applies to all TypeScript files in that shared orchestration layer to distinguish internal programmer mistakes from user/domain errors.

Applied to files:

  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
📚 Learning: 2026-02-09T17:21:25.626Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 78
File: app/(app)/app/practice/practice-page-client.tsx:96-98
Timestamp: 2026-02-09T17:21:25.626Z
Learning: Actionable guideline: In practice-related TSX components, distinguish external pending states (isPending) from internal loading state (e.g., sessionStartStatus === 'loading'). Ensure isPending is used only for external pending states (such as data/question loading initiated outside the component), while internal loading should be represented by dedicated state flags. For the given files, practice-page-client.tsx should use isPending={false} since there is no external pending state after removing the question flow. If you encounter a similar component, confirm whether isPending reflects an external pending condition or rely on internal loading state flags instead, and set props accordingly.

Applied to files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
📚 Learning: 2026-05-04T20:31:16.507Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 306
File: app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow-click-commit.browser.spec.tsx:10-10
Timestamp: 2026-05-04T20:31:16.507Z
Learning: In app/(app)/app/practice/[sessionId]/hooks/, follow the established import convention: when a .test.tsx, .browser.spec.tsx, or .probes.tsx file imports the SUT from the same directory, use a relative import (e.g., "./use-practice-session-question-flow"). Use the "`@/`..." alias only for cross-slice (non-same-directory) imports. Do not flag relative same-directory SUT imports in this hooks directory as guideline violations.

Applied to files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx
🪛 ast-grep (0.44.1)
src/application/use-cases/finalize-exam-answers.ts

[warning] 328-335: Avoid logging sensitive data
Context: this.logger.warn(
{
sessionId: session.id,
userId: session.userId,
questionId: finalDraftAnswer.questionId,
},
'Dropped stale final exam draft flush after grace window',
)
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🔇 Additional comments (24)
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.ts (2)

11-15: LGTM!


64-74: 🩺 Stability & Availability

No sync-throw path in the current caller. useExamTimer is only wired to finalizeExpiredExam, which returns a Promise and doesn't throw synchronously, so this retry-latch path isn’t reachable here.

			> Likely an incorrect or invalid review comment.
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx (1)

101-121: 🩺 Stability & Availability

Timer cleanup is already handled in this suite. The shared afterEach(() => vi.useRealTimers()) restores real timers for every test here, so this case does not leak fake timers.

			> Likely an incorrect or invalid review comment.
app/(app)/app/practice/shared/question-flow-actions.ts (1)

34-34: LGTM!

Also applies to: 90-90, 191-207, 305-305

app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts (1)

16-152: LGTM! Test coverage correctly exercises structured AlreadyEnded recovery vs. reasonless fallback for both load and submit flows.

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts (1)

12-12: LGTM! recoverEndedSessionConflict is correctly threaded into both loadQuestionConfig and commitChoice, with matching dependency-array updates on each useMemo/useCallback.

Also applies to: 41-41, 142-150, 425-425, 446-446

src/application/use-cases/submit-answer.ts (1)

14-14: LGTM!

Also applies to: 189-191

lib/container/use-cases.ts (1)

117-132: 🎯 Functional Correctness

Logger argument order is correct primitives.logger is passed in the final FinalizeExamAnswersUseCase constructor slot, so this wiring is fine.

			> Likely an incorrect or invalid review comment.
src/application/use-cases/finalize-exam-answers.ts (3)

273-303: Double-finalize remap flow reads correctly.

throwAlreadyEndedForDoubleFinalizeLoser correctly filters on the specific conflict, does a fresh out-of-transaction re-read, and only overrides the error when the fresh session is actually ended — otherwise the original error propagates via throw error;. The async catch handler properly supersedes the fallback re-throw when it itself throws.


110-110: LGTM!


325-339: Grace-window split boundary logic is sound.

isAfterGraceWindow (>) and the existing isWithinGraceWindow (<=) at the same deadline + FINALIZE_FLUSH_DEADLINE_GRACE_MS boundary don't overlap or leave a gap, and the pre-existing question-state/NOT_FOUND check still runs before the drop, so a bogus questionId still throws regardless of timing.

src/application/use-cases/finalize-exam-answers.test.ts (3)

48-72: LGTM! Fakes-over-mocks pattern for deterministic sequenced re-reads is a good fit for the double-finalize test scenario.


666-793: LGTM! Both the "recovered to AlreadyEnded" and "left unchanged when still active" cases are covered and match the implementation's fresh-re-read gating.


1159-1196: LGTM! Warning payload assertion matches the Pick<Logger, 'warn'> context shape used in the use case.

src/application/use-cases/get-next-question.ts (1)

175-177: LGTM! Consistent with the structured AlreadyEnded conflict shape used elsewhere in this cohort.

src/application/use-cases/get-next-question-explicit-question.test.ts (1)

293-325: LGTM!

src/application/use-cases/get-next-question-navigation.test.ts (1)

2-2: LGTM!

Also applies to: 111-111

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts (2)

215-269: LGTM! The boolean-returning finalizeExpiredExam correctly resets expiryFinalizeInFlightRef only on failure/error and forwards the outcome to the timer for retry semantics.


335-372: 🎯 Functional Correctness

Check post-exam back-navigation state The post-exam review surface may let users jump back into a question while postExamSummary is still set, so this recovery path needs a closer look.

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx (1)

5-5: LGTM! The new AlreadyEnded conflict recovery tests for tutor getNextQuestion and submitAnswer correctly exercise the recovery-to-summary path and match the underlying question-flow-actions.ts recovery contract.

Also applies to: 28-28, 234-288, 433-504

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.ts (1)

29-29: LGTM! Boolean contract change is applied consistently and finalizeSessionSafely's fire-and-forget usage is unaffected.

Also applies to: 44-44, 152-158

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.ts (1)

244-260: LGTM! Correctly awaits the now-boolean-returning reviewStage.onFinalizeReview() while preserving this function's own Promise<void> contract.

app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts (1)

3-3: LGTM! The endSession boolean contract is threaded correctly through every return path, and recoverEndedSessionConflict is consistently plumbed through the load/submit action inputs.

Also applies to: 66-66, 95-95, 118-118, 144-144, 171-171, 193-267

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsx (1)

165-247: 📐 Maintainability & Code Quality

No action needed: shared afterEach(() => vi.useRealTimers()) already resets timers for this file.

			> Likely an incorrect or invalid review comment.

Comment thread app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts Outdated
Comment thread app/(app)/app/practice/shared/question-flow-actions.ts Outdated
Comment thread app/(app)/app/practice/shared/question-flow-actions.ts Outdated
Comment thread src/application/use-cases/finalize-exam-answers.ts Outdated
Comment thread src/application/use-cases/submit-answer-exam.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/application/use-cases/finalize-exam-answers.ts (1)

304-348: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale-flush drop logic checks out; minor duplication nit.

Boundary math is correct — isAfterGraceWindow uses strict > and isWithinGraceWindow uses <= against the same deadline + FINALIZE_FLUSH_DEADLINE_GRACE_MS threshold, so there's no gap or overlap at the boundary. Silently dropping the flush and logging without userId is a good privacy-conscious call.

Minor: deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MS is computed twice (lines 327 and 342). Not a bug, just avoidable duplication.

♻️ Optional dedup
     const deadline = computeExamDeadline(session);
     const nowMs = now.getTime();
-    const isAfterGraceWindow =
-      deadline !== null &&
-      nowMs > deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MS;
+    const graceDeadlineMs =
+      deadline !== null ? deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MS : null;
+    const isAfterGraceWindow = graceDeadlineMs !== null && nowMs > graceDeadlineMs;
     if (isAfterGraceWindow) {
       ...
     }
-    const isWithinGraceWindow =
-      deadline !== null &&
-      nowMs >= deadline.getTime() &&
-      nowMs <= deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MS;
+    const isWithinGraceWindow =
+      deadline !== null && nowMs >= deadline.getTime() && graceDeadlineMs !== null && nowMs <= graceDeadlineMs;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/use-cases/finalize-exam-answers.ts` around lines 304 - 348,
The boundary logic in applyFinalDraftAnswer is correct, but the grace-deadline
timestamp is computed twice and should be deduplicated. Introduce a single
shared threshold value for deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MS
inside applyFinalDraftAnswer, then use that variable in both isAfterGraceWindow
and isWithinGraceWindow to keep the comparison logic consistent and easier to
maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/`(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx:
- Around line 199-237: The test for usePracticeSessionReviewStage’s
onFinalizeReview currently verifies only side effects and does not assert the
new Promise<boolean> return contract. Update the existing finalize-review spec
to capture the resolved value from onFinalizeReview and assert it is the
expected boolean for the tutor flow, while keeping the current checks on
endPracticeSessionMock, finalizeExamAnswersMock, and the summary state. This
should be done in the test named “awaits tutor finalization from the
finalize-review callback” so the contract is validated directly.

---

Outside diff comments:
In `@src/application/use-cases/finalize-exam-answers.ts`:
- Around line 304-348: The boundary logic in applyFinalDraftAnswer is correct,
but the grace-deadline timestamp is computed twice and should be deduplicated.
Introduce a single shared threshold value for deadline.getTime() +
FINALIZE_FLUSH_DEADLINE_GRACE_MS inside applyFinalDraftAnswer, then use that
variable in both isAfterGraceWindow and isWithinGraceWindow to keep the
comparison logic consistent and easier to maintain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa480f6d-76b6-4d06-ba74-5a7aa79f5dc2

📥 Commits

Reviewing files that changed from the base of the PR and between 44e71d0 and 1c13f8e.

📒 Files selected for processing (19)
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • src/application/errors/application-errors.ts
  • src/application/errors/index.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/domain/services/index.ts
  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: test
🧰 Additional context used
📓 Path-based instructions (30)
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Use constructor dependency injection in all classes; do not import singletons directly (e.g., use constructor(private db: DrizzleDb) instead of import { db } from './db')
Use ApplicationError with typed codes for all custom errors; do not use bare throw new Error('message')
Do not use magic numbers; use constants/configs instead
Dependencies must point inward only; domain layer has ZERO external imports, application layer depends only on domain, adapters depend on application
Bare catch {} blocks allowed only for intentionally suppressed secondary failures (telemetry, logging, rollback, parse fallback); add a short nearby comment and use shared application/adapters logger-safety helpers when 3+ files need the same wrapper

Files:

  • src/application/errors/index.ts
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • src/domain/services/index.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Wire dependencies at entry points via lib/container.ts, lib/container/**, and lib/controller-helpers.ts, not ad hoc imports inside use cases

Files:

  • src/application/errors/index.ts
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Keep modules small, prefer pure functions in lib/; avoid non-null assertions (!) and unused imports/variables (Biome errors); prefer importing via @/... alias

Files:

  • src/application/errors/index.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • src/domain/services/time-constants.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/domain/services/index.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome as the source of truth for code style: 2-space indents, single quotes, semicolons required, trailing commas; run pnpm lint:fix to auto-fix

Files:

  • src/application/errors/index.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • src/domain/services/time-constants.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/domain/services/index.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ApplicationError with typed codes for all error handling

Files:

  • src/application/errors/index.ts
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • src/domain/services/index.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Application layer (src/application/) must depend only on domain layer. Implement use cases and define port interfaces.

Files:

  • src/application/errors/index.ts
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**

⚙️ CodeRabbit configuration file

**: # AGENTS.md

Repository guidelines for AI coding agents (Codex CLI, Claude Code, Cursor, GitHub Copilot, etc.) working with this codebase.

This is the single source of truth for all agents. Claude Code also reads CLAUDE.md (slim, Claude-specific supplements) and .claude/rules/ (path-scoped rules). Keep universal project rules here; keep Claude-only or path-scoped guidance in those Claude-specific files so instructions do not drift or conflict.


⚠️ CRITICAL: React 19 + Vitest Testing Requirements

READ THIS FIRST. Tests will fail in git hooks/CI without these requirements.

For ALL .test.tsx files:

// `@vitest-environment` jsdom   ← MUST be first line
import { renderToStaticMarkup } from 'react-dom/server';
import { beforeAll, describe, expect, it } from 'vitest';

let MyComponent: typeof import('./MyComponent').default;

beforeAll(async () => {
  MyComponent = (await import('./MyComponent')).default;
});

// Use renderToStaticMarkup for render-output tests
describe('MyComponent', () => {
  it('renders output', () => {
    const html = renderToStaticMarkup(<MyComponent />);
    expect(html).toContain('Expected text');
  });
});

Why:

  • @testing-library/react has a known bug with React 19 + Vitest — no fix coming
  • Git hooks and CI load production builds where act() is undefined
  • renderToStaticMarkup is a stable first-party React API that works everywhere

Import placement + timeout policy (DEBT-225):

  • Keep dynamic imports, but load them in beforeAll (or beforeEach only when mock/module-reset order requires it)
  • Do not import heavy modules inside individual it() blocks
  • Do not add per-test timeout overrides (it(..., 10_000) or { timeout: 15_000 })
  • Global Vitest policy is configured in vitest.config.ts, vitest.browser.config.ts, and vitest.integration.config.ts

DO NOT USE for jsdom component t...

Files:

  • src/application/errors/index.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • src/domain/services/time-constants.ts
  • src/adapters/repositories/drizzle-attempt-repository.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/domain/services/index.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/**

⚙️ CodeRabbit configuration file

src/application/**: This is the APPLICATION layer (use cases + ports).
Dependencies flow INWARD only (can import from domain/).
Use cases orchestrate, they don't implement business rules.
Ports (interfaces) define contracts for adapters.
Tests should use fakes from src/application/test-helpers/fakes/, NOT vi.mock().

Files:

  • src/application/errors/index.ts
  • src/application/errors/application-errors.ts
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/test-helpers/fakes/fake-attempt-repository.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.browser.spec.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use vitest-browser-react in *.browser.spec.tsx files for interactive and async hook tests, run with pnpm test:browser

Use vitest-browser-react for *.browser.spec.tsx files and run with pnpm test:browser

**/*.browser.spec.tsx: Use *.browser.spec.tsx for testing hooks with async state transitions (useEffect, useState after await)
Use *.browser.spec.tsx for interactive UI testing (clicks, forms, user events)
Use *.browser.spec.tsx for tests that need real DOM behavior (not jsdom simulation)
Import render from 'vitest-browser-react' and expect, test from 'vitest' in browser mode tests
Use expect.element() with built-in retry-ability instead of manual polling for DOM assertions in browser mode tests
Do not use // @vitest-environment jsdom directive in browser mode test files
Mock server-action controllers with vi.mock() using { spy: true } in browser mode tests to handle Node-only code that cannot execute in Chromium
Keep hook inputs and callback refs stable in browser mode tests to avoid infinite React effect re-triggers
Use useMemo/useCallback in test wrapper components if needed to maintain stable references in browser mode tests

Hook tests (async/interactive) should be colocated with source files as *.browser.spec.tsx and run with pnpm test:browser

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx

⚙️ CodeRabbit configuration file

**/*.browser.spec.tsx: Browser mode tests use vitest-browser-react and run with pnpm test:browser.
Use real browser interactions and async expectations.
Do not suggest @testing-library/react or jsdom-only interaction patterns here.

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
{app,components}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

{app,components}/**/*.tsx: Never use raw hex colors (#fff, #121212) or palette colors (bg-zinc-400, text-slate-300) in .tsx UI code except documented third-party API seams; always use semantic tokens (bg-primary, text-foreground, border-border, etc.)
Use the canonical focus-ring pattern focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]; never hand-roll variants
All interactive click targets MUST use the <Button> component; raw <button> is allowed only inside components/ui/ primitives and app-shell disclosure toggles per Pattern Registry I-6
Use the canonical muted/layer-2 opacity scale (/20, /40, /50, /60) for bg-muted-class fills; documented foreground-ramp arbitrary values allowed only in Pattern Registry contexts; undocumented arbitrary values forbidden
Semantic tokens handle light/dark mode automatically; component-specific dark: overrides allowed only when they appear in pattern-registry.md or contrast-policy.md; duplicated dark overrides across 2+ components must promote to shared primitive or lib/shared-styles.ts

{app,components}/**/*.tsx: Use <Button> for ALL interactive click targets; raw <button> only inside components/ui/ and app-shell disclosure toggles per Pattern Registry I-6
Use single canonical focus ring: focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]
Use semantic tokens only in UI code; never raw hex or palette colors in .tsx files except documented third-party API seams
Use muted/layer-2 opacity scale: /20//40//50//60 from pattern-registry.md; documented foreground-ramp arbitrary values allowed only in documented Pattern Registry contexts; undocumented arbitrary values forbidden

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
**/*.{test.ts,test.tsx,spec.ts,spec.tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{test.ts,test.tsx,spec.ts,spec.tsx}: Use FakeXxxRepository from src/application/test-helpers/fakes/ instead of mocks in tests (fakes over mocks)
Write the test first before implementation (TDD mandatory)
Apply process.env snapshot/restore and vi.stubEnv cleanup with combined ordering in test files

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
app/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

For browser visual verification of /app/* pages requiring Clerk auth, use agent-browser --profile /tmp/clerk-profile with prior human login via --headed mode; close daemon with agent-browser close before re-opening with --profile

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
{app,lib,db}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Outermost layer (app/, lib/, db/) contains Next.js framework code and may depend on inner layers.

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/fixture-integrity.md)

**/*.{test,spec}.{ts,tsx}: Test fixtures must match production validators when they cross a real boundary. Application-owned IDs are UUID-shaped at the boundary: use zUuid = z.guid() in controller schemas and Drizzle uuid() columns in db/schema.ts. Use UUID-valid values for fixtures that model controller input/output DTOs, adapter repository rows, mocked SQL rows, mapper rows, app-auth userId values, E2E-helper app DB rows, and shared factory/fake-generated defaults.
Prefer UUID-emitting factories for ID fields. Otherwise use named, role-bearing variables like const userId = crypto.randomUUID() and const questionId = crypto.randomUUID() to make entity relationships clear in tests.
FIX UUID values that cross zUuid or Drizzle uuid() boundaries. LEAVE values that are not application-owned UUID fixtures: provider IDs (cus_, sub_, evt_, price_, Clerk, Svix), slugs, labels, HTML ids, data-testid, React-only keys, intentionally invalid negative-validation fixtures, readable fake-backed test keys, and fake-repository behavior-test keys.
Preserve entity relationships in test fixtures by reusing the same named UUID variable everywhere the same entity is referenced. For example, use the same questionId variable when creating a question, its choices, and attempts that reference it.
Capture generated IDs in test assertions and error strings. Do not assert old deterministic placeholders after switching a factory or fake to generated IDs.
Do not add as any, as unknown as, @ts-ignore, widened DTO types, or relaxed expectations to make a fixture fit. Use a production-shaped fixture instead.
Use vi.hoisted() for fixture values only when the value is read inside a vi.mock(path, () => ...) factory body. Do not use vi.hoisted() for UUID values passed to vi.mocked(controllerFn).mockResolvedValue(...), mockResolvedValueOnce(...), or mockImplementation(...) in normal beforeEach/it scope after a { spy: true } controller mock. Do not consistency-hoist ...

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
**/{app,components}/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

**/{app,components}/**/*.{tsx,jsx}: Use the canonical focus ring pattern: focus-visible:outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] for interactive elements OR use the <Button> component which has the ring built in
Use canonical opacity values from pattern-registry.md when using layer fills: /20 (tint), /40 (subtle hover), /50 (standard hover), /60 (exception-only), never use /80 or /100 for hover, and documented foreground-ramp values only in exact Pattern Registry contexts
All interactive click targets MUST use the <Button> component. Raw <button> elements are allowed only inside components/ui/ primitives and documented app-shell disclosure toggles (components/mobile-nav.tsx Pattern Registry I-6 exception)
Component-specific dark: CSS overrides are allowed ONLY when documented in pattern-registry.md or contrast-policy.md. If the same dark override appears in 2+ components, promote it into a shared primitive or constant in lib/shared-styles.ts
Import routes from lib/routes.ts using the ROUTES constant. NEVER hard-code route strings like '/app/dashboard'
Keep components small, targeting less than 300 lines per file
Use components/ui/ shadcn primitives and don't reinvent custom UI primitives
Use Server Components by default; add 'use client' directive only when necessary for client-side interactivity

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
**/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Use semantic tokens (e.g., bg-primary, text-foreground, text-muted-foreground, border-border) NEVER use raw hex colors or palette colors (e.g., bg-zinc-400, text-slate-300) in .tsx UI code except documented third-party API seams like Clerk appearance.variables

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
**/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Extract custom hooks into hooks/ subdirectories rather than keeping them in component files

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
**/{app,components,lib,hooks}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.claude/rules/frontend.md)

Use @/... alias for all imports instead of relative paths

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts}

📄 CodeRabbit inference engine (.claude/rules/testing.md)

**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts}: Use Vitest exclusively for testing. Do NOT use Jest APIs or jest.mock().
Import testing utilities from 'vitest': import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
Use existing fakes from src/application/test-helpers/fakes/ instead of vi.mock() for internal code. Consult src/application/test-helpers/fakes/index.ts for available fakes.
vi.mock() is ONLY acceptable for external SDKs (@clerk/nextjs, next/link, server-only) and Browser Mode sealed ESM modules (vi.mock(path, { spy: true }) for controller modules)
Tests that mutate process.env MUST snapshot/restore via tests/shared/process-env.ts and follow cleanup ordering rules from .claude/rules/test-isolation.md
Use factories createQuestion(), createChoice() from src/domain/test-helpers/ instead of manually constructing test objects
Use descriptive test names that describe the expected behavior (e.g., 'returns isCorrect=false when incorrect choice selected')

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.{test.ts,test.tsx,spec.tsx,integration.test.ts,helpers.ts}

📄 CodeRabbit inference engine (.claude/rules/testing.md)

Tests and test helpers that create boundary-shaped fixtures MUST keep application-owned IDs valid at controller/DB boundaries and MUST leave provider IDs, fake-backed semantic keys, UI tokens, and intentional-invalid fixtures alone per .claude/rules/fixture-integrity.md

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • src/application/use-cases/finalize-exam-answers.test.ts
app/**

⚙️ CodeRabbit configuration file

app/**: Next.js App Router with Server Components and Server Actions.
Server Actions must be in files with 'use server' directive.
Client components must have 'use client' directive.
Use composition root pattern - wire dependencies at entry points.

Files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use dynamic imports in beforeAll (or beforeEach only when mock/module-reset order requires it) to load test modules; do not import heavy modules inside individual it() blocks
Do not add per-test timeout overrides (it(..., 10_000) or { timeout: 15_000 }); global Vitest policy is configured in vitest.config.ts, vitest.browser.config.ts, and vitest.integration.config.ts
Use fake repository/gateway classes from src/application/test-helpers/fakes/ (e.g., FakeAttemptRepository, FakeAuthGateway) instead of vi.mock() for our own code; only use vi.mock() for external packages like Drizzle, Clerk, or Stripe SDK
Use vi.fn() inline objects only for external dependencies (Drizzle db, Clerk, Stripe SDK) that cannot be dependency-injected; use { spy: true } for sealed ESM namespaces like server-action controllers
Never use vi.mock() for our own code in tests; use fake classes from src/application/test-helpers/fakes/ instead (e.g., FakeQuestionRepository, FakeAttemptRepository)

Files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use plain Vitest for *.test.ts files without environment directive

Unit tests (logic) should be colocated with source files as *.test.ts and run with pnpm test

Files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts

⚙️ CodeRabbit configuration file

**/*.test.ts: TESTING CONVENTIONS (Vitest, NOT Jest):

  1. Use fakes via dependency injection, NOT vi.mock() for our own code
  2. vi.mock() is ONLY acceptable for external packages (Clerk, Next.js, server-only)
  3. vi.fn() inside fake objects for spying is CORRECT
  4. One concept per test, use Arrange-Act-Assert pattern
  5. Test behavior, not implementation details
  6. Use factories: createQuestion(), createChoice() from test-helpers/
  7. Do NOT require dynamic import()/beforeAll loading in pure TypeScript .test.ts suites.
    That React 19 compatibility rule is scoped to **/
    .test.tsx component tests.
  8. FIXTURE IDs (see .claude/rules/fixture-integrity.md): in fakes-only
    application/domain/use-case tests, readable string IDs such as 'q1',
    'user-1', or 'attempt-1' are INTENTIONAL and correct — do NOT suggest
    replacing them with UUIDs or crypto.randomUUID(). UUID-shaped fixtures are
    required ONLY where an ID crosses a zUuid/Drizzle uuid() boundary, which is
    covered in adapter/controller tests (those legitimately assert UUID shape and
    the 'not-a-uuid' rejection path). Flagging readable fake-backed IDs in
    fakes-only tests is a known recurring false positive in this repo.

Files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
src/application/test-helpers/fakes/**

📄 CodeRabbit inference engine (.claude/rules/testing.md)

Create fakes in src/application/test-helpers/fakes/ when no fake exists for internal code, then use them instead of vi.mock()

Files:

  • src/application/test-helpers/fakes/fake-attempt-repository.ts
src/domain/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/domain/**/*.ts: Domain entities must have ZERO vendor identifiers; external IDs (Clerk user ID, Stripe subscription ID) exist only in the persistence layer
Use vendor-agnostic value objects (e.g., SubscriptionPlan for monthly/annual) in domain layer, not Stripe price IDs; mapping happens in adapters

src/domain/**/*.ts: Domain layer must have ZERO external imports - no framework code (Next.js, React, Drizzle, Clerk, Stripe), no infrastructure (database, HTTP, file system), no application layer imports
Domain layer should only import TypeScript built-ins and other domain modules
Do NOT place database queries or ORM calls in the domain layer
Do NOT place API calls or HTTP concerns in the domain layer
Do NOT place UI components or React code in the domain layer

Files:

  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/domain/services/index.ts
src/domain/**

📄 CodeRabbit inference engine (CLAUDE.md)

Domain layer must follow zero-import purity rules (no external imports in src/domain/)

Files:

  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/domain/services/index.ts

⚙️ CodeRabbit configuration file

src/domain/**: This is the DOMAIN layer (Clean Architecture innermost layer).
MUST have ZERO external dependencies - only pure TypeScript.
Entities must not contain vendor IDs (Clerk, Stripe).
Value objects must be immutable with factory functions.
Domain services must be pure functions.
All domain code MUST be 100% unit testable without mocks.

Files:

  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/domain/services/index.ts
src/domain/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Domain layer (src/domain/) must have ZERO external imports. Contains only pure business logic.

Files:

  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/domain/services/index.ts
src/domain/**/*.test.ts

📄 CodeRabbit inference engine (.claude/rules/domain-layer.md)

Colocate unit tests with source code using the *.test.ts file naming convention in the domain layer

Files:

  • src/domain/services/time-constants.test.ts
src/adapters/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.claude/rules/architecture.md)

Adapters layer (src/adapters/) must depend only on application layer. Implement repositories, gateways, and controllers.

Files:

  • src/adapters/repositories/drizzle-attempt-repository.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
src/adapters/**

⚙️ CodeRabbit configuration file

src/adapters/**: This is the ADAPTERS layer (repositories, gateways, controllers).
Implements ports defined in application layer.
Controllers handle validation, error mapping, and orchestration.
Repositories wrap database operations.
Gateways wrap external services (Clerk, Stripe).
Tests MUST use fake DB objects via DI, NOT vi.mock() on our own code.

Files:

  • src/adapters/repositories/drizzle-attempt-repository.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
🧠 Learnings (10)
📚 Learning: 2026-02-09T17:21:25.626Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 78
File: app/(app)/app/practice/practice-page-client.tsx:96-98
Timestamp: 2026-02-09T17:21:25.626Z
Learning: Actionable guideline: In practice-related TSX components, distinguish external pending states (isPending) from internal loading state (e.g., sessionStartStatus === 'loading'). Ensure isPending is used only for external pending states (such as data/question loading initiated outside the component), while internal loading should be represented by dedicated state flags. For the given files, practice-page-client.tsx should use isPending={false} since there is no external pending state after removing the question flow. If you encounter a similar component, confirm whether isPending reflects an external pending condition or rely on internal loading state flags instead, and set props accordingly.

Applied to files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
📚 Learning: 2026-05-04T20:31:16.507Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 306
File: app/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow-click-commit.browser.spec.tsx:10-10
Timestamp: 2026-05-04T20:31:16.507Z
Learning: In app/(app)/app/practice/[sessionId]/hooks/, follow the established import convention: when a .test.tsx, .browser.spec.tsx, or .probes.tsx file imports the SUT from the same directory, use a relative import (e.g., "./use-practice-session-question-flow"). Use the "`@/`..." alias only for cross-slice (non-same-directory) imports. Do not flag relative same-directory SUT imports in this hooks directory as guideline violations.

Applied to files:

  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx
  • app/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsx
📚 Learning: 2026-03-07T17:42:02.738Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 178
File: components/marketing/marketing-home.test.tsx:33-35
Timestamp: 2026-03-07T17:42:02.738Z
Learning: In test files across the repository, do not flag or suggest extracting duplicated local helpers like getClassTokens(className: string): Set<string> unless there is a repo-wide test-helper cleanup ticket. Only consider extraction when a dedicated, cross-cutting refactor scope is approved. Do not perform partial extractions scoped to a single feature ticket, as per maintainer policy.

Applied to files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-03-21T18:11:54.932Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 243
File: components/marketing/marketing-layout.test.tsx:44-64
Timestamp: 2026-03-21T18:11:54.932Z
Learning: In Node.js environments that support runtime TZ changes (Node >= 13 on POSIX; Node >= 16.2 on Windows), mutating `process.env.TZ` inside a test case is a valid way to simulate a non-UTC runtime. This should reliably affect subsequent `Date` calls within that test, so review comments should not flag `process.env.TZ` mid-test mutations as ineffective when the test is explicitly asserting UTC-based date logic.

Applied to files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-04-25T21:05:17.378Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 287
File: src/application/use-cases/save-exam-draft-answer.ts:8-8
Timestamp: 2026-04-25T21:05:17.378Z
Learning: Within `src/application/use-cases/`, sibling use-case modules should use same-directory relative imports (e.g., `./save-exam-draft-answer`, `./practice-session-summary`, `./submit-answer`) for intra-use-case references. Treat this as the established local convention and do not recommend converting these sibling relative imports to the `@/...` path alias.

Applied to files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-06-19T15:08:41.673Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 464
File: src/application/use-cases/discard-practice-session.test.ts:1-1
Timestamp: 2026-06-19T15:08:41.673Z
Learning: In this repo, the dynamic `import()` + `beforeAll` module-loading convention is for React component tests (`**/*.test.tsx`). For pure TypeScript tests (`**/*.test.ts`, e.g. use-case/unit tests), use static top-level imports by convention and do NOT flag static imports as a violation of the dynamic-import convention.

Applied to files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/domain/services/time-constants.test.ts
  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • src/adapters/repositories/drizzle-attempt-repository.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-06-30T01:19:56.106Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 540
File: src/application/use-cases/set-bookmark.test.ts:11-12
Timestamp: 2026-06-30T01:19:56.106Z
Learning: In application-layer use-case tests that run fully behind fake repositories (i.e., they do not cross a real adapter/schema/database boundary), fixtures may use readable fake-backed semantic IDs (e.g., `user-1`, `q1`) and should not be flagged for missing UUID conversion. Only require UUID-valid application-owned IDs at real boundaries—for example, controller/request validation using `zUuid` or persistence/column typing via Drizzle `uuid()`—where inputs are validated or stored in real adapters. For bookmarks, ensure UUID-boundary coverage exists in `src/adapters/controllers/bookmark-controller.test.ts`, not in `src/application/use-cases/set-bookmark.test.ts`.

Applied to files:

  • src/application/use-cases/submit-answer-exam.test.ts
  • src/application/use-cases/submit-answer-tutor.test.ts
  • src/application/use-cases/finalize-exam-answers.test.ts
📚 Learning: 2026-03-18T13:32:20.731Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 233
File: src/domain/services/statistics.ts:1-3
Timestamp: 2026-03-18T13:32:20.731Z
Learning: Within the src/domain/services directory, sibling modules should import using relative paths (e.g., ./time-constants) rather than the `@/`... alias. This is an established local convention for intra-domain references. Do not flag or suggest converting these intra-domain relative imports to alias imports. Apply this pattern to all .ts files in src/domain/services.

Applied to files:

  • src/domain/services/time-constants.test.ts
  • src/domain/services/time-constants.ts
  • src/domain/services/index.ts
📚 Learning: 2026-03-03T17:31:31.300Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 165
File: app/(app)/app/practice/shared/question-flow-actions.ts:18-31
Timestamp: 2026-03-03T17:31:31.300Z
Learning: In app-layer orchestration helpers under app/practice/shared, throw plain Error for programmer misuse (e.g., internal invariants like assertRequestSequencingHooks) and reserve ApplicationError with typed codes for domain/business failures. This guideline applies to all TypeScript files in that shared orchestration layer to distinguish internal programmer mistakes from user/domain errors.

Applied to files:

  • app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts
  • app/(app)/app/practice/shared/question-flow-actions.ts
📚 Learning: 2026-07-02T03:09:23.233Z
Learnt from: The-Obstacle-Is-The-Way
Repo: The-Obstacle-Is-The-Way/naltrexone-university PR: 537
File: src/adapters/repositories/drizzle-practice-session-repository-reads.test.ts:413-462
Timestamp: 2026-07-02T03:09:23.233Z
Learning: In test files under src/adapters/repositories (e.g., *.test.ts), avoid extracting/generalizing shared DB/tx stubbing helpers when the duplicated stub pattern appears only twice. Use a rule-of-three: only propose the extraction when a third instance of the same stub shape appears, and the person adding that third instance should perform the extraction then (don’t pre-generalize for a hypothetical future use). This applies to patterns like repeatable-read DB/tx stubs with optional state-select overrides.

Applied to files:

  • src/adapters/repositories/drizzle-attempt-repository.test.ts
🔇 Additional comments (29)
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsx (1)

9-9: LGTM!

Also applies to: 123-145

src/application/use-cases/finalize-exam-answers.ts (4)

3-3: LGTM!

Also applies to: 17-17, 45-45


73-79: Centralization fix confirmed.

isAttemptAlreadyAnsweredConflict now matches AttemptConflictMessages.AlreadyAnsweredInSession instead of a duplicated literal string, resolving the prior review request to avoid drift between this use case and the attempt repository/fake.


104-111: LGTM!


273-303: 🗄️ Data Integrity & Integration

No change needed.

app/(app)/app/practice/shared/question-flow-actions.ts (2)

65-86: Duplication resolved.

tryRecoverEndedSessionConflict cleanly replaces the two inline recovery blocks flagged in the prior review pass, with a 'stale-request' outcome added for the unmounted/superseded case. Both call sites (Line 156, Line 372) now share identical logic.


96-187: LGTM!

Also applies to: 304-389

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.ts (3)

43-49: Grace window is now shared with the server.

Prior nitpick about the hardcoded 15s duplicate is resolved — this now derives from the domain constant with a clear comment on why the client copy still exists (optimization only, server remains authoritative).


336-393: Dedup fix looks solid.

recoverEndedSessionSummaryPromiseRef correctly caches and clears the in-flight promise, addressing the previously flagged double-fetch/apply risk when load and submit both hit an AlreadyEnded conflict concurrently.


88-93: 🩺 Stability & Availability

Drop this race concern. Both expiry paths funnel through reviewStage.finalizeExamSession and share finalizeExamIdempotencyKeyRef, so they do not use separate finalize keys. The server-side use case also turns the concurrent loser into AlreadyEnded, so this overlap is handled.

			> Likely an incorrect or invalid review comment.
app/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.ts (1)

1-151: LGTM!

Good coverage of both structured (details.reason) and reasonless CONFLICT paths for both runLoadQuestionFlow and runSubmitAnswerFlow, and the prior vi.hoisted() nitpick is resolved.

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsx (1)

1-225: LGTM!

Good coverage of the recovery success/failure/reject/dedup matrix for both load and submit conflict paths, and the dedup test (Lines 192-224) directly exercises the fix in use-practice-session-page-model.ts.

app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsx (1)

5-5: LGTM!

Also applies to: 426-426

src/application/errors/application-errors.ts (1)

32-36: LGTM!

src/application/errors/index.ts (1)

6-6: LGTM!

src/application/test-helpers/fakes/fake-attempt-repository.ts (1)

1-4: LGTM!

Also applies to: 49-49

src/adapters/repositories/drizzle-attempt-repository.ts (1)

21-24: LGTM!

Also applies to: 196-196

src/adapters/repositories/drizzle-attempt-repository.test.ts (1)

8-11: LGTM!

Also applies to: 486-486

src/application/use-cases/submit-answer-exam.test.ts (2)

2-5: LGTM!


133-137: LGTM! Resolves the previously flagged hardcoded-message drift risk by asserting against PracticeSessionConflictMessages.AlreadyEnded and PracticeSessionConflictReasons.AlreadyEnded.

src/application/use-cases/submit-answer-tutor.test.ts (3)

2-6: LGTM!


217-221: LGTM! Same drift-safe pattern applied here as in submit-answer-exam.test.ts.


360-360: LGTM!

src/application/use-cases/finalize-exam-answers.test.ts (3)

50-76: LGTM!


731-796: LGTM!

Also applies to: 798-855


1060-1073: LGTM!

Also applies to: 1225-1290

src/domain/services/time-constants.ts (1)

3-3: LGTM!

src/domain/services/index.ts (1)

45-51: LGTM!

src/domain/services/time-constants.test.ts (1)

2-12: LGTM!

@The-Obstacle-Is-The-Way

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@The-Obstacle-Is-The-Way

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@The-Obstacle-Is-The-Way
The-Obstacle-Is-The-Way merged commit a3be333 into dev Jul 6, 2026
6 checks passed
@The-Obstacle-Is-The-Way
The-Obstacle-Is-The-Way deleted the fix/finalize-tutor-conflict-reasons branch July 6, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant