Files
nodeagent/tests/unit/log-query.test.mjs
T
jkooe e23788d717 refactor(versioning): 统一版本号 —— semver 为唯一版本号,开发批次代号并入其归属版本
## 问题(皇上指出)
项目里两套编号并行,无法判断哪个是"当前版本":
- **发布版本**(semver):v1.1.0 ~ v1.6.0、v2.0.0
- **开发批次代号**:v1 ~ v26(README 路线表 / 代码注释 / 文档)
时间线证据:v1.1.0 首发(09-29)时批次已推进到 v12.4 —— **二者从来不是一对一**(26 批次只发过 10 版)。

## 规则(新增 docs/VERSIONING.md §0 版本号唯一性规范)
- **唯一版本号 = semver `vX.Y.Z`**,两个来源必须永远一致:git tag 与 package.json(构建期注入 agent)
- **批次代号不是版本号**,不再单独出现;需要区分同版内多批改动时用**功能名**
- 附「批次 → 归属发布版本」映射表(按 tag 时间线与功能内容推定,并注明以 CHANGELOG 为准)

## 统一动作
1. **批次 v23 ~ v26 → v2.0.0**(22 文件 / 53 处)—— 它们确实属于 v2.0.0
2. **臣的批次号 v1.7 / v1.8 → v2.0.0**(27 文件)—— 未单独发版
3. **简写补全三段式**:v1.5 → v1.5.0、v1.6 → v1.6.0
4. **README 路线表**:`**v1**` → `**批次 1**`;表头加「批次 ≠ 版本号」说明并链到 §0;
   补齐批次 16~29 与 **v2.0.0 发布行**;删掉过时的「待办」行(提到的分支早已删除)
5. `docs/VERSIONING.md` 里**故意保留** 3 处批次号 —— 那是规范说明本身

## 保留项(真实发布版本,不动)
v1.1.0 / v1.2.1 / v1.3.0 / v1.4.0~v1.4.3 / v1.5.0 / v1.6.0 / v2.0.0(10 个 tag)

## 验证
- 版本号一致性:package.json = tag = 8 个 workspace = tauri.conf.json = **2.0.0** ✓
- v1.7/v1.8/v23~v26 全仓残留 **3 处**(均在 VERSIONING.md 规范说明中,属预期)
- 7 包构建 · 单测 273 · e2e 30 · ps1-encoding 24 全绿
2026-10-10 14:27:12 +08:00

100 lines
4.4 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 { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { logQuery, parseLeadingTs } from '../../apps/agent/dist/capabilities/log.js';
/**
* log.query 的纯逻辑测试(v2.0.0 方向二第一块)。
* 文件级行为(流式读取/白名单)用真实临时文件验证,无需被控端进程。
*/
const dir = mkdtempSync(join(tmpdir(), 'logquery-'));
const sample = join(dir, 'app.log');
writeFileSync(
sample,
[
'[2026-10-07 10:00:01] [INFO] agent started',
'[2026-10-07 10:00:05] [DEBUG] connecting to relay',
'[2026-10-07 10:00:09] [ERROR] handshake timeout after 5000ms',
'[2026-10-07 10:00:12] [WARN] retrying (attempt 2)',
'[2026-10-07 10:00:15] [INFO] handshake ok',
'plain line without level',
'[2026-10-07 10:00:20] [ERROR] proxy unreachable: ECONNREFUSED',
].join('\n') + '\n',
'utf8',
);
test('log.query:无过滤时按行号顺序回全部', async () => {
const r = await logQuery({ path: sample });
assert.equal(r.matched.length, 7);
assert.equal(r.matched[0].text.includes('agent started'), true);
assert.equal(r.matched[0].n, 1, '行号从 1 开始');
assert.equal(r.scanned, 7);
});
test('log.query:pattern 正则过滤(忽略大小写)', async () => {
const r = await logQuery({ path: sample, pattern: 'handshake' });
assert.equal(r.matched.length, 2, 'handshake timeout + handshake ok(pattern 忽略大小写)');
assert.ok(r.matched.every((m) => /handshake/i.test(m.text)));
});
test('log.query:level 过滤只回该级别(含 WARNING→WARN 归一)', async () => {
const errs = await logQuery({ path: sample, level: 'ERROR' });
assert.equal(errs.matched.length, 2);
assert.ok(errs.matched.every((m) => m.text.includes('[ERROR]')));
const warns = await logQuery({ path: sample, level: 'WARN' });
assert.equal(warns.matched.length, 1);
// 无级别的行不会被 level 过滤命中
assert.ok(!warns.matched.some((m) => m.text.includes('plain line')));
});
test('log.query:limit 与 offset 分页', async () => {
const r = await logQuery({ path: sample, limit: 2, offset: 2 });
assert.equal(r.matched.length, 2);
assert.equal(r.matched[0].n, 3, 'offset=2 跳过前 2 条命中');
assert.equal(r.matched[1].n, 4);
});
test('log.query:tail 取末尾 N 条', async () => {
const r = await logQuery({ path: sample, tail: true, limit: 3 });
assert.equal(r.matched.length, 3);
assert.equal(r.matched[2].n, 7, '最后一条是第 7 行');
assert.ok(r.matched[2].text.includes('ECONNREFUSED'));
});
test('log.query:since 按行首时间戳过滤', async () => {
const since = Date.parse('2026-10-07T10:00:12');
const r = await logQuery({ path: sample, since });
// 10:00:12 及之后共 4 行(12/15/20 + plain 行无时间戳 → 放行)
assert.equal(r.matched.length, 4);
assert.ok(!r.matched.some((m) => m.text.includes('agent started')));
});
test('log.query:非法 pattern / level / 路径均被拒', async () => {
await assert.rejects(() => logQuery({ path: sample, pattern: '[' }), /不是合法正则/);
await assert.rejects(() => logQuery({ path: sample, level: 'TRACEY' }), /不支持的 level/);
await assert.rejects(() => logQuery({ path: join(dir, 'nope.log') }), /不存在或不可读/);
await assert.rejects(() => logQuery({ path: dir }), /不是普通文件/);
});
test('log.query:limit 上限裁剪(5000)', async () => {
// limit=99999 应被裁剪到 5000,不报错
const r = await logQuery({ path: sample, limit: 99999 });
assert.equal(r.matched.length, 7);
});
test('parseLeadingTs:支持 ISO 与方括号形态', () => {
assert.equal(parseLeadingTs('[2026-10-07 10:00:12] [WARN] x'), Date.parse('2026-10-07T10:00:12'));
assert.equal(parseLeadingTs('2026-10-07T10:00:12Z hello'), Date.parse('2026-10-07T10:00:12Z'), 'Z 必须按 UTC 解析(曾被吃掉 → 偏 8 小时)');
assert.equal(parseLeadingTs('2026-10-07T10:00:12+08:00 x'), Date.parse('2026-10-07T10:00:12+08:00'), '带偏移量同理');
assert.equal(parseLeadingTs('[2026-10-07 10:00:12] x'), Date.parse('2026-10-07T10:00:12'), '无时区 → 本地时间');
assert.equal(parseLeadingTs('2026/10/07 10:00:12 x'), Date.parse('2026-10-07T10:00:12'));
assert.equal(parseLeadingTs('no timestamp here'), null);
});
process.on('exit', () => {
try { rmSync(dir, { recursive: true, force: true }); } catch { /* 忽略 */ }
});