Fix finalize and tutor conflict reason recovery - #563
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 40 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughThis 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. ChangesTimer retry, conflict recovery, and finalize flow
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
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
src/application/use-cases/submit-answer-tutor.test.ts (1)
213-217: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueSame hardcoded-message duplication as the exam test counterpart.
Same concern as
submit-answer-exam.test.ts— asserting against the literal string instead ofPracticeSessionConflictMessages.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
📒 Files selected for processing (20)
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/practice-session-page-logic.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tsapp/(app)/app/practice/shared/question-flow-actions.tslib/container/use-cases.tssrc/application/use-cases/finalize-exam-answers.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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 inbeforeAll(orbeforeEachonly when mock/module-reset order requires it) to load test modules; do not import heavy modules inside individualit()blocks
Do not add per-test timeout overrides (it(..., 10_000)or{ timeout: 15_000 }); global Vitest policy is configured invitest.config.ts,vitest.browser.config.ts, andvitest.integration.config.ts
Use fake repository/gateway classes fromsrc/application/test-helpers/fakes/(e.g.,FakeAttemptRepository,FakeAuthGateway) instead ofvi.mock()for our own code; only usevi.mock()for external packages like Drizzle, Clerk, or Stripe SDK
Usevi.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 usevi.mock()for our own code in tests; use fake classes fromsrc/application/test-helpers/fakes/instead (e.g.,FakeQuestionRepository,FakeAttemptRepository)
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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., useconstructor(private db: DrizzleDb)instead ofimport { db } from './db')
UseApplicationErrorwith typed codes for all custom errors; do not use barethrow 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
Barecatch {}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.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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/**, andlib/controller-helpers.ts, not ad hoc imports inside use cases
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tslib/container/use-cases.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tssrc/application/use-cases/finalize-exam-answers.test.tsapp/(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:fixto auto-fix
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tslib/container/use-cases.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tssrc/application/use-cases/finalize-exam-answers.test.tsapp/(app)/app/practice/[sessionId]/practice-session-page-logic.ts
**/*.test.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Use plain Vitest for
*.test.tsfiles without environment directiveUnit tests (logic) should be colocated with source files as
*.test.tsand run withpnpm test
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.test.ts
⚙️ CodeRabbit configuration file
**/*.test.ts: TESTING CONVENTIONS (Vitest, NOT Jest):
- Use fakes via dependency injection, NOT vi.mock() for our own code
- vi.mock() is ONLY acceptable for external packages (Clerk, Next.js, server-only)
- vi.fn() inside fake objects for spying is CORRECT
- One concept per test, use Arrange-Act-Assert pattern
- Test behavior, not implementation details
- Use factories: createQuestion(), createChoice() from test-helpers/
- 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.- 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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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}: UseFakeXxxRepositoryfromsrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxsrc/application/use-cases/finalize-exam-answers.test.ts
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use
ApplicationErrorwith typed codes for all error handling
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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: usezUuid = z.guid()in controller schemas and Drizzleuuid()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 likeconst userId = crypto.randomUUID()andconst questionId = crypto.randomUUID()to make entity relationships clear in tests.
FIX UUID values that crosszUuidor Drizzleuuid()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 samequestionIdvariable 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 addas any,as unknown as,@ts-ignore, widened DTO types, or relaxed expectations to make a fixture fit. Use a production-shaped fixture instead.
Usevi.hoisted()for fixture values only when the value is read inside avi.mock(path, () => ...)factory body. Do not usevi.hoisted()for UUID values passed tovi.mocked(controllerFn).mockResolvedValue(...),mockResolvedValueOnce(...), ormockImplementation(...)in normalbeforeEach/itscope after a{ spy: true }controller mock. Do not consistency-hoist ...
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxsrc/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 orjest.mock().
Import testing utilities from 'vitest':import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
Use existing fakes fromsrc/application/test-helpers/fakes/instead ofvi.mock()for internal code. Consultsrc/application/test-helpers/fakes/index.tsfor 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 mutateprocess.envMUST snapshot/restore viatests/shared/process-env.tsand follow cleanup ordering rules from.claude/rules/test-isolation.md
Use factoriescreateQuestion(),createChoice()fromsrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxsrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxsrc/application/use-cases/finalize-exam-answers.test.ts
**
⚙️ CodeRabbit configuration file
**: # AGENTS.mdRepository 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 RequirementsREAD THIS FIRST. Tests will fail in git hooks/CI without these requirements.
For ALL
.test.tsxfiles:// `@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/reacthas a known bug with React 19 + Vitest — no fix coming- Git hooks and CI load production builds where
act()is undefinedrenderToStaticMarkupis a stable first-party React API that works everywhereImport placement + timeout policy (DEBT-225):
- Keep dynamic imports, but load them in
beforeAll(orbeforeEachonly 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, andvitest.integration.config.tsDO NOT USE for jsdom component t...
Files:
src/application/use-cases/get-next-question-explicit-question.test.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/use-cases/submit-answer-tutor.test.tslib/container/use-cases.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tssrc/application/use-cases/finalize-exam-answers.test.tsapp/(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.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxlib/container/use-cases.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tsapp/(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.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxlib/container/use-cases.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tsapp/(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.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.tsapp/(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.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage-state.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-question-flow.ts
**/*.browser.spec.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use
vitest-browser-reactin*.browser.spec.tsxfiles for interactive and async hook tests, run withpnpm test:browserUse
vitest-browser-reactfor*.browser.spec.tsxfiles and run withpnpm test:browser
**/*.browser.spec.tsx: Use*.browser.spec.tsxfor testing hooks with async state transitions (useEffect,useStateafterawait)
Use*.browser.spec.tsxfor interactive UI testing (clicks, forms, user events)
Use*.browser.spec.tsxfor tests that need real DOM behavior (not jsdom simulation)
Importrenderfrom 'vitest-browser-react' andexpect,testfrom 'vitest' in browser mode tests
Useexpect.element()with built-in retry-ability instead of manual polling for DOM assertions in browser mode tests
Do not use//@vitest-environmentjsdomdirective in browser mode test files
Mock server-action controllers withvi.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
UseuseMemo/useCallbackin test wrapper components if needed to maintain stable references in browser mode testsHook tests (async/interactive) should be colocated with source files as
*.browser.spec.tsxand run withpnpm test:browser
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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/reactor jsdom-only interaction patterns here.
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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.tsxUI code except documented third-party API seams; always use semantic tokens (bg-primary,text-foreground,border-border, etc.)
Use the canonical focus-ring patternfocus-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 insidecomponents/ui/primitives and app-shell disclosure toggles per Pattern Registry I-6
Use the canonical muted/layer-2 opacity scale (/20,/40,/50,/60) forbg-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-specificdark:overrides allowed only when they appear inpattern-registry.mdorcontrast-policy.md; duplicated dark overrides across 2+ components must promote to shared primitive orlib/shared-styles.ts
{app,components}/**/*.tsx: Use<Button>for ALL interactive click targets; raw<button>only insidecomponents/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.tsxfiles except documented third-party API seams
Use muted/layer-2 opacity scale:/20//40//50//60frompattern-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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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, useagent-browser --profile /tmp/clerk-profilewith prior human login via--headedmode; close daemon withagent-browser closebefore re-opening with--profile
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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/80or/100for 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 insidecomponents/ui/primitives and documented app-shell disclosure toggles (components/mobile-nav.tsx Pattern Registry I-6 exception)
Component-specificdark: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 inlib/shared-styles.ts
Import routes fromlib/routes.tsusing the ROUTES constant. NEVER hard-code route strings like '/app/dashboard'
Keep components small, targeting less than 300 lines per file
Usecomponents/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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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.tsxUI code except documented third-party API seams like Clerkappearance.variables
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/get-next-question.tssrc/application/use-cases/submit-answer.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/application/use-cases/get-next-question-navigation.test.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tsapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(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 & AvailabilityNo sync-throw path in the current caller.
useExamTimeris only wired tofinalizeExpiredExam, 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 & AvailabilityTimer 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 structuredAlreadyEndedrecovery 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!recoverEndedSessionConflictis correctly threaded into bothloadQuestionConfigandcommitChoice, with matching dependency-array updates on eachuseMemo/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 CorrectnessLogger argument order is correct
primitives.loggeris passed in the finalFinalizeExamAnswersUseCaseconstructor 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.
throwAlreadyEndedForDoubleFinalizeLosercorrectly 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 viathrow error;. Theasynccatch 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 existingisWithinGraceWindow(<=) at the samedeadline + FINALIZE_FLUSH_DEADLINE_GRACE_MSboundary 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 thePick<Logger, 'warn'>context shape used in the use case.src/application/use-cases/get-next-question.ts (1)
175-177: LGTM! Consistent with the structuredAlreadyEndedconflict 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-returningfinalizeExpiredExamcorrectly resetsexpiryFinalizeInFlightRefonly on failure/error and forwards the outcome to the timer for retry semantics.
335-372: 🎯 Functional CorrectnessCheck post-exam back-navigation state The post-exam review surface may let users jump back into a question while
postExamSummaryis 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 newAlreadyEndedconflict recovery tests for tutorgetNextQuestionandsubmitAnswercorrectly exercise the recovery-to-summary path and match the underlyingquestion-flow-actions.tsrecovery 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 andfinalizeSessionSafely'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-returningreviewStage.onFinalizeReview()while preserving this function's ownPromise<void>contract.app/(app)/app/practice/[sessionId]/practice-session-page-logic.ts (1)
3-3: LGTM! TheendSessionboolean contract is threaded correctly through every return path, andrecoverEndedSessionConflictis 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 QualityNo action needed: shared
afterEach(() => vi.useRealTimers())already resets timers for this file.> Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
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 valueStale-flush drop logic checks out; minor duplication nit.
Boundary math is correct —
isAfterGraceWindowuses strict>andisWithinGraceWindowuses<=against the samedeadline + FINALIZE_FLUSH_DEADLINE_GRACE_MSthreshold, so there's no gap or overlap at the boundary. Silently dropping the flush and logging withoutuserIdis a good privacy-conscious call.Minor:
deadline.getTime() + FINALIZE_FLUSH_DEADLINE_GRACE_MSis 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
📒 Files selected for processing (19)
app/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tsapp/(app)/app/practice/shared/question-flow-actions.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/adapters/repositories/drizzle-attempt-repository.tssrc/application/errors/application-errors.tssrc/application/errors/index.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/application/use-cases/finalize-exam-answers.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/domain/services/index.tssrc/domain/services/time-constants.test.tssrc/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., useconstructor(private db: DrizzleDb)instead ofimport { db } from './db')
UseApplicationErrorwith typed codes for all custom errors; do not use barethrow 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
Barecatch {}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.tssrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/domain/services/time-constants.test.tssrc/domain/services/time-constants.tssrc/adapters/repositories/drizzle-attempt-repository.tssrc/domain/services/index.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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/**, andlib/controller-helpers.ts, not ad hoc imports inside use cases
Files:
src/application/errors/index.tssrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxsrc/domain/services/time-constants.tssrc/adapters/repositories/drizzle-attempt-repository.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/domain/services/index.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/shared/question-flow-actions.tssrc/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:fixto auto-fix
Files:
src/application/errors/index.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxsrc/domain/services/time-constants.tssrc/adapters/repositories/drizzle-attempt-repository.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/domain/services/index.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/shared/question-flow-actions.tssrc/application/use-cases/finalize-exam-answers.test.ts
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use
ApplicationErrorwith typed codes for all error handling
Files:
src/application/errors/index.tssrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/domain/services/time-constants.test.tssrc/domain/services/time-constants.tssrc/adapters/repositories/drizzle-attempt-repository.tssrc/domain/services/index.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tssrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/application/use-cases/finalize-exam-answers.test.ts
**
⚙️ CodeRabbit configuration file
**: # AGENTS.mdRepository 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 RequirementsREAD THIS FIRST. Tests will fail in git hooks/CI without these requirements.
For ALL
.test.tsxfiles:// `@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/reacthas a known bug with React 19 + Vitest — no fix coming- Git hooks and CI load production builds where
act()is undefinedrenderToStaticMarkupis a stable first-party React API that works everywhereImport placement + timeout policy (DEBT-225):
- Keep dynamic imports, but load them in
beforeAll(orbeforeEachonly 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, andvitest.integration.config.tsDO NOT USE for jsdom component t...
Files:
src/application/errors/index.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxsrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxsrc/domain/services/time-constants.tssrc/adapters/repositories/drizzle-attempt-repository.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/domain/services/index.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tssrc/application/use-cases/finalize-exam-answers.tsapp/(app)/app/practice/shared/question-flow-actions.tssrc/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.tssrc/application/errors/application-errors.tssrc/application/use-cases/submit-answer-exam.test.tssrc/application/test-helpers/fakes/fake-attempt-repository.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/application/use-cases/finalize-exam-answers.test.ts
**/*.browser.spec.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use
vitest-browser-reactin*.browser.spec.tsxfiles for interactive and async hook tests, run withpnpm test:browserUse
vitest-browser-reactfor*.browser.spec.tsxfiles and run withpnpm test:browser
**/*.browser.spec.tsx: Use*.browser.spec.tsxfor testing hooks with async state transitions (useEffect,useStateafterawait)
Use*.browser.spec.tsxfor interactive UI testing (clicks, forms, user events)
Use*.browser.spec.tsxfor tests that need real DOM behavior (not jsdom simulation)
Importrenderfrom 'vitest-browser-react' andexpect,testfrom 'vitest' in browser mode tests
Useexpect.element()with built-in retry-ability instead of manual polling for DOM assertions in browser mode tests
Do not use//@vitest-environmentjsdomdirective in browser mode test files
Mock server-action controllers withvi.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
UseuseMemo/useCallbackin test wrapper components if needed to maintain stable references in browser mode testsHook tests (async/interactive) should be colocated with source files as
*.browser.spec.tsxand run withpnpm test:browser
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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/reactor jsdom-only interaction patterns here.
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tsxUI code except documented third-party API seams; always use semantic tokens (bg-primary,text-foreground,border-border, etc.)
Use the canonical focus-ring patternfocus-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 insidecomponents/ui/primitives and app-shell disclosure toggles per Pattern Registry I-6
Use the canonical muted/layer-2 opacity scale (/20,/40,/50,/60) forbg-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-specificdark:overrides allowed only when they appear inpattern-registry.mdorcontrast-policy.md; duplicated dark overrides across 2+ components must promote to shared primitive orlib/shared-styles.ts
{app,components}/**/*.tsx: Use<Button>for ALL interactive click targets; raw<button>only insidecomponents/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.tsxfiles except documented third-party API seams
Use muted/layer-2 opacity scale:/20//40//50//60frompattern-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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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}: UseFakeXxxRepositoryfromsrc/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.tsxsrc/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxsrc/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, useagent-browser --profile /tmp/clerk-profilewith prior human login via--headedmode; close daemon withagent-browser closebefore re-opening with--profile
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(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: usezUuid = z.guid()in controller schemas and Drizzleuuid()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 likeconst userId = crypto.randomUUID()andconst questionId = crypto.randomUUID()to make entity relationships clear in tests.
FIX UUID values that crosszUuidor Drizzleuuid()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 samequestionIdvariable 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 addas any,as unknown as,@ts-ignore, widened DTO types, or relaxed expectations to make a fixture fit. Use a production-shaped fixture instead.
Usevi.hoisted()for fixture values only when the value is read inside avi.mock(path, () => ...)factory body. Do not usevi.hoisted()for UUID values passed tovi.mocked(controllerFn).mockResolvedValue(...),mockResolvedValueOnce(...), ormockImplementation(...)in normalbeforeEach/itscope 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.tsxsrc/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxsrc/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/80or/100for 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 insidecomponents/ui/primitives and documented app-shell disclosure toggles (components/mobile-nav.tsx Pattern Registry I-6 exception)
Component-specificdark: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 inlib/shared-styles.ts
Import routes fromlib/routes.tsusing the ROUTES constant. NEVER hard-code route strings like '/app/dashboard'
Keep components small, targeting less than 300 lines per file
Usecomponents/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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tsxUI code except documented third-party API seams like Clerkappearance.variables
Files:
app/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-init-load.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(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 orjest.mock().
Import testing utilities from 'vitest':import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
Use existing fakes fromsrc/application/test-helpers/fakes/instead ofvi.mock()for internal code. Consultsrc/application/test-helpers/fakes/index.tsfor 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 mutateprocess.envMUST snapshot/restore viatests/shared/process-env.tsand follow cleanup ordering rules from.claude/rules/test-isolation.md
Use factoriescreateQuestion(),createChoice()fromsrc/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.tsxsrc/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxsrc/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.tsxsrc/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxsrc/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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tsapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-review-stage.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model.tsapp/(app)/app/practice/shared/question-flow-actions.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.test.{ts,tsx}: Use dynamic imports inbeforeAll(orbeforeEachonly when mock/module-reset order requires it) to load test modules; do not import heavy modules inside individualit()blocks
Do not add per-test timeout overrides (it(..., 10_000)or{ timeout: 15_000 }); global Vitest policy is configured invitest.config.ts,vitest.browser.config.ts, andvitest.integration.config.ts
Use fake repository/gateway classes fromsrc/application/test-helpers/fakes/(e.g.,FakeAttemptRepository,FakeAuthGateway) instead ofvi.mock()for our own code; only usevi.mock()for external packages like Drizzle, Clerk, or Stripe SDK
Usevi.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 usevi.mock()for our own code in tests; use fake classes fromsrc/application/test-helpers/fakes/instead (e.g.,FakeQuestionRepository,FakeAttemptRepository)
Files:
src/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Use plain Vitest for
*.test.tsfiles without environment directiveUnit tests (logic) should be colocated with source files as
*.test.tsand run withpnpm test
Files:
src/application/use-cases/submit-answer-exam.test.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.test.ts
⚙️ CodeRabbit configuration file
**/*.test.ts: TESTING CONVENTIONS (Vitest, NOT Jest):
- Use fakes via dependency injection, NOT vi.mock() for our own code
- vi.mock() is ONLY acceptable for external packages (Clerk, Next.js, server-only)
- vi.fn() inside fake objects for spying is CORRECT
- One concept per test, use Arrange-Act-Assert pattern
- Test behavior, not implementation details
- Use factories: createQuestion(), createChoice() from test-helpers/
- 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.- 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.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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 ofvi.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.,SubscriptionPlanfor 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.tssrc/domain/services/time-constants.tssrc/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.tssrc/domain/services/time-constants.tssrc/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.tssrc/domain/services/time-constants.tssrc/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.tssrc/domain/services/time-constants.tssrc/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.tssrc/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.tssrc/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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tsxapp/(app)/app/practice/[sessionId]/hooks/use-exam-timer.browser.spec.tsxapp/(app)/app/practice/[sessionId]/hooks/use-practice-session-page-model-ended-session-conflict.browser.spec.tsxapp/(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.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/application/use-cases/finalize-exam-answers.tssrc/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.tssrc/domain/services/time-constants.test.tsapp/(app)/app/practice/shared/question-flow-actions-ended-session-conflict.test.tssrc/adapters/repositories/drizzle-attempt-repository.test.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/application/use-cases/submit-answer-tutor.test.tssrc/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.tssrc/domain/services/time-constants.tssrc/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.tsapp/(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.
isAttemptAlreadyAnsweredConflictnow matchesAttemptConflictMessages.AlreadyAnsweredInSessioninstead 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 & IntegrationNo change needed.
app/(app)/app/practice/shared/question-flow-actions.ts (2)
65-86: Duplication resolved.
tryRecoverEndedSessionConflictcleanly 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.
recoverEndedSessionSummaryPromiseRefcorrectly caches and clears the in-flight promise, addressing the previously flagged double-fetch/apply risk when load and submit both hit anAlreadyEndedconflict concurrently.
88-93: 🩺 Stability & AvailabilityDrop this race concern. Both expiry paths funnel through
reviewStage.finalizeExamSessionand sharefinalizeExamIdempotencyKeyRef, so they do not use separate finalize keys. The server-side use case also turns the concurrent loser intoAlreadyEnded, 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 reasonlessCONFLICTpaths for bothrunLoadQuestionFlowandrunSubmitAnswerFlow, and the priorvi.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 againstPracticeSessionConflictMessages.AlreadyEndedandPracticeSessionConflictReasons.AlreadyEnded.src/application/use-cases/submit-answer-tutor.test.ts (3)
2-6: LGTM!
217-221: LGTM! Same drift-safe pattern applied here as insubmit-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!
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Summary
Verification
Summary by CodeRabbit
Bug Fixes
Tests