fix(contracts): 守卫收紧必填字段类型与对象形状;修复包级 test 脚本;补齐守卫测试与 REQUIRED 类型锁
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json"
|
||||||
"test": "vitest run"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,22 @@ export interface UniversalRow {
|
|||||||
rawLocation: string
|
rawLocation: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const REQUIRED: (keyof UniversalRow)[] = [
|
type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T]
|
||||||
|
|
||||||
|
// 契约冻结:REQUIRED = 接口必填字段全集;接口新增必填字段而列表漏改 → 编译报错
|
||||||
|
const REQUIRED = [
|
||||||
'sourceType', 'sourceId', 'status', 'locale',
|
'sourceType', 'sourceId', 'status', 'locale',
|
||||||
'taxonomyRefs', 'attachmentRefs', 'seo', 'knownFields', 'extras', 'rawLocation',
|
'taxonomyRefs', 'attachmentRefs', 'seo', 'knownFields', 'extras', 'rawLocation',
|
||||||
]
|
] as const
|
||||||
|
|
||||||
|
type _RequiredKeysMatch = [RequiredKeys<UniversalRow>] extends [typeof REQUIRED[number]] ? true : never
|
||||||
|
const _requiredKeysComplete: _RequiredKeysMatch = true
|
||||||
|
|
||||||
export function isUniversalRow(v: unknown): v is UniversalRow {
|
export function isUniversalRow(v: unknown): v is UniversalRow {
|
||||||
if (typeof v !== 'object' || v === null) return false
|
if (typeof v !== 'object' || v === null) return false
|
||||||
const row = v as Record<string, unknown>
|
const row = v as Record<string, unknown>
|
||||||
return REQUIRED.every((k) => k in row)
|
return REQUIRED.every((k) => k in row)
|
||||||
&& Array.isArray(row.taxonomyRefs) && Array.isArray(row.attachmentRefs)
|
&& Array.isArray(row.taxonomyRefs) && Array.isArray(row.attachmentRefs)
|
||||||
&& row.seo !== null && typeof row.seo === 'object'
|
&& ['sourceType','sourceId','status','locale','rawLocation'].every((k) => typeof row[k] === 'string')
|
||||||
&& row.knownFields !== null && typeof row.knownFields === 'object'
|
&& [row.seo, row.knownFields, row.extras].every((x) => typeof x === 'object' && x !== null && !Array.isArray(x))
|
||||||
&& row.extras !== null && typeof row.extras === 'object'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,3 +25,33 @@ describe('UniversalRow 契约', () => {
|
|||||||
expect(isUniversalRow(bad)).toBe(false)
|
expect(isUniversalRow(bad)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const REQUIRED_KEYS = [
|
||||||
|
'sourceType', 'sourceId', 'status', 'locale',
|
||||||
|
'taxonomyRefs', 'attachmentRefs', 'seo', 'knownFields', 'extras', 'rawLocation',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
describe('UniversalRow 守卫拒绝路径', () => {
|
||||||
|
it.each(REQUIRED_KEYS)('缺必填字段 %s 拒绝', (key) => {
|
||||||
|
const clone: Record<string, unknown> = { ...valid }
|
||||||
|
delete clone[key]
|
||||||
|
expect(isUniversalRow(clone)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('非对象输入拒绝(null / number)', () => {
|
||||||
|
expect(isUniversalRow(null)).toBe(false)
|
||||||
|
expect(isUniversalRow(42)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('taxonomyRefs 非数组拒绝', () => {
|
||||||
|
expect(isUniversalRow({ ...valid, taxonomyRefs: 'oops' })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('必填字符串字段非字符串拒绝(sourceId 数字)', () => {
|
||||||
|
expect(isUniversalRow({ ...valid, sourceId: 101 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('seo 为数组拒绝(数组伪装对象)', () => {
|
||||||
|
expect(isUniversalRow({ ...valid, seo: [] })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user