/** 路由契约:每条都得说清归哪个端(ADR-012)—— 中间件靠它,漏一条就是一个没人管的口子 */ import { describe, expect, it } from 'vitest'; import * as contracts from '../index.ts'; import type { RouteContract } from '../index.ts'; import { SURFACES } from '../index.ts'; function isRoute(value: unknown): value is RouteContract { if (typeof value !== 'object' || value === null) return false; const v = value as { method?: unknown; path?: unknown; output?: unknown }; return typeof v.method === 'string' && typeof v.path === 'string' && v.output !== undefined; } /** * `contracts` 是异质的命名空间(schema / 常量 / 路由混杂),`Object.entries` 推出的元素类型 * 因而是一个几百项的大联合;`entry is [string, RouteContract]` 直接对着那个联合做型别谓词, * TS 判不出谓词类型与参数类型互相兼容(TS2677)。先收窄成 `[string, unknown][]` 再筛 —— * 语义不变(照旧靠 `isRoute` 的运行时判断筛路由),只是把关系判断让给 `unknown`。 */ const routes = (Object.entries(contracts) as [string, unknown][]).filter( (entry): entry is [string, RouteContract] => isRoute(entry[1]), ); const surfaces: readonly string[] = SURFACES; describe('路由契约的归属(access)', () => { it('收得到全部路由(至少 90 条)', () => { expect(routes.length).toBeGreaterThanOrEqual(90); }); it('access 只取 public / any / 端名 / 非空端名数组', () => { for (const [name, route] of routes) { const a = route.access; const ok = a === 'public' || a === 'any' || (typeof a === 'string' ? surfaces.includes(a) : a.length > 0 && a.every((s) => surfaces.includes(s))); expect(ok, `${name}.access = ${JSON.stringify(a)}`).toBe(true); } }); it('公开路由只有健康检查、入站邮件与退役八条', () => { const publicNames = routes.filter(([, r]) => r.access === 'public').map(([n]) => n).sort(); expect(publicNames).toEqual( [ 'healthRoute', 'ingestMailRoute', 'loginRoute', 'logoutRoute', 'sessionRoute', 'createStaffRoute', 'disableStaffCheckRoute', 'disableStaffRoute', 'listGrantsRoute', 'setGrantsRoute', ].sort(), ); }); it('只要登录不看端的只有 me / 票据', () => { const anyNames = routes.filter(([, r]) => r.access === 'any').map(([n]) => n).sort(); expect(anyNames).toEqual(['authTicketRoute', 'meRoute']); }); it('两端共用的读路由就是 spec §4.4 点名的那八条', () => { const both = routes .filter(([, r]) => Array.isArray(r.access)) .map(([n]) => n) .sort(); expect(both).toEqual( [ 'listStaffRoute', 'listProcessPackagesRoute', 'listTriggersRoute', 'listChannelsRoute', 'listLedgerObjectsRoute', 'ledgerByActivityRoute', 'browseLedgerRoute', 'govWorkflowRoute', ].sort(), ); }); });