Skip to content

Claude/test ai system routes jw ai9 - #81

Merged
2witstudios merged 20 commits into
masterfrom
claude/test-ai-system-routes-JWAi9
Dec 15, 2025
Merged

2witstudios merged 20 commits into
masterfrom
claude/test-ai-system-routes-JWAi9

Conversation

@2witstudios

@2witstudios 2witstudios commented Dec 14, 2025 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Improvements

    • Stronger permission checks and clearer 4xx/5xx responses across messages, conversations, agents, and global conversations.
    • Improved audit logging for conversation and message deletions.
    • More reliable usage summaries and billing totals; consistent error handling.
    • Consistent AI settings and subscription handling.
  • New Features

    • Safer message-content updates that preserve structured content when possible.
    • Helpers to extract preview text and generate conversation titles.
    • Stricter validation for agent creation and config updates; invalid tool selections rejected.
  • Tests

    • Expanded boundary-driven tests covering auth, validation, authorization, success and error paths.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit establishes the repository seam pattern for proper contract testing:

1. Created pageAgentRepository seam:
   - getDriveById, getParentPage, getNextPosition, createAgent
   - Extracts database operations from route handler
   - Includes calculateNextPosition pure function for unit testing

2. Refactored page-agents/create route:
   - Uses repository instead of direct ORM queries
   - Route logic is now focused on request handling and orchestration

3. Rewrote route tests as Contract tests:
   - Mock repository seam instead of ORM chain mocking
   - No selectCallCount, no thenable chain mocks
   - Tests verify Request → Response contract
   - Boundary obligations tested with payload assertions

4. Added repository unit tests:
   - Tests calculateNextPosition pure function
   - Covers edge cases (empty siblings, single sibling, position 0)

This pattern will be applied to remaining 13 AI route test files.
- Create conversation-repository.ts with all conversation DB operations
- Extend page-agent-repository.ts with getAgentById and updateAgentConfig
- Refactor conversations route to use repository seam
- Refactor conversationId route (PATCH/DELETE) to use repository seam
- Refactor config route to use extended page-agent-repository
- Rewrite all conversation tests as proper Contract tests
- Extract pure functions extractPreviewText and generateTitle for unit testing
- All 69 page-agents tests passing
- Create chat-message-repository.ts with DB operations
- Refactor chat/messages route to use repository
- Refactor chat/messages/[messageId] route to use repository
- Rewrite all chat message tests as proper Contract tests
- Extract processMessageContentUpdate as pure function for testing
- All 33 chat message tests passing
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Dec 14, 2025 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b081ed6 and f34b429.

📒 Files selected for processing (4)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (4 hunks)

Walkthrough

Data-access and pure logic were moved into new repository modules; route handlers now call repository methods and exported helpers. Tests were updated to mock repository, authentication, permission, and logging boundaries and to unit‑test the new pure helpers.

Changes

Cohort / File(s) Summary
Repository layer (new / barrel)
apps/web/src/lib/repositories/index.ts, apps/web/src/lib/repositories/chat-message-repository.ts, apps/web/src/lib/repositories/conversation-repository.ts, apps/web/src/lib/repositories/global-conversation-repository.ts, apps/web/src/lib/repositories/page-agent-repository.ts, apps/web/src/lib/repositories/ai-settings-repository.ts
Added repository modules and a barrel export. Introduced public types and repository methods for chat messages, conversations, global conversations, page agents, and AI settings. Added pure helpers: processMessageContentUpdate, calculateUsageSummary, calculateNextPosition, extractPreviewText, generateTitle.
Chat message routes & tests
apps/web/src/app/api/ai/chat/messages/route.ts, apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts, apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts, apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
GET/PATCH/DELETE refactored to use chatMessageRepository and processMessageContentUpdate. Tests converted to repository/auth/logging boundary mocks and expanded to cover auth, validation, not-found, authorization, success, and repository error scenarios.
Page-agent repo, routes & tests
apps/web/src/lib/repositories/page-agent-repository.ts, apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts, apps/web/src/app/api/ai/page-agents/create/route.ts, apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts, apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts, apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
Replaced inline DB logic with pageAgentRepository (drive/page lookup, next-position calc, create, update). Added AgentData/AgentConfigUpdate types and calculateNextPosition with unit tests. Tests refactored to mock repository, auth, websocket, and cache boundaries.
Page-agent conversations routes & tests
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts, apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts, apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts, apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
Delegated GET/POST/PATCH/DELETE to conversationRepository. Added exported helpers extractPreviewText and generateTitle. Tests updated to mock conversationRepository, auth and permission boundaries; pagination and preview/title logic moved to helpers.
Global conversation routes & tests
apps/web/src/app/api/ai/global/route.ts, apps/web/src/app/api/ai/global/[id]/route.ts, apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts, apps/web/src/app/api/ai/global/[id]/usage/route.ts, apps/web/src/app/api/ai/global/active/route.ts, many __tests__/*
Replaced inline DB logic with globalConversationRepository (list/create/get/update/soft-delete, message ops, usage logs). Routes now call repository methods and use/re-export pure helpers like processMessageContentUpdate and calculateUsageSummary. Tests converted to repository/auth/logging boundary mocks and include pure helper unit tests.
AI settings route & tests
apps/web/src/app/api/ai/settings/route.ts, apps/web/src/app/api/ai/settings/__tests__/route.test.ts
Replaced direct user DB access with aiSettingsRepository (getUserSettings, updateProviderSettings). Tests updated to repository/auth/logging boundary mocks.
Imports & minor route adaptations
e.g. apps/web/src/app/api/ai/chat/messages/route.ts, apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts, apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts, apps/web/src/app/api/ai/global/active/route.ts
Updated imports to use repository modules and adjusted route logic to delegate DB and content/usage computation to repository helpers while preserving route signatures and error-handling behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Route
  participant Auth
  participant Repo
  participant Logger

  Client->>Route: PATCH /api/.../messages/[messageId] { newContent }
  Route->>Auth: authenticate & permission checks
  Auth-->>Route: user context
  Route->>Repo: getMessageById(messageId)
  Repo-->>Route: message | null
  alt message found
    Route->>Repo: processMessageContentUpdate(existingContent, newContent)
    Repo-->>Route: updatedContent
    Route->>Repo: updateMessageContent(messageId, updatedContent)
    Repo-->>Route: success
    Route->>Logger: info/audit
    Route-->>Client: 200 { success: true }
  else not found
    Route-->>Client: 404
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas needing extra attention:
    • Repository public contracts: return shapes, null vs throw behavior, timestamp and isActive semantics.
    • Pure helpers: processMessageContentUpdate, calculateUsageSummary, extractPreviewText, generateTitle, calculateNextPosition (edge cases and JSON handling).
    • Tests’ mocked return types vs actual repository return types and exported helper signatures.
    • Permission/ownership checks (canUserViewPage / canUserEditPage) and consistent error codes/messages.
    • Soft-delete and audit/logging call shapes and metadata passed to logging or event broadcasting.

Poem

🐇 I nibble through code and tidy each trail,

Repos hold the crumbs of every fresh tale.
Helpers hum softly, tests dance in a ring,
Routes pass the carrots — hooray, refactor spring!
🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Claude/test ai system routes jw ai9' is vague and uses branch naming conventions rather than describing the actual changes, making it unclear what the pull request accomplishes. Replace with a descriptive title that summarizes the main change, such as 'Refactor AI routes to use repository abstraction layer' or 'Introduce repository pattern for AI system data access'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
apps/web/src/lib/repositories/index.ts (1)

1-7: Consider exporting all repository modules from the barrel for consistency.

Only page-agent-repository is exported here, but other route files import conversation-repository and chat-message-repository directly. For a consistent API surface, consider adding:

 export * from './page-agent-repository';
+export * from './conversation-repository';
+export * from './chat-message-repository';

This allows consumers to import from @/lib/repositories consistently rather than mixing barrel and direct imports.

apps/web/src/lib/repositories/chat-message-repository.ts (1)

10-21: ChatMessage interface is missing fields from the schema.

The ChatMessage interface is missing userId and messageType fields that exist in the chatMessages schema (see packages/db/src/schema/core.ts). This could cause issues if consumers need these fields.

 export interface ChatMessage {
   id: string;
   pageId: string;
   conversationId: string;
   role: string;
   content: string;
   isActive: boolean;
   createdAt: Date;
   editedAt: Date | null;
   toolCalls: unknown | null;
   toolResults: unknown | null;
+  userId: string | null;
+  messageType: 'standard' | 'todo_list';
 }
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (1)

166-168: Consider static import for createId.

The dynamic import of createId adds unnecessary async overhead on every POST request. Since @paralleldrive/cuid2 is already a project dependency (used in the schema), a static import would be more efficient.

Add to imports at the top of the file:

import { createId } from '@paralleldrive/cuid2';

Then simplify:

-    // Generate new conversation ID using createId
-    const { createId } = await import('@paralleldrive/cuid2');
-    const conversationId = createId();
+    // Generate new conversation ID using createId
+    const conversationId = createId();
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (1)

353-377: Good coverage of pure function edge cases.

The tests for extractPreviewText cover the main scenarios well. Consider adding tests for additional edge cases:

  • Empty array format: JSON.stringify([])
  • Object without parts property: JSON.stringify({})

Both should return "New conversation" based on the implementation logic.

it('should return "New conversation" for empty array', () => {
  const content = JSON.stringify([]);
  expect(extractPreviewText(content)).toBe('New conversation');
});

it('should return "New conversation" for object without text', () => {
  const content = JSON.stringify({ parts: [] });
  expect(extractPreviewText(content)).toBe('New conversation');
});
apps/web/src/lib/repositories/page-agent-repository.ts (2)

138-149: Interface types assume controlled data shapes for JSONB fields.

The enabledTools: string[] | null type assumes the stored JSONB always contains a string array. This works if the application consistently writes this format, but note that JSONB fields don't enforce structure at the database level. If there's any concern about data integrity, consider adding runtime validation.


137-167: Consider moving interface definitions before their usage.

The interfaces AgentDetails, AgentConfigUpdate, and UpdatedAgent are defined after being referenced in the repository methods. While TypeScript hoisting makes this work, defining interfaces before the repository object would improve code readability and follow conventional top-down organization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fad40b1 and 05c999b.

📒 Files selected for processing (17)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts (5 hunks)
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (6 hunks)
  • apps/web/src/app/api/ai/chat/messages/route.ts (3 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (7 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts (4 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (5 hunks)
  • apps/web/src/app/api/ai/page-agents/create/route.ts (5 hunks)
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts (1 hunks)
  • apps/web/src/lib/repositories/chat-message-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/conversation-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/index.ts (1 hunks)
  • apps/web/src/lib/repositories/page-agent-repository.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and must be awaited before destructuring
Get request body using const body = await request.json();
Return JSON responses using Response.json(data) or NextResponse.json(data) in route handlers

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
🧠 Learnings (13)
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to **/*.{ts,tsx} : Always structure message content using the message parts structure: `{ parts: [{ type: 'text', text: '...' }] }`

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/*.{ts,tsx} : Message content must always use the message parts structure with type and text fields (e.g., `{ parts: [{ type: 'text', text: '...' }] }`)

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the message parts structure with `{ parts: [{ type: 'text', text: '...' }] }` format for message content

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to **/*.{ts,tsx} : Use Drizzle client from `pagespace/db` for all database access

Applied to files:

  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/web/src/app/**/*.ts : Return JSON responses using `return Response.json(data)` or `return NextResponse.json(data)`

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/route.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/lib/repositories/page-agent-repository.ts
🧬 Code graph analysis (11)
apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts (1)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • chatMessageRepository (23-88)
apps/web/src/lib/repositories/chat-message-repository.ts (2)
packages/db/src/index.ts (3)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
packages/db/src/schema/core.ts (1)
  • chatMessages (68-89)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (1)
packages/db/src/schema/conversations.ts (1)
  • conversations (10-25)
apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts (1)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • AgentConfigUpdate (151-158)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateHybridRequest (200-202)
  • isAuthError (204-206)
apps/web/src/lib/repositories/conversation-repository.ts (1)
  • conversationRepository (40-222)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (4)
  • GET (43-125)
  • POST (133-183)
  • extractPreviewText (11-27)
  • generateTitle (33-35)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (1)
apps/web/src/lib/repositories/conversation-repository.ts (1)
  • conversationRepository (40-222)
apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (2)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateHybridRequest (200-202)
  • isAuthError (204-206)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • chatMessageRepository (23-88)
apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (3)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • pageAgentRepository (46-136)
apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts (1)
  • PUT (15-165)
apps/web/src/lib/websocket/socket-utils.ts (2)
  • createPageEventPayload (214-231)
  • broadcastPageEvent (88-122)
apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts (1)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • calculateNextPosition (13-15)
apps/web/src/lib/repositories/page-agent-repository.ts (2)
packages/db/src/index.ts (5)
  • db (20-20)
  • eq (8-8)
  • and (8-8)
  • isNull (9-9)
  • desc (8-8)
packages/db/src/schema/core.ts (2)
  • drives (7-22)
  • pages (24-66)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (2)
  • WebAuthResult (25-28)
  • authenticateHybridRequest (200-202)
apps/web/src/lib/repositories/conversation-repository.ts (1)
  • conversationRepository (40-222)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (2)
  • PATCH (13-72)
  • DELETE (79-142)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (39)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (3)

1-104: Well-structured test setup with proper repository boundary mocking.

The test scaffolding correctly mocks the repository seam instead of direct database access, and the createContext helper properly handles Next.js 15's async params with Promise.resolve. The beforeEach defaults provide a clean baseline for each test case.


106-208: Comprehensive PATCH route test coverage.

Tests properly verify authentication, authorization, resource existence checks, and error handling. The boundary obligation test at lines 180-190 confirms the repository method is called with correct parameters.


210-364: Thorough DELETE route test coverage including audit logging verification.

The boundary obligation tests (lines 309-347) properly verify that both the audit log repository method and the logger are called with the expected metadata. This ensures the deletion flow maintains proper traceability.

apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts (2)

