9a8a3e7795
build / build (push) Successful in 57s
现象:点「公司级技术支持知识库」登记,返回 HTTP 409:space_registration_conflict。 根因(真数据取证): - source_connections.name 有唯一约束。这 3 个钉钉来源是**早期路径**登记的 (base_url 为空,id 与 dws 现在推导的确定性 id 不同): sc-18d10ca0… 公司级技术支持知识库 / sc-eaef0ec1… ATOM知识库 / sc-dc7cea1c… 技术诊断部知识沉淀。 - 上一步「去掉来源」只是归档下架,**名字仍占着唯一槽位** ⇒ 重新登记时 INSERT 撞 UNIQUE(name)。 - 处理器把**所有**存储层错误都翻译成同一个 409 space_registration_conflict(真实原因被吞)。 修复: - store:同名的另一行若**已归档** ⇒ 改名腾位(`…(已下架 · <短ID>)`,归档行不显示, 数据与审计一行不动);若**仍在使用** ⇒ ErrDingtalkSpaceNameTaken(绝不悄悄改名)。 同一个登记键解析到别的 root 时:在用中的行报 ErrDingtalkSpaceIdentityConflict; 归档过的行允许就地改写("去掉后再用 dws 重新获取"的语义)。 - httpapi:分类映射 —— space_name_conflict / space_identity_conflict(409 + 可行动 hint), 其余内部故障改回 500 space_registration_failed,不再伪装成冲突。 - web:读一次响应体同时取 error 与 hint,**hint 优先**;补两个新码的中文说明。 验证:真 postgres 集成测试复刻线上形态(旧行占名 + 归档 ⇒ 登记成功、旧行改名保持归档、 内容不丢;同名在用 ⇒ 拒绝;同键换 root 在用 ⇒ 拒绝、归档 ⇒ 就地改写);handler 分类测试 (409/409/500,且不回显内部细节);前端 hint 优先用例;make verify 15/15。
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { DingtalkRequestError, registerDingtalkSpace, startDingtalkSession } from './dingtalkSession'
|
|
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
|
|
describe('DingTalk session client', () => {
|
|
it('returns the starting state immediately while the QR is being generated', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
id: 'session-1',
|
|
state: 'starting',
|
|
created_at: 1,
|
|
expires_at: 2,
|
|
}),
|
|
{ status: 201 }
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const target = 'https://alidocs.dingtalk.com/i/spaces/9JOGOQneRgkneX4Q/overview'
|
|
await expect(startDingtalkSession(target)).resolves.toMatchObject({ id: 'session-1', state: 'starting' })
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/v1/dingtalk-session', expect.objectContaining({ method: 'POST' }))
|
|
expect(JSON.parse(String(fetchMock.mock.calls[0][1]?.body))).toEqual({ space: target })
|
|
})
|
|
|
|
it('preserves login_required as a machine-readable error for automatic QR fallback', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ error: 'login_required' }), {
|
|
status: 409,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
)
|
|
)
|
|
|
|
const error = await registerDingtalkSpace('https://alidocs.dingtalk.com/i/spaces/9JOGOQneRgkneX4Q/overview').then(
|
|
() => null,
|
|
(cause: unknown) => cause
|
|
)
|
|
expect(error).toBeInstanceOf(DingtalkRequestError)
|
|
expect(error).toMatchObject({ code: 'login_required', status: 409 })
|
|
})
|
|
})
|
|
|
|
describe('钉钉知识库登记的失败说明', () => {
|
|
it('后端 hint 优先,其次本地文案;错误码始终带出', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ error: 'space_name_conflict', hint: '已有同名来源仍在使用:先「去掉来源」' }), {
|
|
status: 409,
|
|
})
|
|
)
|
|
)
|
|
await expect(registerDingtalkSpace('https://alidocs.dingtalk.com/i/spaces/x')).rejects.toThrow(
|
|
'已有同名来源仍在使用:先「去掉来源」'
|
|
)
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: 'space_identity_conflict' }), { status: 409 }))
|
|
)
|
|
await expect(registerDingtalkSpace('https://alidocs.dingtalk.com/i/spaces/x')).rejects.toThrow(
|
|
'这个登记键已指向另一个知识库空间'
|
|
)
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: 'space_registration_failed' }), { status: 500 }))
|
|
)
|
|
await expect(registerDingtalkSpace('https://alidocs.dingtalk.com/i/spaces/x')).rejects.toThrow(
|
|
'登记失败(服务端内部错误)'
|
|
)
|
|
})
|
|
})
|