From 71606e9fe52e41489457ad24ae460e1704825a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 20:11:49 +0800 Subject: [PATCH] =?UTF-8?q?task-28:=20=E6=AD=BB=E4=BB=A3=E7=A0=81=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除拆分后失去引用的 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。 --- frontend-vue/src/shared/api/call.ts | 35 ------ frontend-vue/src/shared/api/download.ts | 22 ---- frontend-vue/src/shared/api/status.ts | 26 ----- frontend-vue/tests/call-abort.test.ts | 115 ------------------- frontend-vue/tests/call.test.ts | 77 ------------- frontend-vue/tests/dead-code-cleanup.test.ts | 99 ++++++++++++++++ frontend-vue/tests/download.test.ts | 50 -------- frontend-vue/tests/status.test.ts | 40 ------- 8 files changed, 99 insertions(+), 365 deletions(-) delete mode 100644 frontend-vue/src/shared/api/call.ts delete mode 100644 frontend-vue/src/shared/api/download.ts delete mode 100644 frontend-vue/src/shared/api/status.ts delete mode 100644 frontend-vue/tests/call-abort.test.ts delete mode 100644 frontend-vue/tests/call.test.ts create mode 100644 frontend-vue/tests/dead-code-cleanup.test.ts delete mode 100644 frontend-vue/tests/download.test.ts delete mode 100644 frontend-vue/tests/status.test.ts diff --git a/frontend-vue/src/shared/api/call.ts b/frontend-vue/src/shared/api/call.ts deleted file mode 100644 index a4147e42..00000000 --- a/frontend-vue/src/shared/api/call.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { http, type RequestOptions } from './http.ts' - -export interface JavaApiResponse { - success: boolean - message: string - data: T | null -} - -export interface LegacyApiSuccess { - success: true - msg?: string - data?: T -} - -export async function callJava( - config: RequestOptions, -): Promise { - const body = await http.request(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 -} diff --git a/frontend-vue/src/shared/api/download.ts b/frontend-vue/src/shared/api/download.ts deleted file mode 100644 index f73d8380..00000000 --- a/frontend-vue/src/shared/api/download.ts +++ /dev/null @@ -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 || '' -} diff --git a/frontend-vue/src/shared/api/status.ts b/frontend-vue/src/shared/api/status.ts deleted file mode 100644 index 307f552a..00000000 --- a/frontend-vue/src/shared/api/status.ts +++ /dev/null @@ -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) -} diff --git a/frontend-vue/tests/call-abort.test.ts b/frontend-vue/tests/call-abort.test.ts deleted file mode 100644 index a0983b9d..00000000 --- a/frontend-vue/tests/call-abort.test.ts +++ /dev/null @@ -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[1] extends (t: infer T) => unknown ? T : never, impl: (config: { signal?: AbortSignal }) => Promise) { - 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({ 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', '已中止的调用应失败') -}) diff --git a/frontend-vue/tests/call.test.ts b/frontend-vue/tests/call.test.ts deleted file mode 100644 index aa0e69c7..00000000 --- a/frontend-vue/tests/call.test.ts +++ /dev/null @@ -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[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({ 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({ 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 接口不存在/, - ) -}) diff --git a/frontend-vue/tests/dead-code-cleanup.test.ts b/frontend-vue/tests/dead-code-cleanup.test.ts new file mode 100644 index 00000000..2fa82c13 --- /dev/null +++ b/frontend-vue/tests/dead-code-cleanup.test.ts @@ -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}`, + ) + } +}) diff --git a/frontend-vue/tests/download.test.ts b/frontend-vue/tests/download.test.ts deleted file mode 100644 index 7a37c02b..00000000 --- a/frontend-vue/tests/download.test.ts +++ /dev/null @@ -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), '') -}) diff --git a/frontend-vue/tests/status.test.ts b/frontend-vue/tests/status.test.ts deleted file mode 100644 index e38d9aed..00000000 --- a/frontend-vue/tests/status.test.ts +++ /dev/null @@ -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) -})