From 05e59689fae3d9ab27ba8617ff51a077fd29181e Mon Sep 17 00:00:00 2001
From: larryro <371767072@qq.com>
Date: Wed, 4 Mar 2026 03:20:18 +0800
Subject: [PATCH 1/3] fix(platform): handle unhandled promise rejections in
 query handlers

Wrap async operations (storage.getUrl, db.get, runQuery) in try/catch
blocks across branding, conversations, integrations, members, and
team_members queries to prevent unhandled rejections from crashing
reactive queries. Extract inline handlers into named testable functions
where applicable.
---
 .../convex/branding/__tests__/queries.test.ts |  31 ++
 services/platform/convex/branding/queries.ts  |  19 +-
 .../conversations/transform_conversation.ts   |  53 +--
 .../platform/convex/integrations/queries.ts   |  11 +-
 .../convex/members/__tests__/queries.test.ts  | 344 ++++++++++++++++++
 services/platform/convex/members/queries.ts   | 329 +++++++++--------
 .../platform/convex/team_members/queries.ts   |  32 +-
 7 files changed, 617 insertions(+), 202 deletions(-)
 create mode 100644 services/platform/convex/members/__tests__/queries.test.ts

diff --git a/services/platform/convex/branding/__tests__/queries.test.ts b/services/platform/convex/branding/__tests__/queries.test.ts
index e3f7776215..a1ff9978ee 100644
--- a/services/platform/convex/branding/__tests__/queries.test.ts
+++ b/services/platform/convex/branding/__tests__/queries.test.ts
@@ -155,4 +155,35 @@ describe('getBrandingHandler', () => {
       accentColor: undefined,
     });
   });
+
+  it('returns null for URLs when storage fetch fails', async () => {
+    const { ctx } = createMockQueryCtx({
+      _id: 'branding_1',
+      organizationId: 'org_1',
+      appName: 'Acme',
+      logoStorageId: 'storage_logo',
+      faviconLightStorageId: 'storage_fav_light',
+      faviconDarkStorageId: 'storage_fav_dark',
+      updatedAt: 1000,
+    });
+
+    ctx.storage.getUrl
+      .mockResolvedValueOnce('https://storage.example.com/storage_logo')
+      .mockRejectedValueOnce(new Error('Storage unavailable'))
+      .mockResolvedValueOnce('https://storage.example.com/storage_fav_dark');
+
+    const result = await getBrandingHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual({
+      appName: 'Acme',
+      textLogo: undefined,
+      logoUrl: 'https://storage.example.com/storage_logo',
+      faviconLightUrl: null,
+      faviconDarkUrl: 'https://storage.example.com/storage_fav_dark',
+      brandColor: undefined,
+      accentColor: undefined,
+    });
+  });
 });
