Archived
契约:三个端与路由归属(access 必填)、Principal / 角色→端 / 名册同步 schema、退役路由标 @deprecated(ADR-012)
- packages/contracts/src/api/http.ts:新增 SURFACES / surfaceSchema / Surface / SURFACE_LABEL / RouteAccess;RouteContract.access 必填;API_ERROR_CODES 增 'gone'。 - packages/contracts/src/org.ts:staffSchema 补 email? / roles(realm 角色快照);新增 principalSchema/Principal、roleAccessSchema/RoleAccess、rosterSyncOutcomeSchema/RosterSyncOutcome、 inFlightWorkSchema/InFlightWork;旧岗位授权(grant* / governanceFunction*)与 sessionSchema/Session 全部标 @deprecated 留一轮(web 编译不受影响,下个迭代删)。 - workbench.ts / governance.ts / ingest.ts:每条既有路由补 access(按所属端/两端共用/退役 public); 新增 meRoute、authTicketRoute(access:'any')与 syncStaffRoute、setStaffPositionRoute、 staffInFlightRoute、listRoleAccessRoute、setRoleAccessRoute、deleteRoleAccessRoute(access:'ops')。 - apps/api 的 org-service.ts 两处 staff.insert 补 roles: [](staffSchema 新必填字段,该文件下个 迭代会整份替换,这里只是让本次 typecheck 过)。 - 新增 packages/contracts/src/__tests__/route-access.test.ts:95 条路由的 access 归属断言。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/** 路由契约:每条都得说清归哪个端(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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user