11-28: Tests correctly document the implicit contract that siblings must be pre-sorted descending.

The comment at line 20 clarifies the assumption that siblings come "ordered desc by DB". This is important since calculateNextPosition relies on siblingPages[0] being the highest position. Consider adding a JSDoc comment to the function signature in page-agent-repository.ts to make this contract explicit for future maintainers.


46-53: Good defensive edge-case testing.

The comment acknowledges that negative positions are not expected but tests the behavior anyway. This is good practice for documenting how the function handles unexpected inputs.

apps/web/src/app/api/ai/chat/messages/route.ts (2)

34-38: Clean repository integration for message retrieval.

The switch from direct database queries to chatMessageRepository.getMessagesForPage() properly abstracts the data access layer. The conversationId || undefined correctly handles the null return from searchParams.get() for the optional parameter.


1-51: Route handler follows established patterns.

Authentication, authorization, and error handling are properly implemented. The repository abstraction enables cleaner testing without mocking ORM chains.

apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts (2)

9-9: Repository pattern correctly applied with proper typing.

The route properly uses pageAgentRepository.getAgentById() and pageAgentRepository.updateAgentConfig() with the AgentConfigUpdate type. The async context.params is correctly awaited per Next.js 15 requirements.

Also applies to: 29-29, 68-68, 104-104


127-156: Comprehensive response payload with appropriate fallbacks.

The response correctly combines updated values with original agent values for fields that weren't modified. The nextSteps array provides helpful guidance for API consumers.

apps/web/src/lib/repositories/chat-message-repository.ts (3)

27-49: LGTM!

The getMessagesForPage method correctly handles optional conversationId filtering and orders results chronologically. The conditional where clause is well-structured.


54-61: LGTM!

The getMessageById method correctly handles the not-found case. Not filtering by isActive is appropriate since edit/delete operations may need to access inactive messages for validation.


66-87: LGTM!

Both updateMessageContent and softDeleteMessage follow the established pattern. The route handlers verify message existence before calling these methods, so the silent no-op behavior on non-existent IDs is acceptable.

apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts (3)

14-30: LGTM!

Good extraction of the content transformation logic into a pure function for testability. The function correctly preserves structured message formats while gracefully handling plain text content.


36-101: LGTM!

The PATCH handler follows proper Next.js 15 patterns (awaiting context.params), includes comprehensive validation, authorization checks, and audit logging. The repository abstraction cleanly separates concerns.


107-160: LGTM!

The DELETE handler correctly implements soft-delete semantics with proper authentication, authorization, and audit logging.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (1)

13-72: LGTM!

The PATCH handler properly validates the agent and conversation existence before responding. The placeholder behavior for custom titles is clearly documented.

apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (5)

13-47: LGTM!

Clean mock setup at the repository boundary. The pattern of defining mocks before imports ensures proper hoisting in Vitest.


49-85: LGTM!

Test fixtures correctly model the expected types, including the Next.js 15 pattern of Promise-wrapped params.


87-268: LGTM!

Comprehensive test coverage for the PATCH handler covering authentication, validation, authorization, success paths, and error handling. The tests properly verify repository interactions and logging.


270-403: LGTM!

DELETE handler tests mirror the PATCH coverage appropriately, verifying authentication, authorization, and the soft-delete flow.


405-439: LGTM!

Excellent coverage for the pure processMessageContentUpdate function, testing plain text, invalid JSON, structured content preservation, and JSON without expected fields.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (4)

11-27: LGTM!

The extractPreviewText function correctly handles multiple content formats (array, parts structure, raw text) with appropriate fallbacks. The 100-character truncation and "New conversation" default are sensible choices.


33-35: LGTM!

Simple and effective title generation with appropriate truncation.


43-125: LGTM!

The GET handler correctly implements pagination, uses the repository abstraction for all data access, and formats responses using the extracted pure functions.


153-160: LGTM!

Using canUserViewPage for conversation creation is appropriate—users who can view an agent should be able to start conversations with it.

apps/web/src/app/api/ai/page-agents/create/route.ts (1)

1-172: Well-structured repository pattern implementation.

The route handler correctly uses the repository abstraction for database operations, follows proper authentication patterns, and handles permissions appropriately. The code:

  • Uses await request.json() as per guidelines
  • Returns responses via NextResponse.json()
  • Uses centralized permission functions (canUserEditPage)
  • Properly validates inputs before processing
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (1)

78-80: Context params correctly wrapped in Promise for Next.js 15.

The createContext helper properly returns params as a Promise, which aligns with Next.js 15's requirement that dynamic route params must be awaited before destructuring. As per coding guidelines, this is the correct approach.

apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (1)

1-372: Comprehensive boundary-based test coverage.

The test file demonstrates excellent testing practices:

  • Clear separation into authentication, validation, authorization, and boundary obligation tests
  • Proper mocking at repository boundaries rather than ORM level
  • Verifies cache invalidation conditions (Lines 322-353) based on field changes
  • Context params correctly use Promise wrapper for Next.js 15 compatibility
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (1)

86-90: Helper function follows good testing patterns.

The createRequest helper encapsulates request construction cleanly, making tests more readable and maintainable.

apps/web/src/lib/repositories/conversation-repository.ts (4)

189-200: Soft delete is idempotent, which is appropriate.

The softDeleteConversation method updates all messages without filtering by isActive = true. This makes the operation idempotent—calling it multiple times has the same effect—which is a good design choice for delete operations.


65-130: Well-structured CTE query for conversation listing.

The SQL query uses multiple CTEs to compute conversation statistics. The structure is clear and produces the correct results. Note that the chat_messages table is scanned multiple times (in ranked_messages and conversation_stats CTEs), which could impact performance for agents with very large conversation histories.

For now, this is acceptable. If performance becomes a concern, consider consolidating the CTEs or adding an index on (pageId, isActive, conversationId, createdAt).


9-38: Clean interface definitions with proper TypeScript types.

The exported interfaces are well-defined with appropriate types. No any types are used, which complies with the coding guidelines. The ConversationMetadata interface correctly marks timestamps as nullable (Date | null) to handle empty conversations.


169-187: No action needed. The Drizzle .as() aliasing works correctly in this implementation.

In Drizzle ORM, the object keys in .select({ key: expression }) determine the TypeScript property names in the returned object, not the .as() aliases. The .as() method provides the SQL column alias and serves as documentation. The code correctly structures:

.select({
  messageCount: sql<number>`COUNT(*)`.as('messageCount'),
  firstMessageTime: sql<Date>`MIN(created_at)`.as('firstMessageTime'),
  lastMessageTime: sql<Date>`MAX(created_at)`.as('lastMessageTime'),
})

These object keys (messageCount, firstMessageTime, lastMessageTime) match the ConversationMetadata interface properties exactly, and TypeScript properly infers the return type. The .as() calls provide corresponding SQL aliases for clarity and consistency.

Likely an incorrect or invalid review comment.

apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (3)

1-49: LGTM! Well-structured contract tests with proper boundary mocking.

The test file correctly mocks the repository seam and authentication boundaries, enabling isolated contract testing without ORM chain mocking. The mock implementations are minimal and focused.


55-94: Test fixtures are properly structured and match the expected interfaces.

The mockWebAuth function correctly implements WebAuthResult with all required fields, and createRequest provides a clean helper for constructing test requests with optional query parameters.


187-208: Good coverage for repository parameter forwarding.

These tests correctly verify that conversationId is forwarded to the repository when provided and passed as undefined when omitted, ensuring the optional filtering behavior is properly tested.

apps/web/src/lib/repositories/page-agent-repository.ts (3)

7-7: Correct use of centralized database imports.

The import from @pagespace/db follows the established pattern for database access, as per the coding guidelines. Based on learnings, this is the correct approach.


9-15: Good extraction of pure function for testability.

Separating calculateNextPosition as a pure function allows for direct unit testing without database involvement. The logic correctly handles both populated and empty sibling arrays.


92-102: LGTM! Drizzle's insert guarantees a return value on success.

The non-nullable return type is acceptable because Drizzle's insert().returning() will either return the inserted row or throw an exception on failure—it won't silently return an empty array.

Comment thread apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
Comment thread apps/web/src/lib/repositories/page-agent-repository.ts
- Create globalConversationRepository with methods for:
  - listConversations, createConversation (main route)
  - getActiveGlobalConversation (active route)
  - getConversationById, updateConversationTitle, softDeleteConversation ([id] route)
  - getMessageById, updateMessageContent, softDeleteMessage (messages route)
  - getUsageLogs (usage route)
- Extract pure functions: processMessageContentUpdate, calculateUsageSummary
- Refactor all 5 global routes to use repository seam
- Rewrite all 63 global tests as Contract tests (no ORM chain mocking)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (9)
apps/web/src/app/api/ai/global/[id]/route.ts (1)

43-66: Consider adding input validation for title.

The title is extracted from the request body but not validated before being passed to the repository. While the repository might handle edge cases, defensive validation at the route level would improve robustness.

     const { id } = await context.params;
     const body = await request.json();
     const { title } = body;
 
