Smail/test/frontend/logs.test.mjs
smail ea2808f72b
Some checks are pending
Security Checks / security (push) Waiting to run
精简启动横幅日志并保证历史过滤分页准确
2026-09-09 00:39:01 +08:00

174 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { compileFunction } from 'node:vm'
import { createRequire } from 'node:module'
import { computed, ref } from './vue.mjs'
import { formatFileSize, parseRuntimeLine, summarizeRuntime, timeParts } from '../../frontend/src/views/components/LogSettings/RuntimeLog.js'
const root = new URL('../../frontend/src/views/components/LogSettings/', import.meta.url)
const read = name => readFileSync(new URL(name, root), 'utf8')
const create = compileFunction(
read('LogSettings.js').replace(/^import .*$/gm, '').replace('export function', 'function') + '\nreturn useLogs()',
['computed', 'onBeforeUnmount', 'onMounted', 'ref', 'watch', 'http', 'formatFileSize', 'parseRuntimeLine', 'summarizeRuntime', 'timeParts']
)
function fixture(get) {
let dispose
const page = create(computed, callback => { dispose = callback }, () => {}, ref, () => {}, { get }, formatFileSize, parseRuntimeLine, summarizeRuntime, timeParts)
return { page, dispose: () => dispose() }
}
test('日志页面使用共享 HTTP 客户端的命名导出', () => {
const transport = readFileSync(new URL('../../frontend/src/utils/axios.js', import.meta.url), 'utf8')
assert.match(transport, /export\s*\{\s*http\s*\}/)
assert.match(read('LogSettings.js'), /import\s*\{\s*http\s*\}\s*from\s*['"]@\/utils\/axios['"]/)
})
test('日志页面模板和样式能编译,详情不使用原始 HTML 渲染', () => {
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc')
const { descriptor, errors } = parse(read('LogSettings.vue'))
assert.deepEqual(errors, [])
assert.doesNotThrow(() => compileScript(descriptor, { id: 'logs', inlineTemplate: true }))
assert.deepEqual(compileStyle({ source: read('LogSettings.css'), id: 'logs', scoped: true }).errors, [])
assert.doesNotMatch(read('LogSettings.vue'), /v-html/)
})
test('日志查询传递时间与操作者筛选,并将业务失败显示为错误', async () => {
const requests = []
const { page } = fixture(async (_, options) => {
requests.push(options.params)
return requests.length === 1
? { errorNo: 0, data: { items: [{ id: 'safe' }], hasMore: false } }
: { errorNo: 500, errorMsg: '日志暂不可用' }
})
page.actorId.value = ' 42 '
page.dateRange.value = ['2026-09-01T00:00:00Z', '2026-09-08T00:00:00Z']
await page.search()
assert.equal(requests[0].actorId, '42')
assert.equal(requests[0].from, '2026-09-01T00:00:00.000Z')
assert.equal(page.items.value.length, 1)
await page.search()
assert.equal(page.error.value, '日志暂不可用')
assert.equal(page.items.value.length, 0)
assert.equal(page.displayActor({ actorId: '0' }), '系统')
})
test('压缩日志短页正确返回上一页,文件过期后重置偏移', async () => {
let filename = 'runtime-2026-08-15.log.gz'
const offsets = []
const { page } = fixture(async (_, { params }) => {
if (!params.file) return { errorNo: 0, data: { files: [{ name: filename, compressed: true }] } }
offsets.push(params.offset)
return { errorNo: 0, data: { lines: Array(params.offset === 0 ? 8 : 12).fill('日志'), hasMore: true } }
})
page.category.value = 'runtime'
await page.search()
await page.next()
await page.next()
await page.previous()
assert.deepEqual(offsets, [0, 8, 20, 8])
filename = 'runtime-2026-08-16.log.gz'
await page.next()
assert.equal(page.offset.value, 0)
assert.equal(offsets.at(-1), 0)
})
test('慢响应及卸载后的响应不得覆盖最新日志', async () => {
let complete
let count = 0
const { page, dispose } = fixture(async () => {
if (++count === 1) return new Promise(resolve => { complete = resolve })
return { errorNo: 0, data: { items: [{ id: 'new' }] } }
})
const first = page.search()
await page.search()
complete({ errorNo: 0, data: { items: [{ id: 'old' }] } })
await first
assert.equal(page.items.value[0].id, 'new')
const last = page.search()
dispose()
await last
assert.equal(page.items.value[0].id, 'new')
})
test('运行日志拆分时间级别来源及请求标识,同时保留完整原文', () => {
const raw = '[2026-09-08T14:55:23.2188949Z] [warning][2026-09-08 22:55:23][request-42][git.pcux.cn/xuans/Smail/res_init/init.go:423]开发模式:已停用恢复通道'
const row = parseRuntimeLine(raw, 8)
assert.equal(row.sequence, 8)
assert.equal(row.level, 'warning')
assert.equal(row.label, '警告')
assert.equal(row.type, 'warning')
assert.equal(row.requestId, 'request-42')
assert.equal(row.sourceShort, 'res_init/init.go:423')
assert.equal(row.message, '开发模式:已停用恢复通道')
assert.equal(row.raw, raw)
assert.equal(row.time, timeParts(row.createdAt).time)
})
test('无请求标识、Windows 代码路径与原文方括号均能安全展示', () => {
const row = parseRuntimeLine('[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23][D:\\source\\app.go:39][业务]启动成功\\n下一行', 1)
assert.equal(row.source, 'D:\\source\\app.go:39')
assert.equal(row.requestId, '')
assert.equal(row.message, '[业务]启动成功\\n下一行')
assert.equal(row.level, 'info')
})
test('旧启动信息只精简摘要,不改原始记录或其他业务消息', () => {
const header = '[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23]'
for (const source of ['git.pcux.cn/xuans/Smail/internal/app/app.go', 'D:\\source\\internal\\app\\app.go']) {
for (const suffix of [' ', ' \\n', '\\r\\n']) {
const message = `***\tServer Start Success${suffix}`
const raw = `${header}[${source}:40]${message}`
const row = parseRuntimeLine(raw, 1)
assert.equal(row.summary, '服务启动')
assert.equal(row.message, message)
assert.equal(row.raw, raw)
}
}
for (const raw of [
`${header}[other/app.go:40]***\tServer Start Success`,
`${header}[internal/app/app.go:40]***\tServer Start Success 后续内容`,
`${header.replace('[info]', '[warning]')}[internal/app/app.go:40]***\tServer Start Success`
]) {
const row = parseRuntimeLine(raw, 1)
assert.equal(row.summary, row.message.trim())
assert.equal(row.raw, raw)
}
})
test('未知格式和非法时间不丢失消息,分隔线只改摘要不改原文', () => {
for (const raw of ['[2026-99-99T00:00:00Z] 错误', '<img src=x onerror=alert(1)>', '[业务]消息', '']) {
const row = parseRuntimeLine(raw, 1)
assert.equal(row.raw, raw)
assert.equal(row.message, raw)
assert.equal(row.level, 'unknown')
assert.equal(row.time, '—')
}
const row = parseRuntimeLine('[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23][app.go:39]********', 1)
assert.equal(row.summary, '启动分隔线')
assert.equal(row.message, '********')
assert.equal(parseRuntimeLine('[2026-09-08T14:55:23Z] [业务]保留内容', 2).message, '[业务]保留内容')
})
test('本页级别统计准确,压缩文件大小和空页区间明确', () => {
const rows = ['info', 'warn', 'error', 'fatal', 'panic', 'debug'].map(level => (
parseRuntimeLine(`[2026-09-08T14:55:23Z] [${level}][2026-09-08 22:55:23][app.go:39]内容`, 1)
))
assert.deepEqual(summarizeRuntime(rows), { total: 6, info: 1, warning: 1, error: 3 })
assert.equal(formatFileSize(2048), '2.0 KB')
assert.equal(formatFileSize(0), '0 B')
assert.equal(formatFileSize(undefined), '—')
const { page } = fixture(async () => ({ errorNo: 0, data: {} }))
assert.equal(page.pageRange.value, '暂无记录')
page.category.value = 'runtime'
page.lines.value = rows.map(row => row.raw)
page.offset.value = 8
assert.equal(page.runtimeRows.value[0].sequence, 9)
assert.equal(page.pageRange.value, '第 9–14 条')
assert.deepEqual(page.runtimeSummary.value, summarizeRuntime(rows))
assert.match(read('LogSettings.vue'), /本页日志统计/)
assert.doesNotMatch(read('LogSettings.vue'), /lines\.join/)
})