Skip to content
Draft
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
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