diff --git a/apps/web/src/controllers/HomePage/HomePageController.test.tsx b/apps/web/src/controllers/HomePage/HomePageController.test.tsx
new file mode 100644
index 000000000..6efda55cd
--- /dev/null
+++ b/apps/web/src/controllers/HomePage/HomePageController.test.tsx
@@ -0,0 +1,195 @@
+import { render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { HomePageController } from './HomePageController';
+
+// Mock the dependencies
+vi.mock('@/api/buster_rest/users/useGetUserInfo', () => ({
+ useGetUserBasicInfo: vi.fn(),
+}));
+
+vi.mock('./useNewChatWarning', () => ({
+ useNewChatWarning: vi.fn(),
+}));
+
+vi.mock('./NewChatWarning', () => ({
+ NewChatWarning: vi.fn(({ showWarning, hasDatasets, hasDatasources, isAdmin }) => (
+
+ Warning Component - showWarning: {showWarning.toString()}, hasDatasets:{' '}
+ {hasDatasets.toString()}, hasDatasources: {hasDatasources.toString()}, isAdmin:{' '}
+ {isAdmin?.toString() || 'undefined'}
+
+ )),
+}));
+
+vi.mock('./NewChatInput', () => ({
+ NewChatInput: vi.fn(({ initialValue, autoSubmit }) => (
+
+ Chat Input - initialValue: {initialValue || 'none'}, autoSubmit:{' '}
+ {autoSubmit?.toString() || 'false'}
+
+ )),
+}));
+
+// Mock ClientOnly to render children directly in tests
+vi.mock('@tanstack/react-router', () => ({
+ ClientOnly: vi.fn(({ children }) => <>{children}>),
+}));
+
+import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
+import { useNewChatWarning } from './useNewChatWarning';
+
+const mockUseGetUserBasicInfo = vi.mocked(useGetUserBasicInfo);
+const mockUseNewChatWarning = vi.mocked(useNewChatWarning);
+
+describe('HomePageController', () => {
+ beforeEach(() => {
+ // Default user info
+ mockUseGetUserBasicInfo.mockReturnValue({
+ id: 'user-1',
+ name: 'John Doe',
+ email: 'john.doe@example.com',
+ avatar_url: null,
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-01T00:00:00Z',
+ attributes: {
+ organization_id: 'org-1',
+ organization_role: 'querier',
+ user_email: 'john.doe@example.com',
+ user_id: 'user-1',
+ },
+ favorites: [],
+ });
+
+ // Mock time to ensure consistent greeting
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2023-01-01 10:00:00')); // 10 AM - morning
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.clearAllMocks();
+ });
+
+ it('should render NewChatWarning when showWarning is true', () => {
+ // Mock the hook to return showWarning: true
+ mockUseNewChatWarning.mockReturnValue({
+ showWarning: true,
+ hasDatasets: false,
+ hasDatasources: false,
+ isFetched: true,
+ isAdmin: true,
+ userRole: 'workspace_admin',
+ });
+
+ render();
+
+ // Should show the warning component
+ expect(screen.getByTestId('new-chat-warning')).toBeInTheDocument();
+ expect(screen.getByText(/Warning Component.*showWarning: true/)).toBeInTheDocument();
+
+ // Should NOT show the main interface components
+ expect(screen.queryByTestId('new-chat-input')).not.toBeInTheDocument();
+ expect(screen.queryByText('Good morning, John Doe')).not.toBeInTheDocument();
+ expect(screen.queryByText('How can I help you today?')).not.toBeInTheDocument();
+ });
+
+ it('should render main interface when showWarning is false', () => {
+ // Mock the hook to return showWarning: false
+ mockUseNewChatWarning.mockReturnValue({
+ showWarning: false,
+ hasDatasets: true,
+ hasDatasources: true,
+ isFetched: true,
+ isAdmin: false,
+ userRole: 'querier',
+ });
+
+ render();
+
+ // Should show the main interface components
+ expect(screen.getByText('Good morning, John Doe')).toBeInTheDocument();
+ expect(screen.getByText('How can I help you today?')).toBeInTheDocument();
+ expect(screen.getByTestId('new-chat-input')).toBeInTheDocument();
+ expect(
+ screen.getByText(/Chat Input.*initialValue: hello world.*autoSubmit: false/)
+ ).toBeInTheDocument();
+
+ // Should NOT show the warning component
+ expect(screen.queryByTestId('new-chat-warning')).not.toBeInTheDocument();
+ });
+
+ it('should pass correct props to NewChatInput', () => {
+ mockUseNewChatWarning.mockReturnValue({
+ showWarning: false,
+ hasDatasets: true,
+ hasDatasources: true,
+ isFetched: true,
+ isAdmin: false,
+ userRole: 'querier',
+ });
+
+ render();
+
+ expect(
+ screen.getByText(/Chat Input.*initialValue: custom input.*autoSubmit: true/)
+ ).toBeInTheDocument();
+ });
+
+ it('should pass all newChatWarningProps to NewChatWarning', () => {
+ const warningProps = {
+ showWarning: true,
+ hasDatasets: false,
+ hasDatasources: true,
+ isFetched: true,
+ isAdmin: true,
+ userRole: 'workspace_admin' as const,
+ };
+
+ mockUseNewChatWarning.mockReturnValue(warningProps);
+
+ render();
+
+ expect(
+ screen.getByText(
+ /Warning Component.*showWarning: true.*hasDatasets: false.*hasDatasources: true.*isAdmin: true/
+ )
+ ).toBeInTheDocument();
+ });
+
+ describe('greeting logic', () => {
+ beforeEach(() => {
+ mockUseNewChatWarning.mockReturnValue({
+ showWarning: false,
+ hasDatasets: true,
+ hasDatasources: true,
+ isFetched: true,
+ isAdmin: false,
+ userRole: 'querier',
+ });
+ });
+
+ it('should show morning greeting at 10 AM', () => {
+ vi.setSystemTime(new Date('2023-01-01 10:00:00'));
+ render();
+ expect(screen.getByText('Good morning, John Doe')).toBeInTheDocument();
+ });
+
+ it('should show afternoon greeting at 3 PM', () => {
+ vi.setSystemTime(new Date('2023-01-01 15:00:00'));
+ render();
+ expect(screen.getByText('Good afternoon, John Doe')).toBeInTheDocument();
+ });
+
+ it('should show evening greeting at 8 PM', () => {
+ vi.setSystemTime(new Date('2023-01-01 20:00:00'));
+ render();
+ expect(screen.getByText('Good evening, John Doe')).toBeInTheDocument();
+ });
+
+ it('should show night greeting at 2 AM', () => {
+ vi.setSystemTime(new Date('2023-01-01 02:00:00'));
+ render();
+ expect(screen.getByText('Good night, John Doe')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/apps/web/src/controllers/HomePage/useNewChatWarning.test.ts b/apps/web/src/controllers/HomePage/useNewChatWarning.test.ts
new file mode 100644
index 000000000..4f96e4c86
--- /dev/null
+++ b/apps/web/src/controllers/HomePage/useNewChatWarning.test.ts
@@ -0,0 +1,185 @@
+import { renderHook } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { useNewChatWarning } from './useNewChatWarning';
+
+// Mock the API hooks
+vi.mock('@/api/buster_rest/data_source', () => ({
+ useListDatasources: vi.fn(),
+}));
+
+vi.mock('@/api/buster_rest/datasets', () => ({
+ useGetDatasets: vi.fn(),
+}));
+
+vi.mock('@/api/buster_rest/users/useGetUserInfo', () => ({
+ useIsUserAdmin: vi.fn(),
+ useGetUserRole: vi.fn(),
+}));
+
+import { useListDatasources } from '@/api/buster_rest/data_source';
+import { useGetDatasets } from '@/api/buster_rest/datasets';
+import { useGetUserRole, useIsUserAdmin } from '@/api/buster_rest/users/useGetUserInfo';
+
+const mockUseListDatasources = vi.mocked(useListDatasources);
+const mockUseGetDatasets = vi.mocked(useGetDatasets);
+const mockUseIsUserAdmin = vi.mocked(useIsUserAdmin);
+const mockUseGetUserRole = vi.mocked(useGetUserRole);
+
+describe('useNewChatWarning', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should show warning when datasets are empty and data is fetched', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [{ id: '1', name: 'Test Datasource' }],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(true);
+ mockUseGetUserRole.mockReturnValue('workspace_admin');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(true);
+ expect(result.current.hasDatasets).toBe(false);
+ expect(result.current.hasDatasources).toBe(true);
+ expect(result.current.isFetched).toBe(true);
+ });
+
+ it('should show warning when datasources are empty and data is fetched', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [{ id: '1', name: 'Test Dataset' }],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(false);
+ mockUseGetUserRole.mockReturnValue('querier');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(true);
+ expect(result.current.hasDatasets).toBe(true);
+ expect(result.current.hasDatasources).toBe(false);
+ expect(result.current.isFetched).toBe(true);
+ });
+
+ it('should show warning when both datasets and datasources are empty', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(true);
+ mockUseGetUserRole.mockReturnValue('workspace_admin');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(true);
+ expect(result.current.hasDatasets).toBe(false);
+ expect(result.current.hasDatasources).toBe(false);
+ expect(result.current.isFetched).toBe(true);
+ });
+
+ it('should not show warning when both datasets and datasources have data', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [{ id: '1', name: 'Test Dataset' }],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [{ id: '1', name: 'Test Datasource' }],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(false);
+ mockUseGetUserRole.mockReturnValue('viewer');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(false);
+ expect(result.current.hasDatasets).toBe(true);
+ expect(result.current.hasDatasources).toBe(true);
+ expect(result.current.isFetched).toBe(true);
+ });
+
+ it('should not show warning when datasets are not yet fetched', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: undefined,
+ isFetched: false,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(true);
+ mockUseGetUserRole.mockReturnValue('workspace_admin');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(false);
+ expect(result.current.isFetched).toBe(false);
+ });
+
+ it('should not show warning when datasources are not yet fetched', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: undefined,
+ isFetched: false,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(false);
+ mockUseGetUserRole.mockReturnValue('querier');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(false);
+ expect(result.current.isFetched).toBe(false);
+ });
+
+ it('should not show warning when neither datasets nor datasources are fetched', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: undefined,
+ isFetched: false,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: undefined,
+ isFetched: false,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(true);
+ mockUseGetUserRole.mockReturnValue('workspace_admin');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.showWarning).toBe(false);
+ expect(result.current.isFetched).toBe(false);
+ });
+
+ it('should correctly return admin status and user role', () => {
+ mockUseGetDatasets.mockReturnValue({
+ data: [{ id: '1', name: 'Test Dataset' }],
+ isFetched: true,
+ } as any);
+ mockUseListDatasources.mockReturnValue({
+ data: [{ id: '1', name: 'Test Datasource' }],
+ isFetched: true,
+ } as any);
+ mockUseIsUserAdmin.mockReturnValue(true);
+ mockUseGetUserRole.mockReturnValue('data_admin');
+
+ const { result } = renderHook(() => useNewChatWarning());
+
+ expect(result.current.isAdmin).toBe(true);
+ expect(result.current.userRole).toBe('data_admin');
+ expect(result.current.showWarning).toBe(false);
+ });
+});
diff --git a/apps/web/src/lib/user.test.ts b/apps/web/src/lib/user.test.ts
new file mode 100644
index 000000000..560758007
--- /dev/null
+++ b/apps/web/src/lib/user.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from 'vitest';
+import { checkIfUserIsAdmin } from './user';
+
+describe('checkIfUserIsAdmin', () => {
+ it('should return false when userOrganization is undefined', () => {
+ expect(checkIfUserIsAdmin(undefined)).toBe(false);
+ });
+
+ it('should return false when userOrganization is null', () => {
+ expect(checkIfUserIsAdmin(null)).toBe(false);
+ });
+
+ it('should return true when user role is data_admin', () => {
+ const userOrganization = { role: 'data_admin' as const };
+ expect(checkIfUserIsAdmin(userOrganization)).toBe(true);
+ });
+
+ it('should return true when user role is workspace_admin', () => {
+ const userOrganization = { role: 'workspace_admin' as const };
+ expect(checkIfUserIsAdmin(userOrganization)).toBe(true);
+ });
+
+ it('should return false when user role is neither data_admin nor workspace_admin', () => {
+ const userOrganization = { role: 'restricted_querier' as const };
+ expect(checkIfUserIsAdmin(userOrganization)).toBe(false);
+ });
+});
diff --git a/packages/ai/src/agents/analyst-agent/analyst-agent.ts b/packages/ai/src/agents/analyst-agent/analyst-agent.ts
index 57f8d9108..23ab7f86a 100644
--- a/packages/ai/src/agents/analyst-agent/analyst-agent.ts
+++ b/packages/ai/src/agents/analyst-agent/analyst-agent.ts
@@ -98,10 +98,10 @@ export function createAnalystAgent(analystAgentOptions: AnalystAgentOptions) {
const docsSystemMessage = docsContent
? ({
- role: 'system',
- content: `\n${docsContent}\n`,
- providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
- } as ModelMessage)
+ role: 'system',
+ content: `\n${docsContent}\n`,
+ providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
+ } as ModelMessage)
: null;
async function stream({ messages }: AnalystStreamOptions) {
@@ -134,19 +134,19 @@ export function createAnalystAgent(analystAgentOptions: AnalystAgentOptions) {
// Create analyst instructions system message with proper escaping
const analystInstructionsMessage = analystInstructions
? ({
- role: 'system',
- content: `\n${analystInstructions}\n`,
- providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
- } as ModelMessage)
+ role: 'system',
+ content: `\n${analystInstructions}\n`,
+ providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
+ } as ModelMessage)
: null;
// Create user personalization system message
const userPersonalizationSystemMessage = userPersonalizationMessageContent
? ({
- role: 'system',
- content: userPersonalizationMessageContent,
- providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
- } as ModelMessage)
+ role: 'system',
+ content: userPersonalizationMessageContent,
+ providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
+ } as ModelMessage)
: null;
return wrapTraced(
diff --git a/packages/ai/src/agents/analyst-agent/get-analyst-agent-system-prompt.test.ts b/packages/ai/src/agents/analyst-agent/get-analyst-agent-system-prompt.test.ts
index d95e585ef..71b8947b5 100644
--- a/packages/ai/src/agents/analyst-agent/get-analyst-agent-system-prompt.test.ts
+++ b/packages/ai/src/agents/analyst-agent/get-analyst-agent-system-prompt.test.ts
@@ -83,10 +83,14 @@ describe('Analyst Agent Instructions', () => {
expect(result).toContain('MANDATORY SQL NAMING CONVENTIONS');
// Ensure table references require full qualification
- expect(result).toContain('All Table References: MUST be fully qualified: `DATABASE_NAME.SCHEMA_NAME.TABLE_NAME`');
+ expect(result).toContain(
+ 'All Table References: MUST be fully qualified: `DATABASE_NAME.SCHEMA_NAME.TABLE_NAME`'
+ );
// Ensure column references use table aliases (not full qualifiers)
- expect(result).toContain('All Column References: MUST be qualified with their table alias (e.g., `c.customerid`)');
+ expect(result).toContain(
+ 'All Column References: MUST be qualified with their table alias (e.g., `c.customerid`)'
+ );
// Ensure examples show table alias usage without full qualification
expect(result).toContain('c.customerid');
diff --git a/packages/ai/src/agents/think-and-prep-agent/get-think-and-prep-agent-system-prompt.test.ts b/packages/ai/src/agents/think-and-prep-agent/get-think-and-prep-agent-system-prompt.test.ts
index 8a4bca719..0486169ce 100644
--- a/packages/ai/src/agents/think-and-prep-agent/get-think-and-prep-agent-system-prompt.test.ts
+++ b/packages/ai/src/agents/think-and-prep-agent/get-think-and-prep-agent-system-prompt.test.ts
@@ -151,16 +151,23 @@ describe('Think and Prep Agent Instructions', () => {
['investigation', 'investigation'],
])('SQL naming conventions in %s mode', (modeName, mode) => {
it(`should contain mandatory SQL naming conventions in ${modeName} mode`, () => {
- const result = getThinkAndPrepAgentSystemPrompt('Test guidance', mode as 'standard' | 'investigation');
+ const result = getThinkAndPrepAgentSystemPrompt(
+ 'Test guidance',
+ mode as 'standard' | 'investigation'
+ );
// Check for MANDATORY SQL NAMING CONVENTIONS section
expect(result).toContain('MANDATORY SQL NAMING CONVENTIONS');
// Ensure table references require full qualification
- expect(result).toContain('All Table References: MUST be fully qualified: `DATABASE_NAME.SCHEMA_NAME.TABLE_NAME`');
+ expect(result).toContain(
+ 'All Table References: MUST be fully qualified: `DATABASE_NAME.SCHEMA_NAME.TABLE_NAME`'
+ );
// Ensure column references use table aliases (not full qualifiers)
- expect(result).toContain('All Column References: MUST be qualified with their table alias (e.g., `c.customerid`)');
+ expect(result).toContain(
+ 'All Column References: MUST be qualified with their table alias (e.g., `c.customerid`)'
+ );
// Ensure examples show table alias usage without full qualification
expect(result).toContain('c.customerid');
@@ -172,7 +179,10 @@ describe('Think and Prep Agent Instructions', () => {
});
it(`should use column names qualified with table aliases in ${modeName} mode`, () => {
- const result = getThinkAndPrepAgentSystemPrompt('Test guidance', mode as 'standard' | 'investigation');
+ const result = getThinkAndPrepAgentSystemPrompt(
+ 'Test guidance',
+ mode as 'standard' | 'investigation'
+ );
// Check for the updated description
expect(result).toContain('Use column names qualified with table aliases');
diff --git a/packages/ai/src/llm/providers/gateway.ts b/packages/ai/src/llm/providers/gateway.ts
index ba263c218..3198fc468 100644
--- a/packages/ai/src/llm/providers/gateway.ts
+++ b/packages/ai/src/llm/providers/gateway.ts
@@ -14,7 +14,7 @@ export const DEFAULT_ANTHROPIC_OPTIONS = {
additionalModelRequestFields: {
anthropic_beta: ['fine-grained-tool-streaming-2025-05-14'],
},
- }
+ },
};
export const DEFAULT_OPENAI_OPTIONS = {