## 问题(皇上指出) 项目里两套编号并行,无法判断哪个是"当前版本": - **发布版本**(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 全绿
100 lines
4.4 KiB
JavaScript
100 lines
4.4 KiB
JavaScript
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 { /* 忽略 */ }
|
||
});
|