// 路由域集成测试:route_registry 唯一索引 route_exact_uq (site_id, host, exact_route_key, // query_key) NULLS NOT DISTINCT(0005 迁移手写 SQL——drizzle-kit 0.31 不生成该子句)。 // 断言组(V2 设计 ADR-007 双键语义): // 1. exact_route_key 区分大小写——/About 与 /about 是两条路由,均可插入(设计语义,不是 bug); // 2. 纯路径唯一:同 (site, host, exact_route_key) 且 query_key 均 NULL 的第二条插入拒绝 // ——这正是 NULLS NOT DISTINCT 的意义(默认 NULLS DISTINCT 下两行 NULL 不冲突,挡不住); // 3. query_key 参与唯一:同 exact key 下 query_key='p' 与 NULL 共存;同 'p' 第二条拒绝; // 4. route_kind enum 六值(html/asset/redirect/gone/dynamic/unresolved)往返 + 第七值拒绝; // 5. query_policy enum 四值(ignore/exact/preserve/dynamic)往返 + 第五值拒绝。 // Task 13 起夹具走公共 helpers(createSiteFixture/withTestDb/rejectionMessage); // 生命周期纪律不变:随机后缀建 site + finally 反序清理(route_registry → sites);本表无不可变触发器。 import { describe, it, expect } from 'vitest' import { createSiteFixture, withTestDb, rejectionMessage, type Db } from './helpers.js' type Fixture = { siteId: string; rand: string } async function setupRoutesFixture(db: Db): Promise { const site = await createSiteFixture(db, 'routes') return { siteId: site.siteId, rand: site.rand } } // 反序清理:route_registry(无不可变触发器,直接删)→ sites(级联挡在 site 前,先删子表)。 async function cleanupRoutesFixture(db: Db, f: Fixture) { await db.execute(`delete from route_registry where site_id = '${f.siteId}'`) await db.execute(`delete from sites where id = '${f.siteId}'`) } async function withRoutesFixture(fn: (db: Db, f: Fixture) => Promise) { await withTestDb(async (db) => { const f = await setupRoutesFixture(db) try { await fn(db, f) } finally { await cleanupRoutesFixture(db, f) } }) } // 探测插入:除显式指定的列外全部换新值(随机 suffix 的 id/path/collision_key),保证被测 // 唯一索引 (site_id, host, exact_route_key, query_key) 是唯一可能的冲突源;默认 query_key=NULL // (纯路径路由)。其余 NOT NULL 列填合法基线值。 function routeProbe(f: Fixture, overrides: { host?: string exactRouteKey?: string queryKey?: string | null routeKind?: string queryPolicy?: string } = {}) { const suffix = Math.random().toString(36).slice(2, 8) const key = overrides.exactRouteKey ?? `/probe-${suffix}` const host = overrides.host ?? 'example.com' const queryKeySql = overrides.queryKey == null ? 'null' : `'${overrides.queryKey}'` return `insert into route_registry (id, site_id, host, source_path_raw, route_path, exact_route_key, collision_key, query_policy, query_key, source_status, target_status, route_kind, indexable, priority, verification_status) values (gen_random_uuid(), '${f.siteId}', '${host}', '/raw-${suffix}', '${key}', '${key}', 'collision-${suffix}', '${overrides.queryPolicy ?? 'ignore'}', ${queryKeySql}, '200', '200', '${overrides.routeKind ?? 'html'}', true, 100, 'pending') returning id` } describe('路由域约束', () => { it('exact_route_key 区分大小写(ADR-007 语义):同站同 host 下 /About 与 /about 两条均可插入', async () => { await withRoutesFixture(async (db, f) => { await db.execute(routeProbe(f, { exactRouteKey: '/About' })) await db.execute(routeProbe(f, { exactRouteKey: '/about' })) // 两条都真实入库,各自独立成行(exact key 不折叠大小写——这是请求匹配键的设计语义); // 排序在 JS 侧做,不依赖库端 collation(默认排序 /about 在 /About 前) const rows = (await db.execute(`select exact_route_key from route_registry where site_id = '${f.siteId}' and exact_route_key in ('/About', '/about')`)) as any expect(rows.map((r: any) => r.exact_route_key).sort()).toEqual(['/About', '/about']) }) }) it('纯路径唯一(NULLS NOT DISTINCT):同 (site, host, exact_route_key) 且 query_key 均 NULL 的第二条拒绝', async () => { await withRoutesFixture(async (db, f) => { await db.execute(routeProbe(f, { exactRouteKey: '/dup-null' })) // 若索引是默认 NULLS DISTINCT,两行 query_key=NULL 不冲突、会静默放行——本断言专守这一点 expect(await rejectionMessage(db, routeProbe(f, { exactRouteKey: '/dup-null' }))) .toMatch(/route_exact_uq/) }) }) it('query_key 参与唯一:同 exact key 下 NULL 与 p 共存;同 query_key 第二条拒绝', async () => { await withRoutesFixture(async (db, f) => { // 纯路径路由(NULL)与查询路由('p')是不同路由,合法共存 await db.execute(routeProbe(f, { exactRouteKey: '/query', queryKey: null })) await db.execute(routeProbe(f, { exactRouteKey: '/query', queryKey: 'p' })) // 同 exact key 同 query_key 的第二条 → 唯一索引拒绝(身份断言匹配索引名) expect(await rejectionMessage(db, routeProbe(f, { exactRouteKey: '/query', queryKey: 'p' }))) .toMatch(/route_exact_uq/) }) }) it('route_kind enum 六值(html/asset/redirect/gone/dynamic/unresolved)可写往返,第七值拒绝', async () => { await withRoutesFixture(async (db, f) => { const inserted = (await db.execute(routeProbe(f))) as any const routeId = inserted[0].id for (const kind of ['asset', 'redirect', 'gone', 'dynamic', 'unresolved', 'html']) { await db.execute(`update route_registry set route_kind = '${kind}' where id = '${routeId}'`) const row = (await db.execute(`select route_kind from route_registry where id = '${routeId}'`)) as any expect(row[0].route_kind).toBe(kind) } // enum 在库:类型化列拒绝六值之外的任意字符串 expect(await rejectionMessage(db, `update route_registry set route_kind = 'bogus' where id = '${routeId}'`)) .toMatch(/route_kind/) }) }) it('query_policy enum 四值(ignore/exact/preserve/dynamic)可写往返,第五值拒绝', async () => { await withRoutesFixture(async (db, f) => { const inserted = (await db.execute(routeProbe(f))) as any const routeId = inserted[0].id for (const policy of ['exact', 'preserve', 'dynamic', 'ignore']) { await db.execute(`update route_registry set query_policy = '${policy}' where id = '${routeId}'`) const row = (await db.execute(`select query_policy from route_registry where id = '${routeId}'`)) as any expect(row[0].query_policy).toBe(policy) } // enum 在库:类型化列拒绝四值之外的任意字符串 expect(await rejectionMessage(db, `update route_registry set query_policy = 'bogus' where id = '${routeId}'`)) .toMatch(/route_query_policy/) }) }) })