feat(usage): enrich audit metadata and detail views

This commit is contained in:
elky
2026-07-17 19:20:16 +08:00
parent 0be380243b
commit 664c063a06
79 changed files with 6968 additions and 1294 deletions
@@ -0,0 +1,38 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getMock } = vi.hoisted(() => ({ getMock: vi.fn() }))
vi.mock('@/api/client', () => ({
default: {
get: getMock,
},
}))
import { getProviderKeysPage } from '@/api/endpoints/keys'
describe('getProviderKeysPage', () => {
beforeEach(() => {
getMock.mockReset()
})
it('normalizes a legacy array response for the provider drawer', async () => {
getMock.mockResolvedValue({
data: [{ id: 'key-1' }, { id: 'key-2' }],
})
const result = await getProviderKeysPage('provider-demo', { page: 1, page_size: 1 })
expect(result).toMatchObject({ total: 2, page: 1, page_size: 1 })
expect(result.keys).toEqual([{ id: 'key-1' }])
})
it('normalizes a malformed object without exposing a non-array keys field', async () => {
getMock.mockResolvedValue({
data: { total: null, page: null, page_size: null, keys: {} },
})
const result = await getProviderKeysPage('provider-demo', { page: 2, page_size: 3 })
expect(result).toEqual({ total: 0, page: 2, page_size: 3, keys: [] })
})
})
@@ -0,0 +1,101 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getMock } = vi.hoisted(() => ({ getMock: vi.fn() }))
vi.mock('@/api/client', () => ({
default: {
get: getMock,
},
}))
import { getProviderMappingPreview, getProvidersSummary } from '@/api/endpoints/providers'
const provider = {
id: 'provider-1',
name: 'Provider 1',
provider_type: 'openai',
is_active: true,
endpoints: [],
}
describe('getProvidersSummary', () => {
beforeEach(() => {
getMock.mockReset()
})
it('normalizes the paginated summary response', async () => {
getMock.mockResolvedValue({
data: { total: 1, page: 1, page_size: 20, items: [provider] },
})
const result = await getProvidersSummary({ page: 1, page_size: 20, search: 'paged' })
expect(result.total).toBe(1)
expect(result.items).toHaveLength(1)
expect(result.items[0]?.kiro_simulated_cache_enabled).toBe(false)
})
it('supports the legacy array response without reading an undefined items field', async () => {
getMock.mockResolvedValue({ data: [provider] })
const result = await getProvidersSummary({ page: 2, page_size: 20, search: 'legacy' })
expect(result).toMatchObject({ total: 1, page: 2, page_size: 20 })
expect(result.items).toHaveLength(1)
})
})
describe('getProviderMappingPreview', () => {
beforeEach(() => {
getMock.mockReset()
})
it('normalizes a non-contract response instead of exposing missing arrays to the UI', async () => {
getMock.mockResolvedValue({
data: { message: '演示模式:该接口暂未模拟', demo_mode: true },
})
const result = await getProviderMappingPreview('provider-demo')
expect(result).toEqual({
provider_id: 'provider-demo',
provider_name: '',
keys: [],
total_keys: 0,
total_matches: 0,
truncated: false,
truncated_keys: 0,
truncated_models: 0,
})
})
it('normalizes missing nested mapping arrays', async () => {
getMock.mockResolvedValue({
data: {
provider_id: 'provider-nested',
provider_name: 'Nested Provider',
keys: [{
key_id: 'key-1',
key_name: 'Primary',
masked_key: 'sk-***',
is_active: true,
allowed_models: null,
matching_global_models: [{
global_model_id: 'model-1',
global_model_name: 'gpt-5',
display_name: 'GPT-5',
is_active: true,
matched_models: null,
}],
}],
},
})
const result = await getProviderMappingPreview('provider-nested')
expect(result.keys[0]?.allowed_models).toEqual([])
expect(result.keys[0]?.matching_global_models[0]?.matched_models).toEqual([])
expect(result.total_keys).toBe(1)
expect(result.total_matches).toBe(1)
})
})
+55 -1
View File
@@ -1,13 +1,15 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getMock, cachedRequestMock } = vi.hoisted(() => ({
const { getMock, postMock, cachedRequestMock } = vi.hoisted(() => ({
getMock: vi.fn(),
postMock: vi.fn(),
cachedRequestMock: vi.fn(async (_key: string, fn: () => Promise<unknown>) => fn()),
}))
vi.mock('@/api/client', () => ({
default: {
get: getMock,
post: postMock,
},
}))
@@ -20,6 +22,7 @@ import { usersApi } from '@/api/users'
describe('usersApi admin list query', () => {
beforeEach(() => {
getMock.mockReset()
postMock.mockReset()
cachedRequestMock.mockClear()
getMock.mockResolvedValue({
data: {
@@ -49,4 +52,55 @@ describe('usersApi admin list query', () => {
},
})
})
it('keeps user management renderable when the group response has no items array', async () => {
getMock.mockResolvedValueOnce({
data: {
message: '演示模式:该接口暂未模拟',
demo_mode: true,
},
})
await expect(usersApi.listUserGroups()).resolves.toEqual({
message: '演示模式:该接口暂未模拟',
demo_mode: true,
items: [],
})
})
it('creates a managed key through the selected target user route', async () => {
postMock.mockResolvedValueOnce({
data: {
id: 'target-key',
key: 'sk-target',
},
})
const payload = {
name: 'target key',
feature_settings: {
chat_pii_redaction: { enabled: true },
},
}
await usersApi.createApiKey('target-user', payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/users/target-user/api-keys',
payload,
)
})
it('reads managed keys from the production api_keys envelope', async () => {
getMock.mockResolvedValueOnce({
data: {
api_keys: [{ id: 'target-key', created_at: '2026-07-17T00:00:00Z' }],
total: 1,
},
})
await expect(usersApi.getUserApiKeys('target-user')).resolves.toEqual([
{ id: 'target-key', created_at: '2026-07-17T00:00:00Z' },
])
expect(getMock).toHaveBeenCalledWith('/api/admin/users/target-user/api-keys')
})
})