+    if (title !== undefined && typeof title !== 'string') {
+      return NextResponse.json({
+        error: 'Title must be a string'
+      }, { status: 400 });
+    }
+
     const updatedConversation = await globalConversationRepository.updateConversationTitle(
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (2)

101-110: Add JSON Content-Type header to PATCH requests (more realistic contract tests).

Right now request.json() will still work, but adding the header better matches real clients.

 const createPatchRequest = (id: string, messageId: string, body: Record<string, unknown>) =>
   new Request(`https://example.com/api/ai/global/${id}/messages/${messageId}`, {
     method: 'PATCH',
+    headers: { 'content-type': 'application/json' },
     body: JSON.stringify(body),
   });

198-229: Consider asserting the route uses processMessageContentUpdate output (not just “some string”).

You already test the pure helper separately; this would confirm the PATCH handler is actually wired to use it and persists the transformed value.

 import { globalConversationRepository } from '@/lib/repositories/global-conversation-repository';
+import { processMessageContentUpdate as mockedProcessMessageContentUpdate } from '@/lib/repositories/global-conversation-repository';

 ...
   it('should call repository methods with correct arguments', async () => {
     const request = createPatchRequest(mockConversationId, mockMessageId, { content: 'Updated' });
     const context = createContext(mockConversationId, mockMessageId);

     await PATCH(request, context);

+    expect(mockedProcessMessageContentUpdate).toHaveBeenCalledWith('Hello, AI!', 'Updated');
     expect(globalConversationRepository.updateMessageContent).toHaveBeenCalledWith(
       mockMessageId,
-      expect.any(String)
+      'Updated'
     );
   });
apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts (1)

85-118: Avoid null as unknown as number by letting the mockUsageLog fixture accept nullable fields.

This keeps the tests honest and removes the type-lying casts.

 const mockUsageLog = (overrides: Partial<{
   id: string;
   timestamp: Date;
   userId: string;
   provider: string;
   model: string;
-  inputTokens: number;
-  outputTokens: number;
-  totalTokens: number;
-  cost: number;
+  inputTokens: number | null;
+  outputTokens: number | null;
+  totalTokens: number | null;
+  cost: number | null;
   conversationId: string;
   contextSize: number;
   messageCount: number;
   wasTruncated: boolean;
 }> = {}) => ({
   id: overrides.id ?? 'log_123',
   timestamp: overrides.timestamp ?? new Date(),
   userId: overrides.userId ?? mockUserId,
   provider: overrides.provider ?? 'openrouter',
   model: overrides.model ?? 'anthropic/claude-3-opus',
-  inputTokens: overrides.inputTokens ?? 1000,
-  outputTokens: overrides.outputTokens ?? 500,
-  totalTokens: overrides.totalTokens ?? 1500,
-  cost: overrides.cost ?? 0.015,
+  inputTokens: overrides.inputTokens === undefined ? 1000 : overrides.inputTokens,
+  outputTokens: overrides.outputTokens === undefined ? 500 : overrides.outputTokens,
+  totalTokens: overrides.totalTokens === undefined ? 1500 : overrides.totalTokens,
+  cost: overrides.cost === undefined ? 0.015 : overrides.cost,
   conversationId: overrides.conversationId ?? mockConversationId,
   messageId: null,
   pageId: null,
   driveId: null,
   success: true,
   error: null,
   contextSize: overrides.contextSize ?? 50000,
   messageCount: overrides.messageCount ?? 10,
   wasTruncated: overrides.wasTruncated ?? false,
 });

 ...
   it('should handle null token values gracefully', async () => {
     const { calculateUsageSummary } = await actualModule;
     const logs = [
-      {
-        ...mockUsageLog(),
-        inputTokens: null as unknown as number,
-        outputTokens: null as unknown as number,
-        totalTokens: null as unknown as number,
-        cost: null as unknown as number,
-      },
+      mockUsageLog({ inputTokens: null, outputTokens: null, totalTokens: null, cost: null }),
     ];

Also applies to: 355-373

apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts (1)

88-93: Add JSON Content-Type header to PATCH request helper for realism.

 const createPatchRequest = (id: string, body: Record<string, unknown>) =>
   new Request(`https://example.com/api/ai/global/${id}`, {
     method: 'PATCH',
+    headers: { 'content-type': 'application/json' },
     body: JSON.stringify(body),
   });
apps/web/src/lib/repositories/global-conversation-repository.ts (4)

99-148: Consider extracting magic number to a named constant.

The hardcoded 200000 default context window size (line 125) would benefit from being a named constant for clarity and maintainability.

+const DEFAULT_CONTEXT_WINDOW_SIZE = 200000;
+
 export function calculateUsageSummary(
   logs: UsageLog[],
   getContextWindow: (model: string) => number
 ): UsageSummary {
   // ...
-  const contextWindowSize = mostRecentModel ? getContextWindow(mostRecentModel) : 200000;
+  const contextWindowSize = mostRecentModel ? getContextWindow(mostRecentModel) : DEFAULT_CONTEXT_WINDOW_SIZE;

238-274: Consider adding isActive check to prevent updating soft-deleted conversations.

Both updateConversationTitle and softDeleteConversation don't check isActive status. This could allow updating a title or re-deleting an already soft-deleted conversation. While likely harmless, it may be worth adding the check for consistency:

     .where(and(
       eq(conversations.id, conversationId),
-      eq(conversations.userId, userId)
+      eq(conversations.userId, userId),
+      eq(conversations.isActive, true)
     ))

291-302: Add conversationId parameter for defense-in-depth.

updateMessageContent only filters by messageId without verifying the conversation context. While the caller should validate ownership before calling, including conversationId in the WHERE clause provides defense-in-depth against authorization bugs in route handlers.

-  async updateMessageContent(messageId: string, content: string): Promise<void> {
+  async updateMessageContent(conversationId: string, messageId: string, content: string): Promise<void> {
     await db
       .update(messages)
       .set({
         content,
         editedAt: new Date()
       })
-      .where(eq(messages.id, messageId));
+      .where(and(
+        eq(messages.id, messageId),
+        eq(messages.conversationId, conversationId)
+      ));
   },

304-312: Same defense-in-depth concern: add conversationId parameter.

Consistent with the previous suggestion, softDeleteMessage should also accept conversationId for defense-in-depth:

-  async softDeleteMessage(messageId: string): Promise<void> {
+  async softDeleteMessage(conversationId: string, messageId: string): Promise<void> {
     await db
       .update(messages)
-      .set({ isActive: false })
-      .where(eq(messages.id, messageId));
+      .set({ isActive: false })
+      .where(and(
+        eq(messages.id, messageId),
+        eq(messages.conversationId, conversationId)
+      ));
   },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05c999b and c51d27c.

📒 Files selected for processing (11)
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (6 hunks)
  • apps/web/src/app/api/ai/global/[id]/route.ts (4 hunks)
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts (4 hunks)
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts (3 hunks)
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts (3 hunks)
  • apps/web/src/app/api/ai/global/active/route.ts (2 hunks)
  • apps/web/src/app/api/ai/global/route.ts (3 hunks)
  • apps/web/src/lib/repositories/global-conversation-repository.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/route.ts
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/route.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/route.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and must be awaited before destructuring
Get request body using const body = await request.json();
Return JSON responses using Response.json(data) or NextResponse.json(data) in route handlers

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/active/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/route.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/active/route.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
🧠 Learnings (7)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/active/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: The tech stack consists of Next.js 15 with App Router, TypeScript, Tailwind, shadcn/ui, PostgreSQL with Drizzle ORM, Ollama/Vercel AI SDK, custom JWT auth, and Socket.IO for real-time features

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/route.ts
  • apps/web/src/app/api/ai/global/[id]/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/active/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Tech stack: Next.js 15 App Router + TypeScript + Tailwind + shadcn/ui (frontend), PostgreSQL + Drizzle ORM (database), Ollama + Vercel AI SDK + OpenRouter + Google AI SDK (AI), custom JWT auth, local filesystem storage, Socket.IO for real-time, Docker deployment

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to **/*.{ts,tsx} : Use Drizzle client from `pagespace/db` for all database access

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/global/active/route.ts
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/app/api/ai/global/__tests__/route.test.ts
🧬 Code graph analysis (8)
apps/web/src/app/api/ai/global/[id]/usage/route.ts (1)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • calculateUsageSummary (99-148)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (2)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • processMessageContentUpdate (81-94)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (3)
  • PATCH (16-75)
  • DELETE (81-128)
  • processMessageContentUpdate (131-131)
apps/web/src/app/api/ai/global/[id]/__tests__/route.test.ts (2)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • globalConversationRepository (150-343)
apps/web/src/app/api/ai/global/[id]/route.ts (3)
  • GET (12-38)
  • PATCH (43-75)
  • DELETE (80-109)
apps/web/src/app/api/ai/global/active/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • globalConversationRepository (150-343)
apps/web/src/app/api/ai/global/active/route.ts (1)
  • GET (12-26)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (1)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • processMessageContentUpdate (81-94)
apps/web/src/app/api/ai/global/[id]/usage/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (3)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/global-conversation-repository.ts (2)
  • globalConversationRepository (150-343)
  • calculateUsageSummary (99-148)
apps/web/src/app/api/ai/global/[id]/usage/route.ts (2)
  • GET (15-50)
  • calculateUsageSummary (53-53)
apps/web/src/lib/repositories/global-conversation-repository.ts (3)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (1)
  • processMessageContentUpdate (131-131)
apps/web/src/app/api/ai/global/[id]/usage/route.ts (1)
  • calculateUsageSummary (53-53)
packages/db/src/index.ts (4)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
  • desc (8-8)
apps/web/src/app/api/ai/global/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (3)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • globalConversationRepository (150-343)
apps/web/src/app/api/ai/global/route.ts (2)
  • GET (15-30)
  • POST (35-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (22)
apps/web/src/app/api/ai/global/__tests__/route.test.ts (5)

1-6: Well-structured contract tests with clear boundary mocking.

The test file properly mocks at the repository seam rather than at the database level, which is the correct approach for contract testing. The documentation clearly explains the testing strategy.


13-40: Correct mock hoisting pattern.

The mocks are defined before imports, which is the correct pattern for Vitest/Jest to properly hoist the mocks. The repository mock matches the expected interface.


57-77: Good use of factory pattern for test fixtures.

The mockConversation factory with partial overrides provides flexible test data creation while maintaining type safety.


88-168: Comprehensive GET test coverage.

The test suite properly covers authentication, successful retrieval (with and without data), repository call verification, and error handling. The beforeEach setup provides sensible defaults that individual tests can override.


170-280: POST tests properly verify repository contract.

The tests comprehensively verify the POST endpoint including authentication, successful creation with various inputs, default type handling, and error scenarios. The assertion at line 240-247 correctly verifies the repository is called with the expected parameters.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (4)

16-27: Correct Next.js 15 async params handling.

The route correctly awaits context.params before destructuring (line 26), which is required in Next.js 15 where params are Promise objects. As per coding guidelines, this is the correct pattern.


37-56: Clean repository-based authorization and update flow.

The PATCH handler correctly:

  1. Verifies user owns the conversation via getConversationById
  2. Verifies message belongs to the conversation via getMessageById
  3. Processes content preserving structure via processMessageContentUpdate
  4. Persists via updateMessageContent

This is a well-structured authorization pattern.


81-128: Consistent authorization pattern in DELETE handler.

The DELETE handler mirrors the PATCH handler's authorization flow, ensuring consistency. The soft delete approach via softDeleteMessage maintains data integrity and audit trails.


130-131: Good practice: exporting pure function for testability.

Re-exporting processMessageContentUpdate allows the pure content transformation logic to be unit tested independently of the route handler.

apps/web/src/app/api/ai/global/active/route.ts (1)

12-26: Clean read-only endpoint implementation.

The GET handler is straightforward and correctly delegates to the repository. Returning null when no active conversation exists is appropriate behavior documented in the repository method.

apps/web/src/app/api/ai/global/[id]/route.ts (2)

12-38: Correct dynamic route handling for GET.

The GET handler properly awaits context.params (line 21) and uses the repository to fetch the conversation. The 404 response for non-existent conversations is appropriate.


80-109: DELETE handler correctly implements soft delete.

The handler properly awaits params, uses the repository's soft delete method, and returns appropriate responses for found/not-found cases.

apps/web/src/app/api/ai/global/route.ts (2)

15-30: GET handler correctly lists user conversations.

The handler properly authenticates, retrieves conversations via the repository, and returns the list. Error handling follows the established pattern.


35-57: POST handler follows coding guidelines.

The handler correctly uses const body = await request.json() as per coding guidelines and properly delegates to the repository with the extracted fields. The default type = 'global' provides sensible behavior.

apps/web/src/app/api/ai/global/[id]/usage/route.ts (1)

15-49: LGTM: correct Next.js 15 async params usage + clean repository/pure-function split.

Also applies to: 52-53

apps/web/src/app/api/ai/global/active/__tests__/route.test.ts (1)

13-157: LGTM: good boundary-driven contract tests (auth, repo, logging) with clear fixtures.

apps/web/src/lib/repositories/global-conversation-repository.ts (6)

1-8: LGTM!

Well-structured module header with clear documentation. Imports correctly use the Drizzle client from @pagespace/db as per coding guidelines.


9-77: LGTM!

Type definitions are well-structured with proper TypeScript typing. Good use of interface extension for Conversation extends ConversationSummary, and appropriate nullable field handling throughout.


78-94: LGTM with minor observation.

The pure function correctly handles both structured JSON content and plain text fallback. The empty catch block is appropriate here since it's intentional for the plain text case.

One minor note: partsOrder is checked in the condition (line 84) but not actually used in the update. If this is intentional validation that the structure has both fields, it's fine. Otherwise, consider whether the check is necessary.


150-195: LGTM!

Well-implemented repository methods with proper user ownership filtering. The createConversation method correctly uses .returning() to get the inserted record with sensible defaults.


197-236: LGTM!

Both methods properly enforce user ownership through userId filtering and correctly handle soft-deleted records with isActive checks. The null coalescing pattern is clean.


314-342: LGTM!

The method is well-structured with explicit field selection and proper ordering. The descending timestamp order ensures logs[0] is the most recent, which aligns with the assumption in calculateUsageSummary.

Note: This method assumes the caller has verified conversation ownership beforehand (typically via getConversationById). This is a reasonable pattern for the repository layer.

- Create aiSettingsRepository for user AI settings operations
- Refactor settings route to use repository instead of direct DB calls
- Rewrite settings tests to mock repository seam (no more ORM chain mocking)
- Fix CodeRabbit feedback:
  - Export all repositories from barrel index.ts
  - Add missing userId and messageType fields to ChatMessage interface
  - Use static import for createId in conversations route
  - Add 404 check for non-existent conversation in DELETE handler
  - Add test for DELETE 404 case

All 241 tests passing (236 AI route tests + 5 repository tests)
- Remove unused processMessageContentUpdate import from message route test
- Remove unused loggers import from page-agents/create test
…ute exports

Next.js route files can only export route handlers (GET, POST, etc.).
Moved the pure function to chat-message-repository.ts where it belongs.
Updated tests to use vi.importActual for pure function testing.
Next.js route files can only export route handlers (GET, POST, etc.).
Moved pure functions to their respective repository files:
- extractPreviewText and generateTitle to conversation-repository.ts
- Removed calculateUsageSummary re-export from usage route

Updated tests to use vi.importActual for pure function testing.
All 236 tests passing, lint clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (1)

419-436: The route handler still exposes detailed error messages—remove error.message from the client response.

The route handler at line 168 currently returns detailed error messages to the client:

{ error: `Failed to create AI agent: ${error instanceof Error ? error.message : String(error)}` }

This violates the principle of not exposing implementation details to clients. The test only verifies that a specific credential string isn't present, but does not prevent other sensitive information (e.g., database queries, internal service names, stack traces) from leaking.

Update the error response to return a generic message:

  } catch (error) {
    loggers.api.error('Error creating AI agent:', error as Error);
    return NextResponse.json(
-      { error: `Failed to create AI agent: ${error instanceof Error ? error.message : String(error)}` },
+      { error: 'Failed to create AI agent' },
      { status: 500 }
    );
  }
🧹 Nitpick comments (7)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1)

211-229: Consider verifying processMessageContentUpdate is called with correct arguments.

The test verifies updateMessageContent is called but doesn't assert that processMessageContentUpdate receives the correct arguments (existingContent from the message and newContent from the request). This would strengthen the contract test.

+    it('should call processMessageContentUpdate with existing and new content', async () => {
+      const { processMessageContentUpdate } = await import('@/lib/repositories/global-conversation-repository');
+      const existingMessage = mockMessage({ content: 'Existing content' });
+      vi.mocked(globalConversationRepository.getMessageById).mockResolvedValue(existingMessage);
+
+      const request = createPatchRequest(mockConversationId, mockMessageId, { content: 'New content' });
+      const context = createContext(mockConversationId, mockMessageId);
+
+      await PATCH(request, context);
+
+      expect(processMessageContentUpdate).toHaveBeenCalledWith('Existing content', 'New content');
+    });
apps/web/src/lib/repositories/chat-message-repository.ts (2)

70-73: Consider avoiding type assertion for query results.

The as ChatMessage[] cast could mask type mismatches if the database schema changes. Drizzle's inferred types from the chatMessages table should align with your interface.

If the Drizzle schema matches ChatMessage, you can leverage TypeScript inference:

-    return messages as ChatMessage[];
+    return messages;

Alternatively, ensure the ChatMessage interface is derived from or validated against the Drizzle schema type.


90-101: Consider returning update result for confirmation.

The updateMessageContent method returns void. Consider returning the number of affected rows or the updated record to confirm the update succeeded, especially useful for idempotency checks.

apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (1)

67-82: Consider adding messageType and userId to mock fixture.

The mockChatMessage fixture is missing messageType and userId fields that are defined in the ChatMessage interface. While tests may still pass if the route doesn't use these fields, including them ensures the fixture accurately represents the data shape.

 const mockChatMessage = (overrides: Partial<{
   id: string;
   pageId: string;
   content: string;
+  userId: string | null;
+  messageType: 'standard' | 'todo_list';
 }> = {}) => ({
   id: overrides.id || mockMessageId,
   pageId: overrides.pageId || mockPageId,
   conversationId: 'conv_123',
+  userId: overrides.userId ?? mockUserId,
   role: 'user',
   content: overrides.content || 'Hello, AI!',
+  messageType: overrides.messageType || 'standard',
   isActive: true,
   createdAt: new Date(),
   editedAt: null,
   toolCalls: null,
   toolResults: null,
 });
apps/web/src/app/api/ai/settings/route.ts (2)

154-154: Consider extracting provider validation to a shared constant or helper.

The provider arrays are repeated across POST (line 154), PATCH (line 255), and DELETE (line 333) with slight variations. Consider extracting these to constants to reduce duplication and ensure consistency.

// At the top of the file, after imports
const API_KEY_PROVIDERS = ['openrouter', 'google', 'openai', 'anthropic', 'xai', 'glm', 'minimax'] as const;
const BASE_URL_PROVIDERS = ['ollama', 'lmstudio'] as const;
const SAVE_PROVIDERS = [...API_KEY_PROVIDERS, ...BASE_URL_PROVIDERS] as const;
const SELECT_PROVIDERS = ['pagespace', 'openrouter_free', ...SAVE_PROVIDERS] as const;

// Then use in validation:
if (!provider || !SAVE_PROVIDERS.includes(provider)) { ... }

Also applies to: 255-255, 333-333


129-129: Long conditional expression could be extracted.

This line checks if any provider is configured. Consider extracting to improve readability.

const allProviderSettings = [
  pageSpaceSettings,
  openRouterSettings,
  googleSettings,
  openAISettings,
  anthropicSettings,
  xaiSettings,
  ollamaSettings,
  lmstudioSettings,
  glmSettings,
  minimaxSettings,
];
const isAnyProviderConfigured = allProviderSettings.some(s => s?.isConfigured);
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (1)

12-28: Edge case: Successfully parsed JSON may return fallback unexpectedly.

When JSON parses successfully but doesn't match either expected format (array with text field or object with parts[0].text), the function returns 'New conversation' instead of the raw content. This could lose meaningful content in unexpected JSON formats.

Consider returning the raw content substring as a fallback after the JSON format checks:

export function extractPreviewText(content: string | null): string {
  if (!content) return 'New conversation';

  try {
    const parsed = JSON.parse(content);
    if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].text) {
      return parsed[0].text.substring(0, 100);
    } else if (typeof parsed === 'object' && parsed.parts?.[0]?.text) {
      return parsed.parts[0].text.substring(0, 100);
    }
+    // JSON parsed but didn't match expected formats - use raw content
+    return content.substring(0, 100);
  } catch {
    // If parsing fails, use raw content substring
    return content.substring(0, 100);
  }
-
-  return 'New conversation';
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c51d27c and b282e8e.

📒 Files selected for processing (14)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts (5 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (5 hunks)
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts (2 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (5 hunks)
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts (19 hunks)
  • apps/web/src/app/api/ai/settings/route.ts (4 hunks)
  • apps/web/src/lib/repositories/ai-settings-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/chat-message-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/ai-settings-repository.ts
  • apps/web/src/app/api/ai/settings/route.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/ai-settings-repository.ts
  • apps/web/src/app/api/ai/settings/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/ai-settings-repository.ts
  • apps/web/src/app/api/ai/settings/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/lib/repositories/chat-message-repository.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/ai-settings-repository.ts
  • apps/web/src/app/api/ai/settings/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and must be awaited before destructuring
Get request body using const body = await request.json();
Return JSON responses using Response.json(data) or NextResponse.json(data) in route handlers

Files:

  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
  • apps/web/src/app/api/ai/settings/route.ts
🧠 Learnings (14)
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/web/src/app/**/*.ts : Get request body using `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get request body using `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/app/**/*.ts : Use `const body = await request.json();` to read request body in route handlers

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: The tech stack consists of Next.js 15 with App Router, TypeScript, Tailwind, shadcn/ui, PostgreSQL with Drizzle ORM, Ollama/Vercel AI SDK, custom JWT auth, and Socket.IO for real-time features

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/lib/repositories/index.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to **/*.{ts,tsx} : Use Drizzle client from `pagespace/db` for all database access

Applied to files:

  • apps/web/src/lib/repositories/index.ts
  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Tech stack: Next.js 15 App Router + TypeScript + Tailwind + shadcn/ui (frontend), PostgreSQL + Drizzle ORM (database), Ollama + Vercel AI SDK + OpenRouter + Google AI SDK (AI), custom JWT auth, local filesystem storage, Socket.IO for real-time, Docker deployment

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/usage/route.ts
🧬 Code graph analysis (8)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/global-conversation-repository.ts (2)
  • globalConversationRepository (150-343)
  • processMessageContentUpdate (81-94)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (2)
  • PATCH (16-75)
  • DELETE (81-128)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (2)
apps/web/src/lib/repositories/conversation-repository.ts (1)
  • conversationRepository (40-222)
packages/db/src/schema/conversations.ts (1)
  • conversations (10-25)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateHybridRequest (200-202)
  • isAuthError (204-206)
apps/web/src/lib/repositories/conversation-repository.ts (1)
  • conversationRepository (40-222)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (2)
  • PATCH (13-72)
  • DELETE (79-151)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (1)
apps/web/src/lib/repositories/global-conversation-repository.ts (2)
  • globalConversationRepository (150-343)
  • processMessageContentUpdate (81-94)
apps/web/src/app/api/ai/global/[id]/usage/route.ts (1)
apps/web/src/lib/repositories/global-conversation-repository.ts (2)
  • globalConversationRepository (150-343)
  • calculateUsageSummary (99-148)
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (4)
apps/web/src/lib/auth/index.ts (3)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • pageAgentRepository (46-136)
apps/web/src/app/api/ai/page-agents/create/route.ts (1)
  • POST (15-172)
apps/web/src/lib/websocket/socket-utils.ts (1)
  • createPageEventPayload (214-231)
apps/web/src/lib/repositories/ai-settings-repository.ts (1)
packages/db/src/index.ts (2)
  • db (20-20)
  • eq (8-8)
apps/web/src/app/api/ai/settings/route.ts (1)
apps/web/src/lib/repositories/ai-settings-repository.ts (1)
  • aiSettingsRepository (27-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (41)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (3)

1-11: Clean import structure using repository boundaries.

The refactor to use globalConversationRepository and processMessageContentUpdate from the repository module aligns well with the boundary-driven architecture. This improves testability and separates concerns appropriately.


16-75: PATCH handler follows Next.js 15 patterns and coding guidelines correctly.

The implementation properly:

  • Awaits context.params before destructuring (Line 26)
  • Uses await request.json() for request body (Line 27)
  • Validates input with appropriate error responses
  • Verifies conversation ownership via repository
  • Uses the centralized processMessageContentUpdate helper

81-128: DELETE handler implementation is correct.

The soft-delete flow properly verifies ownership and message existence before deletion. The logging with masked identifiers maintains privacy while providing audit trails.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (5)

1-22: Well-structured boundary mocks for repository pattern testing.

The mock setup correctly isolates the repository seam, enabling focused contract testing. The processMessageContentUpdate mock with identity function (existing, newContent) => newContent is appropriate for most tests while allowing the pure function to be tested separately.


50-110: Test fixtures and helpers are well-designed.

The factory functions with partial overrides (mockConversation, mockMessage) follow good testing practices, enabling flexible test data creation while maintaining sensible defaults.


112-261: Comprehensive PATCH test coverage.

The test suite covers:

  • Authentication failures
  • Input validation (missing/invalid content)
  • Resource not found scenarios
  • Successful updates with repository call verification
  • Error handling with 500 responses

The boundary-driven approach with repository mocks is clean and maintainable.


263-377: DELETE test suite mirrors PATCH structure appropriately.

Good parallel structure with the PATCH tests, covering authentication, resource validation, success paths, and error handling consistently.


379-423: Pure function tests effectively use vi.importActual to bypass mocks.

Testing the real processMessageContentUpdate function separately from the mocked repository is a good practice. The test cases cover:

  • Plain text passthrough
  • Invalid JSON handling
  • Structured content preservation (textParts/partsOrder)
  • JSON without expected structure

This ensures the content transformation logic is validated independently.

apps/web/src/lib/repositories/chat-message-repository.ts (3)

10-23: LGTM on the interface definition.

The ChatMessage interface correctly defines the shape of chat messages. The use of unknown for toolCalls and toolResults is acceptable when the exact shape varies, though consider defining stricter types if the structure is known.


29-45: Well-structured pure function for content processing.

Good extraction of processMessageContentUpdate as a pure function. The try-catch gracefully handles invalid JSON and falls back to plain text, which is the correct behavior.


78-85: Potential issue with falsy return value.

Line 84 uses (message as ChatMessage) || null, but if message exists but has falsy properties, it will still be returned. The current logic is correct since message would be an object or undefined. However, the type assertion is unnecessary if Drizzle types align.

apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (3)

13-21: Good repository mock setup.

The mock correctly stubs all repository methods and provides a simple pass-through implementation for processMessageContentUpdate. This allows testing the route's behavior independently of the content processing logic.


84-86: Correct handling of Next.js 15 async params.

The createContext function correctly returns params as a Promise, aligning with Next.js 15's requirement that dynamic route params must be awaited. As per coding guidelines.


406-460: Excellent approach for testing pure functions.

Using vi.importActual to test processMessageContentUpdate separately from the mocked module is a clean pattern. This ensures the pure function logic is verified independently while still allowing the route tests to use mocked behavior.

apps/web/src/app/api/ai/settings/route.ts (2)

34-34: Repository integration looks correct.

The GET handler now uses aiSettingsRepository.getUserSettings(userId) instead of direct DB access, properly aligning with the repository pattern introduced in this PR.

Also applies to: 50-51


269-292: Repository usage in PATCH is well-implemented.

Both getUserSettings for subscription tier checks and updateProviderSettings for the update operation properly delegate to the repository boundary.

apps/web/src/lib/repositories/index.ts (1)

1-11: Clean barrel export for repository layer.

The barrel export is well-organized with a clear docstring explaining the architectural purpose. This provides a single import point for all repositories.

apps/web/src/lib/repositories/ai-settings-repository.ts (2)

8-8: Correct use of Drizzle client from @pagespace/db.

The import follows the coding guideline to use the Drizzle client from @pagespace/db for all database access. Based on learnings.


27-59: Well-structured repository with focused methods.

The repository correctly encapsulates user AI settings operations with clear method signatures. The getUserSettings method efficiently selects only the needed columns.

apps/web/src/app/api/ai/settings/__tests__/route.test.ts (6)

13-19: Repository mock setup is correct.

The mock correctly stubs getUserSettings and updateProviderSettings methods, enabling isolated testing of route handlers without database dependencies.


39-69: Comprehensive AI provider function mocking.

All provider settings functions are properly mocked, covering the full range of supported providers. This allows testing each provider path independently.


126-136: Mock user settings fixture aligns with interface.

The mockUserSettings fixture correctly matches the UserAISettings interface with appropriate nullable fields and default values.


249-256: Good verification of repository method calls.

The test explicitly verifies that aiSettingsRepository.getUserSettings is called with the correct userId, ensuring the route properly delegates to the repository boundary.


583-592: Thorough verification of updateProviderSettings call.

The test correctly verifies both the userId and the settings object { provider, model } are passed to the repository method.


340-447: Comprehensive POST success tests for all providers.

Good coverage of all provider types including API-key providers and base-URL providers (Ollama, LM Studio). Each test verifies the correct create function is called with sanitized input.

apps/web/src/app/api/ai/global/[id]/usage/route.ts (2)

5-8: Good use of repository boundary pattern.

The refactoring correctly delegates data access to globalConversationRepository and extracts usage calculation to a pure function calculateUsageSummary. This improves testability and maintains clean separation of concerns.


24-42: LGTM - Clean implementation following Next.js 15 conventions.

The route correctly:

  • Awaits context.params before destructuring (Line 24)
  • Uses repository methods for data access
  • Delegates calculation to a pure function for testability
  • Returns proper JSON response format
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (3)

5-6: Repository boundary pattern correctly implemented.

The route now properly delegates all data access operations to conversationRepository, improving testability and maintaining separation of concerns.


108-115: Good addition - Conversation existence check before deletion.

This addresses the previously raised concern about proceeding with deletion without verifying the conversation exists. The route now properly returns a 404 if the conversation doesn't exist before attempting any deletion or audit logging operations.


117-129: Well-structured audit logging for deletions.

The deletion flow correctly:

  1. Fetches metadata before deletion for audit purposes
  2. Performs soft-delete via repository
  3. Logs the deletion with comprehensive metadata including userId, conversationId, agentId, and metadata object

This provides good traceability for compliance and debugging.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (3)

2-6: Clean module organization with repository boundary.

Good separation of concerns with createId for ID generation and conversationRepository for data access. The top-level import of createId is appropriate since it's used synchronously.


79-106: Repository-based pagination implementation looks correct.

The pagination logic properly:

  • Uses repository methods for data retrieval and counting
  • Applies offset-based pagination with configurable page size
  • Calculates pagination metadata correctly (totalPages, hasMore)

The response transformation using pure helper functions improves testability.


163-174: POST handler returns generated ID without persisting the conversation.

The handler generates a conversation ID but doesn't create a database record. This appears intentional based on the comment at Line 132 ("automatically generated using CUID2 for security and uniqueness") and likely follows a lazy-creation pattern where the conversation is created on first message.

Verify this is the intended behavior - if conversations should be persisted on creation, a repository method call would be needed here.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts (4)

13-22: Clean repository boundary mocking pattern.

The tests correctly mock at the repository seam level rather than the ORM/database level. This provides:

  • Faster test execution (no DB setup)
  • Clearer contract testing
  • Easier maintenance when implementation changes

70-86: Well-designed test helpers.

The createRequest and createContext helpers properly construct test fixtures with:

  • Proper URL structure matching the route
  • Promise-wrapped params (matching Next.js 15 behavior)
  • Optional body support for different HTTP methods

325-363: Good boundary obligation tests for audit logging.

These tests verify critical compliance requirements:

  • Audit log creation with correct metadata structure
  • Info logging with relevant context (conversationId, agentId, userId, messageCount)

This ensures the deletion audit trail is properly maintained.


269-280: Verify DELETE returns 404 for non-existent conversation.

This test case validates the fix for the previously flagged issue where DELETE would proceed without checking conversation existence. The test correctly verifies the route now returns 404 when conversationExists returns false.

apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (4)

13-21: Repository boundary mocking pattern correctly implemented.

The test properly mocks pageAgentRepository at the boundary level with all required methods: getDriveById, getParentPage, getNextPosition, and createAgent.


43-52: Mock implementation for createPageEventPayload correctly mirrors actual function.

The mock implementation spreads the data parameter into the return object, matching the actual function's behavior (as shown in the relevant code snippets from socket-utils.ts).


365-398: Thorough boundary obligation tests.

These tests verify important side effects:

  • createPageEventPayload is called with correct parameters (driveId, agentId, 'created', metadata)
  • broadcastPageEvent is invoked
  • agentAwarenessCache.invalidateDriveAgents is called with the driveId

This ensures the route properly triggers necessary downstream effects.


273-363: Comprehensive successful creation tests.

Good coverage of creation scenarios:

  • Root-level agent creation
  • Agent in parent folder
  • Verification of repository call parameters
  • Response structure validation including agentConfig

- Convert subscriptionTier null to undefined for requiresProSubscription
- Remove detailed error message from page-agents/create to prevent info leakage

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (3)

13-32: Mock implementations duplicate actual function logic.

The mock implementations for extractPreviewText and generateTitle (lines 20-31) duplicate the actual implementation logic. This is acceptable for contract testing but creates maintenance overhead—if the actual implementation changes, the mock must be updated too.

Consider using vi.fn() without implementation and relying on the pure function tests (lines 365-435) that use vi.importActual to verify the real behavior.

 vi.mock('@/lib/repositories/conversation-repository', () => ({
   conversationRepository: {
     getAiAgent: vi.fn(),
     listConversations: vi.fn(),
     countConversations: vi.fn(),
   },
-  extractPreviewText: vi.fn((content: string | null) => {
-    if (!content) return 'New conversation';
-    try {
-      const parsed = JSON.parse(content);
-      if (Array.isArray(parsed) && parsed[0]?.text) return parsed[0].text.substring(0, 100);
-      if (parsed.parts?.[0]?.text) return parsed.parts[0].text.substring(0, 100);
-    } catch {
-      return content.substring(0, 100);
-    }
-    return 'New conversation';
-  }),
-  generateTitle: vi.fn((preview: string) => preview.length > 50 ? preview.substring(0, 50) + '...' : preview),
+  extractPreviewText: vi.fn((content: string | null) => content ? 'Mocked preview' : 'New conversation'),
+  generateTitle: vi.fn((preview: string) => preview),
 }));

308-346: Consider using HTTP 201 for resource creation.

The tests expect HTTP 200 (line 316), but semantically HTTP 201 (Created) is more appropriate for POST endpoints that create new resources. This is a minor REST convention issue.

If you want to follow REST conventions more strictly, update the route handler to return status 201:

-    return NextResponse.json({
+    return NextResponse.json({
       conversationId,
       title: customTitle || 'New conversation',
       createdAt: new Date(),
-    });
+    }, { status: 201 });

And update tests accordingly:

-      expect(response.status).toBe(200);
+      expect(response.status).toBe(201);

365-414: Repeated vi.importActual calls in each test.

Each test case imports the actual module separately. Consider hoisting to a beforeAll block for efficiency and cleaner code.

 describe('extractPreviewText (pure function)', () => {
+  let extractPreviewText: (content: string | null) => string;
+
+  beforeAll(async () => {
+    const actualModule = await vi.importActual<
+      typeof import('@/lib/repositories/conversation-repository')
+    >('@/lib/repositories/conversation-repository');
+    extractPreviewText = actualModule.extractPreviewText;
+  });
+
   it('should return "New conversation" for null content', async () => {
-    const actualModule = await vi.importActual<
-      typeof import('@/lib/repositories/conversation-repository')
-    >('@/lib/repositories/conversation-repository');
-    const { extractPreviewText } = actualModule;
-
     expect(extractPreviewText(null)).toBe('New conversation');
   });
   // ... apply same pattern to other tests
 });
apps/web/src/lib/repositories/conversation-repository.ts (1)

44-60: Consider adding type guards for parsed JSON content.

The parsed variable from JSON.parse is implicitly any. While the runtime checks are correct, explicit type narrowing would improve type safety and align with the "no any types" coding guideline.

 export function extractPreviewText(content: string | null): string {
   if (!content) return 'New conversation';

   try {
-    const parsed = JSON.parse(content);
-    if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].text) {
+    const parsed: unknown = JSON.parse(content);
+    if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && 'text' in parsed[0] && typeof parsed[0].text === 'string') {
       return parsed[0].text.substring(0, 100);
-    } else if (typeof parsed === 'object' && parsed.parts?.[0]?.text) {
+    } else if (typeof parsed === 'object' && parsed !== null && 'parts' in parsed) {
+      const parts = (parsed as { parts?: unknown }).parts;
+      if (Array.isArray(parts) && parts.length > 0 && typeof parts[0] === 'object' && parts[0] !== null && 'text' in parts[0] && typeof parts[0].text === 'string') {
-      return parsed.parts[0].text.substring(0, 100);
+        return parts[0].text.substring(0, 100);
+      }
     }
   } catch {
     // If parsing fails, use raw content substring
     return content.substring(0, 100);
   }

   return 'New conversation';
 }

Alternatively, define a type for the expected message format and use a type guard function.

apps/web/src/app/api/ai/page-agents/create/route.ts (1)

21-23: Consider strongly typing the request body to align with strict TypeScript usage

Right now body comes from await request.json() and is effectively any, but later fields like systemPrompt are treated as strings (e.g., substring in the response) and wired into AgentData. To better match the “no any types” / strict TS intent, consider introducing a dedicated request type and asserting to it here, e.g.:

type CreateAgentRequestBody = {
  driveId: string;
  parentId?: string | null;
  title: string;
  systemPrompt: string;
  enabledTools?: string[];
  aiProvider?: string;
  aiModel?: string;
  welcomeMessage?: string;
};

// ...
const body = (await request.json()) as CreateAgentRequestBody;

This keeps the handler’s assumptions about agentData inputs explicit and makes it easier for future changes (e.g., adding required/optional fields) to be caught at compile time.

Also applies to: 89-98, 101-109

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b282e8e and 4499af1.

📒 Files selected for processing (6)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (5 hunks)
  • apps/web/src/app/api/ai/page-agents/create/route.ts (6 hunks)
  • apps/web/src/app/api/ai/settings/route.ts (5 hunks)
  • apps/web/src/lib/repositories/conversation-repository.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/api/ai/settings/route.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and must be awaited before destructuring
Get request body using const body = await request.json();
Return JSON responses using Response.json(data) or NextResponse.json(data) in route handlers

Files:

  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts
  • apps/web/src/app/api/ai/page-agents/create/route.ts
🧠 Learnings (1)
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (3)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • isAuthError (204-206)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • pageAgentRepository (46-136)
apps/web/src/lib/websocket/socket-utils.ts (2)
  • createPageEventPayload (214-231)
  • broadcastPageEvent (88-122)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (1)
apps/web/src/lib/repositories/conversation-repository.ts (3)
  • conversationRepository (70-252)
  • extractPreviewText (44-60)
  • generateTitle (66-68)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (2)
apps/web/src/lib/repositories/conversation-repository.ts (3)
  • conversationRepository (70-252)
  • extractPreviewText (44-60)
  • generateTitle (66-68)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (2)
  • GET (18-100)
  • POST (108-157)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (16)
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (6)

1-68: LGTM: Clean boundary-based mocking strategy.

The test setup correctly mocks at repository and boundary seams rather than ORM level, which improves test maintainability and clarity. The documentation clearly states the testing approach.


70-118: LGTM: Well-structured test fixtures and setup.

The fixtures and beforeEach setup provide sensible defaults for the happy path, allowing individual tests to override specific mocks for failure scenarios. The createRequest helper promotes code reuse.


120-271: LGTM: Comprehensive coverage of authentication, validation, and authorization.

The tests systematically verify all failure scenarios including authentication failures, missing/invalid inputs, resource not found conditions, and permission checks. Good coverage of both root-level and parent-folder authorization.


273-363: LGTM: Thorough testing of successful creation scenarios.

The tests verify both the response contract and the data passed to repository methods. Good coverage of root-level and nested agent creation, plus verification of agent configuration in responses.


365-398: LGTM: Proper verification of boundary obligations.

The tests correctly verify side effects including event broadcasting and cache invalidation, which are essential for maintaining system-wide consistency beyond the HTTP response.


419-438: LGTM: Proper verification that sensitive details are not leaked.

This test correctly verifies that the error response contains only the generic sanitized message using exact match (toBe), with additional defensive checks ensuring sensitive keywords are not present. This addresses the concerns raised in the previous review.

Note: This test assumes the route handler has been updated to return only the generic message "Failed to create AI agent" without including error.message. Based on the past review comments, ensure the route handler (around line 168 in route.ts) has been modified accordingly.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/route.ts (3)

1-10: Clean imports and proper repository boundary usage.

The refactoring to use conversationRepository with extractPreviewText and generateTitle helpers cleanly separates data access from route logic. This aligns well with the coding guidelines requiring Drizzle client usage from @pagespace/db (which the repository encapsulates).


18-100: Well-structured GET handler following Next.js 15 patterns.

The handler correctly:

  • Awaits context.params before destructuring (line 26)
  • Uses centralized permission function canUserViewPage
  • Delegates data access to the repository
  • Returns JSON via NextResponse.json

All coding guidelines are followed.


137-148: Conversation not persisted to database—this is intentional lazy creation.

The POST handler generates a conversationId and returns it without persisting anything to the database. Conversations are created implicitly when the first message is sent with that ID. The GET endpoint retrieves conversations by querying the chatMessages table and grouping by conversationId, confirming this lazy creation pattern is by design.

apps/web/src/app/api/ai/page-agents/[agentId]/conversations/__tests__/route.test.ts (2)

60-92: Well-designed test fixtures.

The fixtures properly:

  • Create WebAuthResult with all required fields
  • Wrap params in Promise.resolve() to match Next.js 15's async params pattern (line 91)
  • Provide reusable helpers for request/context creation

157-230: Comprehensive pagination and response structure tests.

The tests properly verify:

  • Response structure with expected fields (lines 181-192)
  • Empty conversations case (lines 195-205)
  • Pagination parameters are correctly passed to repository (lines 207-229)

Good coverage of the GET endpoint's contract.

apps/web/src/lib/repositories/conversation-repository.ts (3)

1-38: Well-documented repository module with clear interfaces.

Good use of:

  • JSDoc comment explaining the repository's purpose (lines 1-5)
  • Explicit TypeScript interfaces for domain entities
  • Centralized imports from @pagespace/db per coding guidelines

219-230: Soft delete is idempotent.

The method sets isActive: false without checking current state, which is correct—it's idempotent and won't cause issues if called multiple times.


232-251: Good audit logging implementation.

The deletion logging properly captures:

  • User context (userId)
  • Resource identification (conversationId, agentId)
  • Metadata snapshot before deletion

This supports compliance and debugging needs.

apps/web/src/app/api/ai/page-agents/create/route.ts (2)

9-9: Repository abstraction and agent creation flow look clean

Switching to pageAgentRepository for drive lookup, parent validation, position calculation, and final createAgent keeps this handler thin and focused on auth/validation, which matches the PR’s goals and improves testability. The sequence of checks (drive → optional parent → permissions → tools → position → create) is coherent and I don’t see logic regressions in the new repository-based calls.

Also applies to: 32-32, 43-43, 86-87, 112-113


165-170: Generic 500 response is appropriate

Catching unexpected errors, logging the actual Error, and returning a generic { error: 'Failed to create AI agent' } with status 500 is a good balance between observability and not leaking internal details to clients.

Comment thread apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
Comment thread apps/web/src/lib/repositories/conversation-repository.ts
- Add index signature to ConversationStats for Drizzle execute compatibility
- Remove duplicate processMessageContentUpdate from global-conversation-repository
- Update global messages route to import processMessageContentUpdate from chat-message-repository
- Add userId and messageType to chat-messages test fixtures
- Add isTrashed to page-agents config test fixtures
- Add provider field to settings test mock for getDefaultPageSpaceSettings
- Fix baseUrl type in lmstudio/ollama model tests (use empty string instead of null)
- Fix enabledTools type casting in page-agent-repository getAgentById
- Update processMessageContentUpdate pure function tests to import from correct module
Change MIN/MAX(created_at) to MIN/MAX("createdAt") in
getConversationMetadata to match the actual column name.
Change toContain to toBe in page-agents create error test to prevent
internal error details from leaking through loose matching.
Add explicit check for empty result array in updateAgentConfig to prevent
returning undefined when the agent doesn't exist, ensuring type safety.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1)

91-107: Test mocks an unrealistic scenario that contradicts the implementation.

According to the getUserLMStudioSettings implementation (see apps/web/src/lib/ai/core/ai-utils.ts:617-636), the function returns null when baseUrl is falsy—which includes empty strings. However, this test mocks the function to return { isConfigured: true, baseUrl: '' }, a value the real implementation would never produce.

This means the test validates a code path that cannot occur in production, potentially giving false confidence in error handling.

Consider one of these approaches:

  1. Remove this test case since the scenario cannot happen (the first test at line 75 already covers the null return case).

  2. If the route handler truly needs to defend against empty baseUrl, update the test to properly reflect the actual data flow rather than bypassing the implementation contract.

Apply this diff to align the test with the actual implementation behavior:

-    it('should return 400 when LM Studio settings exist but baseUrl is empty', async () => {
-      vi.mocked(getUserLMStudioSettings).mockResolvedValue({
-        isConfigured: true,
-        baseUrl: '',
-      });
-
-      const request = new Request('https://example.com/api/ai/lmstudio/models', {
-        method: 'GET',
-      });
-
-      const response = await GET(request);
-      const body = await response.json();
-
-      expect(response.status).toBe(400);
-      expect(body.success).toBe(false);
-      expect(body.error).toContain('LM Studio not configured');
-    });
apps/web/src/app/api/ai/settings/__tests__/route.test.ts (1)

159-275: GET contract coverage is solid; LM Studio part of one test is currently unverified

The GET tests cover auth failure, happy path, provider configuration flags, repository error, and logging. In it('should return ollama and lmstudio with baseUrl check', ...) you only mock and assert the Ollama path; LM Studio isn’t mocked or asserted despite being in the test name. Either drop “and lmstudio” from the description or extend the test to also mock getUserLMStudioSettings and assert LM Studio’s isConfigured/hasBaseUrl fields to match the intent.

🧹 Nitpick comments (14)
apps/web/src/app/api/ai/settings/__tests__/route.test.ts (3)

111-137: Test fixtures are clear and typed; consider aligning mockUserSettings with real shape

mockWebAuth and mockAuthError line up well with WebAuthResult/AuthError and keep auth setup DRY. For mockUserSettings, you only surface id, currentAiProvider, currentAiModel, and subscriptionTier; if the real UserAISettings type also includes something like userId, consider mirroring that field in the fixture to avoid future surprises if the route starts relying on it.


138-157: Request builders are good; optional: add JSON content type for fidelity

The createGetRequest/createPostRequest/createPatchRequest/createDeleteRequest helpers keep tests tidy and consistent. Optionally, you could add a Content-Type: application/json header for non‑GET methods to more closely mimic real client requests, though request.json() will still work without it.


466-612: PATCH tests are strong; consider asserting subscription boundary interaction

The PATCH tests nicely cover auth, validation, user‑not‑found, subscription enforcement, success cases (including special providers), repository params, and 500 + logging. In the “subscription checks” block you stub requiresProSubscription’s return value but don’t assert that it was called; adding an expectation like expect(requiresProSubscription).toHaveBeenCalledWith('pagespace', 'pro-model', 'free') would lock in the route’s reliance on the centralized subscription helper rather than inline logic, aligning with the boundary‑driven design.

apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (1)

38-44: Consider explicit typing for mock function parameter.

The msg parameter in the mock function is not explicitly typed. While TypeScript can infer the type, adding an explicit type annotation would improve clarity and align with the coding guideline requiring proper TypeScript types.

Apply this diff to add explicit typing:

-vi.mock('@/lib/ai/core', () => ({
-  convertDbMessageToUIMessage: vi.fn((msg) => ({
+vi.mock('@/lib/ai/core', () => ({
+  convertDbMessageToUIMessage: vi.fn((msg: any) => ({
     id: msg.id,
     role: msg.role,
     content: msg.content,
   })),
 }));

Note: The appropriate type would be the ChatMessage type from the repository. If you have access to that type at this level, consider using it instead of any.

apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (3)

44-51: Align mocked createPageEventPayload shape with production implementation (optional)

The real createPageEventPayload returns { driveId, pageId, operation, ...options }, while the mock currently uses an action property. It doesn’t break anything now (tests only care about call arguments), but mirroring the real shape will avoid confusion and make it safer if you later assert on the returned payload.

-vi.mock('@/lib/websocket', () => ({
-  broadcastPageEvent: vi.fn(),
-  createPageEventPayload: vi.fn((driveId, pageId, action, data) => ({
-    driveId,
-    pageId,
-    action,
-    ...data,
-  })),
-}));
+vi.mock('@/lib/websocket', () => ({
+  broadcastPageEvent: vi.fn(),
+  createPageEventPayload: vi.fn((driveId, pageId, operation, data) => ({
+    driveId,
+    pageId,
+    operation,
+    ...data,
+  })),
+}));

70-81: Consider adding Content-Type header in createRequest helper

The helper works today because request.json() doesn’t strictly require headers, but adding Content-Type: application/json would make the test requests closer to real traffic and more robust to any future body‑parsing changes.

-const createRequest = (body: Record<string, unknown>) =>
-  new Request('https://example.com/api/ai/page-agents/create', {
-    method: 'POST',
-    body: JSON.stringify(body),
-  });
+const createRequest = (body: Record<string, unknown>) =>
+  new Request('https://example.com/api/ai/page-agents/create', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(body),
+  });

Also applies to: 86-90


96-118: Optional: tighten not‑found and authorization tests with repository/permission call assertions

The 404/403 tests correctly exercise the response contracts. To further lock in boundary behavior, you could also assert the arguments passed to the repository and permission functions (e.g., getDriveById, getParentPage, canUserEditPage) so refactors don’t accidentally change which IDs are used:

// Drive not found
expect(pageAgentRepository.getDriveById).toHaveBeenCalledWith('nonexistent_drive');

// Parent page not found
expect(pageAgentRepository.getParentPage).toHaveBeenCalledWith(
  'nonexistent_parent',
  mockDriveId,
);

// Lacking edit permission
expect(canUserEditPage).toHaveBeenCalledWith(mockUserId, 'parent_123');

Not required for correctness, but it would strengthen the contract aspect of these tests.

Also applies to: 197-232, 234-271

apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (2)

109-117: Consider adding JSON Content-Type to the test Request helper

createRequest nicely centralizes request construction and the Promise-based params in createContext matches the Next.js 15 contract for dynamic routes. To better mirror real clients and catch any future header-sensitive logic, you could set the JSON Content-Type here.

-const createRequest = (agentId: string, body: Record<string, unknown>) =>
-  new Request(`https://example.com/api/ai/page-agents/${agentId}/config`, {
-    method: 'PUT',
-    body: JSON.stringify(body),
-  });
+const createRequest = (agentId: string, body: Record<string, unknown>) =>
+  new Request(`https://example.com/api/ai/page-agents/${agentId}/config`, {
+    method: 'PUT',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(body),
+  });

228-303: Successful updates and boundary obligations are well-tested; consider tightening a couple of assertions

The success-path tests verify:

  • specific fields in updatedFields for single and multi-field updates,
  • the exact payload passed into pageAgentRepository.updateAgentConfig, and
  • cache invalidation behavior and websocket payload composition via createPageEventPayload and agentAwarenessCache.invalidateDriveAgents.

Two optional refinements you might consider:

  1. For the “multiple fields at once” case, assert the actual set of fields instead of just length === 3, to make expectations more explicit and resilient to future additions:
expect(body.updatedFields).toEqual(
  expect.arrayContaining(['systemPrompt', 'enabledTools', 'aiProvider'])
);
  1. In the broadcast test, you already assert createPageEventPayload arguments; optionally also assert that broadcastPageEvent is called with that payload to fully lock down the wiring between the two boundaries.

Also applies to: 305-356

apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (1)

423-478: Pure processMessageContentUpdate tests are good; minor DRY and structure-alignment nits

The pure-function tests correctly use vi.importActual to bypass mocks and cover plain text, invalid JSON, structured textParts, and JSON-without-textParts cases. Two small, optional improvements:

  • Factor the repeated vi.importActual + destructuring into a small helper or beforeAll to avoid repetition.
  • The structured-case test asserts on textParts/partsOrder rather than the { parts: [{ type, text }] } shape we use when constructing messages for AI; just make sure upstream code that calls this helper is the one responsible for mapping into the canonical parts structure so this remains consistent. Based on learnings, this keeps the repository helper focused on its narrow concern.
apps/web/src/lib/repositories/conversation-repository.ts (1)

17-26: Consider narrowing the index signature if possible.

The [key: string]: unknown index signature is needed for Drizzle's execute<T> compatibility, but it weakens type safety by allowing arbitrary property access at compile time. If this becomes problematic, consider using a separate internal type for the raw result and mapping to the stricter ConversationStats type.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1)

383-437: Pure function tests correctly use vi.importActual to bypass mocks.

Using vi.importActual to test the real processMessageContentUpdate implementation is the right approach when the same module is mocked elsewhere. The tests cover the key content formats (plain text, invalid JSON, structured JSON with/without textParts).

Consider adding an edge case test for empty string content ('') to verify behavior.

apps/web/src/lib/repositories/global-conversation-repository.ts (2)

78-130: Pure function with implicit ordering dependency.

The calculateUsageSummary function assumes logs are ordered by timestamp descending (most recent first). This assumption is valid when called after getUsageLogs, which orders by desc(aiUsageLogs.timestamp). Consider documenting this precondition in the JSDoc.

 /**
  * Pure function: Calculate usage summary from logs
+ * @param logs - Usage logs, must be sorted by timestamp descending (most recent first)
+ * @param getContextWindow - Callback to get context window size for a given model
  */
 export function calculateUsageSummary(

258-271: Consider filtering by isActive in getMessageById.

The method returns messages regardless of their active status. In the route handlers, this could allow:

  • PATCH: editing an already-deleted message (updating invisible content)
  • DELETE: "deleting" an already-deleted message (harmless but unexpected)

If messages should only be editable/deletable when active, add the filter:

   async getMessageById(conversationId: string, messageId: string): Promise<Message | null> {
     const [message] = await db
       .select()
       .from(messages)
       .where(and(
         eq(messages.id, messageId),
-        eq(messages.conversationId, conversationId)
+        eq(messages.conversationId, conversationId),
+        eq(messages.isActive, true)
       ));

     return message || null;
   },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4499af1 and bf5968c.

📒 Files selected for processing (12)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (6 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (5 hunks)
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (7 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (5 hunks)
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts (19 hunks)
  • apps/web/src/lib/repositories/conversation-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/global-conversation-repository.ts (1 hunks)
  • apps/web/src/lib/repositories/page-agent-repository.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/lib/repositories/page-agent-repository.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/lib/repositories/conversation-repository.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and must be awaited before destructuring
Get request body using const body = await request.json();
Return JSON responses using Response.json(data) or NextResponse.json(data) in route handlers

Files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
🧠 Learnings (8)
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: The tech stack consists of Next.js 15 with App Router, TypeScript, Tailwind, shadcn/ui, PostgreSQL with Drizzle ORM, Ollama/Vercel AI SDK, custom JWT auth, and Socket.IO for real-time features

Applied to files:

  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:15.308Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.308Z
Learning: Applies to apps/web/src/app/**/*.ts : Get request body using `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get request body using `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/app/**/*.ts : Use `const body = await request.json();` to read request body in route handlers

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
🧬 Code graph analysis (9)
apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1)
apps/web/src/lib/ai/core/ai-utils.ts (1)
  • getUserOllamaSettings (513-532)
apps/web/src/lib/repositories/conversation-repository.ts (3)
packages/db/src/index.ts (2)
  • db (20-20)
  • and (8-8)
packages/db/src/schema/core.ts (2)
  • pages (24-66)
  • chatMessages (68-89)
packages/db/src/schema/monitoring.ts (1)
  • userActivities (111-137)
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (4)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • pageAgentRepository (46-155)
apps/web/src/app/api/ai/page-agents/create/route.ts (1)
  • POST (15-172)
apps/web/src/lib/websocket/socket-utils.ts (2)
  • createPageEventPayload (214-231)
  • broadcastPageEvent (88-122)
apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (3)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateHybridRequest (200-202)
  • isAuthError (204-206)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • chatMessageRepository (47-112)
apps/web/src/app/api/ai/chat/messages/route.ts (1)
  • GET (12-51)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (1)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • processMessageContentUpdate (29-45)
apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1)
apps/web/src/lib/ai/core/ai-utils.ts (1)
  • getUserLMStudioSettings (618-637)
apps/web/src/app/api/ai/settings/__tests__/route.test.ts (4)
apps/web/src/lib/auth/index.ts (2)
  • WebAuthResult (25-28)
  • AuthError (32-34)
apps/web/src/lib/repositories/ai-settings-repository.ts (1)
  • aiSettingsRepository (27-60)
apps/web/src/lib/ai/core/ai-utils.ts (6)
  • getUserOpenRouterSettings (58-85)
  • createOpenRouterSettings (91-124)
  • createOpenAISettings (273-306)
  • createAnthropicSettings (364-397)
  • createXAISettings (455-488)
  • createOllamaSettings (538-593)
apps/web/src/lib/subscription/rate-limit-middleware.ts (1)
  • requiresProSubscription (92-100)
apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (4)
apps/web/src/lib/auth/index.ts (4)
  • WebAuthResult (25-28)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/page-agent-repository.ts (1)
  • pageAgentRepository (46-155)
apps/web/src/app/api/ai/page-agents/[agentId]/config/route.ts (1)
  • PUT (15-165)
apps/web/src/lib/websocket/socket-utils.ts (2)
  • createPageEventPayload (214-231)
  • broadcastPageEvent (88-122)
apps/web/src/lib/repositories/global-conversation-repository.ts (3)
packages/db/src/index.ts (3)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
packages/db/src/schema/conversations.ts (2)
  • conversations (10-25)
  • messages (30-46)
packages/db/src/schema/monitoring.ts (1)
  • aiUsageLogs (142-201)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (45)
apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1)

91-95: Empty baseUrl scenario is now clearly and correctly covered

Updating the test to mock isConfigured: true with an empty baseUrl and adjusting the description makes the configuration validation more explicit and aligned with the route’s responsibility. This is a good, focused refinement to the test suite.

apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1)

1-415: Well-structured test suite with comprehensive coverage.

The test file demonstrates good testing practices:

  • Proper mock isolation and cleanup
  • Clear test organization with logical grouping
  • Comprehensive coverage of success paths, failures, and edge cases
  • Good helper functions for creating test fixtures
  • Appropriate assertions and logging verification
apps/web/src/app/api/ai/settings/__tests__/route.test.ts (3)

13-19: Boundary mocking and imports look consistent and well‑scoped

The module‑level vi.mock setup cleanly isolates the repository, auth, logging, AI core, and subscription seams, and the later imports (aiSettingsRepository, authenticateRequestWithOptions, isAuthError, loggers, AI helpers, requiresProSubscription) correctly align with those mocks. This matches the PR’s goal of testing only the route contracts against boundary interfaces.

Also applies to: 21-25, 27-37, 39-69, 71-75, 76-108


277-464: POST tests comprehensively exercise providers, validation, and error paths

Nice separation of auth, validation, success, and error handling for POST. The suite walks through invalid providers, required API key/base URL checks, and each provider’s happy path, then verifies 500 + logging when the save helper throws. This gives strong confidence in the POST route’s contract with the AI core helpers.


614-763: DELETE tests cleanly cover auth, validation, all providers, and failure logging

The DELETE suite mirrors the POST/PATCH structure: 401 auth, 400 invalid provider, 204 for each supported provider with correct helper calls, and 500 + loggers.ai.error on failure. This is a solid contract test for the delete behavior across all providers.

apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (5)

1-7: LGTM! Clear test documentation.

The header accurately describes the boundary-based testing approach, which aligns well with the PR objectives of introducing repository-layer modules and testing at boundary seams.


51-88: Excellent test fixtures.

The fixtures are comprehensive and well-structured. The mockChatMessage function includes all database fields (including isActive, createdAt, editedAt, toolCalls, toolResults) and uses a proper Partial pattern for overrides. This ensures tests accurately reflect the data structure from the repository layer.


90-98: LGTM! Clean helper function.

The createRequest helper properly encapsulates URL construction with query parameters, reducing duplication across test cases and making tests more readable.


100-113: Well-structured test setup.

The beforeEach properly resets mocks and establishes a default successful state, allowing individual tests to override only what they need. This "happy path by default" approach makes tests more focused and maintainable.


115-230: Excellent test coverage.

The test suite comprehensively covers all critical paths:

  • Authentication failure (401)
  • Validation failure (400 for missing pageId)
  • Authorization failure (403 when lacking view permission)
  • Authorization parameter verification (lines 153-159) - great addition to ensure correct pageId is passed
  • Successful retrieval with and without messages
  • ConversationId parameter handling (both present and absent)
  • Error handling with proper logging

The tests are well-organized by concern and verify both responses and boundary call parameters. This boundary-driven approach aligns perfectly with the PR's refactoring goals.

apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (5)

1-7: Boundary‑focused mocks and contract‑style framing look solid

Framing these as contract tests and mocking only the repository, auth, permissions/server, websocket, and AI tools seams keeps the suite aligned with the production boundaries and avoids ORM coupling. This matches the intended architecture and should make future refactors inside those modules low‑impact on the tests.

Also applies to: 13-21, 23-27, 29-41, 43-51, 54-63, 65-69


137-195: Validation tests nicely cover required fields and tool validation

The missing driveId/title/systemPrompt cases and invalid enabledTools case all map cleanly onto the route’s validation logic and assert on meaningful substrings, giving good coverage of client‑visible error messages without over‑specifying wording.


273-363: Repository contract and response‑shape checks for successful creation look comprehensive

The success tests exercise root‑level and folder‑level creation, verify the repository receives a well‑formed AgentData (including optional enabledTools, aiProvider, aiModel, welcomeMessage → content, and parentId semantics), and assert key fields in the JSON response and agentConfig. This gives strong confidence that the route respects the repository contract and returns the expected public API.


365-398: Good coverage of websocket and cache boundary obligations

The boundary obligation tests ensure createPageEventPayload is invoked with the correct IDs and operation and that both broadcastPageEvent and agentAwarenessCache.invalidateDriveAgents are called with the right drive. That’s exactly the sort of side effect that’s easy to regress without tests, so this coverage is valuable.


400-438: Error‑handling tests correctly enforce sanitized, non‑leaking messages

The repository‑failure tests now assert the exact generic message (toBe('Failed to create AI agent')) and explicitly check that sensitive substrings from the thrown error never appear in the response body. This closes the earlier leakage risk while still allowing detailed logging server‑side.

apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (6)

13-61: Boundary mocks and imports are well-structured and aligned with the route’s seams

Mocking the repository, auth, permissions/cache, websocket, and AI tools at module boundaries keeps these tests purely contract-focused and decoupled from DB or infra details, which fits the PR’s repository/boundary goals. The follow-up imports of the real functions under test also align with Vitest’s mocking pattern.

Also applies to: 63-67


68-80: Typed fixtures cleanly model auth and agent state

The strongly typed helpers (mockWebAuth, mockAuthError, mockAgent with a Partial overrides object) give clear, reusable defaults for different scenarios while staying type-safe and easy to extend if the agent/auth contracts evolve.

Also applies to: 85-107


119-140: beforeEach wiring establishes a solid “happy path” baseline

Using beforeEach to set the default authenticated user, granted permissions, existing agent, and successful repository update keeps individual tests focused on their specific deviations from the happy path. The combination of vi.clearAllMocks() plus re-applying defaults per test is appropriate here.


142-154: Good coverage of auth, not-found, and validation contracts

The tests around authentication (401), missing agents (404), AI_CHAT type validation, “no valid fields” handling, and invalid tools all exercise the key error branches and assert on both status codes and error message contents. This gives strong confidence that the PUT handler enforces its input and type constraints correctly.

Also applies to: 156-211


213-225: Authorization behavior is clearly specified

The 403 case with canUserEditPage mocked to false plus message assertion ensures the route respects centralized permission checks and returns a clear error when edits are not allowed.


359-372: 500-path error handling test correctly exercises repository failure

Forcing pageAgentRepository.updateAgentConfig to reject and asserting a 500 plus an error message containing “Failed to update” gives good regression coverage on the generic failure path without tying the test to specific internal logging behavior.

apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts (4)

13-21: Boundary mocks and fixtures are well-structured

The repository/auth/permissions/logging mocks and the shared fixtures (mockWebAuth, mockAuthError, mockChatMessage, createContext) cleanly express the external seams and keep the tests focused on handler behavior, especially the params: Promise.resolve({ messageId }) shape matching the dynamic route context contract. No issues here.

Also applies to: 23-49, 55-91


92-242: PATCH route contract coverage looks solid

The PATCH suite thoroughly exercises authentication, input validation, not-found, authorization (including correct canUserEditPage arguments and warning log), success response shape, and repository error handling with logging. This gives good confidence in the route’s external contract and boundary interactions.


288-420: DELETE route tests mirror PATCH coverage appropriately

The DELETE suite closely mirrors PATCH: it covers authentication, not-found, authorization (including warning logging), successful soft-delete (with repository call and info log), and repository error → 500 with error logging. The symmetry with PATCH helps keep behavior consistent and maintainable.


243-254: This comment appears to be based on incorrect assumptions about the current code.

The test at lines 243-254 already uses the correct approach. Line 244 imports processMessageContentUpdate via await import() without vi.importActual, which means it gets the mocked version defined at line 20 (processMessageContentUpdate: vi.fn(...)). The expectation at line 253 will work correctly because it's asserting against the vi.fn() mock that the route handler actually uses. No changes are needed.

apps/web/src/lib/repositories/conversation-repository.ts (7)

1-15: Well-structured repository module with proper imports.

The module correctly imports from @pagespace/db as per coding guidelines and defines clean interfaces for domain entities. The AiAgent interface appropriately captures the minimal fields needed from the pages table for AI_CHAT agents.


41-69: Clean pure functions with good testability.

Both extractPreviewText and generateTitle are well-designed pure functions that are easy to unit test. The JSON parsing in extractPreviewText correctly handles the two expected message content formats (array format and parts object format) with safe fallbacks.


71-91: LGTM - proper agent lookup with appropriate filtering.

The method correctly filters for non-trashed AI_CHAT pages and selects only the necessary columns. Using the Drizzle query builder's findFirst with columns projection is efficient.


96-161: Complex query is well-structured with proper column quoting.

The multi-CTE SQL approach efficiently retrieves conversation stats in a single query. The column names are correctly quoted to match the camelCase schema definitions. The logic for extracting first user message and last message is sound.

One minor note: LIMIT/OFFSET pagination can become slow at large offsets. If this list grows significantly, consider cursor-based pagination in the future.


197-218: Column naming issue has been correctly addressed.

The MIN("createdAt") and MAX("createdAt") now use the proper quoted camelCase column names matching the schema definition.


220-231: Soft delete correctly marks all conversation messages.

The method appropriately updates all messages in the conversation without filtering by isActive, ensuring idempotent behavior if called multiple times.


233-253: Audit logging correctly records deletion metadata.

The method properly logs the deletion event with comprehensive metadata including message count and timestamps. This provides good audit trail for compliance purposes.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (3)

5-6: Repository imports align with boundary-driven architecture.

The route correctly imports from the repository modules rather than directly accessing the database, enabling clean unit testing through boundary mocks.


14-73: PATCH handler correctly implements authorization and content update.

The handler properly:

  1. Awaits context.params per Next.js 15 requirements
  2. Validates input before any database operations
  3. Verifies user ownership of the conversation before proceeding
  4. Uses the repository boundary for all database operations
  5. Logs with masked identifiers for security

79-126: DELETE handler follows consistent patterns with proper soft-delete.

The handler correctly verifies ownership and message existence before performing the soft delete. The error handling and logging are consistent with the PATCH handler.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (4)

13-26: Boundary mocks are correctly structured.

The mocks for globalConversationRepository and processMessageContentUpdate properly isolate the route handler tests from actual database operations. The simplified mock for processMessageContentUpdate (just returning newContent) is appropriate since the actual function behavior is tested separately.


54-114: Test fixtures and request helpers are well-designed.

The factory functions for creating mock data and requests follow good testing patterns. The createContext helper correctly wraps params in Promise.resolve to match Next.js 15's async params behavior.


116-265: PATCH route tests provide comprehensive coverage.

The tests cover all expected scenarios: authentication failure, input validation, resource not found (both conversation and message), successful updates with repository call verification, and error handling. The boundary obligations (correct arguments to repository methods) are properly verified.


267-381: DELETE route tests maintain consistent coverage patterns.

The tests mirror the PATCH test structure appropriately, covering authentication, not found scenarios, successful deletion, and error handling. The verification that softDeleteMessage is called with the correct messageId confirms the boundary contract.

apps/web/src/lib/repositories/global-conversation-repository.ts (6)

1-8: Imports are correctly structured.

The repository properly imports from @pagespace/db for database operations and @paralleldrive/cuid2 for ID generation, following the project's established patterns.


9-77: Interface definitions are clean and well-organized.

The interface hierarchy with Conversation extending ConversationSummary is a good pattern for distinguishing between list view data and full entity data. The UsageLog interface correctly allows nullable fields matching the database schema.


132-152: LGTM - efficient conversation listing.

The method correctly filters by user and active status, returning only the necessary summary fields ordered by most recent activity.


154-177: Conversation creation correctly uses explicit ID generation.

Using createId() explicitly allows the repository to return the full conversation object synchronously without needing a separate query. The default values for type and title are reasonable.


273-284: Message update correctly sets editedAt timestamp.

The method appropriately updates both the content and editedAt timestamp. Authorization is handled at the route handler level before this method is called, which is a valid separation of concerns.


286-324: Soft delete and usage log retrieval are correctly implemented.

The softDeleteMessage method performs a simple state update. The getUsageLogs method selects all relevant fields and orders by timestamp descending, which satisfies the precondition for calculateUsageSummary.

- Remove tests mocking empty baseUrl for lmstudio/ollama since the real
  getUserLMStudioSettings/getUserOllamaSettings returns null when baseUrl
  is falsy - making those test scenarios impossible in production
- Add LM Studio mock and assertions to settings test that was only
  testing Ollama despite claiming to test both
Test improvements:
- Add Content-Type headers to non-GET request helpers for fidelity
- Add explicit ChatMessage typing to convertDbMessageToUIMessage mock
- Align createPageEventPayload mock with production (action → operation)
- Add requiresProSubscription call assertion in subscription checks
- Add repository/permission call assertions in 404/403 tests
- Tighten updatedFields assertions with expect.arrayContaining
- Assert broadcastPageEvent called with payload from createPageEventPayload
- Factor repeated vi.importActual into beforeAll for pure function tests
- Add edge case test for empty string content

Repository improvements:
- Document ordering precondition in calculateUsageSummary JSDoc
- Add isActive filter to getMessageById to prevent editing deleted messages

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1)

91-92: Avoid locking this test file to getUserOllamaSettings implementation details

The comment is helpful, but phrasing it as an implementation detail (“returns null when baseUrl is falsy”) can become stale if getUserOllamaSettings changes. Consider either:

  • Framing it as a contract: e.g. “This route only distinguishes between ‘configured’ and ‘not configured’; getUserOllamaSettings represents any invalid/missing baseUrl as null, which is covered by the test above.”, and/or
  • Referencing the dedicated tests for getUserOllamaSettings that assert the falsy‑baseUrl → null behavior.

This keeps the route tests focused on the boundary contract and reduces future maintenance if the underlying implementation evolves.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1)

383-434: Good pure function test coverage.

Using vi.importActual to test the real implementation while keeping boundary mocks is a solid pattern. Tests cover the main code paths including plain text, invalid JSON, and structured content.

Consider adding a test for JSON with textParts but missing partsOrder to fully cover the conditional at line 35 of chat-message-repository.ts.

+  it('should return new content when JSON has textParts but lacks partsOrder', () => {
+    const jsonWithTextPartsOnly = JSON.stringify({ textParts: ['Original'] });
+
+    const result = processMessageContentUpdate(jsonWithTextPartsOnly, 'New text');
+
+    expect(result).toBe('New text');
+  });
apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (1)

373-392: Consider verifying the payload passed to broadcastPageEvent.

The test verifies that createPageEventPayload is called with correct arguments and that broadcastPageEvent is called, but doesn't verify that broadcastPageEvent receives the payload returned by createPageEventPayload.

Consider this enhancement to ensure the route correctly pipes the payload:

 expect(createPageEventPayload).toHaveBeenCalledWith(
   mockDriveId,
   'agent_123',
   'created',
   expect.objectContaining({
     title: 'Test Agent',
     type: 'AI_CHAT',
   })
 );
-expect(broadcastPageEvent).toHaveBeenCalled();
+expect(broadcastPageEvent).toHaveBeenCalledWith(
+  expect.objectContaining({
+    driveId: mockDriveId,
+    pageId: 'agent_123',
+    operation: 'created',
+  })
+);
apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (3)

157-170: 404 resource-not-found path is exercised; consider asserting no update attempt

The test correctly forces getAgentById to return null and asserts a 404 with a “not found” message. You might optionally also assert that updateAgentConfig is not called in this case to lock in the non-mutating behavior for missing agents.


172-212: Validation scenarios are well-covered and reflect likely failure modes

You exercise wrong type (DOCUMENT), empty body, and invalid enabledTools (including echoing the invalid tool name), which gives good confidence that the route guards input before touching persistence. Additional edge cases (e.g., empty tools array, null vs. undefined fields) would be nice-to-have but not strictly necessary.


214-227: Authorization check is straightforward; optional: assert no repository update

The 403 path toggles canUserEditPage to false and asserts both status and error message, which is sufficient for the contract; you could optionally also assert that updateAgentConfig was not invoked to ensure no writes occur when permissions fail.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf5968c and 0edd7bb.

📒 Files selected for processing (8)
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (5 hunks)
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts (1 hunks)
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (6 hunks)
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (4 hunks)
  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts (17 hunks)
  • apps/web/src/lib/repositories/global-conversation-repository.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{ts,tsx}: No any types - always use proper TypeScript types
Use kebab-case for filenames (e.g., image-processor.ts)
Use camelCase for variables and functions
Use UPPER_SNAKE_CASE for constants
Use PascalCase for types and enums
Use centralized permission logic: import getUserAccessLevel and canUserEditPage from @pagespace/lib/permissions
Use Drizzle client from @pagespace/db for all database access
Always structure message content using the message parts structure: { parts: [{ type: 'text', text: '...' }] }
Use ESM modules and TypeScript strict mode

Files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier and lint with ESLint using the configuration at apps/web/eslint.config.mjs

Files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/**/*.{ts,tsx}: Use Zustand for client-side state management
Use SWR for server state and caching

Files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/ollama/models/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts
  • apps/web/src/lib/repositories/global-conversation-repository.ts
🧠 Learnings (3)
📚 Learning: 2025-12-14T14:54:47.103Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:47.103Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for `packages/lib` and `apps/processor` with `*.test.ts` files next to source or in `__tests__/` directories

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/ai/settings/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (4)
apps/web/src/lib/auth/index.ts (3)
  • AuthError (32-34)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • globalConversationRepository (135-330)
apps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.ts (2)
  • PATCH (14-73)
  • DELETE (79-126)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • processMessageContentUpdate (29-45)
apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (2)
apps/web/src/lib/repositories/chat-message-repository.ts (2)
  • ChatMessage (10-23)
  • chatMessageRepository (47-112)
apps/web/src/lib/auth/index.ts (2)
  • WebAuthResult (25-28)
  • AuthError (32-34)
apps/web/src/lib/repositories/global-conversation-repository.ts (4)
apps/ios/PageSpace/Core/Models/UsageData.swift (1)
  • getContextWindow (120-209)
packages/db/src/index.ts (3)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
packages/db/src/schema/conversations.ts (2)
  • conversations (10-25)
  • messages (30-46)
packages/db/src/schema/monitoring.ts (1)
  • aiUsageLogs (142-201)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (42)
apps/web/src/app/api/ai/lmstudio/models/__tests__/route.test.ts (1)

91-92: Verify that getUserLMStudioSettings returns null for falsy baseUrl.

The comment assumes that getUserLMStudioSettings returns null when baseUrl is falsy, which would mean the null-case test at lines 75-89 covers both "not configured" and "empty baseUrl" scenarios. Please verify this behavior in the actual implementation to ensure test coverage is complete.

Run the following script to examine the getUserLMStudioSettings implementation:

#!/bin/bash
# Description: Find and examine the getUserLMStudioSettings implementation to verify it returns null for falsy baseUrl

# Find the implementation file
echo "=== Finding getUserLMStudioSettings implementation ==="
ast-grep --pattern 'export function getUserLMStudioSettings($$$) {
  $$$
}'

# Also search with async
ast-grep --pattern 'export async function getUserLMStudioSettings($$$) {
  $$$
}'

# If ast-grep doesn't find it, use ripgrep with context
echo ""
echo "=== Searching for getUserLMStudioSettings definition ==="
rg -A 20 'function getUserLMStudioSettings' --type ts
apps/web/src/lib/repositories/global-conversation-repository.ts (5)

1-7: LGTM! Repository pattern with correct Drizzle client usage.

The module correctly uses the Drizzle client from @pagespace/db as required by the coding guidelines, properly isolating database operations from route handlers.


10-76: Well-structured type definitions.

The interfaces correctly model the database schema with appropriate nullable types. No any types are used, adhering to coding guidelines.


78-133: Well-designed pure function with good edge case handling.

The function correctly handles empty logs arrays by returning null for the context object. The docstring properly documents the expectation that logs are sorted in descending order by timestamp.


265-299: Authorization relies on caller to verify ownership first.

The getMessageById, updateMessageContent, and softDeleteMessage methods don't directly verify user ownership. This is acceptable for a repository seam since the route handlers verify conversation ownership via getConversationById(userId, conversationId) before calling these methods.

Ensure this pattern is documented or consistently followed to prevent future misuse.


304-329: LGTM! Correct usage log retrieval.

The method correctly filters by conversationId and orders by timestamp descending, which aligns with the calculateUsageSummary function's expectation.

apps/web/src/app/api/ai/global/[id]/messages/[messageId]/__tests__/route.test.ts (4)

13-52: Well-structured boundary mocks.

The test setup correctly mocks all boundary dependencies (repository, auth, logging), enabling isolated unit testing of the route handlers. This aligns with the PR's boundary-driven testing approach.


101-114: Correctly handles Next.js 15 async params.

The createContext helper correctly wraps params in Promise.resolve(), properly testing the Next.js 15 behavior where route context.params must be awaited.


116-265: Comprehensive PATCH route test coverage.

Tests cover authentication, validation, not-found scenarios, successful updates, and error handling. The assertions verify both response status/body and that repository methods are called with correct arguments.


267-381: Good DELETE route test coverage.

Tests properly verify the soft delete behavior and logging, mirroring the PATCH test structure for consistency.

apps/web/src/app/api/ai/settings/__tests__/route.test.ts (6)

1-74: LGTM! Excellent boundary mock setup.

The contract test approach is well-documented, and the mocking strategy correctly isolates boundaries (repository, auth, logging, subscription). This allows the tests to verify the route handler contracts without coupling to implementation details.


111-160: LGTM! Well-designed fixtures and builders.

The test fixtures and request builders follow the DRY principle effectively. The mockUserSettings function with partial overrides is particularly well-designed, allowing tests to customize specific fields while maintaining sensible defaults.


162-284: LGTM! Comprehensive GET endpoint coverage.

The GET tests thoroughly cover authentication, successful retrieval scenarios, and error handling. The explicit verification at line 264 that the repository is called with the correct userId is excellent practice for ensuring contract compliance.


286-473: LGTM! Thorough POST endpoint coverage.

The POST tests comprehensively validate all supported providers with proper authentication, validation, success, and error scenarios. The test at line 354-363 verifying API key trimming is a nice touch for security hygiene.


475-623: LGTM! Excellent PATCH endpoint coverage with subscription verification.

The PATCH tests thoroughly cover all scenarios including authentication, validation, user-not-found, subscription checks, and error handling. The explicit verification at lines 558 and 600-603 that the subscription middleware and repository are called with correct parameters demonstrates strong contract testing practices.


625-774: LGTM! Complete DELETE endpoint coverage.

The DELETE tests comprehensively verify deletion for all supported providers with proper authentication, validation, success (204), and error handling. The provider coverage matches the POST tests, ensuring consistent test coverage across the API surface.

apps/web/src/app/api/ai/page-agents/create/__tests__/route.test.ts (7)

1-69: Well-structured test setup with proper boundary mocking.

The test file correctly mocks all boundaries (repository, auth, permissions, websocket) at the module level before importing them. This follows vitest best practices and aligns with the contract testing approach described in the file header.


70-92: Clean and reusable test fixtures.

The helper functions (mockWebAuth, mockAuthError, createRequest) are well-defined and make the tests more readable and maintainable.


93-119: Excellent use of beforeEach for test isolation.

Setting up "happy path" defaults in beforeEach and clearing mocks ensures test isolation while keeping individual tests focused on their specific scenarios. Tests can selectively override mocks to test edge cases.


121-237: Comprehensive coverage of authentication, validation, and not-found scenarios.

These test suites thoroughly verify error handling with proper assertions on both response structure and boundary interactions. The verification of repository method arguments (lines 215, 235) ensures the route calls dependencies correctly.


239-370: Thorough testing of authorization and successful creation flows.

The tests verify both happy-path scenarios and authorization failures, with detailed assertions on the data passed to the repository. The use of expect.objectContaining (lines 314-326, 344-348) appropriately validates the core fields while allowing flexibility for framework-added properties.


407-446: Excellent defensive testing for error handling without information leakage.

The error handling tests properly verify that:

  1. Repository failures return 500 status (line 422)
  2. Error messages use exact generic text without internal details (line 423, 441)
  3. Sensitive information is explicitly excluded (lines 442-444)

The use of toBe('Failed to create AI agent') for exact matching (lines 423, 441) correctly addresses the past review comments about preventing error message leaks.


394-404: Proper verification of cache invalidation side effect.

The test ensures the agent awareness cache is correctly invalidated after agent creation, maintaining cache consistency.

apps/web/src/app/api/ai/chat/messages/__tests__/route.test.ts (10)

1-7: Excellent documentation for contract testing approach.

The header clearly articulates the boundary-based testing strategy, which aligns perfectly with the PR's architectural goals of introducing repository boundaries.


14-45: Well-structured boundary mocks following best practices.

All external dependencies are mocked at appropriate boundaries (repository, auth, permissions, converter), which enables true unit testing of the route handler logic. The mock implementations are clean and focused on the contract.


52-99: Comprehensive and reusable test fixtures.

The fixtures are well-designed with proper typing:

  • mockChatMessage includes all required ChatMessage fields from the repository interface
  • createRequest helper cleanly assembles URLs with query parameters
  • Use of Partial<> for overrides provides flexibility while maintaining type safety

102-114: Excellent test setup with sensible defaults.

The beforeEach establishes a "happy path" baseline (authenticated, authorized, empty messages), allowing individual tests to override only the specific behaviors they're testing. This reduces test boilerplate and makes test intent clearer.


116-127: Authentication test properly verifies the auth boundary contract.

The test correctly simulates authentication failure and verifies the expected 401 response.


129-139: Validation test properly verifies required parameter checking.

The test confirms that missing pageId returns a 400 error with an appropriate message, correctly testing the validation boundary.


141-161: Thorough authorization testing with interaction verification.

Excellent practice to have two tests:

  1. Verifying the 403 response when permission is denied
  2. Verifying that canUserViewPage is called with the correct userId and pageId

This ensures both the outcome and the contract are tested properly.


163-214: Comprehensive coverage of successful retrieval scenarios.

The four tests thoroughly cover:

  1. Multiple messages returned and properly converted
  2. Empty results handled correctly
  3. Optional conversationId parameter forwarded when provided
  4. Optional conversationId explicitly passed as undefined when omitted

The explicit testing of parameter forwarding (tests 3 and 4) is particularly valuable for verifying the repository boundary contract.


216-231: Error handling properly tested with logging verification.

The test confirms that repository errors are:

  1. Caught and converted to a 500 response
  2. Returned with an appropriate error message
  3. Logged via loggers.ai.error

This ensures both client response and observability are working correctly.


1-232: Exemplary boundary-based contract testing implementation.

This test file demonstrates excellent testing practices:

  • Comprehensive coverage: Authentication, validation, authorization, successful paths, and error handling
  • Proper boundaries: All external dependencies mocked at appropriate seams (repository, auth, permissions, converter)
  • Type safety: Proper TypeScript usage throughout with no any types, adhering to coding guidelines
  • Interaction testing: Not just testing outcomes but also verifying correct parameters passed to boundaries (e.g., lines 154-160, 192-213)
  • Maintainability: Clear fixtures, reusable helpers, and well-organized describe blocks

The test structure serves as a strong template for other route handler tests in this codebase.

apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts (9)

13-39: Boundary mocks are well-structured and align with the refactor goal

The repository, auth, permissions/loggers, websocket, and AI tools are all mocked at clear seams, which is exactly what you want for contract tests around the route handler; this keeps tests focused on HTTP and boundary behavior rather than implementation details.

Also applies to: 52-62


63-71: Imports and ID fixtures look consistent with boundary contracts

Importing the repository, auth helpers, permission/cache utilities, and websocket functions directly mirrors the real route dependencies, and the mockUserId / mockAgentId / mockDriveId fixtures give the tests clear, reusable identifiers.


73-107: Agent and auth fixtures provide a solid, reusable baseline

mockWebAuth, mockAuthError, and mockAgent cover the core shape the route expects (including type, driveId, tools, provider/model, and isTrashed), which keeps the individual tests concise while still realistic. This is a good pattern for contract tests.


109-119: Helpers correctly model Next.js 15 request and dynamic-route context

createRequest builds a proper PUT JSON request, and createContext returning { params: Promise.resolve({ agentId }) } matches the Next.js 15 convention where context.params is a Promise, so these helpers should keep tests aligned with the actual route signature over time.


120-141: beforeEach default wiring keeps tests focused and reduces duplication

Centralizing the “happy path” defaults (auth ok, permission granted, agent exists, repository update succeeds) in beforeEach makes individual test cases much easier to read and reason about while still allowing targeted overrides per scenario.


143-155: Authentication contract coverage is clear and minimal

The 401 case cleanly flips isAuthError and the auth result to an AuthError, then asserts only on the HTTP status, which is exactly what you need from a contract test here.


229-308: Successful update tests nicely pin both response shape and repository call

The cases for single-field updates, repository payload contents, agentDefinition, visibleToGlobalAssistant, and multi-field updates collectively give strong coverage of updatedFields semantics and the repository contract, including an explicit arrayContaining check that will tolerate future additions. This is a solid set of positive-path assertions.


310-363: Boundary-obligation tests for broadcasts and cache invalidation are precise

You verify that createPageEventPayload is called with the expected agent metadata and that broadcastPageEvent receives exactly that payload, plus you distinguish which field changes should trigger agentAwarenessCache.invalidateDriveAgents versus not. This is an excellent way to lock in side-effect behavior at the boundaries.


365-379: Repository error handling test cleanly asserts on 500 contract

Forcing updateAgentConfig to reject and asserting on a 500 with a “Failed to update” message ensures that unexpected repository errors are surfaced as generic server errors without leaking implementation details. This is a good, focused negative-path test.

Comment thread apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts Outdated
Import PageOperation type and add explicit parameter types to the mock
to satisfy noImplicitAny/strict typing rules.
- Reframe ollama test comment to focus on contract, not implementation
- Add test for JSON with textParts but missing partsOrder
- Verify broadcastPageEvent receives payload from createPageEventPayload
- Assert updateAgentConfig not called when agent not found (404)
- Assert updateAgentConfig not called when permissions fail (403)
@2witstudios
2witstudios merged commit a90dc44 into master Dec 15, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants