feat: 配置中心-提示词与词表(四件套编辑/结构化词表/试金石面板/保存前验证)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关)
|
||||
* 配置中心 API 单元测试(Phase 3 Task 7a——品牌相关;Task 7b——试金石)
|
||||
*
|
||||
* 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 与
|
||||
* upload_routes.py);HTTP 层行为已在 client.test.ts 覆盖,此处 mock http
|
||||
@@ -7,6 +7,8 @@
|
||||
* - GET config:快照 + version(原样组——缺键防御读取靠 normalizeBrandGroup)
|
||||
* - PUT config/brand:请求体是**组值本身**(整组替换语义,非 {note,value}
|
||||
* 包装),备注走 comment 查询参数,无备注时不带 params
|
||||
* - POST wall/test(Task 7b):句子列表 + wall 覆盖(当前表单值预演,
|
||||
* 不落库)+ has_contact/source 可选项——保存前自动验证复用本函数
|
||||
* - 头像上传:multipart file 字段(FormData)
|
||||
* - normalizeBrandGroup:缺键补空串 / 类型漂移防御 / topics 只留字符串项
|
||||
*/
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
fetchTenantConfig,
|
||||
normalizeBrandGroup,
|
||||
putConfigGroup,
|
||||
testWall,
|
||||
uploadBrandAvatar
|
||||
} from '@/api/config'
|
||||
|
||||
@@ -116,6 +119,81 @@ describe('配置中心 API(src/api/config.ts)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('试金石 API(testWall · Phase 3 Task 7b)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('POST wall/test:句子 + wall 覆盖(当前表单值预演)+ 判定选项', async () => {
|
||||
const response = {
|
||||
slug: 'jiancai',
|
||||
wall_source: 'override',
|
||||
results: [
|
||||
{
|
||||
sentence: 'The price is 500 USD per ton.',
|
||||
action: 'blocked',
|
||||
reason: 'boundary',
|
||||
replacement: 'Prices vary a lot by config...'
|
||||
},
|
||||
{ sentence: 'PPR pipes in stock?', action: 'pass', reason: null, replacement: null }
|
||||
]
|
||||
}
|
||||
vi.mocked(http.post).mockResolvedValue(response)
|
||||
const wallOverride = {
|
||||
price_patterns: ['\\$\\s?\\d'],
|
||||
deflections: { en: ['...'], zh: ['...'] }
|
||||
}
|
||||
|
||||
const res = await testWall(
|
||||
'jiancai',
|
||||
{
|
||||
sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
|
||||
wall: wallOverride,
|
||||
has_contact: true,
|
||||
source: 'app'
|
||||
},
|
||||
{ showErrorMessage: false }
|
||||
)
|
||||
|
||||
expect(res).toEqual(response)
|
||||
expect(http.post).toHaveBeenCalledTimes(1)
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/api/platform/tenants/jiancai/wall/test',
|
||||
{
|
||||
sentences: ['The price is 500 USD per ton.', 'PPR pipes in stock?'],
|
||||
wall: wallOverride, // 覆盖墙原样直传——后端 compile_wall 校验后纯内存判定
|
||||
has_contact: true,
|
||||
source: 'app'
|
||||
},
|
||||
{ showErrorMessage: false } // 坏墙 400 字段路径由调用方内联展示
|
||||
)
|
||||
})
|
||||
|
||||
it('POST wall/test:最小 body(仅句子——缺省用当前生效墙)', async () => {
|
||||
const response = {
|
||||
slug: 'jiancai',
|
||||
wall_source: 'current',
|
||||
results: [{ sentence: 'Hello', action: 'pass', reason: null, replacement: null }]
|
||||
}
|
||||
vi.mocked(http.post).mockResolvedValue(response)
|
||||
|
||||
const res = await testWall('jiancai', { sentences: ['Hello'] })
|
||||
|
||||
expect(res).toEqual(response)
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/api/platform/tenants/jiancai/wall/test',
|
||||
{ sentences: ['Hello'] },
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('CONFIG_ENDPOINTS.wallTest:端点路径与后端 config_routes 一致', () => {
|
||||
expect(CONFIG_ENDPOINTS.wallTest('jiancai')).toBe(
|
||||
'/api/platform/tenants/jiancai/wall/test'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeBrandGroup(GET 原样组 → 表单完整值)', () => {
|
||||
it('缺键补空串、topics 缺省为空数组(快照是原样组,可能缺键)', () => {
|
||||
const out = normalizeBrandGroup({ title: 'HESUPIPE Support' })
|
||||
|
||||
+31
-3
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数)
|
||||
* 租户配置中心 API(Phase 3 Task 3 骨架;Task 7a 实装品牌相关调用函数;
|
||||
* Task 7b 补词表试金石调用)
|
||||
*
|
||||
* 端点事实源:app/platform/routes/config_routes.py
|
||||
* (配置快照 / 分组编辑 / 版本历史 / 回滚 / 重置 / 试金石)
|
||||
@@ -16,6 +17,14 @@
|
||||
* - 版本行字段是 updated_by / comment(非骨架期误写的 created_by / note);
|
||||
* - 回滚请求体只认 version(note 不被后端读取);
|
||||
* - 试金石响应逐句为 {sentence, action, reason, replacement}。
|
||||
*
|
||||
* ## Task 7b 试金石契约(config_routes.test_wall)
|
||||
*
|
||||
* - body:sentences 必填;wall 可选覆盖(编辑器当前表单值的**预演**——
|
||||
* 后端先 compile_wall 校验后纯内存判定,不落库不 bump 版本),坏墙
|
||||
* 400 带字段路径;has_contact / source 可选(默认 false / web);
|
||||
* - 判定优先级与 conversation.py 执行侧一致:价格墙(blocked)→
|
||||
* 零号码墙(replaced)→ 再索要墙(仅 has_contact,blocked 无替换)。
|
||||
*/
|
||||
|
||||
import { http, type RequestOptions } from './client'
|
||||
@@ -94,9 +103,12 @@ export interface ConfigRollbackResponse {
|
||||
config: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
/** 词表试金石请求(POST .../wall/test) */
|
||||
/** 词表试金石请求(POST .../wall/test)
|
||||
* wall 覆盖 = 编辑器当前表单值的预演(后端 compile_wall 校验 + 纯内存
|
||||
* 判定,不落库);缺省用当前生效墙。 */
|
||||
export interface WallTestRequest {
|
||||
sentences: string[]
|
||||
wall?: Record<string, unknown>
|
||||
has_contact?: boolean
|
||||
source?: 'web' | 'app'
|
||||
}
|
||||
@@ -169,6 +181,20 @@ export function putConfigGroup(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 词表试金石(POST .../wall/test):逐句墙判定预览。传 wall 覆盖 =
|
||||
* 编辑器当前表单值的预演(不落库不 bump 版本)——保存词表前的自动
|
||||
* 验证复用本函数;坏墙(正则编译失败等)400 带字段路径,调用方需
|
||||
* showErrorMessage: false 自行内联展示。
|
||||
*/
|
||||
export function testWall(
|
||||
slug: string,
|
||||
body: WallTestRequest,
|
||||
config?: RequestOptions
|
||||
): Promise<WallTestResponse> {
|
||||
return http.post<WallTestResponse>(CONFIG_ENDPOINTS.wallTest(slug), body, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像上传(multipart file 字段):魔数校验 / 2MB 上限在后端
|
||||
* (upload_routes.py),前端做扩展名与大小预检提前拦截。
|
||||
@@ -190,7 +216,9 @@ export function uploadBrandAvatar(
|
||||
|
||||
/**
|
||||
* GET 的 brand 组(原样快照,可能缺键/类型漂移)→ 表单友好的完整值。
|
||||
* 缺键补空串(与 DEFAULT_CONFIG.brand 的空串出厂一致);topics 只留字符串项。
|
||||
* 缺键补空串作表单中性值:提交空串 = 显式清空覆盖出厂(出厂
|
||||
* title/color/avatar 本非空——"AI Support"/"#066fc0"/内置头像),快照
|
||||
* 缺省键则由消费侧深合并回落出厂;UI 所见即所得。topics 只留字符串项。
|
||||
*/
|
||||
export function normalizeBrandGroup(
|
||||
value: Record<string, unknown> | null | undefined
|
||||
|
||||
Reference in New Issue
Block a user