fix(admin): 变量集差替代配对递归(删尾行/整键检出)+ 标签契约钉位
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
|||||||
rowToEntry,
|
rowToEntry,
|
||||||
rowsToEntries,
|
rowsToEntries,
|
||||||
rowToSingleEntry,
|
rowToSingleEntry,
|
||||||
|
SCENARIO_WHEN_LABELS,
|
||||||
scopedConfigJson,
|
scopedConfigJson,
|
||||||
secretFieldState,
|
secretFieldState,
|
||||||
snapshotGroupKeys,
|
snapshotGroupKeys,
|
||||||
@@ -795,7 +796,7 @@ describe('模板变量保护', () => {
|
|||||||
expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl'])
|
expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl'])
|
||||||
expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([])
|
expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([])
|
||||||
})
|
})
|
||||||
it('wall 整组递归检出(池+模板+提醒),同变量去重', () => {
|
it('wall 整组递归检出(池+模板+提醒),同变量去重(全域消失才报)', () => {
|
||||||
const before = {
|
const before = {
|
||||||
deflections: { en: ['call {intl}'] },
|
deflections: { en: ['call {intl}'] },
|
||||||
cc_templates: { echo: { en: 'at {intl}' } },
|
cc_templates: { echo: { en: 'at {intl}' } },
|
||||||
@@ -803,14 +804,20 @@ describe('模板变量保护', () => {
|
|||||||
}
|
}
|
||||||
const after = {
|
const after = {
|
||||||
deflections: { en: ['call me'] },
|
deflections: { en: ['call me'] },
|
||||||
cc_templates: { echo: { en: 'at {intl}' } },
|
cc_templates: { echo: { en: 'at email' } },
|
||||||
reminders: { cc_guide: { echo: '补全为 {intl}' } }
|
reminders: { cc_guide: { echo: '补全为手机号' } }
|
||||||
}
|
}
|
||||||
expect(collectRemovedWallVariables(before, after)).toEqual(['intl'])
|
expect(collectRemovedWallVariables(before, after)).toEqual(['intl'])
|
||||||
})
|
})
|
||||||
it('快照缺键不误报', () => {
|
it('快照缺键不误报', () => {
|
||||||
expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([])
|
expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([])
|
||||||
})
|
})
|
||||||
|
it('删除含变量的尾行/整语言键同样检出(位置无关)', () => {
|
||||||
|
const before = { deflections: { en: ['plain', 'call {intl}'] } }
|
||||||
|
expect(collectRemovedWallVariables(before, { deflections: { en: ['plain'] } })).toEqual(['intl'])
|
||||||
|
const before2 = { cc_templates: { echo: { en: 'at {intl}' } } }
|
||||||
|
expect(collectRemovedWallVariables(before2, { cc_templates: { echo: {} } })).toEqual(['intl'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('WALL_SCENARIOS(场景卡定义)', () => {
|
describe('WALL_SCENARIOS(场景卡定义)', () => {
|
||||||
@@ -819,4 +826,7 @@ describe('WALL_SCENARIOS(场景卡定义)', () => {
|
|||||||
expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact'])
|
expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact'])
|
||||||
expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given'])
|
expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given'])
|
||||||
})
|
})
|
||||||
|
it('状态标签契约(ScenarioCard 消费)', () => {
|
||||||
|
expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -489,31 +489,30 @@ export function removedTemplateVariables(before: string, after: string): string[
|
|||||||
return templateVariables(before).filter((v) => !afterSet.has(v))
|
return templateVariables(before).filter((v) => !afterSet.has(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectRemovedStrings(before: unknown, after: unknown, out: string[]): void {
|
function collectVariables(value: unknown, out: string[]): void {
|
||||||
if (typeof before === 'string' && typeof after === 'string') {
|
if (typeof value === 'string') {
|
||||||
out.push(...removedTemplateVariables(before, after))
|
out.push(...templateVariables(value))
|
||||||
} else if (
|
} else if (value !== null && typeof value === 'object') {
|
||||||
before !== null && after !== null &&
|
// typeof 收窄只到 object(无索引签名)——与上方 asRecord 同款断言收形
|
||||||
typeof before === 'object' && typeof after === 'object'
|
const record = value as Record<string, unknown>
|
||||||
) {
|
for (const key of Object.keys(record)) collectVariables(record[key], out)
|
||||||
const b = before as Record<string, unknown>
|
|
||||||
const a = after as Record<string, unknown>
|
|
||||||
for (const key of Object.keys(b)) collectRemovedStrings(b[key], a[key], out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** wall 整组文本区(池/no_contact/cc_templates/reminders)递归比对——被删变量去重清单 */
|
/** wall 整组文本区(池/no_contact/cc_templates/reminders)变量集差——
|
||||||
|
* before 出现过且 after 全域不再出现的变量(位置无关:删除尾行/整键同样检出) */
|
||||||
export function collectRemovedWallVariables(
|
export function collectRemovedWallVariables(
|
||||||
before: Record<string, unknown> | null | undefined,
|
before: Record<string, unknown> | null | undefined,
|
||||||
after: Record<string, unknown> | null | undefined
|
after: Record<string, unknown> | null | undefined
|
||||||
): string[] {
|
): string[] {
|
||||||
const out: string[] = []
|
const beforeVars: string[] = []
|
||||||
const b = before ?? {}
|
const afterVars: string[] = []
|
||||||
const a = after ?? {}
|
|
||||||
for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) {
|
for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) {
|
||||||
collectRemovedStrings(b[key], a[key], out)
|
collectVariables((before ?? {})[key], beforeVars)
|
||||||
|
collectVariables((after ?? {})[key], afterVars)
|
||||||
}
|
}
|
||||||
return [...new Set(out)]
|
const afterSet = new Set(afterVars)
|
||||||
|
return [...new Set(beforeVars.filter((v) => !afterSet.has(v)))]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 试金石 ------------------------------------------------------------------------
|
// ---- 试金石 ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ emits 声明追加 `(e: 'navigate', group: string): void`。
|
|||||||
- [ ] **Step 1: 写失败测试**:
|
- [ ] **Step 1: 写失败测试**:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { poolLanguages, templateVariables, removedTemplateVariables, collectRemovedWallVariables, WALL_SCENARIOS } from '../model'
|
import { poolLanguages, templateVariables, removedTemplateVariables, collectRemovedWallVariables, WALL_SCENARIOS, SCENARIO_WHEN_LABELS } from '../model'
|
||||||
|
|
||||||
describe('poolLanguages(场景卡语言 tabs)', () => {
|
describe('poolLanguages(场景卡语言 tabs)', () => {
|
||||||
it('多池语言并集,必留语言在前、附加语言按序', () => {
|
it('多池语言并集,必留语言在前、附加语言按序', () => {
|
||||||
@@ -529,7 +529,7 @@ describe('模板变量保护', () => {
|
|||||||
expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl'])
|
expect(removedTemplateVariables('发到 {intl} 联系', '发到手机联系')).toEqual(['intl'])
|
||||||
expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([])
|
expect(removedTemplateVariables('发到 {intl}', '发到 {intl}')).toEqual([])
|
||||||
})
|
})
|
||||||
it('wall 整组递归检出(池+模板+提醒),同变量去重', () => {
|
it('wall 整组递归检出(池+模板+提醒),同变量去重(全域消失才报)', () => {
|
||||||
const before = {
|
const before = {
|
||||||
deflections: { en: ['call {intl}'] },
|
deflections: { en: ['call {intl}'] },
|
||||||
cc_templates: { echo: { en: 'at {intl}' } },
|
cc_templates: { echo: { en: 'at {intl}' } },
|
||||||
@@ -537,14 +537,20 @@ describe('模板变量保护', () => {
|
|||||||
}
|
}
|
||||||
const after = {
|
const after = {
|
||||||
deflections: { en: ['call me'] },
|
deflections: { en: ['call me'] },
|
||||||
cc_templates: { echo: { en: 'at {intl}' } },
|
cc_templates: { echo: { en: 'at email' } },
|
||||||
reminders: { cc_guide: { echo: '补全为 {intl}' } }
|
reminders: { cc_guide: { echo: '补全为手机号' } }
|
||||||
}
|
}
|
||||||
expect(collectRemovedWallVariables(before, after)).toEqual(['intl'])
|
expect(collectRemovedWallVariables(before, after)).toEqual(['intl'])
|
||||||
})
|
})
|
||||||
it('快照缺键不误报', () => {
|
it('快照缺键不误报', () => {
|
||||||
expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([])
|
expect(collectRemovedWallVariables({}, { deflections: { en: ['x'] } })).toEqual([])
|
||||||
})
|
})
|
||||||
|
it('删除含变量的尾行/整语言键同样检出(位置无关)', () => {
|
||||||
|
const before = { deflections: { en: ['plain', 'call {intl}'] } }
|
||||||
|
expect(collectRemovedWallVariables(before, { deflections: { en: ['plain'] } })).toEqual(['intl'])
|
||||||
|
const before2 = { cc_templates: { echo: { en: 'at {intl}' } } }
|
||||||
|
expect(collectRemovedWallVariables(before2, { cc_templates: { echo: {} } })).toEqual(['intl'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('WALL_SCENARIOS(场景卡定义)', () => {
|
describe('WALL_SCENARIOS(场景卡定义)', () => {
|
||||||
@@ -553,6 +559,9 @@ describe('WALL_SCENARIOS(场景卡定义)', () => {
|
|||||||
expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact'])
|
expect(price?.pools.map((p) => p.field)).toEqual(['deflections', 'deflections_contact'])
|
||||||
expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given'])
|
expect(price?.pools.map((p) => p.when)).toEqual(['not_given', 'given'])
|
||||||
})
|
})
|
||||||
|
it('状态标签契约(ScenarioCard 消费)', () => {
|
||||||
|
expect(SCENARIO_WHEN_LABELS).toEqual({ not_given: '客户还没留联系方式时', given: '客户已经留过之后' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -631,31 +640,30 @@ export function removedTemplateVariables(before: string, after: string): string[
|
|||||||
return templateVariables(before).filter((v) => !afterSet.has(v))
|
return templateVariables(before).filter((v) => !afterSet.has(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectRemovedStrings(before: unknown, after: unknown, out: string[]): void {
|
function collectVariables(value: unknown, out: string[]): void {
|
||||||
if (typeof before === 'string' && typeof after === 'string') {
|
if (typeof value === 'string') {
|
||||||
out.push(...removedTemplateVariables(before, after))
|
out.push(...templateVariables(value))
|
||||||
} else if (
|
} else if (value !== null && typeof value === 'object') {
|
||||||
before !== null && after !== null &&
|
// typeof 收窄只到 object(无索引签名)——断言收形
|
||||||
typeof before === 'object' && typeof after === 'object'
|
const record = value as Record<string, unknown>
|
||||||
) {
|
for (const key of Object.keys(record)) collectVariables(record[key], out)
|
||||||
const b = before as Record<string, unknown>
|
|
||||||
const a = after as Record<string, unknown>
|
|
||||||
for (const key of Object.keys(b)) collectRemovedStrings(b[key], a[key], out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** wall 整组文本区(池/no_contact/cc_templates/reminders)递归比对——被删变量去重清单 */
|
/** wall 整组文本区(池/no_contact/cc_templates/reminders)变量集差——
|
||||||
|
* before 出现过且 after 全域不再出现的变量(位置无关:删除尾行/整键同样检出) */
|
||||||
export function collectRemovedWallVariables(
|
export function collectRemovedWallVariables(
|
||||||
before: Record<string, unknown> | null | undefined,
|
before: Record<string, unknown> | null | undefined,
|
||||||
after: Record<string, unknown> | null | undefined
|
after: Record<string, unknown> | null | undefined
|
||||||
): string[] {
|
): string[] {
|
||||||
const out: string[] = []
|
const beforeVars: string[] = []
|
||||||
const b = before ?? {}
|
const afterVars: string[] = []
|
||||||
const a = after ?? {}
|
|
||||||
for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) {
|
for (const key of [...POOL_FIELDS, 'no_contact', 'cc_templates', 'reminders']) {
|
||||||
collectRemovedStrings(b[key], a[key], out)
|
collectVariables((before ?? {})[key], beforeVars)
|
||||||
|
collectVariables((after ?? {})[key], afterVars)
|
||||||
}
|
}
|
||||||
return [...new Set(out)]
|
const afterSet = new Set(afterVars)
|
||||||
|
return [...new Set(beforeVars.filter((v) => !afterSet.has(v)))]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user