Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@

"@google-cloud/promisify": ["@google-cloud/promisify@4.0.0", "", {}, "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="],

"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.0", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg=="],

"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],

Expand Down Expand Up @@ -1014,7 +1014,7 @@

"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.30.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.30.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/propagator-b3": "1.30.1", "@opentelemetry/propagator-jaeger": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ=="],

"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.37.0", "", {}, "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="],

"@opentui/core": ["@opentui/core@0.1.56", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.56", "@opentui/core-darwin-x64": "0.1.56", "@opentui/core-linux-arm64": "0.1.56", "@opentui/core-linux-x64": "0.1.56", "@opentui/core-win32-arm64": "0.1.56", "@opentui/core-win32-x64": "0.1.56", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-TI5cSCPYythHIQYpAEdXyZhewGACn2TfnfC1qZmrSyEq33zFo4W7zpQ4EZNpy9xZJFCI+elAUVJFARwhudp9EQ=="],

Expand Down Expand Up @@ -1790,7 +1790,7 @@

"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],

"comment-json": ["comment-json@4.5.0", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1" } }, "sha512-aKl8CwoMKxVTfAK4dFN4v54AEvuUh9pzmgVIBeK6gBomLwMgceQUKKWHzJdW1u1VQXQuwnJ7nJGWYYMTl5U4yg=="],
"comment-json": ["comment-json@4.4.1", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1" } }, "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg=="],

"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],

Expand Down Expand Up @@ -3074,7 +3074,7 @@

"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],

"oo-ascii-tree": ["oo-ascii-tree@1.121.0", "", {}, "sha512-Dwzge50NT4bUxynVLtn/eFnl5Vv+8thNDVhw2MFZf6t5DmtIWKCDdQGUrIhN6PMEloDXVvPIW//oZtooSkp79g=="],
"oo-ascii-tree": ["oo-ascii-tree@1.118.0", "", {}, "sha512-ATGzZ+AxeHuGdNlniQNn9xvaVDo8IfET84Xep0XS33KXr19EZum7VpzBuKtcfNM/NQ7uk1d4ePXMqyiHeA9Dxw=="],

"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],

Expand Down Expand Up @@ -3916,7 +3916,7 @@

"zdog": ["zdog@1.1.3", "", {}, "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w=="],

"zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],

"zod-from-json-schema": ["zod-from-json-schema@0.4.2", "", { "dependencies": { "zod": "^3.25.25" } }, "sha512-U+SIzUUT7P6w1UNAz81Sj0Vko77eQPkZ8LbJeXqQbwLmq1MZlrjB3Gj4LuebqJW25/CzS9WA8SjTgR5lvuv+zA=="],

Expand Down
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
Loading