diff --git a/services/platform/convex/branding/queries.ts b/services/platform/convex/branding/queries.ts
index a9741b9ae3..1b49f38226 100644
--- a/services/platform/convex/branding/queries.ts
+++ b/services/platform/convex/branding/queries.ts
@@ -22,14 +22,19 @@ export async function getBrandingHandler(
 
   if (!branding) return null;
 
+  async function safeGetUrl(storageId: string | undefined) {
+    if (!storageId) return null;
+    try {
+      return await ctx.storage.getUrl(storageId);
+    } catch {
+      return null;
+    }
+  }
+
   const [logoUrl, faviconLightUrl, faviconDarkUrl] = await Promise.all([
-    branding.logoStorageId ? ctx.storage.getUrl(branding.logoStorageId) : null,
-    branding.faviconLightStorageId
-      ? ctx.storage.getUrl(branding.faviconLightStorageId)
-      : null,
-    branding.faviconDarkStorageId
-      ? ctx.storage.getUrl(branding.faviconDarkStorageId)
-      : null,
+    safeGetUrl(branding.logoStorageId),
+    safeGetUrl(branding.faviconLightStorageId),
+    safeGetUrl(branding.faviconDarkStorageId),
   ]);
 
   return {
diff --git a/services/platform/convex/conversations/transform_conversation.ts b/services/platform/convex/conversations/transform_conversation.ts
index 910050de55..9413ddf722 100644
--- a/services/platform/convex/conversations/transform_conversation.ts
+++ b/services/platform/convex/conversations/transform_conversation.ts
@@ -20,30 +20,39 @@ export async function transformConversation(
 
   // Load customer and messages in parallel
   const [customerDoc, messageDocs] = await Promise.all([
-    conversation.customerId
-      ? ctx.db.get(conversation.customerId)
-      : Promise.resolve(null),
     (async () => {
-      if (includeAllMessages) {
-        const docs: Array<Doc<'conversationMessages'>> = [];
-        for await (const msg of ctx.db
-          .query('conversationMessages')
-          .withIndex('by_conversationId_and_deliveredAt', (q) =>
-            q.eq('conversationId', conversation._id),
-          )) {
-          docs.push(msg);
+      if (!conversation.customerId) return null;
+      try {
+        return await ctx.db.get(conversation.customerId);
+      } catch {
+        return null;
+      }
+    })(),
+    (async (): Promise<Array<Doc<'conversationMessages'>>> => {
+      try {
+        if (includeAllMessages) {
+          const docs: Array<Doc<'conversationMessages'>> = [];
+          for await (const msg of ctx.db
+            .query('conversationMessages')
+            .withIndex('by_conversationId_and_deliveredAt', (q) =>
+              q.eq('conversationId', conversation._id),
+            )) {
+            docs.push(msg);
+          }
+          return docs;
+        } else {
+          const lastMessage = await ctx.db
+            .query('conversationMessages')
+            .withIndex('by_conversationId_and_deliveredAt', (q) =>
+              q.eq('conversationId', conversation._id),
+            )
+            .order('desc')
+            .first();
+
+          return lastMessage ? [lastMessage] : [];
         }
-        return docs;
-      } else {
-        const lastMessage = await ctx.db
-          .query('conversationMessages')
-          .withIndex('by_conversationId_and_deliveredAt', (q) =>
-            q.eq('conversationId', conversation._id),
-          )
-          .order('desc')
-          .first();
-
-        return lastMessage ? [lastMessage] : [];
+      } catch {
+        return [];
       }
     })(),
   ]);
diff --git a/services/platform/convex/integrations/queries.ts b/services/platform/convex/integrations/queries.ts
index 70a0086676..a10a3647ea 100644
--- a/services/platform/convex/integrations/queries.ts
+++ b/services/platform/convex/integrations/queries.ts
@@ -15,9 +15,14 @@ async function withIconUrl(
   ctx: QueryCtx,
   integration: Doc<'integrations'>,
 ): Promise<IntegrationWithIcon> {
-  const iconUrl = integration.iconStorageId
-    ? await ctx.storage.getUrl(integration.iconStorageId)
-    : null;
+  let iconUrl: string | null = null;
+  if (integration.iconStorageId) {
+    try {
+      iconUrl = await ctx.storage.getUrl(integration.iconStorageId);
+    } catch {
+      // Graceful degradation: integration appears without icon
+    }
+  }
   return { ...integration, iconUrl };
 }
 
diff --git a/services/platform/convex/members/__tests__/queries.test.ts b/services/platform/convex/members/__tests__/queries.test.ts
new file mode 100644
index 0000000000..826116d770
--- /dev/null
+++ b/services/platform/convex/members/__tests__/queries.test.ts
@@ -0,0 +1,344 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+import type { QueryCtx } from '../../_generated/server';
+
+vi.mock('../../_generated/api', () => ({
+  components: {
+    betterAuth: {
+      adapter: {
+        findMany: 'betterAuth:adapter:findMany',
+        findOne: 'betterAuth:adapter:findOne',
+      },
+    },
+  },
+}));
+
+vi.mock('../../lib/rls', () => ({
+  getAuthUserIdentity: vi.fn(),
+  getOrganizationMember: vi.fn(),
+  getUserOrganizations: vi.fn(),
+}));
+
+vi.mock('../../lib/rls/errors', () => ({
+  UnauthenticatedError: class extends Error {
+    constructor() {
+      super('Unauthenticated');
+    }
+  },
+  UnauthorizedError: class extends Error {
+    constructor() {
+      super('Unauthorized');
+    }
+  },
+}));
+
+vi.mock('convex/values', () => {
+  const stub = () => 'validator';
+  return {
+    v: {
+      string: stub,
+      number: stub,
+      boolean: stub,
+      optional: stub,
+      union: stub,
+      object: stub,
+      literal: stub,
+      array: stub,
+      null: stub,
+      id: stub,
+    },
+  };
+});
+
+vi.mock('../validators', () => ({
+  memberRoleValidator: 'memberRoleValidator',
+}));
+
+vi.mock('../../_generated/server', async (importOriginal) => {
+  const mod = await importOriginal<Record<string, unknown>>();
+  return {
+    ...mod,
+    query: (config: Record<string, unknown>) => config,
+  };
+});
+
+const { getAuthUserIdentity, getOrganizationMember } =
+  await import('../../lib/rls');
+const {
+  getMyTeamsHandler,
+  approxCountMyTeamsHandler,
+  listByOrganizationHandler,
+} = await import('../queries');
+
+const mockedGetAuthUser = vi.mocked(getAuthUserIdentity);
+const mockedGetOrgMember = vi.mocked(getOrganizationMember);
+
+function createMockCtx() {
+  return {
+    runQuery: vi.fn(),
+    db: {},
+    auth: {},
+  };
+}
+
+describe('getMyTeamsHandler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('returns empty array when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+
+    const result = await getMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([]);
+  });
+
+  it('returns teams for user with memberships', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        { _id: 'tm_1', teamId: 'team_1', userId: 'user_1' },
+        { _id: 'tm_2', teamId: 'team_2', userId: 'user_1' },
+      ],
+    });
+
+    ctx.runQuery
+      .mockResolvedValueOnce({
+        page: [{ _id: 'team_1', name: 'Alpha', organizationId: 'org_1' }],
+      })
+      .mockResolvedValueOnce({
+        page: [{ _id: 'team_2', name: 'Beta', organizationId: 'org_1' }],
+      });
+
+    const result = await getMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([
+      { id: 'team_1', name: 'Alpha' },
+      { id: 'team_2', name: 'Beta' },
+    ]);
+  });
+
+  it('returns partial results when some team lookups fail', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        { _id: 'tm_1', teamId: 'team_1', userId: 'user_1' },
+        { _id: 'tm_2', teamId: 'team_2', userId: 'user_1' },
+        { _id: 'tm_3', teamId: 'team_3', userId: 'user_1' },
+      ],
+    });
+
+    ctx.runQuery
+      .mockResolvedValueOnce({
+        page: [{ _id: 'team_1', name: 'Alpha', organizationId: 'org_1' }],
+      })
+      .mockRejectedValueOnce(new Error('DB connection lost'))
+      .mockResolvedValueOnce({
+        page: [{ _id: 'team_3', name: 'Gamma', organizationId: 'org_1' }],
+      });
+
+    const result = await getMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([
+      { id: 'team_1', name: 'Alpha' },
+      { id: 'team_3', name: 'Gamma' },
+    ]);
+  });
+
+  it('returns empty array when all team lookups fail', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        { _id: 'tm_1', teamId: 'team_1', userId: 'user_1' },
+        { _id: 'tm_2', teamId: 'team_2', userId: 'user_1' },
+      ],
+    });
+
+    ctx.runQuery
+      .mockRejectedValueOnce(new Error('fail'))
+      .mockRejectedValueOnce(new Error('fail'));
+
+    const result = await getMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([]);
+  });
+});
+
+describe('approxCountMyTeamsHandler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('counts teams correctly', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        { _id: 'tm_1', teamId: 'team_1', userId: 'user_1' },
+        { _id: 'tm_2', teamId: 'team_2', userId: 'user_1' },
+      ],
+    });
+
+    ctx.runQuery
+      .mockResolvedValueOnce({ page: [{ _id: 'team_1' }] })
+      .mockResolvedValueOnce({ page: [{ _id: 'team_2' }] });
+
+    const result = await approxCountMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toBe(2);
+  });
+
+  it('counts only successful lookups on partial failure', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        { _id: 'tm_1', teamId: 'team_1', userId: 'user_1' },
+        { _id: 'tm_2', teamId: 'team_2', userId: 'user_1' },
+        { _id: 'tm_3', teamId: 'team_3', userId: 'user_1' },
+      ],
+    });
+
+    ctx.runQuery
+      .mockResolvedValueOnce({ page: [{ _id: 'team_1' }] })
+      .mockRejectedValueOnce(new Error('fail'))
+      .mockResolvedValueOnce({ page: [{ _id: 'team_3' }] });
+
+    const result = await approxCountMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toBe(2);
+  });
+});
+
+describe('listByOrganizationHandler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('returns members with user details', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockResolvedValue({
+      _id: 'om_1',
+      createdAt: 1000,
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'admin',
+    });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        {
+          _id: 'm_1',
+          organizationId: 'org_1',
+          userId: 'u_1',
+          role: 'admin',
+          createdAt: 1000,
+        },
+      ],
+    });
+
+    ctx.runQuery.mockResolvedValueOnce({
+      name: 'Alice',
+      email: 'alice@example.com',
+    });
+
+    const result = await listByOrganizationHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([
+      {
+        _id: 'm_1',
+        organizationId: 'org_1',
+        userId: 'u_1',
+        role: 'admin',
+        createdAt: 1000,
+        displayName: 'Alice',
+        email: 'alice@example.com',
+      },
+    ]);
+  });
+
+  it('returns member without name/email when user lookup fails', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockResolvedValue({
+      _id: 'om_1',
+      createdAt: 1000,
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'admin',
+    });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        {
+          _id: 'm_1',
+          organizationId: 'org_1',
+          userId: 'u_1',
+          role: 'member',
+          createdAt: 1000,
+        },
+        {
+          _id: 'm_2',
+          organizationId: 'org_1',
+          userId: 'u_2',
+          role: 'admin',
+          createdAt: 2000,
+        },
+      ],
+    });
+
+    ctx.runQuery
+      .mockRejectedValueOnce(new Error('lookup failed'))
+      .mockResolvedValueOnce({ name: 'Bob', email: 'bob@example.com' });
+
+    const result = await listByOrganizationHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([
+      {
+        _id: 'm_1',
+        organizationId: 'org_1',
+        userId: 'u_1',
+        role: 'member',
+        createdAt: 1000,
+        displayName: undefined,
+        email: undefined,
+      },
+      {
+        _id: 'm_2',
+        organizationId: 'org_1',
+        userId: 'u_2',
+        role: 'admin',
+        createdAt: 2000,
+        displayName: 'Bob',
+        email: 'bob@example.com',
+      },
+    ]);
+  });
+});
diff --git a/services/platform/convex/members/queries.ts b/services/platform/convex/members/queries.ts
index d8db221122..98d5929a19 100644
--- a/services/platform/convex/members/queries.ts
+++ b/services/platform/convex/members/queries.ts
@@ -4,7 +4,7 @@ import type { MemberRole } from '../../lib/shared/schemas/organizations';
 import type { BetterAuthFindManyResult, BetterAuthMember } from './types';
 
 import { components } from '../_generated/api';
