fix(contracts): Kind/Policy 单源派生冻结锁;status 整数+域校验;key 放宽注释
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
export const ROUTE_MANIFEST_VERSION = 1
|
||||
|
||||
export type RouteKind = 'html' | 'asset' | 'redirect' | 'gone' | 'dynamic' | 'unresolved'
|
||||
export type QueryPolicy = 'ignore' | 'exact' | 'preserve' | 'dynamic'
|
||||
// 单源派生冻结锁:类型从 as const 数组派生,增删值只改一处,数组与联合类型不再并行维护(防静默漂移)
|
||||
export const KINDS = ['html', 'asset', 'redirect', 'gone', 'dynamic', 'unresolved'] as const
|
||||
export type RouteKind = (typeof KINDS)[number]
|
||||
|
||||
export const QUERY_POLICIES = ['ignore', 'exact', 'preserve', 'dynamic'] as const
|
||||
export type QueryPolicy = (typeof QUERY_POLICIES)[number]
|
||||
|
||||
export interface RouteManifestRoute {
|
||||
exact_key: string
|
||||
@@ -24,10 +28,8 @@ export interface RouteManifest {
|
||||
routes: RouteManifestRoute[]
|
||||
}
|
||||
|
||||
const KINDS = ['html', 'asset', 'redirect', 'gone', 'dynamic', 'unresolved'] as const
|
||||
const QUERY_POLICIES = ['ignore', 'exact', 'preserve', 'dynamic'] as const
|
||||
|
||||
// 守卫查值类型(不只查键存在):版本号、顶层必填字符串、routes 元素的枚举与标量类型全部校验
|
||||
// 守卫查值类型(不只查键存在):版本号、顶层必填字符串、routes 元素的枚举与标量类型全部校验;
|
||||
// status 做结构域校验(整数 + 100–599),kind↔status 语义耦合留在消费方
|
||||
export function isRouteManifest(v: unknown): v is RouteManifest {
|
||||
if (typeof v !== 'object' || v === null) return false
|
||||
const m = v as RouteManifest
|
||||
@@ -37,9 +39,10 @@ export function isRouteManifest(v: unknown): v is RouteManifest {
|
||||
&& typeof m.origin === 'string'
|
||||
&& typeof m.generated_from_route_snapshot_hash === 'string'
|
||||
&& Array.isArray(m.routes) && m.routes.every((r) =>
|
||||
typeof r.exact_key === 'string'
|
||||
&& (QUERY_POLICIES as readonly string[]).includes(r.query_policy)
|
||||
&& (KINDS as readonly string[]).includes(r.kind)
|
||||
&& typeof r.status === 'number'
|
||||
typeof r === 'object' && r !== null
|
||||
&& typeof r.exact_key === 'string'
|
||||
&& QUERY_POLICIES.includes(r.query_policy)
|
||||
&& KINDS.includes(r.kind)
|
||||
&& Number.isInteger(r.status) && r.status >= 100 && r.status <= 599
|
||||
&& typeof r.indexable === 'boolean')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const THEME_MANIFEST_VERSION = 1
|
||||
|
||||
export interface ThemeManifest {
|
||||
// 契约刻意放宽:spec §9.1 的 'pilot-minimal' 字面量属于 pilot 主题自身的 manifest 文件;
|
||||
// 共享注册契约面向多主题(第二个主题不得要求改本契约),故 key 为 string——勿"纠正"回字面量。
|
||||
key: string
|
||||
version: string
|
||||
renderContractVersion: string
|
||||
|
||||
@@ -57,9 +57,10 @@ describe('RouteManifest 守卫拒绝路径', () => {
|
||||
expect(isRouteManifest({ ...validRouteManifest, routes: 'x' })).toBe(false)
|
||||
})
|
||||
|
||||
it('routes 元素缺 exact_key 拒绝', () => {
|
||||
it('routes 元素缺 exact_key / 元素为 null 拒绝', () => {
|
||||
const { exact_key: _omit, ...route } = validRouteManifest.routes[0]
|
||||
expect(isRouteManifest({ ...validRouteManifest, routes: [route] })).toBe(false)
|
||||
expect(isRouteManifest({ ...validRouteManifest, routes: [null] })).toBe(false)
|
||||
})
|
||||
|
||||
it('status 非数字拒绝(status: "200")', () => {
|
||||
@@ -69,6 +70,20 @@ describe('RouteManifest 守卫拒绝路径', () => {
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('status 非整数拒绝(200.5)', () => {
|
||||
expect(isRouteManifest({
|
||||
...validRouteManifest,
|
||||
routes: [{ ...validRouteManifest.routes[0], status: 200.5 }],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('status 超域拒绝(999)', () => {
|
||||
expect(isRouteManifest({
|
||||
...validRouteManifest,
|
||||
routes: [{ ...validRouteManifest.routes[0], status: 999 }],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('indexable 非布尔拒绝(indexable: 1)', () => {
|
||||
expect(isRouteManifest({
|
||||
...validRouteManifest,
|
||||
|
||||
Reference in New Issue
Block a user