task-28: 死代码清理
删除拆分后失去引用的 API 残留文件: - src/shared/api/call.ts(已由 http.ts 取代) - src/shared/api/download.ts(已由 download-url.ts 取代) - src/shared/api/status.ts(页面内已有本地实现) 及其孤儿测试 tests/call.test.ts / call-abort.test.ts / download.test.ts / status.test.ts。 endpoints/url/user 仍被模块文件引用,保留。 新增 8 个验证测试(dead-code-cleanup.test.ts):无重复导出、 vue-tsc 零错误、全量测试绿、无调试日志、无残留函数体、 16+ HTML 入口存在、build 产物齐全、提交变更仅限 frontend-vue。
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
import { http, type RequestOptions } from './http.ts'
|
||||
|
||||
export interface JavaApiResponse<T> {
|
||||
success: boolean
|
||||
message: string
|
||||
data: T | null
|
||||
}
|
||||
|
||||
export interface LegacyApiSuccess<T> {
|
||||
success: true
|
||||
msg?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export async function callJava<T = unknown, D = unknown>(
|
||||
config: RequestOptions<D>,
|
||||
): Promise<T> {
|
||||
const body = await http.request<unknown, unknown, D>(config)
|
||||
const data = (body as { data?: unknown }).data ?? body
|
||||
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'success' in data &&
|
||||
typeof (data as { success: unknown }).success === 'boolean'
|
||||
) {
|
||||
const wrapped = data as { success: boolean; message?: string; msg?: string; error?: string; data?: T }
|
||||
if (!wrapped.success) {
|
||||
throw new Error(wrapped.message || wrapped.msg || wrapped.error || '请求失败')
|
||||
}
|
||||
return wrapped.data as T
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export interface DownloadableItem {
|
||||
freshDownloadUrl?: string | null
|
||||
downloadUrl?: string | null
|
||||
}
|
||||
|
||||
export function buildDownloadUrl(path: string, userId?: number): string {
|
||||
if (userId === undefined || userId === null || Number.isNaN(userId)) {
|
||||
return path
|
||||
}
|
||||
if (/[?&]user_id=/.test(path)) {
|
||||
return path
|
||||
}
|
||||
const separator = path.includes('?') ? '&' : '?'
|
||||
return `${path}${separator}user_id=${userId}`
|
||||
}
|
||||
|
||||
export function resolveDownloadUrl(item: DownloadableItem | null | undefined): string {
|
||||
if (!item) {
|
||||
return ''
|
||||
}
|
||||
return item.freshDownloadUrl || item.downloadUrl || ''
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export function normalizeStatus(raw: string | null | undefined): string {
|
||||
if (typeof raw !== 'string') {
|
||||
return ''
|
||||
}
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
return trimmed.toUpperCase().replace(/[\s-]+/g, '_')
|
||||
}
|
||||
|
||||
export function isTerminalStatus(
|
||||
status: string | null | undefined,
|
||||
extras?: string[],
|
||||
): boolean {
|
||||
const normalized = normalizeStatus(status)
|
||||
if (!normalized) {
|
||||
return false
|
||||
}
|
||||
const terminalSet = new Set([
|
||||
'SUCCESS',
|
||||
'FAILED',
|
||||
...(extras ?? []).map((extra) => normalizeStatus(extra)),
|
||||
])
|
||||
return terminalSet.has(normalized)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { http } from '../src/shared/api/http.ts'
|
||||
import { callJava } from '../src/shared/api/call.ts'
|
||||
|
||||
function mockRequest(t: Parameters<typeof test>[1] extends (t: infer T) => unknown ? T : never, impl: (config: { signal?: AbortSignal }) => Promise<unknown>) {
|
||||
t.mock.method(http, 'request', impl as never)
|
||||
}
|
||||
|
||||
function abortedError() {
|
||||
const error = new Error('canceled')
|
||||
error.name = 'CanceledError'
|
||||
return error
|
||||
}
|
||||
|
||||
test('test_signal_abort_before_send', async (t) => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
mockRequest(t, (config) => {
|
||||
if (config.signal?.aborted) {
|
||||
throw abortedError()
|
||||
}
|
||||
return Promise.resolve({ data: { success: true, message: 'ok', data: 1 } })
|
||||
})
|
||||
await assert.rejects(
|
||||
callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal }),
|
||||
(error: Error) => /canceled|Abort/i.test(error.message),
|
||||
'发送前已中止应立即拒绝',
|
||||
)
|
||||
})
|
||||
|
||||
test('test_signal_abort_mid_request', async (t) => {
|
||||
const controller = new AbortController()
|
||||
mockRequest(t, (config) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
config.signal?.addEventListener('abort', () => reject(abortedError()))
|
||||
}),
|
||||
)
|
||||
const promise = callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal })
|
||||
setTimeout(() => controller.abort(), 5)
|
||||
await assert.rejects(promise, /canceled/, '进行中中止应拒绝')
|
||||
})
|
||||
|
||||
test('test_signal_passthrough', async (t) => {
|
||||
const controller = new AbortController()
|
||||
let captured: unknown
|
||||
mockRequest(t, (config) => {
|
||||
captured = config.signal
|
||||
return Promise.resolve({ data: { success: true, message: 'ok', data: 1 } })
|
||||
})
|
||||
await callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal })
|
||||
assert.equal(captured, controller.signal, 'signal 应透传到 axios 配置')
|
||||
})
|
||||
|
||||
test('test_signal_abort_error_message', async (t) => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
mockRequest(t, (config) => {
|
||||
if (config.signal?.aborted) {
|
||||
throw abortedError()
|
||||
}
|
||||
return Promise.resolve({ data: { success: true, message: 'ok', data: 1 } })
|
||||
})
|
||||
await assert.rejects(
|
||||
callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal }),
|
||||
(error: Error) => /canceled|Abort/i.test(error.message) || error.name === 'CanceledError',
|
||||
'错误消息应含 AbortError 特征',
|
||||
)
|
||||
})
|
||||
|
||||
test('test_signal_not_provided', async (t) => {
|
||||
mockRequest(t, () => Promise.resolve({ data: { success: true, message: 'ok', data: 'ok' } }))
|
||||
const result = await callJava<string>({ url: '/newApi/api/x', method: 'GET' })
|
||||
assert.equal(result, 'ok')
|
||||
})
|
||||
|
||||
test('test_abort_after_resolve', async (t) => {
|
||||
const controller = new AbortController()
|
||||
mockRequest(t, () => Promise.resolve({ data: { success: true, message: 'ok', data: 1 } }))
|
||||
const result = await callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal })
|
||||
controller.abort()
|
||||
assert.equal(result, 1, '完成后 abort 不应有副作用')
|
||||
})
|
||||
|
||||
test('test_abort_rejects_promise', async (t) => {
|
||||
const controller = new AbortController()
|
||||
mockRequest(t, (config) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
config.signal?.addEventListener('abort', () => reject(abortedError()))
|
||||
}),
|
||||
)
|
||||
const promise = callJava({ url: '/newApi/api/x', method: 'GET', signal: controller.signal })
|
||||
controller.abort()
|
||||
await assert.rejects(promise, /canceled/, '中止应拒绝 Promise')
|
||||
})
|
||||
|
||||
test('test_signal_multiple_calls_independent', async (t) => {
|
||||
const first = new AbortController()
|
||||
const second = new AbortController()
|
||||
let calls = 0
|
||||
mockRequest(t, (config) => {
|
||||
calls += 1
|
||||
if (config.signal?.aborted) {
|
||||
throw abortedError()
|
||||
}
|
||||
return Promise.resolve({ data: { success: true, message: 'ok', data: calls } })
|
||||
})
|
||||
second.abort()
|
||||
const [a, b] = await Promise.allSettled([
|
||||
callJava({ url: '/newApi/api/x', method: 'GET', signal: first.signal }),
|
||||
callJava({ url: '/newApi/api/y', method: 'GET', signal: second.signal }),
|
||||
])
|
||||
assert.equal(a.status, 'fulfilled', '未中止的调用应成功')
|
||||
assert.equal(b.status, 'rejected', '已中止的调用应失败')
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { http } from '../src/shared/api/http.ts'
|
||||
import { callJava } from '../src/shared/api/call.ts'
|
||||
|
||||
function mockResponse(t: Parameters<typeof test>[1] extends (t: infer T) => unknown ? T : never, body: unknown) {
|
||||
t.mock.method(http, 'request', async () => ({ data: body }) as never)
|
||||
}
|
||||
|
||||
test('test_call_java_success_unwraps', async (t) => {
|
||||
mockResponse(t, { success: true, message: 'ok', data: { taskId: 1, status: 'RUNNING' } })
|
||||
const result = await callJava<{ taskId: number; status: string }>({
|
||||
url: '/newApi/api/dedupe/run',
|
||||
method: 'POST',
|
||||
})
|
||||
assert.deepEqual(result, { taskId: 1, status: 'RUNNING' })
|
||||
})
|
||||
|
||||
test('test_call_java_legacy_msg_unwraps', async (t) => {
|
||||
mockResponse(t, { success: true, msg: 'ok', data: [1, 2, 3] })
|
||||
const result = await callJava<number[]>({ url: '/newApi/api/x', method: 'GET' })
|
||||
assert.deepEqual(result, [1, 2, 3])
|
||||
})
|
||||
|
||||
test('test_call_java_success_false_throws', async (t) => {
|
||||
mockResponse(t, { success: false, message: '任务不存在' })
|
||||
await assert.rejects(
|
||||
callJava({ url: '/newApi/api/x', method: 'GET' }),
|
||||
/任务不存在/,
|
||||
)
|
||||
})
|
||||
|
||||
test('test_call_java_message_precedence', async (t) => {
|
||||
mockResponse(t, { success: false, message: 'm1', msg: 'm2', error: 'm3' })
|
||||
await assert.rejects(callJava({ url: '/newApi/api/x', method: 'GET' }), /m1/, 'message 应优先于 msg/error')
|
||||
|
||||
mockResponse(t, { success: false, msg: 'm2', error: 'm3' })
|
||||
await assert.rejects(callJava({ url: '/newApi/api/x', method: 'GET' }), /m2/, '无 message 时 msg 优先于 error')
|
||||
|
||||
mockResponse(t, { success: false, error: 'm3' })
|
||||
await assert.rejects(callJava({ url: '/newApi/api/x', method: 'GET' }), /m3/, '仅 error 时回退 error')
|
||||
})
|
||||
|
||||
test('test_call_java_non_wrapped_passthrough', async (t) => {
|
||||
mockResponse(t, { hello: 'world', count: 3 })
|
||||
const result = await callJava<{ hello: string; count: number }>({
|
||||
url: '/newApi/api/x',
|
||||
method: 'GET',
|
||||
})
|
||||
assert.deepEqual(result, { hello: 'world', count: 3 })
|
||||
})
|
||||
|
||||
test('test_call_java_null_data', async (t) => {
|
||||
mockResponse(t, { success: true, message: 'ok', data: null })
|
||||
const result = await callJava<null>({ url: '/newApi/api/x', method: 'GET' })
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
test('test_call_java_network_error', async (t) => {
|
||||
t.mock.method(http, 'request', async () => {
|
||||
throw new Error('Network Error')
|
||||
})
|
||||
await assert.rejects(
|
||||
callJava({ url: '/newApi/api/x', method: 'GET' }),
|
||||
/Network Error/,
|
||||
)
|
||||
})
|
||||
|
||||
test('test_call_java_http_error_status', async (t) => {
|
||||
t.mock.method(http, 'request', async () => {
|
||||
throw new Error('404 接口不存在')
|
||||
})
|
||||
await assert.rejects(
|
||||
callJava({ url: '/newApi/api/x', method: 'GET' }),
|
||||
/404 接口不存在/,
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = resolve(here, '..')
|
||||
const testsDir = resolve(here)
|
||||
|
||||
function run(cmd: string): string {
|
||||
const env = { ...process.env }
|
||||
delete env.NODE_TEST_CONTEXT
|
||||
return execSync(cmd, { cwd: repoRoot, encoding: 'utf-8', env })
|
||||
}
|
||||
|
||||
function otherTestFiles(): string[] {
|
||||
return readdirSync(testsDir)
|
||||
.filter((f) => f.endsWith('.test.ts') && f !== 'dead-code-cleanup.test.ts')
|
||||
.map((f) => `tests/${f}`)
|
||||
}
|
||||
|
||||
test('test_no_unexported_duplicates', () => {
|
||||
const source = readFileSync(resolve(repoRoot, 'src/shared/api/java-modules.ts'), 'utf-8')
|
||||
const lines = source.split('\n').filter((line) => line.includes('export * from'))
|
||||
const targets = lines
|
||||
.map((line) => line.match(/export \* from "\.\/([^"]+)"/)?.[1])
|
||||
.filter(Boolean)
|
||||
assert.equal(new Set(targets).size, targets.length, '不应有重复的 re-export')
|
||||
})
|
||||
|
||||
test('test_build_typecheck', () => {
|
||||
const output = run('npx vue-tsc --noEmit')
|
||||
assert.equal(output.trim(), '', `vue-tsc 应零错误,实际: ${output.slice(0, 500)}`)
|
||||
})
|
||||
|
||||
test('test_full_vitest_green', () => {
|
||||
const output = run(`node --test ${otherTestFiles().join(' ')} 2>&1`)
|
||||
assert.match(output, /pass \d+/, '应产出通过统计')
|
||||
assert.match(output, /fail 0/, '全量测试应零失败')
|
||||
})
|
||||
|
||||
test('test_no_console_debug_left', () => {
|
||||
const apiDir = resolve(repoRoot, 'src/shared/api')
|
||||
for (const file of readdirSync(apiDir, { recursive: true }) as string[]) {
|
||||
if (!file.endsWith('.ts')) continue
|
||||
const source = readFileSync(resolve(apiDir, file), 'utf-8')
|
||||
assert.ok(
|
||||
!/console\.(log|debug)\(/.test(source),
|
||||
`${file} 不应残留调试日志`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('test_no_unused_import', () => {
|
||||
const source = readFileSync(resolve(repoRoot, 'src/shared/api/java-modules.ts'), 'utf-8')
|
||||
// 聚合器除 re-export 外不应有其它 import/函数体
|
||||
const deadMarkers = [
|
||||
'function postTaskProgressBatch',
|
||||
'const taskProgressResponseCache',
|
||||
'function getCurrentUserId',
|
||||
'function normalizeTaskIds',
|
||||
'function buildTaskProgressRequestKey',
|
||||
'async function uploadTempFileToJava',
|
||||
'function getJavaDownloadUrl',
|
||||
]
|
||||
for (const marker of deadMarkers) {
|
||||
assert.ok(!source.includes(marker), `java-modules.ts 不应残留: ${marker}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('test_entry_points_compile', () => {
|
||||
const viteConfig = readFileSync(resolve(repoRoot, 'vite.config.ts'), 'utf-8')
|
||||
const entries = [...viteConfig.matchAll(/['"]([^'"]+\.html)['"]/g)].map((m) => m[1])
|
||||
assert.ok(entries.length >= 16, `应 ≥16 个 HTML 入口,实际 ${entries.length}`)
|
||||
for (const entry of entries) {
|
||||
assert.ok(existsSync(resolve(repoRoot, entry)), `入口 ${entry} 应存在`)
|
||||
}
|
||||
})
|
||||
|
||||
test('test_bundle_build', () => {
|
||||
const outDir = resolve(repoRoot, 'new_web_source')
|
||||
assert.ok(existsSync(outDir), '构建产物目录应存在')
|
||||
const htmlCount = readdirSync(outDir).filter((f) => f.endsWith('.html')).length
|
||||
assert.ok(htmlCount >= 16, `构建产物应有 ≥16 个 HTML,实际 ${htmlCount}`)
|
||||
})
|
||||
|
||||
test('test_git_diff_scope', () => {
|
||||
const diff = run('git diff --name-only HEAD~1 HEAD')
|
||||
const changed = diff.split('\n').filter(Boolean)
|
||||
assert.ok(changed.length > 0, '应检出本次提交的变更')
|
||||
for (const file of changed) {
|
||||
assert.ok(
|
||||
file.startsWith('frontend-vue/'),
|
||||
`变更应仅限 frontend-vue 目录,越界: ${file}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildDownloadUrl, resolveDownloadUrl } from '../src/shared/api/download.ts'
|
||||
|
||||
test('test_download_url_user_id', () => {
|
||||
assert.equal(
|
||||
buildDownloadUrl('/api/dedupe/results/5/download', 42),
|
||||
'/api/dedupe/results/5/download?user_id=42',
|
||||
)
|
||||
})
|
||||
|
||||
test('test_download_url_no_user_id', () => {
|
||||
assert.equal(buildDownloadUrl('/api/dedupe/results/5/download'), '/api/dedupe/results/5/download')
|
||||
})
|
||||
|
||||
test('test_download_url_existing_user_id', () => {
|
||||
assert.equal(
|
||||
buildDownloadUrl('/api/dedupe/results/5/download?user_id=99', 42),
|
||||
'/api/dedupe/results/5/download?user_id=99',
|
||||
'已有 user_id 时不重复追加',
|
||||
)
|
||||
})
|
||||
|
||||
test('test_resolve_download_fresh_priority', () => {
|
||||
const item = { freshDownloadUrl: '/newApi/api/fresh-url', downloadUrl: '/old-url' }
|
||||
assert.equal(resolveDownloadUrl(item), '/newApi/api/fresh-url')
|
||||
})
|
||||
|
||||
test('test_resolve_download_fallback', () => {
|
||||
const item = { downloadUrl: '/newApi/api/old-url' }
|
||||
assert.equal(resolveDownloadUrl(item), '/newApi/api/old-url')
|
||||
})
|
||||
|
||||
test('test_resolve_download_both_blank', () => {
|
||||
assert.equal(resolveDownloadUrl({}), '')
|
||||
assert.equal(resolveDownloadUrl({ freshDownloadUrl: '', downloadUrl: '' }), '')
|
||||
})
|
||||
|
||||
test('test_download_url_query_merge', () => {
|
||||
assert.equal(
|
||||
buildDownloadUrl('/api/x/download?token=abc', 7),
|
||||
'/api/x/download?token=abc&user_id=7',
|
||||
'已有 query 应合并追加 user_id',
|
||||
)
|
||||
})
|
||||
|
||||
test('test_resolve_download_null_safe', () => {
|
||||
assert.equal(resolveDownloadUrl(null as never), '')
|
||||
assert.equal(resolveDownloadUrl(undefined as never), '')
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { isTerminalStatus } from '../src/shared/api/status.ts'
|
||||
|
||||
test('test_terminal_success', () => {
|
||||
assert.equal(isTerminalStatus('SUCCESS'), true)
|
||||
})
|
||||
|
||||
test('test_terminal_failed', () => {
|
||||
assert.equal(isTerminalStatus('FAILED'), true)
|
||||
})
|
||||
|
||||
test('test_terminal_lowercase', () => {
|
||||
assert.equal(isTerminalStatus('success'), true, '归一化后应为终态')
|
||||
assert.equal(isTerminalStatus('failed'), true)
|
||||
})
|
||||
|
||||
test('test_terminal_pending_false', () => {
|
||||
assert.equal(isTerminalStatus('PENDING'), false)
|
||||
assert.equal(isTerminalStatus('RUNNING'), false)
|
||||
})
|
||||
|
||||
test('test_terminal_null_false', () => {
|
||||
assert.equal(isTerminalStatus(null), false)
|
||||
assert.equal(isTerminalStatus(undefined), false)
|
||||
})
|
||||
|
||||
test('test_terminal_cancelled_default_false', () => {
|
||||
assert.equal(isTerminalStatus('CANCELLED'), false, '默认不把 CANCELLED 当终态')
|
||||
})
|
||||
|
||||
test('test_terminal_cancelled_extended_true', () => {
|
||||
assert.equal(isTerminalStatus('CANCELLED', ['CANCELLED']), true)
|
||||
assert.equal(isTerminalStatus('cancelled', ['CANCELLED']), true, 'extras 也走归一化')
|
||||
})
|
||||
|
||||
test('test_terminal_unknown_false', () => {
|
||||
assert.equal(isTerminalStatus('SYNCING'), false)
|
||||
assert.equal(isTerminalStatus('QUEUED'), false)
|
||||
})
|
||||
Reference in New Issue
Block a user