Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ NEXT_PUBLIC_POSTHOG_API_KEY=phc_dummy_posthog_key
NEXT_PUBLIC_POSTHOG_HOST_URL=https://us.i.posthog.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_dummy_publishable
NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/p/login/test_dummy
NEXT_PUBLIC_WEB_PORT=3000
10 changes: 5 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions cli/src/__tests__/integration/api-integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wrapMockAsFetch, type FetchCallFn } from '@codebuff/common/testing/fixtures'
import {
AuthenticationError,
NetworkError,
Expand Down Expand Up @@ -41,10 +42,10 @@ describe('API Integration', () => {
}) as LoggerMocks

const setFetchMock = (
impl: Parameters<typeof mock>[0],
): ReturnType<typeof mock> => {
const fetchMock = mock(impl)
globalThis.fetch = fetchMock as unknown as typeof fetch
impl: FetchCallFn,
): ReturnType<typeof mock<FetchCallFn>> => {
const fetchMock = mock<FetchCallFn>(impl)
globalThis.fetch = wrapMockAsFetch(fetchMock)
return fetchMock
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'path'
import {
clearMockedModules,
mockModule,
} from '@codebuff/common/testing/mock-modules'
} from '@codebuff/common/testing/fixtures'
import {
describe,
test,
Expand Down
36 changes: 13 additions & 23 deletions cli/src/__tests__/integration/usage-refresh-on-completion.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wrapMockAsFetch, type FetchCallFn } from '@codebuff/common/testing/fixtures'
import { QueryClient } from '@tanstack/react-query'
import {
describe,
Expand Down Expand Up @@ -52,8 +53,8 @@ describe('Usage Refresh on SDK Completion', () => {
)

// Mock successful API response
globalThis.fetch = mock(
async () =>
globalThis.fetch = wrapMockAsFetch(
mock<FetchCallFn>(async () =>
new Response(
JSON.stringify({
type: 'usage-response',
Expand All @@ -63,7 +64,8 @@ describe('Usage Refresh on SDK Completion', () => {
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
) as unknown as typeof fetch
),
)
})

afterEach(() => {
Expand All @@ -80,10 +82,7 @@ describe('Usage Refresh on SDK Completion', () => {
expect(useChatStore.getState().inputMode).toBe('usage')

// Spy on invalidateQueries
const invalidateSpy = mock(
queryClient.invalidateQueries.bind(queryClient),
)
queryClient.invalidateQueries = invalidateSpy as any
const invalidateSpy = spyOn(queryClient, 'invalidateQueries')

// Simulate SDK run completion triggering invalidation
const isUsageMode = useChatStore.getState().inputMode === 'usage'
Expand All @@ -101,10 +100,7 @@ describe('Usage Refresh on SDK Completion', () => {
test('should invalidate multiple times for sequential runs', () => {
useChatStore.getState().setInputMode('usage')

const invalidateSpy = mock(
queryClient.invalidateQueries.bind(queryClient),
)
queryClient.invalidateQueries = invalidateSpy as any
const invalidateSpy = spyOn(queryClient, 'invalidateQueries')

// Simulate three sequential SDK runs
for (let i = 0; i < 3; i++) {
Expand All @@ -123,10 +119,7 @@ describe('Usage Refresh on SDK Completion', () => {
useChatStore.getState().setInputMode('default')
expect(useChatStore.getState().inputMode).toBe('default')

const invalidateSpy = mock(
queryClient.invalidateQueries.bind(queryClient),
)
queryClient.invalidateQueries = invalidateSpy as any
const invalidateSpy = spyOn(queryClient, 'invalidateQueries')

// Simulate SDK run completion check
const isUsageMode = useChatStore.getState().inputMode === 'usage'
Expand All @@ -145,10 +138,7 @@ describe('Usage Refresh on SDK Completion', () => {
// User closes banner before run completes
useChatStore.getState().setInputMode('default')

const invalidateSpy = mock(
queryClient.invalidateQueries.bind(queryClient),
)
queryClient.invalidateQueries = invalidateSpy as any
const invalidateSpy = spyOn(queryClient, 'invalidateQueries')

// Simulate run completion
const isUsageMode = useChatStore.getState().inputMode === 'usage'
Expand All @@ -165,8 +155,8 @@ describe('Usage Refresh on SDK Completion', () => {
// Even if banner is visible in store, query won't run if enabled=false
useChatStore.getState().setInputMode('usage')

const fetchMock = mock(globalThis.fetch)
globalThis.fetch = fetchMock as any
const fetchMock = mock<FetchCallFn>(async () => new Response(''))
globalThis.fetch = wrapMockAsFetch(fetchMock)

// Query with enabled=false won't execute
// (This would be the behavior when useUsageQuery({ enabled: false }) is called)
Expand All @@ -180,8 +170,8 @@ describe('Usage Refresh on SDK Completion', () => {
getAuthTokenSpy.mockReturnValue(undefined)
useChatStore.getState().setInputMode('usage')

const fetchMock = mock(globalThis.fetch)
globalThis.fetch = fetchMock as any
const fetchMock = mock<FetchCallFn>(async () => new Response(''))
globalThis.fetch = wrapMockAsFetch(fetchMock)

// Query won't execute without auth token
expect(fetchMock).not.toHaveBeenCalled()
Expand Down
3 changes: 2 additions & 1 deletion cli/src/__tests__/utils/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, test, expect, afterEach } from 'bun:test'

import { getCliEnv, createTestCliEnv } from '../../utils/env'
import { createTestCliEnv } from '../../../testing/env'
import { getCliEnv } from '../../utils/env'

describe('cli/utils/env', () => {
describe('getCliEnv', () => {
Expand Down
72 changes: 35 additions & 37 deletions cli/src/hooks/__tests__/use-usage-query.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wrapMockAsFetch, type FetchCallFn } from '@codebuff/common/testing/fixtures'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import {
Expand All @@ -11,7 +12,7 @@ import {
} from 'bun:test'
import React from 'react'

import type { ClientEnv } from '@codebuff/common/types/contracts/env'
import type { Logger } from '@codebuff/common/types/contracts/logger'

import { useChatStore } from '../../state/chat-store'
import * as authModule from '../../utils/auth'
Expand Down Expand Up @@ -44,42 +45,42 @@ describe('fetchUsageData', () => {
next_quota_reset: '2024-02-01T00:00:00.000Z',
}

globalThis.fetch = mock(
async () =>
new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
) as unknown as typeof fetch
globalThis.fetch = wrapMockAsFetch(
mock<FetchCallFn>(
async () =>
new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
),
)

const result = await fetchUsageData({ authToken: 'test-token' })

expect(result).toEqual(mockResponse)
})

test('should throw error on failed request', async () => {
globalThis.fetch = mock(
async () => new Response('Error', { status: 500 }),
) as unknown as typeof fetch
const mockLogger = {
error: mock(() => {}),
warn: mock(() => {}),
info: mock(() => {}),
debug: mock(() => {}),
globalThis.fetch = wrapMockAsFetch(
mock<FetchCallFn>(async () => new Response('Error', { status: 500 })),
)
const mockLogger: Logger = {
error: mock<Logger['error']>(() => {}),
warn: mock<Logger['warn']>(() => {}),
info: mock<Logger['info']>(() => {}),
debug: mock<Logger['debug']>(() => {}),
}

await expect(
fetchUsageData({ authToken: 'test-token', logger: mockLogger as any }),
fetchUsageData({ authToken: 'test-token', logger: mockLogger }),
).rejects.toThrow('Failed to fetch usage: 500')
})

test('should throw error when app URL is not set', async () => {
await expect(
fetchUsageData({
authToken: 'test-token',
clientEnv: {
NEXT_PUBLIC_CODEBUFF_APP_URL: undefined,
} as unknown as ClientEnv,
clientEnv: { NEXT_PUBLIC_CODEBUFF_APP_URL: undefined },
}),
).rejects.toThrow('NEXT_PUBLIC_CODEBUFF_APP_URL is not set')
})
Expand Down Expand Up @@ -127,13 +128,15 @@ describe('useUsageQuery', () => {
next_quota_reset: '2024-02-01T00:00:00.000Z',
}

globalThis.fetch = mock(
async () =>
new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
) as unknown as typeof fetch
globalThis.fetch = wrapMockAsFetch(
mock<FetchCallFn>(
async () =>
new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
),
)

const { result } = renderHook(() => useUsageQuery(), {
wrapper: createWrapper(),
Expand All @@ -148,10 +151,8 @@ describe('useUsageQuery', () => {
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue(
'test-token',
)
const fetchMock = mock(
async () => new Response('{}'),
) as unknown as typeof fetch
globalThis.fetch = fetchMock
const fetchMock = mock<FetchCallFn>(async () => new Response('{}'))
globalThis.fetch = wrapMockAsFetch(fetchMock)

const { result } = renderHook(() => useUsageQuery({ enabled: false }), {
wrapper: createWrapper(),
Expand All @@ -167,10 +168,8 @@ describe('useUsageQuery', () => {
getAuthTokenSpy = spyOn(authModule, 'getAuthToken').mockReturnValue(
undefined,
)
const fetchMock = mock(
async () => new Response('{}'),
) as unknown as typeof fetch
globalThis.fetch = fetchMock
const fetchMock = mock<FetchCallFn>(async () => new Response('{}'))
globalThis.fetch = wrapMockAsFetch(fetchMock)

renderHook(() => useUsageQuery(), {
wrapper: createWrapper(),
Expand Down Expand Up @@ -199,8 +198,7 @@ describe('useRefreshUsage', () => {
})

test.skip('should invalidate usage queries', async () => {
const invalidateSpy = mock(queryClient.invalidateQueries.bind(queryClient))
queryClient.invalidateQueries = invalidateSpy as any
const invalidateSpy = spyOn(queryClient, 'invalidateQueries')

const { result } = renderHook(() => useRefreshUsage(), {
wrapper: createWrapper(),
Expand Down
2 changes: 1 addition & 1 deletion cli/src/hooks/use-usage-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface UsageResponse {
interface FetchUsageParams {
authToken: string
logger?: Logger
clientEnv?: ClientEnv
clientEnv?: Partial<Pick<ClientEnv, 'NEXT_PUBLIC_CODEBUFF_APP_URL'>>
}

/**
Expand Down
Loading