import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { ApiError } from '@/shared' import { restoreFailureNotice } from '../components/system/systemApi' /** * 恢复失败时的提示口径(P2 A29)。 * * 服务端的恢复是很长一串:校验备份 → 拍恢复前安全副本 → 断连接 → 换库文件 → 复核新库 → 写审计, * **回执在这之后才发出**。所以「没收到回执」与「没执行」是两件事: * 前者可能库已经换掉了。这里钉住这两类失败必须给出不同的说法, * 免得操作员看到「当前库未被覆盖」就再点一次恢复,把刚恢复好的库再覆盖一遍。 */ test('超时按「没收到回执、可能已生效」定性,并给出确认步骤', () => { const notice = restoreFailureNotice(new ApiError('TIMEOUT', '请求超时(60000ms)', 0)) assert.equal(notice.title, '没收到恢复回执,恢复可能已经生效') assert.match(notice.detail, /请求超时/) assert.match(notice.detail, /刷新页面确认/) assert.match(notice.detail, /不要直接再点一次恢复/) assert.doesNotMatch(notice.title, /未被覆盖/) }) test('断连同样按「没收到回执、可能已生效」定性', () => { const notice = restoreFailureNotice(new ApiError('NETWORK', '无法连接服务端', 0)) assert.equal(notice.title, '没收到恢复回执,恢复可能已经生效') assert.match(notice.detail, /刷新页面确认/) }) // 拿到了 2xx 却读不出约定结构:服务端已经做完并回了响应,同样不能宣称「库没动」。 test('响应读不出结构(PARSE_FAILED)也算「没拿到回执」', () => { const notice = restoreFailureNotice( new ApiError('PARSE_FAILED', '服务端响应不是约定的 {ok,data} 结构', 200), ) assert.equal(notice.title, '没收到恢复回执,恢复可能已经生效') }) test('服务端在动库之前给出的业务错误照旧说「未执行、库未被覆盖」', () => { const notice = restoreFailureNotice( new ApiError('VALIDATION_FAILED', '备份文件不可用:缺少关键表 teams', 400), ) assert.equal(notice.title, '恢复未执行,当前库未被覆盖') assert.equal(notice.detail, '备份文件不可用:缺少关键表 teams') }) test('回滚类错误把服务端原文带上(它的说明比笼统措辞更准)', () => { const notice = restoreFailureNotice( new ApiError('INTERNAL', '恢复后校验失败,已自动回滚到恢复前状态', 500, 'pre-restore-event.db'), ) assert.equal(notice.title, '恢复未执行,当前库未被覆盖') assert.match(notice.detail, /已自动回滚/) }) test('非 ApiError 的异常不让页面白屏,仍给出可读文案', () => { const notice = restoreFailureNotice(new Error('boom')) assert.equal(notice.detail, 'boom') const unknown = restoreFailureNotice(undefined) assert.equal(unknown.detail, '未知错误') }) // 页面必须走这个函数(而不是自己拼一句「当前库未被覆盖」),否则口径会再次分叉。 const BACKUP_PANEL_SOURCE = readSource('../components/system/BackupPanel.vue') function readSource(relative: string): string { return readFileSync(new URL(relative, import.meta.url), 'utf8') } test('BackupPanel 的失败分支用 restoreFailureNotice 定性', () => { assert.match(BACKUP_PANEL_SOURCE, /restoreFailureNotice\(caught\)/) assert.match(BACKUP_PANEL_SOURCE, /toast\.error\(notice\.title, notice\.detail\)/) }) test('恢复请求走的是放大过超时的那条客户端路径', () => { const apiSource = readSource('../../shared/api.ts') assert.match(apiSource, /timeoutMs: RESTORE_TIMEOUT_MS/) assert.match(apiSource, /export const RESTORE_TIMEOUT_MS = 60_000/) })