-import { query } from '../_generated/server';
+import { query, QueryCtx } from '../_generated/server';
 import {
   getAuthUserIdentity,
   getOrganizationMember,
@@ -13,13 +13,6 @@ import {
 import { UnauthenticatedError, UnauthorizedError } from '../lib/rls/errors';
 import { memberRoleValidator } from './validators';
 
-interface BetterAuthTeam {
-  _id: string;
-  name: string;
-  organizationId: string;
-  createdAt?: number | null;
-}
-
 interface BetterAuthTeamMember {
   _id: string;
   teamId: string;
@@ -86,6 +79,75 @@ export const getCurrentMemberContext = query({
   },
 });
 
+export async function listByOrganizationHandler(
+  ctx: QueryCtx,
+  args: { organizationId: string },
+) {
+  const authUser = await getAuthUserIdentity(ctx);
+  if (!authUser) {
+    return [];
+  }
+
+  try {
+    await getOrganizationMember(ctx, args.organizationId, authUser);
+  } catch {
+    return [];
+  }
+
+  const result: BetterAuthFindManyResult<BetterAuthMember> = await ctx.runQuery(
+    components.betterAuth.adapter.findMany,
+    {
+      model: 'member',
+      paginationOpts: { cursor: null, numItems: 100 },
+      where: [
+        {
+          field: 'organizationId',
+          value: args.organizationId,
+          operator: 'eq',
+        },
+      ],
+    },
+  );
+
+  if (!result || result.page.length === 0) {
+    return [];
+  }
+
+  return Promise.all(
+    result.page.map(async (member) => {
+      let displayName: string | undefined;
+      let email: string | undefined;
+      try {
+        const userResult = await ctx.runQuery(
+          components.betterAuth.adapter.findOne,
+          {
+            model: 'user',
+            where: [{ field: '_id', value: member.userId, operator: 'eq' }],
+          },
+        );
+        displayName = userResult?.name;
+        email = userResult?.email;
+      } catch {
+        // Graceful degradation: member appears without name/email
+      }
+
+      const role: MemberRole = isValidRole(member.role)
+        ? member.role
+        : 'member';
+
+      return {
+        _id: member._id,
+        organizationId: member.organizationId,
+        userId: member.userId,
+        role,
+        createdAt: member.createdAt,
+        displayName,
+        email,
+      };
+    }),
+  );
+}
+
 export const listByOrganization = query({
   args: { organizationId: v.string() },
   returns: v.array(
@@ -99,61 +161,7 @@ export const listByOrganization = query({
       email: v.optional(v.string()),
     }),
   ),
-  handler: async (ctx, args) => {
-    const authUser = await getAuthUserIdentity(ctx);
-    if (!authUser) {
-      return [];
-    }
-
-    try {
-      await getOrganizationMember(ctx, args.organizationId, authUser);
-    } catch {
-      return [];
-    }
-
-    const result: BetterAuthFindManyResult<BetterAuthMember> =
-      await ctx.runQuery(components.betterAuth.adapter.findMany, {
-        model: 'member',
-        paginationOpts: { cursor: null, numItems: 100 },
-        where: [
-          {
-            field: 'organizationId',
-            value: args.organizationId,
-            operator: 'eq',
-          },
-        ],
-      });
-
-    if (!result || result.page.length === 0) {
-      return [];
-    }
-
-    return Promise.all(
-      result.page.map(async (member) => {
-        const userResult = await ctx.runQuery(
-          components.betterAuth.adapter.findOne,
-          {
-            model: 'user',
-            where: [{ field: '_id', value: member.userId, operator: 'eq' }],
-          },
-        );
-
-        const role: MemberRole = isValidRole(member.role)
-          ? member.role
-          : 'member';
-
-        return {
-          _id: member._id,
-          organizationId: member.organizationId,
-          userId: member.userId,
-          role,
-          createdAt: member.createdAt,
-          displayName: userResult?.name,
-          email: userResult?.email,
-        };
-      }),
-    );
-  },
+  handler: listByOrganizationHandler,
 });
 
 export const getUserIdByEmail = query({
@@ -198,108 +206,115 @@ export const getUserOrganizationsList = query({
   },
 });
 
-export const approxCountMyTeams = query({
-  args: {
-    organizationId: v.string(),
-  },
-  returns: v.number(),
-  handler: async (ctx, args) => {
-    const authUser = await getAuthUserIdentity(ctx);
-    if (!authUser) {
-      return 0;
-    }
-
-    const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
-      await ctx.runQuery(components.betterAuth.adapter.findMany, {
-        model: 'teamMember',
-        paginationOpts: { cursor: null, numItems: 100 },
-        where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
-      });
-
-    if (!membershipsResult || membershipsResult.page.length === 0) {
-      return 0;
-    }
-
-    const teamResults: BetterAuthFindManyResult<BetterAuthTeam>[] =
-      await Promise.all(
-        membershipsResult.page.map((membership) =>
-          ctx.runQuery(components.betterAuth.adapter.findMany, {
-            model: 'team',
-            paginationOpts: { cursor: null, numItems: 1 },
-            where: [
-              { field: '_id', operator: 'eq', value: membership.teamId },
-              {
-                field: 'organizationId',
-                operator: 'eq',
-                value: args.organizationId,
-              },
-            ],
-          }),
-        ),
-      );
-
-    return teamResults.filter((r) => r && r.page.length > 0).length;
-  },
-});
+export async function approxCountMyTeamsHandler(
+  ctx: QueryCtx,
+  args: { organizationId: string },
+) {
+  const authUser = await getAuthUserIdentity(ctx);
+  if (!authUser) {
+    return 0;
+  }
+
+  const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
+    await ctx.runQuery(components.betterAuth.adapter.findMany, {
+      model: 'teamMember',
+      paginationOpts: { cursor: null, numItems: 100 },
+      where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
+    });
 
-export const getMyTeams = query({
-  args: {
-    organizationId: v.string(),
-  },
-  returns: v.array(
-    v.object({
-      id: v.string(),
-      name: v.string(),
+  if (!membershipsResult || membershipsResult.page.length === 0) {
+    return 0;
+  }
+
+  const teamResults = await Promise.all(
+    membershipsResult.page.map(async (membership) => {
+      try {
+        return await ctx.runQuery(components.betterAuth.adapter.findMany, {
+          model: 'team',
+          paginationOpts: { cursor: null, numItems: 1 },
+          where: [
+            { field: '_id', operator: 'eq', value: membership.teamId },
+            {
+              field: 'organizationId',
+              operator: 'eq',
+              value: args.organizationId,
+            },
+          ],
+        });
+      } catch {
+        return null;
+      }
     }),
-  ),
-  handler: async (ctx, args) => {
-    const authUser = await getAuthUserIdentity(ctx);
-    if (!authUser) {
-      return [];
-    }
+  );
 
-    const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
-      await ctx.runQuery(components.betterAuth.adapter.findMany, {
-        model: 'teamMember',
-        paginationOpts: { cursor: null, numItems: 100 },
-        where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
-      });
+  return teamResults.filter((r) => r && r.page.length > 0).length;
+}
 
-    if (!membershipsResult || membershipsResult.page.length === 0) {
-      return [];
-    }
+export const approxCountMyTeams = query({
+  args: { organizationId: v.string() },
+  returns: v.number(),
+  handler: approxCountMyTeamsHandler,
+});
 
-    const teamIds = membershipsResult.page.map((m) => m.teamId);
-
-    const teamResults: BetterAuthFindManyResult<BetterAuthTeam>[] =
-      await Promise.all(
-        teamIds.map((teamId) =>
-          ctx.runQuery(components.betterAuth.adapter.findMany, {
-            model: 'team',
-            paginationOpts: { cursor: null, numItems: 1 },
-            where: [
-              { field: '_id', operator: 'eq', value: teamId },
-              {
-                field: 'organizationId',
-                operator: 'eq',
-                value: args.organizationId,
-              },
-            ],
-          }),
-        ),
-      );
+export async function getMyTeamsHandler(
+  ctx: QueryCtx,
+  args: { organizationId: string },
+) {
+  const authUser = await getAuthUserIdentity(ctx);
+  if (!authUser) {
+    return [];
+  }
+
+  const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
+    await ctx.runQuery(components.betterAuth.adapter.findMany, {
+      model: 'teamMember',
+      paginationOpts: { cursor: null, numItems: 100 },
+      where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
+    });
 
-    const teams: Array<{ id: string; name: string }> = [];
-    for (const teamResult of teamResults) {
-      if (teamResult && teamResult.page.length > 0) {
-        const team = teamResult.page[0];
-        teams.push({
-          id: team._id,
-          name: team.name,
+  if (!membershipsResult || membershipsResult.page.length === 0) {
+    return [];
+  }
+
+  const teamIds = membershipsResult.page.map((m) => m.teamId);
+
+  const teamResults = await Promise.all(
+    teamIds.map(async (teamId) => {
+      try {
+        return await ctx.runQuery(components.betterAuth.adapter.findMany, {
+          model: 'team',
+          paginationOpts: { cursor: null, numItems: 1 },
+          where: [
+            { field: '_id', operator: 'eq', value: teamId },
+            {
+              field: 'organizationId',
+              operator: 'eq',
+              value: args.organizationId,
+            },
+          ],
         });
+      } catch {
+        return null;
       }
+    }),
+  );
+
+  const teams: Array<{ id: string; name: string }> = [];
+  for (const teamResult of teamResults) {
+    if (teamResult && teamResult.page.length > 0) {
+      const team = teamResult.page[0];
+      teams.push({
+        id: team._id,
+        name: team.name,
+      });
     }
+  }
 
-    return teams;
-  },
+  return teams;
+}
+
+export const getMyTeams = query({
+  args: { organizationId: v.string() },
+  returns: v.array(v.object({ id: v.string(), name: v.string() })),
+  handler: getMyTeamsHandler,
 });
diff --git a/services/platform/convex/team_members/queries.ts b/services/platform/convex/team_members/queries.ts
index 447afcd263..0fcbab4149 100644
--- a/services/platform/convex/team_members/queries.ts
+++ b/services/platform/convex/team_members/queries.ts
@@ -66,19 +66,25 @@ export const listByTeam = query({
     const userMap = new Map<string, { name?: string; email?: string }>();
     await Promise.all(
       [...userIds].map(async (userId) => {
-        const userResult = await ctx.runQuery(
-          components.betterAuth.adapter.findOne,
-          {
-            model: 'user',
-            where: [{ field: '_id', value: userId, operator: 'eq' }],
-          },
-        );
-        if (userResult) {
-          const name =
-            typeof userResult.name === 'string' ? userResult.name : undefined;
-          const email =
-            typeof userResult.email === 'string' ? userResult.email : undefined;
-          userMap.set(userId, { name, email });
+        try {
+          const userResult = await ctx.runQuery(
+            components.betterAuth.adapter.findOne,
+            {
+              model: 'user',
+              where: [{ field: '_id', value: userId, operator: 'eq' }],
+            },
+          );
+          if (userResult) {
+            const name =
+              typeof userResult.name === 'string' ? userResult.name : undefined;
+            const email =
+              typeof userResult.email === 'string'
+                ? userResult.email
+                : undefined;
+            userMap.set(userId, { name, email });
+          }
+        } catch {
+          // Graceful degradation: member appears without name/email
         }
       }),
     );

From 40e0b2b4258dec86e871168df3c3960cbcb9cb41 Mon Sep 17 00:00:00 2001
From: larryro <371767072@qq.com>
Date: Wed, 4 Mar 2026 11:21:15 +0800
Subject: [PATCH 2/3] fix(platform): narrow catch blocks to re-throw unexpected
 errors

Replace blanket catch-all blocks with targeted UnauthorizedError checks
so unexpected failures (e.g. DB connection errors) propagate instead of
being silently swallowed. Add diagnostic console.warn logging to
remaining graceful-degradation catches and remove unnecessary try-catch
wrappers in transform_conversation. Add test coverage for auth edge
cases and error re-throwing.
---
 services/platform/convex/branding/queries.ts  |   7 +-
 .../conversations/transform_conversation.ts   |  53 ++--
 .../integrations/__tests__/queries.test.ts    | 254 ++++++++++++++++++
 .../platform/convex/integrations/queries.ts   |  30 ++-
 .../convex/members/__tests__/queries.test.ts  |  93 ++++++-
 services/platform/convex/members/queries.ts   | 110 +++++---
 .../team_members/__tests__/queries.test.ts    | 222 +++++++++++++++
 .../platform/convex/team_members/queries.ts   |  16 +-
 8 files changed, 686 insertions(+), 99 deletions(-)
 create mode 100644 services/platform/convex/integrations/__tests__/queries.test.ts
 create mode 100644 services/platform/convex/team_members/__tests__/queries.test.ts

diff --git a/services/platform/convex/branding/queries.ts b/services/platform/convex/branding/queries.ts
index 1b49f38226..3fbe70a105 100644
--- a/services/platform/convex/branding/queries.ts
+++ b/services/platform/convex/branding/queries.ts
@@ -26,7 +26,12 @@ export async function getBrandingHandler(
     if (!storageId) return null;
     try {
       return await ctx.storage.getUrl(storageId);
-    } catch {
+    } catch (error) {
+      console.warn(
+        '[Branding] Failed to resolve storage URL',
+        storageId,
+        error,
+      );
       return null;
     }
   }
diff --git a/services/platform/convex/conversations/transform_conversation.ts b/services/platform/convex/conversations/transform_conversation.ts
index 9413ddf722..910050de55 100644
--- a/services/platform/convex/conversations/transform_conversation.ts
+++ b/services/platform/convex/conversations/transform_conversation.ts
@@ -20,39 +20,30 @@ export async function transformConversation(
 
   // Load customer and messages in parallel
   const [customerDoc, messageDocs] = await Promise.all([
+    conversation.customerId
+      ? ctx.db.get(conversation.customerId)
+      : Promise.resolve(null),
     (async () => {
-      if (!conversation.customerId) return null;
-      try {
-        return await ctx.db.get(conversation.customerId);
-      } catch {
-        return null;
-      }
-    })(),
-    (async (): Promise<Array<Doc<'conversationMessages'>>> => {
-      try {
-        if (includeAllMessages) {
-          const docs: Array<Doc<'conversationMessages'>> = [];
-          for await (const msg of ctx.db
-            .query('conversationMessages')
-            .withIndex('by_conversationId_and_deliveredAt', (q) =>
-              q.eq('conversationId', conversation._id),
-            )) {
-            docs.push(msg);
-          }
-          return docs;
-        } else {
-          const lastMessage = await ctx.db
-            .query('conversationMessages')
-            .withIndex('by_conversationId_and_deliveredAt', (q) =>
-              q.eq('conversationId', conversation._id),
-            )
-            .order('desc')
-            .first();
-
-          return lastMessage ? [lastMessage] : [];
+      if (includeAllMessages) {
+        const docs: Array<Doc<'conversationMessages'>> = [];
+        for await (const msg of ctx.db
+          .query('conversationMessages')
+          .withIndex('by_conversationId_and_deliveredAt', (q) =>
+            q.eq('conversationId', conversation._id),
+          )) {
+          docs.push(msg);
         }
-      } catch {
-        return [];
+        return docs;
+      } else {
+        const lastMessage = await ctx.db
+          .query('conversationMessages')
+          .withIndex('by_conversationId_and_deliveredAt', (q) =>
+            q.eq('conversationId', conversation._id),
+          )
+          .order('desc')
+          .first();
+
+        return lastMessage ? [lastMessage] : [];
       }
     })(),
   ]);
diff --git a/services/platform/convex/integrations/__tests__/queries.test.ts b/services/platform/convex/integrations/__tests__/queries.test.ts
new file mode 100644
index 0000000000..6ad1801fce
--- /dev/null
+++ b/services/platform/convex/integrations/__tests__/queries.test.ts
@@ -0,0 +1,254 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+import type { Doc } from '../../_generated/dataModel';
+
+vi.mock('../../_generated/api', () => ({
+  components: {
+    betterAuth: {
+      adapter: {
+        findMany: 'betterAuth:adapter:findMany',
+        findOne: 'betterAuth:adapter:findOne',
+      },
+    },
+  },
+}));
+
+vi.mock('../../lib/rls', () => ({
+  getAuthUserIdentity: vi.fn(),
+  getOrganizationMember: vi.fn(),
+}));
+
+vi.mock('../../lib/rls/errors', () => {
+  class RLSError extends Error {
+    constructor(
+      message: string,
+      public code: string,
+    ) {
+      super(message);
+      this.name = 'RLSError';
+    }
+  }
+  return {
+    UnauthorizedError: class extends RLSError {
+      constructor() {
+        super('Not authorized to access this resource', 'UNAUTHORIZED');
+      }
+    },
+  };
+});
+
+vi.mock('convex/values', () => {
+  const stub = () => 'validator';
+  return {
+    v: {
+      string: stub,
+      number: stub,
+      boolean: stub,
+      optional: stub,
+      union: stub,
+      object: stub,
+      literal: stub,
+      array: stub,
+      null: stub,
+      id: stub,
+    },
+  };
+});
+
+vi.mock('../validators', () => ({
+  integrationDocValidator: 'integrationDocValidator',
+}));
+
+vi.mock('../get_integration', () => ({
+  getIntegration: vi.fn(),
+}));
+
+vi.mock('../get_integration_by_name', () => ({
+  getIntegrationByName: vi.fn(),
+}));
+
+vi.mock('../list_integrations', () => ({
+  listIntegrations: vi.fn(),
+}));
+
+vi.mock('../../_generated/server', async (importOriginal) => {
+  const mod = await importOriginal<Record<string, unknown>>();
+  return {
+    ...mod,
+    query: (config: Record<string, unknown>) => config,
+  };
+});
+
+const { getAuthUserIdentity, getOrganizationMember } =
+  await import('../../lib/rls');
+const { UnauthorizedError } = await import('../../lib/rls/errors');
+const { getIntegration } = await import('../get_integration');
+const { listIntegrations } = await import('../list_integrations');
+
+const mockedGetAuthUser = vi.mocked(getAuthUserIdentity);
+const mockedGetOrgMember = vi.mocked(getOrganizationMember);
+const mockedGetIntegration = vi.mocked(getIntegration);
+const mockedListIntegrations = vi.mocked(listIntegrations);
+
+function createMockCtx() {
+  return {
+    runQuery: vi.fn(),
+    storage: { getUrl: vi.fn() },
+    db: {},
+    auth: {},
+  };
+}
+
+function makeIntegrationDoc(
+  overrides: Record<string, unknown> = {},
+): Doc<'integrations'> {
+  return {
+    _id: 'int_1',
+    _creationTime: 1000,
+    organizationId: 'org_1',
+    name: 'Test Integration',
+    type: 'rest_api',
+    iconStorageId: undefined,
+    ...overrides,
+  } as unknown as Doc<'integrations'>;
+}
+
+describe('get handler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('returns null when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const { get } = await import('../queries');
+    const handler = (get as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { integrationId: 'int_1' });
+
+    expect(result).toBeNull();
+  });
+
+  it('returns null when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetIntegration.mockResolvedValue(makeIntegrationDoc());
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const ctx = createMockCtx();
+    const { get } = await import('../queries');
+    const handler = (get as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { integrationId: 'int_1' });
+
+    expect(result).toBeNull();
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetIntegration.mockResolvedValue(makeIntegrationDoc());
+    mockedGetOrgMember.mockRejectedValue(new Error('DB failure'));
+    const ctx = createMockCtx();
+    const { get } = await import('../queries');
+    const handler = (get as unknown as { handler: Function }).handler;
+
+    await expect(handler(ctx, { integrationId: 'int_1' })).rejects.toThrow(
+      'DB failure',
+    );
+  });
+
+  it('returns integration with icon URL on success', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const doc = makeIntegrationDoc({ iconStorageId: 'storage_icon' });
+    mockedGetIntegration.mockResolvedValue(doc);
+    mockedGetOrgMember.mockResolvedValue({} as never);
+    const ctx = createMockCtx();
+    ctx.storage.getUrl.mockResolvedValue('https://storage.example.com/icon');
+    const { get } = await import('../queries');
+    const handler = (get as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { integrationId: 'int_1' });
+
+    expect(result).toMatchObject({
+      _id: 'int_1',
+      iconUrl: 'https://storage.example.com/icon',
+    });
+  });
+
+  it('returns null iconUrl when storage fetch fails', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const doc = makeIntegrationDoc({ iconStorageId: 'storage_icon' });
+    mockedGetIntegration.mockResolvedValue(doc);
+    mockedGetOrgMember.mockResolvedValue({} as never);
+    const ctx = createMockCtx();
+    ctx.storage.getUrl.mockRejectedValue(new Error('Storage unavailable'));
+    const { get } = await import('../queries');
+    const handler = (get as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { integrationId: 'int_1' });
+
+    expect(result).toMatchObject({
+      _id: 'int_1',
+      iconUrl: null,
+    });
+  });
+});
+
+describe('list handler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('returns empty array when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const { list } = await import('../queries');
+    const handler = (list as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toEqual([]);
+  });
+
+  it('returns empty array when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const ctx = createMockCtx();
+    const { list } = await import('../queries');
+    const handler = (list as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toEqual([]);
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new Error('System error'));
+    const ctx = createMockCtx();
+    const { list } = await import('../queries');
+    const handler = (list as unknown as { handler: Function }).handler;
+
+    await expect(handler(ctx, { organizationId: 'org_1' })).rejects.toThrow(
+      'System error',
+    );
+  });
+
+  it('returns integrations with icon URLs', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockResolvedValue({} as never);
+    const docs = [
+      makeIntegrationDoc({ _id: 'int_1', iconStorageId: 'sid_1' }),
+      makeIntegrationDoc({ _id: 'int_2' }),
+    ];
+    mockedListIntegrations.mockResolvedValue(docs);
+    const ctx = createMockCtx();
+    ctx.storage.getUrl.mockResolvedValueOnce('https://storage.example.com/i1');
+    const { list } = await import('../queries');
+    const handler = (list as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toHaveLength(2);
+    expect(result[0].iconUrl).toBe('https://storage.example.com/i1');
+    expect(result[1].iconUrl).toBeNull();
+  });
+});
diff --git a/services/platform/convex/integrations/queries.ts b/services/platform/convex/integrations/queries.ts
index a10a3647ea..0a7e479d16 100644
--- a/services/platform/convex/integrations/queries.ts
+++ b/services/platform/convex/integrations/queries.ts
@@ -4,6 +4,7 @@ import type { Doc } from '../_generated/dataModel';
 
 import { query, QueryCtx } from '../_generated/server';
 import { getAuthUserIdentity, getOrganizationMember } from '../lib/rls';
+import { UnauthorizedError } from '../lib/rls/errors';
 import { getIntegration } from './get_integration';
 import { getIntegrationByName } from './get_integration_by_name';
 import { listIntegrations } from './list_integrations';
@@ -19,8 +20,12 @@ async function withIconUrl(
   if (integration.iconStorageId) {
     try {
       iconUrl = await ctx.storage.getUrl(integration.iconStorageId);
-    } catch {
-      // Graceful degradation: integration appears without icon
+    } catch (error) {
+      console.warn(
+        '[Integrations] Failed to resolve icon URL',
+        integration.iconStorageId,
+        error,
+      );
     }
   }
   return { ...integration, iconUrl };
@@ -44,8 +49,11 @@ export const get = query({
 
     try {
       await getOrganizationMember(ctx, integration.organizationId, authUser);
-    } catch {
-      return null;
+    } catch (error) {
+      if (error instanceof UnauthorizedError) {
+        return null;
+      }
+      throw error;
     }
 
     return await withIconUrl(ctx, integration);
@@ -66,8 +74,11 @@ export const getByName = query({
 
     try {
       await getOrganizationMember(ctx, args.organizationId, authUser);
-    } catch {
-      return null;
+    } catch (error) {
+      if (error instanceof UnauthorizedError) {
+        return null;
+      }
+      throw error;
     }
 
     const integration = await getIntegrationByName(ctx, args);
@@ -93,8 +104,11 @@ export const list = query({
 
     try {
       await getOrganizationMember(ctx, args.organizationId, authUser);
-    } catch {
-      return [];
+    } catch (error) {
+      if (error instanceof UnauthorizedError) {
+        return [];
+      }
+      throw error;
     }
 
     const integrations = await listIntegrations(ctx, args);
diff --git a/services/platform/convex/members/__tests__/queries.test.ts b/services/platform/convex/members/__tests__/queries.test.ts
index 826116d770..ae4b11870f 100644
--- a/services/platform/convex/members/__tests__/queries.test.ts
+++ b/services/platform/convex/members/__tests__/queries.test.ts
@@ -19,18 +19,29 @@ vi.mock('../../lib/rls', () => ({
   getUserOrganizations: vi.fn(),
 }));
 
-vi.mock('../../lib/rls/errors', () => ({
-  UnauthenticatedError: class extends Error {
-    constructor() {
-      super('Unauthenticated');
+vi.mock('../../lib/rls/errors', () => {
+  class RLSError extends Error {
+    constructor(
+      message: string,
+      public code: string,
+    ) {
+      super(message);
+      this.name = 'RLSError';
     }
-  },
-  UnauthorizedError: class extends Error {
-    constructor() {
-      super('Unauthorized');
-    }
-  },
-}));
+  }
+  return {
+    UnauthenticatedError: class extends RLSError {
+      constructor() {
+        super('Authentication required', 'UNAUTHENTICATED');
+      }
+    },
+    UnauthorizedError: class extends RLSError {
+      constructor() {
+        super('Not authorized to access this resource', 'UNAUTHORIZED');
+      }
+    },
+  };
+});
 
 vi.mock('convex/values', () => {
   const stub = () => 'validator';
@@ -64,6 +75,7 @@ vi.mock('../../_generated/server', async (importOriginal) => {
 
 const { getAuthUserIdentity, getOrganizationMember } =
   await import('../../lib/rls');
+const { UnauthorizedError } = await import('../../lib/rls/errors');
 const {
   getMyTeamsHandler,
   approxCountMyTeamsHandler,
@@ -97,6 +109,19 @@ describe('getMyTeamsHandler', () => {
     expect(result).toEqual([]);
   });
 
+  it('returns empty array when user has no team memberships', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({ page: [] });
+
+    const result = await getMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([]);
+  });
+
   it('returns teams for user with memberships', async () => {
     mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
     const ctx = createMockCtx();
@@ -185,6 +210,17 @@ describe('approxCountMyTeamsHandler', () => {
     vi.clearAllMocks();
   });
 
+  it('returns 0 when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+
+    const result = await approxCountMyTeamsHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toBe(0);
+  });
+
   it('counts teams correctly', async () => {
     mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
     const ctx = createMockCtx();
@@ -237,6 +273,41 @@ describe('listByOrganizationHandler', () => {
     vi.clearAllMocks();
   });
 
+  it('returns empty array when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+
+    const result = await listByOrganizationHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([]);
+  });
+
+  it('returns empty array when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const ctx = createMockCtx();
+
+    const result = await listByOrganizationHandler(ctx as unknown as QueryCtx, {
+      organizationId: 'org_1',
+    });
+
+    expect(result).toEqual([]);
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new Error('DB connection lost'));
+    const ctx = createMockCtx();
+
+    await expect(
+      listByOrganizationHandler(ctx as unknown as QueryCtx, {
+        organizationId: 'org_1',
+      }),
+    ).rejects.toThrow('DB connection lost');
+  });
+
   it('returns members with user details', async () => {
     mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
     mockedGetOrgMember.mockResolvedValue({
diff --git a/services/platform/convex/members/queries.ts b/services/platform/convex/members/queries.ts
index 98d5929a19..922270e72c 100644
--- a/services/platform/convex/members/queries.ts
+++ b/services/platform/convex/members/queries.ts
@@ -13,6 +13,13 @@ import {
 import { UnauthenticatedError, UnauthorizedError } from '../lib/rls/errors';
 import { memberRoleValidator } from './validators';
 
+interface BetterAuthTeam {
+  _id: string;
+  name: string;
+  organizationId: string;
+  createdAt?: number | null;
+}
+
 interface BetterAuthTeamMember {
   _id: string;
   teamId: string;
@@ -90,8 +97,11 @@ export async function listByOrganizationHandler(
 
   try {
     await getOrganizationMember(ctx, args.organizationId, authUser);
-  } catch {
-    return [];
+  } catch (error) {
+    if (error instanceof UnauthorizedError) {
+      return [];
+    }
+    throw error;
   }
 
   const result: BetterAuthFindManyResult<BetterAuthMember> = await ctx.runQuery(
@@ -127,8 +137,12 @@ export async function listByOrganizationHandler(
         );
         displayName = userResult?.name;
         email = userResult?.email;
-      } catch {
-        // Graceful degradation: member appears without name/email
+      } catch (error) {
+        console.warn(
+          '[Members] Failed to fetch user details',
+          member.userId,
+          error,
+        );
       }
 
       const role: MemberRole = isValidRole(member.role)
@@ -226,26 +240,32 @@ export async function approxCountMyTeamsHandler(
     return 0;
   }
 
-  const teamResults = await Promise.all(
-    membershipsResult.page.map(async (membership) => {
-      try {
-        return await ctx.runQuery(components.betterAuth.adapter.findMany, {
-          model: 'team',
-          paginationOpts: { cursor: null, numItems: 1 },
-          where: [
-            { field: '_id', operator: 'eq', value: membership.teamId },
-            {
-              field: 'organizationId',
-              operator: 'eq',
-              value: args.organizationId,
-            },
-          ],
-        });
-      } catch {
-        return null;
-      }
-    }),
-  );
+  const teamResults: (BetterAuthFindManyResult<BetterAuthTeam> | null)[] =
+    await Promise.all(
+      membershipsResult.page.map(async (membership) => {
+        try {
+          return await ctx.runQuery(components.betterAuth.adapter.findMany, {
+            model: 'team',
+            paginationOpts: { cursor: null, numItems: 1 },
+            where: [
+              { field: '_id', operator: 'eq', value: membership.teamId },
+              {
+                field: 'organizationId',
+                operator: 'eq',
+                value: args.organizationId,
+              },
+            ],
+          });
+        } catch (error) {
+          console.warn(
+            '[Members] Failed to look up team',
+            membership.teamId,
+            error,
+          );
+          return null;
+        }
+      }),
+    );
 
   return teamResults.filter((r) => r && r.page.length > 0).length;
 }
@@ -278,26 +298,28 @@ export async function getMyTeamsHandler(
 
   const teamIds = membershipsResult.page.map((m) => m.teamId);
 
-  const teamResults = await Promise.all(
-    teamIds.map(async (teamId) => {
-      try {
-        return await ctx.runQuery(components.betterAuth.adapter.findMany, {
-          model: 'team',
-          paginationOpts: { cursor: null, numItems: 1 },
-          where: [
-            { field: '_id', operator: 'eq', value: teamId },
-            {
-              field: 'organizationId',
-              operator: 'eq',
-              value: args.organizationId,
-            },
-          ],
-        });
-      } catch {
-        return null;
-      }
-    }),
-  );
+  const teamResults: (BetterAuthFindManyResult<BetterAuthTeam> | null)[] =
+    await Promise.all(
+      teamIds.map(async (teamId) => {
+        try {
+          return await ctx.runQuery(components.betterAuth.adapter.findMany, {
+            model: 'team',
+            paginationOpts: { cursor: null, numItems: 1 },
+            where: [
+              { field: '_id', operator: 'eq', value: teamId },
+              {
+                field: 'organizationId',
+                operator: 'eq',
+                value: args.organizationId,
+              },
+            ],
+          });
+        } catch (error) {
+          console.warn('[Members] Failed to look up team', teamId, error);
+          return null;
+        }
+      }),
+    );
 
   const teams: Array<{ id: string; name: string }> = [];
   for (const teamResult of teamResults) {
diff --git a/services/platform/convex/team_members/__tests__/queries.test.ts b/services/platform/convex/team_members/__tests__/queries.test.ts
new file mode 100644
index 0000000000..d3a906a8c7
--- /dev/null
+++ b/services/platform/convex/team_members/__tests__/queries.test.ts
@@ -0,0 +1,222 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+vi.mock('../../_generated/api', () => ({
+  components: {
+    betterAuth: {
+      adapter: {
+        findMany: 'betterAuth:adapter:findMany',
+        findOne: 'betterAuth:adapter:findOne',
+      },
+    },
+  },
+}));
+
+vi.mock('../../lib/rls', () => ({
+  getAuthUserIdentity: vi.fn(),
+  getOrganizationMember: vi.fn(),
+}));
+
+vi.mock('../../lib/rls/errors', () => {
+  class RLSError extends Error {
+    constructor(
+      message: string,
+      public code: string,
+    ) {
+      super(message);
+      this.name = 'RLSError';
+    }
+  }
+  return {
+    UnauthorizedError: class extends RLSError {
+      constructor() {
+        super('Not authorized to access this resource', 'UNAUTHORIZED');
+      }
+    },
+  };
+});
+
+vi.mock('convex/values', () => {
+  const stub = () => 'validator';
+  return {
+    v: {
+      string: stub,
+      number: stub,
+      boolean: stub,
+      optional: stub,
+      union: stub,
+      object: stub,
+      literal: stub,
+      array: stub,
+      null: stub,
+      id: stub,
+    },
+  };
+});
+
+vi.mock('../../_generated/server', async (importOriginal) => {
+  const mod = await importOriginal<Record<string, unknown>>();
+  return {
+    ...mod,
+    query: (config: Record<string, unknown>) => config,
+  };
+});
+
+const { getAuthUserIdentity, getOrganizationMember } =
+  await import('../../lib/rls');
+const { UnauthorizedError } = await import('../../lib/rls/errors');
+
+const mockedGetAuthUser = vi.mocked(getAuthUserIdentity);
+const mockedGetOrgMember = vi.mocked(getOrganizationMember);
+
+function createMockCtx() {
+  return {
+    runQuery: vi.fn(),
+    db: {},
+    auth: {},
+  };
+}
+
+describe('listByTeam handler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  async function getHandler() {
+    const { listByTeam } = await import('../queries');
+    return (listByTeam as unknown as { handler: Function }).handler;
+  }
+
+  it('returns empty array when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { teamId: 'team_1' });
+
+    expect(result).toEqual([]);
+  });
+
+  it('returns empty array when team not found', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+    ctx.runQuery.mockResolvedValueOnce(null);
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { teamId: 'team_1' });
+
+    expect(result).toEqual([]);
+  });
+
+  it('returns empty array when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+    ctx.runQuery.mockResolvedValueOnce({
+      _id: 'team_1',
+      organizationId: 'org_1',
+    });
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { teamId: 'team_1' });
+
+    expect(result).toEqual([]);
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+    ctx.runQuery.mockResolvedValueOnce({
+      _id: 'team_1',
+      organizationId: 'org_1',
+    });
+    mockedGetOrgMember.mockRejectedValue(new Error('DB failure'));
+    const handler = await getHandler();
+
+    await expect(handler(ctx, { teamId: 'team_1' })).rejects.toThrow(
+      'DB failure',
+    );
+  });
+
+  it('returns members with user details', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      _id: 'team_1',
+      organizationId: 'org_1',
+    });
+    mockedGetOrgMember.mockResolvedValue({} as never);
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        {
+          _id: 'tm_1',
+          teamId: 'team_1',
+          userId: 'u_1',
+          role: 'admin',
+          createdAt: 1000,
+        },
+      ],
+    });
+
+    ctx.runQuery.mockResolvedValueOnce({
+      name: 'Alice',
+      email: 'alice@example.com',
+    });
+
+    const handler = await getHandler();
+    const result = await handler(ctx, { teamId: 'team_1' });
+
+    expect(result).toEqual([
+      {
+        _id: 'tm_1',
+        teamId: 'team_1',
+        userId: 'u_1',
+        role: 'admin',
+        joinedAt: 1000,
+        displayName: 'Alice',
+        email: 'alice@example.com',
+      },
+    ]);
+  });
+
+  it('returns member without name/email when user lookup fails', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    const ctx = createMockCtx();
+
+    ctx.runQuery.mockResolvedValueOnce({
+      _id: 'team_1',
+      organizationId: 'org_1',
+    });
+    mockedGetOrgMember.mockResolvedValue({} as never);
+
+    ctx.runQuery.mockResolvedValueOnce({
+      page: [
+        {
+          _id: 'tm_1',
+          teamId: 'team_1',
+          userId: 'u_1',
+          role: 'member',
+          createdAt: 2000,
+        },
+      ],
+    });
+
+    ctx.runQuery.mockRejectedValueOnce(new Error('lookup failed'));
+
+    const handler = await getHandler();
+    const result = await handler(ctx, { teamId: 'team_1' });
+
+    expect(result).toEqual([
+      {
+        _id: 'tm_1',
+        teamId: 'team_1',
+        userId: 'u_1',
+        role: 'member',
+        joinedAt: 2000,
+        displayName: undefined,
+        email: undefined,
+      },
+    ]);
+  });
+});
diff --git a/services/platform/convex/team_members/queries.ts b/services/platform/convex/team_members/queries.ts
index 0fcbab4149..1c18b2d9dc 100644
--- a/services/platform/convex/team_members/queries.ts
+++ b/services/platform/convex/team_members/queries.ts
@@ -3,6 +3,7 @@ import { v } from 'convex/values';
 import { components } from '../_generated/api';
 import { query } from '../_generated/server';
 import { getAuthUserIdentity, getOrganizationMember } from '../lib/rls';
+import { UnauthorizedError } from '../lib/rls/errors';
 
 export const listByTeam = query({
   args: {
@@ -35,8 +36,11 @@ export const listByTeam = query({
 
     try {
       await getOrganizationMember(ctx, team.organizationId, authUser);
-    } catch {
-      return [];
+    } catch (error) {
+      if (error instanceof UnauthorizedError) {
+        return [];
+      }
+      throw error;
     }
 
     const result = await ctx.runQuery(components.betterAuth.adapter.findMany, {
@@ -83,8 +87,12 @@ export const listByTeam = query({
                 : undefined;
             userMap.set(userId, { name, email });
           }
-        } catch {
-          // Graceful degradation: member appears without name/email
+        } catch (error) {
+          console.warn(
+            '[TeamMembers] Failed to fetch user details',
+            userId,
+            error,
+          );
         }
       }),
     );

From 93c3ca6f180678e18873eaf338e0019ebd156136 Mon Sep 17 00:00:00 2001
From: larryro <371767072@qq.com>
Date: Wed, 4 Mar 2026 11:44:39 +0800
Subject: [PATCH 3/3] test(platform): add tests for getByName and
 getCurrentMemberContext query handlers

---
 .../integrations/__tests__/queries.test.ts    | 86 ++++++++++++++++
 .../convex/members/__tests__/queries.test.ts  | 97 ++++++++++++++++++-
 2 files changed, 182 insertions(+), 1 deletion(-)

diff --git a/services/platform/convex/integrations/__tests__/queries.test.ts b/services/platform/convex/integrations/__tests__/queries.test.ts
index 6ad1801fce..ab246bb579 100644
--- a/services/platform/convex/integrations/__tests__/queries.test.ts
+++ b/services/platform/convex/integrations/__tests__/queries.test.ts
@@ -83,11 +83,13 @@ const { getAuthUserIdentity, getOrganizationMember } =
   await import('../../lib/rls');
 const { UnauthorizedError } = await import('../../lib/rls/errors');
 const { getIntegration } = await import('../get_integration');
+const { getIntegrationByName } = await import('../get_integration_by_name');
 const { listIntegrations } = await import('../list_integrations');
 
 const mockedGetAuthUser = vi.mocked(getAuthUserIdentity);
 const mockedGetOrgMember = vi.mocked(getOrganizationMember);
 const mockedGetIntegration = vi.mocked(getIntegration);
+const mockedGetIntegrationByName = vi.mocked(getIntegrationByName);
 const mockedListIntegrations = vi.mocked(listIntegrations);
 
 function createMockCtx() {
@@ -192,6 +194,90 @@ describe('get handler', () => {
   });
 });
 
+describe('getByName handler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('returns null when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const { getByName } = await import('../queries');
+    const handler = (getByName as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, {
+      organizationId: 'org_1',
+      name: 'My Integration',
+    });
+
+    expect(result).toBeNull();
+  });
+
+  it('returns null when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const ctx = createMockCtx();
+    const { getByName } = await import('../queries');
+    const handler = (getByName as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, {
+      organizationId: 'org_1',
+      name: 'My Integration',
+    });
+
+    expect(result).toBeNull();
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockRejectedValue(new Error('DB failure'));
+    const ctx = createMockCtx();
+    const { getByName } = await import('../queries');
+    const handler = (getByName as unknown as { handler: Function }).handler;
+
+    await expect(
+      handler(ctx, { organizationId: 'org_1', name: 'My Integration' }),
+    ).rejects.toThrow('DB failure');
+  });
+
+  it('returns null when integration not found', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockResolvedValue({} as never);
+    mockedGetIntegrationByName.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const { getByName } = await import('../queries');
+    const handler = (getByName as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, {
+      organizationId: 'org_1',
+      name: 'Nonexistent',
+    });
+
+    expect(result).toBeNull();
+  });
+
+  it('returns integration with icon URL on success', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1' });
+    mockedGetOrgMember.mockResolvedValue({} as never);
+    const doc = makeIntegrationDoc({ iconStorageId: 'storage_icon' });
+    mockedGetIntegrationByName.mockResolvedValue(doc);
+    const ctx = createMockCtx();
+    ctx.storage.getUrl.mockResolvedValue('https://storage.example.com/icon');
+    const { getByName } = await import('../queries');
+    const handler = (getByName as unknown as { handler: Function }).handler;
+
+    const result = await handler(ctx, {
+      organizationId: 'org_1',
+      name: 'Test Integration',
+    });
+
+    expect(result).toMatchObject({
+      _id: 'int_1',
+      iconUrl: 'https://storage.example.com/icon',
+    });
+  });
+});
+
 describe('list handler', () => {
   beforeEach(() => {
     vi.clearAllMocks();
diff --git a/services/platform/convex/members/__tests__/queries.test.ts b/services/platform/convex/members/__tests__/queries.test.ts
index ae4b11870f..61a3836d97 100644
--- a/services/platform/convex/members/__tests__/queries.test.ts
+++ b/services/platform/convex/members/__tests__/queries.test.ts
@@ -75,7 +75,8 @@ vi.mock('../../_generated/server', async (importOriginal) => {
 
 const { getAuthUserIdentity, getOrganizationMember } =
   await import('../../lib/rls');
-const { UnauthorizedError } = await import('../../lib/rls/errors');
+const { UnauthenticatedError, UnauthorizedError } =
+  await import('../../lib/rls/errors');
 const {
   getMyTeamsHandler,
   approxCountMyTeamsHandler,
@@ -93,6 +94,100 @@ function createMockCtx() {
   };
 }
 
+describe('getCurrentMemberContext handler', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  async function getHandler() {
+    const { getCurrentMemberContext } = await import('../queries');
+    return (getCurrentMemberContext as unknown as { handler: Function })
+      .handler;
+  }
+
+  it('throws UnauthenticatedError when not authenticated', async () => {
+    mockedGetAuthUser.mockResolvedValue(null);
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    await expect(
+      handler(ctx, { organizationId: 'org_1' }),
+    ).rejects.toBeInstanceOf(UnauthenticatedError);
+  });
+
+  it('returns null when unauthorized', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1', name: 'Alice' });
+    mockedGetOrgMember.mockRejectedValue(new UnauthorizedError());
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toBeNull();
+  });
+
+  it('re-throws non-authorization errors from getOrganizationMember', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1', name: 'Alice' });
+    mockedGetOrgMember.mockRejectedValue(new Error('DB failure'));
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    await expect(handler(ctx, { organizationId: 'org_1' })).rejects.toThrow(
+      'DB failure',
+    );
+  });
+
+  it('returns member context on success', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1', name: 'Alice' });
+    mockedGetOrgMember.mockResolvedValue({
+      _id: 'om_1',
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'admin',
+      createdAt: 1000,
+    });
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toEqual({
+      memberId: 'om_1',
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'admin',
+      createdAt: 1000,
+      displayName: 'Alice',
+      isAdmin: true,
+    });
+  });
+
+  it('defaults to member role for invalid roles', async () => {
+    mockedGetAuthUser.mockResolvedValue({ userId: 'user_1', name: 'Bob' });
+    mockedGetOrgMember.mockResolvedValue({
+      _id: 'om_2',
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'superadmin',
+      createdAt: 2000,
+    });
+    const ctx = createMockCtx();
+    const handler = await getHandler();
+
+    const result = await handler(ctx, { organizationId: 'org_1' });
+
+    expect(result).toEqual({
+      memberId: 'om_2',
+      organizationId: 'org_1',
+      userId: 'user_1',
+      role: 'member',
+      createdAt: 2000,
+      displayName: 'Bob',
+      isAdmin: false,
+    });
+  });
+});
+
 describe('getMyTeamsHandler', () => {
   beforeEach(() => {
     vi.clearAllMocks();
