@yottameta/yotta-logs 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -4
- package/LICENSE +20 -20
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/SKILL.md +2 -1
- package/bin/install.js +245 -163
- package/install.sh +131 -131
- package/package.json +9 -2
- package/references/agent-formats.md +7 -7
- package/references/cli.md +1 -1
- package/references/faq.md +54 -0
- package/scripts/yotta_logs.py +23 -10
- package/scripts/test_yotta_logs.py +0 -757
|
@@ -1,757 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""test_yotta_logs.py — 元史(yotta-logs)测试。
|
|
4
|
-
|
|
5
|
-
覆盖:JSONL 解析容错 / 会话发现 / sessions.json 索引 / 角色与文本提取 /
|
|
6
|
-
默认脱敏 / scan / search(关键词·正则·日期·会话·角色·截断)/ session 提取 /
|
|
7
|
-
stats(角色·成本·token·每日汇总)/ tools 排行 / CLI 退出码 / JSON 输出 /
|
|
8
|
-
GBK 控制台 / 只读保证。纯标准库,无 pytest 依赖。
|
|
9
|
-
|
|
10
|
-
运行:python scripts/test_yotta_logs.py
|
|
11
|
-
"""
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import subprocess
|
|
15
|
-
import sys
|
|
16
|
-
import tempfile
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
_HERE = Path(__file__).resolve().parent
|
|
20
|
-
sys.path.insert(0, str(_HERE))
|
|
21
|
-
|
|
22
|
-
import yotta_logs as YL # noqa: E402
|
|
23
|
-
|
|
24
|
-
PASS = 0
|
|
25
|
-
FAIL = 0
|
|
26
|
-
FAILED = []
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def check(name, cond, detail=""):
|
|
30
|
-
global PASS, FAIL
|
|
31
|
-
if cond:
|
|
32
|
-
PASS += 1
|
|
33
|
-
else:
|
|
34
|
-
FAIL += 1
|
|
35
|
-
FAILED.append(name)
|
|
36
|
-
print(" FAIL: %s %s" % (name, detail))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def wjsonl(p, rows):
|
|
40
|
-
lines = []
|
|
41
|
-
for r in rows:
|
|
42
|
-
if isinstance(r, str):
|
|
43
|
-
lines.append(r)
|
|
44
|
-
else:
|
|
45
|
-
lines.append(json.dumps(r, ensure_ascii=False))
|
|
46
|
-
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def build_fixture(base):
|
|
50
|
-
"""构造一个真实形态的会话日志目录,返回目录 Path。"""
|
|
51
|
-
d = base / "sessions"
|
|
52
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
53
|
-
a1 = [
|
|
54
|
-
{"type": "session", "timestamp": "2026-08-26T03:00:00+08:00",
|
|
55
|
-
"session_id": "a1", "title": "部署讨论"},
|
|
56
|
-
{"type": "message", "timestamp": "2026-08-26T03:00:01+08:00",
|
|
57
|
-
"message": {"role": "user", "content": [
|
|
58
|
-
{"type": "text", "text": "你好,部署方案定了吗?"}]}},
|
|
59
|
-
{"type": "message", "timestamp": "2026-08-26T03:00:05+08:00",
|
|
60
|
-
"message": {"role": "assistant", "content": [
|
|
61
|
-
{"type": "text",
|
|
62
|
-
"text": "定了,按灰度发布执行。密钥 sk-abcdef1234567890 已就位。"}],
|
|
63
|
-
"usage": {"cost": {"total": 0.01}, "input_tokens": 100,
|
|
64
|
-
"output_tokens": 50}}},
|
|
65
|
-
{"type": "message", "timestamp": "2026-08-26T03:01:00+08:00",
|
|
66
|
-
"message": {"role": "assistant", "content": [
|
|
67
|
-
{"type": "toolCall", "name": "read_file"},
|
|
68
|
-
{"type": "text", "text": "我读一下配置。"}]}},
|
|
69
|
-
{"type": "message", "timestamp": "2026-08-26T03:02:00+08:00",
|
|
70
|
-
"message": {"role": "toolResult", "content": [
|
|
71
|
-
{"type": "toolResult", "name": "read_file",
|
|
72
|
-
"content": "{\"ok\": true}"}]}},
|
|
73
|
-
"this line is not valid json",
|
|
74
|
-
]
|
|
75
|
-
b2 = [
|
|
76
|
-
{"type": "message", "timestamp": "2026-08-27T10:00:00+08:00",
|
|
77
|
-
"message": {"role": "user", "content": [
|
|
78
|
-
{"type": "text", "text": "CI 又失败了,看下日志。"}]}},
|
|
79
|
-
{"type": "message", "timestamp": "2026-08-27T10:01:00+08:00",
|
|
80
|
-
"message": {"role": "assistant", "content": [
|
|
81
|
-
{"type": "text",
|
|
82
|
-
"text": "好的,我去查。Bearer abcDEF123ghiJKL789 拿来用。"}]}},
|
|
83
|
-
{"type": "message", "timestamp": "2026-08-27T10:02:00+08:00",
|
|
84
|
-
"message": {"role": "assistant", "content": [
|
|
85
|
-
{"type": "toolCall", "name": "run_shell"},
|
|
86
|
-
{"type": "text", "text": "执行命令。"}]}},
|
|
87
|
-
{"type": "message", "timestamp": "2026-08-27T10:03:00+08:00",
|
|
88
|
-
"message": {"role": "user", "content": [
|
|
89
|
-
{"type": "text", "text": "hello, please retry with the new endpoint"}]}},
|
|
90
|
-
]
|
|
91
|
-
wjsonl(d / "a1.jsonl", a1)
|
|
92
|
-
wjsonl(d / "b2.jsonl", b2)
|
|
93
|
-
(d / "sessions.json").write_text(
|
|
94
|
-
json.dumps({"微信-部署": "a1", "ci-排查": "b2"}, ensure_ascii=False),
|
|
95
|
-
encoding="utf-8")
|
|
96
|
-
(d / "notes.txt").write_text("not a session file", encoding="utf-8")
|
|
97
|
-
return d
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_parse_jsonl():
|
|
101
|
-
with tempfile.TemporaryDirectory() as td:
|
|
102
|
-
p = Path(td) / "x.jsonl"
|
|
103
|
-
p.write_text('{"a":1}\nnot json\n[1,2,3]\n{"b":2}\n',
|
|
104
|
-
encoding="utf-8")
|
|
105
|
-
records, invalid = YL.parse_jsonl(p)
|
|
106
|
-
check("parse_jsonl 记录数", len(records) == 2, "got %d" % len(records))
|
|
107
|
-
check("parse_jsonl 无效行计数", invalid == 2, "got %d" % invalid)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_list_sessions():
|
|
111
|
-
with tempfile.TemporaryDirectory() as td:
|
|
112
|
-
d = Path(td)
|
|
113
|
-
(d / "a.jsonl").write_text("\n", encoding="utf-8")
|
|
114
|
-
(d / "b.jsonl").write_text("\n", encoding="utf-8")
|
|
115
|
-
(d / "c.txt").write_text("x", encoding="utf-8")
|
|
116
|
-
(d / "sessions.json").write_text("{}", encoding="utf-8")
|
|
117
|
-
sess = YL.list_sessions(d)
|
|
118
|
-
check("list_sessions 只收 jsonl", len(sess) == 2, "got %s" % sess)
|
|
119
|
-
check("list_sessions 会话 ID = 文件名主干",
|
|
120
|
-
{s["session"] for s in sess} == {"a", "b"})
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def test_load_index():
|
|
124
|
-
with tempfile.TemporaryDirectory() as td:
|
|
125
|
-
d = Path(td)
|
|
126
|
-
(d / "sessions.json").write_text(
|
|
127
|
-
json.dumps({"k1": "s1", "k2": "s2"}), encoding="utf-8")
|
|
128
|
-
idx = YL.load_index(d)
|
|
129
|
-
check("load_index dict 形态", idx == {"k1": "s1", "k2": "s2"}, str(idx))
|
|
130
|
-
(d / "sessions.json").write_text(
|
|
131
|
-
json.dumps([{"key": "k1", "sessionId": "s1"}]), encoding="utf-8")
|
|
132
|
-
idx = YL.load_index(d)
|
|
133
|
-
check("load_index list 形态", idx == {"k1": "s1"}, str(idx))
|
|
134
|
-
(d / "sessions.json").write_text("not json", encoding="utf-8")
|
|
135
|
-
check("load_index 坏文件容错", YL.load_index(d) == {})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def test_rec_parsing():
|
|
139
|
-
rec = {"type": "message", "timestamp": "2026-08-26T03:00:00Z",
|
|
140
|
-
"message": {"role": "user", "content": "直接字符串"}}
|
|
141
|
-
check("_rec_ts 顶层", YL._rec_ts(rec) == "2026-08-26T03:00:00Z")
|
|
142
|
-
check("_rec_role user", YL._rec_role(rec) == "user")
|
|
143
|
-
check("_rec_text 字符串", YL._rec_text(rec) == "直接字符串")
|
|
144
|
-
|
|
145
|
-
rec2 = {"type": "message", "message": {"role": "toolResult", "content": [
|
|
146
|
-
{"type": "text", "text": "A"}, {"type": "thinking", "text": "隐藏"},
|
|
147
|
-
{"type": "toolCall", "name": "run_shell"}]}}
|
|
148
|
-
check("_rec_role toolResult 归一为 tool", YL._rec_role(rec2) == "tool",
|
|
149
|
-
YL._rec_role(rec2))
|
|
150
|
-
check("_rec_text 只取 text", YL._rec_text(rec2) == "A",
|
|
151
|
-
repr(YL._rec_text(rec2)))
|
|
152
|
-
check("_rec_tool_names", YL._rec_tool_names(rec2) == ["run_shell"])
|
|
153
|
-
|
|
154
|
-
rec3 = {"type": "message", "message": {"role": "assistant", "content": [
|
|
155
|
-
{"type": "text", "text": "x"}]},
|
|
156
|
-
"usage": {"cost": {"total": 0.5}, "input_tokens": 10,
|
|
157
|
-
"output_tokens": 20}}
|
|
158
|
-
check("_rec_cost", YL._rec_cost(rec3) == 0.5)
|
|
159
|
-
check("_rec_tokens", YL._rec_tokens(rec3) == (10, 20))
|
|
160
|
-
check("_is_message 排除 session 元数据",
|
|
161
|
-
YL._is_message({"type": "session", "role": "session"}) is False)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def test_redact():
|
|
165
|
-
check("redact sk-", "sk-" not in YL.redact("密钥 sk-abcdef1234567890 已就位"))
|
|
166
|
-
check("redact ghp_", "ghp_" not in YL.redact("token ghp_ABCDEFGHIJKLMNOPQRST"))
|
|
167
|
-
check("redact AKIA", "AKIA" not in YL.redact("AKIA1234567890ABCDEF"))
|
|
168
|
-
check("redact JWT",
|
|
169
|
-
"eyJ" not in YL.redact("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"))
|
|
170
|
-
check("redact Bearer",
|
|
171
|
-
YL.redact("Bearer abcDEF123ghiJKL789") == "Bearer ***")
|
|
172
|
-
check("redact URL 口令",
|
|
173
|
-
YL.redact("https://user:pass@example.com/path")
|
|
174
|
-
== "https://user:***@example.com/path")
|
|
175
|
-
check("redact 赋值",
|
|
176
|
-
"token=***" in YL.redact("token=sk-abcdef1234567890x"))
|
|
177
|
-
check("redact 长串",
|
|
178
|
-
"***" in YL.redact("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH"))
|
|
179
|
-
check("redact URL 路径保留",
|
|
180
|
-
"https://example.com/api/v1/items" in YL.redact("看 https://example.com/api/v1/items 这里"))
|
|
181
|
-
check("redact 普通中文不动",
|
|
182
|
-
YL.redact("你好,今天天气不错。") == "你好,今天天气不错。")
|
|
183
|
-
check("redact PEM",
|
|
184
|
-
"PRIVATE KEY REDACTED" in YL.redact(
|
|
185
|
-
"-----BEGIN RSA PRIVATE KEY-----\nabc\n-----END RSA PRIVATE KEY-----"))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def test_scan(fx):
|
|
189
|
-
res = YL.scan_sessions(str(fx))
|
|
190
|
-
check("scan 会话数 2", res["total_sessions"] == 2,
|
|
191
|
-
str(res["rows"]))
|
|
192
|
-
check("scan 消息合计 8", res["total_messages"] == 8,
|
|
193
|
-
str(res["total_messages"]))
|
|
194
|
-
check("scan 无效行 1", res["total_invalid"] == 1)
|
|
195
|
-
by = {r["session"]: r for r in res["rows"]}
|
|
196
|
-
check("scan a1 消息 4", by["a1"]["messages"] == 4)
|
|
197
|
-
check("scan b2 日期", by["b2"]["date"] == "2026-08-27")
|
|
198
|
-
check("scan 别名映射", by["a1"]["alias"] == "微信-部署")
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def test_search(fx):
|
|
202
|
-
r = YL.search_sessions(str(fx), "部署")
|
|
203
|
-
check("search 关键词命中", len(r["matches"]) == 1
|
|
204
|
-
and r["matches"][0]["session"] == "a1", str(r))
|
|
205
|
-
r = YL.search_sessions(str(fx), "HELLO")
|
|
206
|
-
check("search 不区分大小写", len(r["matches"]) == 1
|
|
207
|
-
and r["matches"][0]["session"] == "b2", str(r))
|
|
208
|
-
r = YL.search_sessions(str(fx), r"CI \w+", regex=True)
|
|
209
|
-
check("search 正则命中", len(r["matches"]) == 1
|
|
210
|
-
and r["matches"][0]["session"] == "b2", str(r))
|
|
211
|
-
r = YL.search_sessions(str(fx), "看下", date="2026-08-27")
|
|
212
|
-
check("search 日期过滤", len(r["matches"]) == 1
|
|
213
|
-
and r["matches"][0]["session"] == "b2", str(r))
|
|
214
|
-
r = YL.search_sessions(str(fx), "灰度", sessions=["微信-部署"])
|
|
215
|
-
check("search 会话别名过滤", len(r["matches"]) == 1
|
|
216
|
-
and r["matches"][0]["session"] == "a1", str(r))
|
|
217
|
-
r = YL.search_sessions(str(fx), "灰度", sessions=["b2"])
|
|
218
|
-
check("search 会话 ID 过滤无命中", r["matches"] == [], str(r))
|
|
219
|
-
r = YL.search_sessions(str(fx), "了", role="assistant")
|
|
220
|
-
check("search 角色过滤", r["matches"] and all(
|
|
221
|
-
m["role"] == "assistant" for m in r["matches"]), str(r))
|
|
222
|
-
r = YL.search_sessions(str(fx), "了", limit=1)
|
|
223
|
-
check("search limit 截断", r["truncated"] is True and len(r["matches"]) == 1
|
|
224
|
-
and r["sessions_hit"] == 1, str(r))
|
|
225
|
-
r = YL.search_sessions(str(fx), "绝不存在的词xyz")
|
|
226
|
-
check("search 无命中空列表", r["matches"] == [] and r["sessions_hit"] == 0)
|
|
227
|
-
r = YL.search_sessions(str(fx), "sk-abcdef1234567890")
|
|
228
|
-
check("search 命中脱敏打码", r["matches"] and
|
|
229
|
-
"sk-" not in r["matches"][0]["text"] and "***" in r["matches"][0]["text"],
|
|
230
|
-
str(r["matches"]))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def test_extract(fx):
|
|
234
|
-
r = YL.extract_session(str(fx), "a1")
|
|
235
|
-
check("extract 消息 4", len(r["messages"]) == 4, str(len(r["messages"])))
|
|
236
|
-
check("extract 首条", r["messages"][0]["role"] == "user"
|
|
237
|
-
and "部署方案" in r["messages"][0]["text"])
|
|
238
|
-
check("extract 脱敏", "sk-" not in r["messages"][1]["text"]
|
|
239
|
-
and "***" in r["messages"][1]["text"], repr(r["messages"][1]["text"]))
|
|
240
|
-
r2 = YL.extract_session(str(fx), "a1", role="assistant")
|
|
241
|
-
check("extract 角色过滤", len(r2["messages"]) == 2
|
|
242
|
-
and all(m["role"] == "assistant" for m in r2["messages"]))
|
|
243
|
-
r3 = YL.extract_session(str(fx), "ci-排查")
|
|
244
|
-
check("extract 别名解析", r3["session"] == "b2", str(r3["session"]))
|
|
245
|
-
r4 = YL.extract_session(str(fx), "b2", with_tools=True)
|
|
246
|
-
tools = [t for m in r4["messages"] for t in m["tools"]]
|
|
247
|
-
check("extract 工具标注", "run_shell" in tools, str(tools))
|
|
248
|
-
try:
|
|
249
|
-
YL.extract_session(str(fx), "nope")
|
|
250
|
-
check("extract 未知会话抛错", False)
|
|
251
|
-
except SystemExit:
|
|
252
|
-
check("extract 未知会话抛错", True)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def test_stats(fx):
|
|
256
|
-
r = YL.session_stats(str(fx))
|
|
257
|
-
check("stats 会话 2", r["sessions"] == 2)
|
|
258
|
-
check("stats 消息 8", r["messages"] == 8)
|
|
259
|
-
check("stats 角色分布", r["roles"] == {"user": 3, "assistant": 4, "tool": 1},
|
|
260
|
-
str(r["roles"]))
|
|
261
|
-
check("stats 成本", abs(r["cost"] - 0.01) < 1e-9, str(r["cost"]))
|
|
262
|
-
check("stats token", r["tokens_in"] == 100 and r["tokens_out"] == 50)
|
|
263
|
-
check("stats 首末时间", r["first"].startswith("2026-08-26")
|
|
264
|
-
and r["last"].startswith("2026-08-27"))
|
|
265
|
-
r2 = YL.session_stats(str(fx), daily=True)
|
|
266
|
-
check("stats 每日两天", set(r2["days"].keys()) == {"2026-08-26", "2026-08-27"},
|
|
267
|
-
str(r2["days"].keys()))
|
|
268
|
-
check("stats 每日成本", abs(r2["days"]["2026-08-26"]["cost"] - 0.01) < 1e-9)
|
|
269
|
-
r3 = YL.session_stats(str(fx), session_id="微信-部署")
|
|
270
|
-
check("stats 单会话", r3["sessions"] == 1 and r3["messages"] == 4,
|
|
271
|
-
str((r3["sessions"], r3["messages"])))
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def test_tools(fx):
|
|
275
|
-
items = YL.tool_breakdown(str(fx))
|
|
276
|
-
by = dict(items)
|
|
277
|
-
check("tools 排行 read_file 2", by.get("read_file") == 2, str(items))
|
|
278
|
-
check("tools 排行 run_shell 1", by.get("run_shell") == 1, str(items))
|
|
279
|
-
items2 = YL.tool_breakdown(str(fx), session_id="a1")
|
|
280
|
-
by2 = dict(items2)
|
|
281
|
-
check("tools 单会话", by2.get("read_file") == 2 and "run_shell" not in by2,
|
|
282
|
-
str(items2))
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def _run(args, inp=None, env=None, cwd=None):
|
|
286
|
-
e = dict(os.environ)
|
|
287
|
-
if env:
|
|
288
|
-
e.update(env)
|
|
289
|
-
return subprocess.run(
|
|
290
|
-
[sys.executable, str(_HERE / "yotta_logs.py")] + args,
|
|
291
|
-
input=inp, capture_output=True, text=True, encoding="utf-8",
|
|
292
|
-
errors="replace", env=e, cwd=cwd)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def test_cli(fx):
|
|
296
|
-
r = _run(["version"])
|
|
297
|
-
check("CLI version", r.returncode == 0 and YL.VERSION in r.stdout,
|
|
298
|
-
"rc=%d" % r.returncode)
|
|
299
|
-
|
|
300
|
-
r = _run(["scan", "--dir", str(fx), "--json"])
|
|
301
|
-
try:
|
|
302
|
-
obj = json.loads(r.stdout)
|
|
303
|
-
check("CLI scan --json", r.returncode == 0
|
|
304
|
-
and obj["total_sessions"] == 2, r.stdout[:120])
|
|
305
|
-
except Exception as e: # noqa: BLE001
|
|
306
|
-
check("CLI scan --json", False, str(e))
|
|
307
|
-
|
|
308
|
-
r = _run(["search", "部署", "--dir", str(fx), "--json"])
|
|
309
|
-
try:
|
|
310
|
-
obj = json.loads(r.stdout)
|
|
311
|
-
check("CLI search --json", r.returncode == 0
|
|
312
|
-
and obj["total_matches"] == 1 and len(obj["matches"]) == 1,
|
|
313
|
-
r.stdout[:120])
|
|
314
|
-
except Exception as e: # noqa: BLE001
|
|
315
|
-
check("CLI search --json", False, str(e))
|
|
316
|
-
|
|
317
|
-
r = _run(["search", "绝不存在的词xyz", "--dir", str(fx)])
|
|
318
|
-
check("CLI search 无命中退出码 1", r.returncode == 1,
|
|
319
|
-
"rc=%d" % r.returncode)
|
|
320
|
-
|
|
321
|
-
r = _run(["session", "a1", "--dir", str(fx)])
|
|
322
|
-
check("CLI session 文本输出", r.returncode == 0 and "部署方案" in r.stdout,
|
|
323
|
-
"rc=%d" % r.returncode)
|
|
324
|
-
|
|
325
|
-
r = _run(["stats", "--dir", str(fx), "--daily"])
|
|
326
|
-
check("CLI stats 每日", r.returncode == 0 and "每日汇总" in r.stdout,
|
|
327
|
-
"rc=%d" % r.returncode)
|
|
328
|
-
|
|
329
|
-
r = _run(["tools", "--dir", str(fx), "--json"])
|
|
330
|
-
try:
|
|
331
|
-
obj = json.loads(r.stdout)
|
|
332
|
-
by = {t["name"]: t["count"] for t in obj["tools"]}
|
|
333
|
-
check("CLI tools --json", r.returncode == 0 and by.get("read_file") == 2,
|
|
334
|
-
r.stdout[:120])
|
|
335
|
-
except Exception as e: # noqa: BLE001
|
|
336
|
-
check("CLI tools --json", False, str(e))
|
|
337
|
-
|
|
338
|
-
r = _run(["scan", "--dir", str(Path(fx).parent / "no-such-dir")])
|
|
339
|
-
check("CLI 目录不存在退出码 4", r.returncode == 4, "rc=%d" % r.returncode)
|
|
340
|
-
|
|
341
|
-
r = _run(["badcmd"])
|
|
342
|
-
check("CLI 未知子命令退出码 4", r.returncode == 4, "rc=%d" % r.returncode)
|
|
343
|
-
|
|
344
|
-
r = _run(["search", "Bearer", "--dir", str(fx), "--no-redact"])
|
|
345
|
-
check("CLI --no-redact 原文保留", r.returncode == 0
|
|
346
|
-
and "abcDEF123ghiJKL789" in r.stdout, repr(r.stdout[:120]))
|
|
347
|
-
|
|
348
|
-
r = _run(["scan"], env={"YOTTA_LOGS_DIR": str(fx)})
|
|
349
|
-
check("CLI YOTTA_LOGS_DIR 环境变量", r.returncode == 0
|
|
350
|
-
and "会话 2 个" in r.stdout, "rc=%d" % r.returncode)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def test_gbk_console(fx):
|
|
354
|
-
env = dict(os.environ)
|
|
355
|
-
env["PYTHONIOENCODING"] = "gbk"
|
|
356
|
-
r = subprocess.run(
|
|
357
|
-
[sys.executable, str(_HERE / "yotta_logs.py"),
|
|
358
|
-
"search", "部署", "--dir", str(fx)],
|
|
359
|
-
capture_output=True, text=True, encoding="gbk", errors="replace",
|
|
360
|
-
env=env)
|
|
361
|
-
check("GBK 控制台中文输出不炸", r.returncode == 0,
|
|
362
|
-
"rc=%d err=%r" % (r.returncode, r.stderr[:100]))
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def test_readonly(fx):
|
|
366
|
-
before = sorted((p.name, p.stat().st_size) for p in fx.iterdir())
|
|
367
|
-
YL.scan_sessions(str(fx))
|
|
368
|
-
YL.search_sessions(str(fx), "部署")
|
|
369
|
-
YL.extract_session(str(fx), "a1")
|
|
370
|
-
YL.session_stats(str(fx), daily=True)
|
|
371
|
-
YL.tool_breakdown(str(fx))
|
|
372
|
-
after = sorted((p.name, p.stat().st_size) for p in fx.iterdir())
|
|
373
|
-
check("只读保证:目录内容不变", before == after,
|
|
374
|
-
"before=%s after=%s" % (before, after))
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
# ── v0.2.0 通用化测试 ────────────────────────────────────────────────────
|
|
378
|
-
|
|
379
|
-
def build_json_fixture(base):
|
|
380
|
-
"""单文件 JSON 会话:一个数组文件 + 一个 dict-of-lists 文件。"""
|
|
381
|
-
d = base / "jsons"
|
|
382
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
383
|
-
arr = [
|
|
384
|
-
{"role": "user", "content": "你好,单文件 JSON 测试。", "ts": "2026-08-26T09:00:00+08:00"},
|
|
385
|
-
{"role": "assistant", "content": "收到。", "ts": "2026-08-26T09:01:00+08:00"},
|
|
386
|
-
]
|
|
387
|
-
(d / "convo.json").write_text(json.dumps(arr, ensure_ascii=False),
|
|
388
|
-
encoding="utf-8")
|
|
389
|
-
multi = {
|
|
390
|
-
"s1": [{"role": "user", "content": "s1 的第一个问题"},
|
|
391
|
-
{"role": "assistant", "content": "s1 的回复"}],
|
|
392
|
-
"s2": [{"role": "user", "content": "s2 的问题"}],
|
|
393
|
-
}
|
|
394
|
-
(d / "multi.json").write_text(json.dumps(multi, ensure_ascii=False),
|
|
395
|
-
encoding="utf-8")
|
|
396
|
-
return d
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
def build_sqlite_opencode(p):
|
|
400
|
-
import sqlite3 as _sq
|
|
401
|
-
con = _sq.connect(str(p))
|
|
402
|
-
con.execute("CREATE TABLE session (id TEXT, title TEXT, time_created INTEGER,"
|
|
403
|
-
" cost REAL, tokens_input INTEGER, tokens_output INTEGER)")
|
|
404
|
-
con.execute("CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, data TEXT)")
|
|
405
|
-
con.execute("CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT,"
|
|
406
|
-
" time_created INTEGER, data TEXT)")
|
|
407
|
-
con.execute("INSERT INTO session VALUES ('ses_a','部署讨论',1785834902916,0.05,100,50)")
|
|
408
|
-
con.execute("INSERT INTO session VALUES ('ses_b','CI 排查',1785835000000,0.0,0,0)")
|
|
409
|
-
con.execute("INSERT INTO message VALUES ('msg_1','ses_a',1785834903000,'{\"role\":\"user\"}')")
|
|
410
|
-
con.execute("INSERT INTO part VALUES ('prt_1','msg_1','ses_a',1785834903001,"
|
|
411
|
-
"'{\"type\":\"text\",\"text\":\"你好,部署方案定了吗?\"}')")
|
|
412
|
-
con.execute("INSERT INTO message VALUES ('msg_2','ses_a',1785834904000,'{\"role\":\"assistant\"}')")
|
|
413
|
-
con.execute("INSERT INTO part VALUES ('prt_2','msg_2','ses_a',1785834904001,"
|
|
414
|
-
"'{\"type\":\"text\",\"text\":\"定了,按灰度发布执行。\"}')")
|
|
415
|
-
con.execute("INSERT INTO part VALUES ('prt_3','msg_2','ses_a',1785834904002,"
|
|
416
|
-
"'{\"type\":\"tool\",\"tool\":\"read_file\"}')")
|
|
417
|
-
con.execute("INSERT INTO part VALUES ('prt_4','msg_2','ses_a',1785834904003,"
|
|
418
|
-
"'{\"type\":\"reasoning\",\"text\":\"隐藏推理不输出\"}')")
|
|
419
|
-
con.execute("INSERT INTO message VALUES ('msg_3','ses_b',1785835001000,'{\"role\":\"user\"}')")
|
|
420
|
-
con.execute("INSERT INTO part VALUES ('prt_5','msg_3','ses_b',1785835001001,"
|
|
421
|
-
"'{\"type\":\"text\",\"text\":\"CI 又失败了,看下日志。\"}')")
|
|
422
|
-
con.commit()
|
|
423
|
-
con.close()
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def build_sqlite_generic(p):
|
|
427
|
-
import sqlite3 as _sq
|
|
428
|
-
con = _sq.connect(str(p))
|
|
429
|
-
con.execute("CREATE TABLE messages (id INTEGER, session_id TEXT, role TEXT,"
|
|
430
|
-
" content TEXT, created_at TEXT)")
|
|
431
|
-
con.execute("INSERT INTO messages VALUES (1,'g1','user','泛型表问题甲','2026-08-26T03:00:00+08:00')")
|
|
432
|
-
con.execute("INSERT INTO messages VALUES (2,'g1','assistant','泛型表回复甲','2026-08-26T03:01:00+08:00')")
|
|
433
|
-
con.execute("INSERT INTO messages VALUES (3,'g2','user','乙的问题','2026-08-27T10:00:00+08:00')")
|
|
434
|
-
con.commit()
|
|
435
|
-
con.close()
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def build_md_fixture(base):
|
|
439
|
-
facts = base / ".yottamemory" / "facts"
|
|
440
|
-
facts.mkdir(parents=True, exist_ok=True)
|
|
441
|
-
(facts / "2026-08-25-0002.md").write_text(
|
|
442
|
-
"---\ntype: FACT\nsubject: 共享记忆引擎接入指南\n"
|
|
443
|
-
"statement: 本机运行 yotta-memory 记忆引擎,接入方式见正文。\n"
|
|
444
|
-
"confidence: 1\ncreated: 2026-08-25\ntags: [memory, guide]\n---\n"
|
|
445
|
-
"正文补充。\n", encoding="utf-8")
|
|
446
|
-
notes = base / ".CodexData" / "memories"
|
|
447
|
-
notes.mkdir(parents=True, exist_ok=True)
|
|
448
|
-
(notes / "note.md").write_text(
|
|
449
|
-
"# 推送闸门红线\n\n规则:测试通过才能推。\n", encoding="utf-8")
|
|
450
|
-
return facts, notes
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def test_norm_time():
|
|
454
|
-
check("毫秒转 ISO", YL._norm_time(1785834903000)
|
|
455
|
-
.startswith("20") and "T" in YL._norm_time(1785834903000),
|
|
456
|
-
YL._norm_time(1785834903000))
|
|
457
|
-
check("秒时间戳", YL._norm_time(1785834903).startswith("20"),
|
|
458
|
-
YL._norm_time(1785834903))
|
|
459
|
-
check("ISO 原样", YL._norm_time("2026-08-26T03:00:00+08:00")
|
|
460
|
-
== "2026-08-26T03:00:00+08:00")
|
|
461
|
-
check("日期原样", YL._norm_time("2026-08-25") == "2026-08-25")
|
|
462
|
-
check("Z 归一", YL._norm_time("2026-08-26T03:00:00Z")
|
|
463
|
-
== "2026-08-26T03:00:00+00:00")
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
def test_json_reader():
|
|
467
|
-
with tempfile.TemporaryDirectory() as td:
|
|
468
|
-
d = build_json_fixture(Path(td))
|
|
469
|
-
src = YL.sniff_source(str(d))
|
|
470
|
-
check("JSON 目录嗅探", src["format"] == "json" and src["kind"] == "session",
|
|
471
|
-
str(src))
|
|
472
|
-
reader = YL.JSONReader()
|
|
473
|
-
rows, tm, inv = reader.iter_sessions(src)
|
|
474
|
-
check("JSON scan 3 会话", len(rows) == 3 and tm == 5, str(rows))
|
|
475
|
-
r = YL.search_all([src], "单文件")
|
|
476
|
-
check("JSON search 命中", len(r["matches"]) == 1
|
|
477
|
-
and r["matches"][0]["session"] == "convo", str(r))
|
|
478
|
-
r2 = YL.search_all([src], "s2")
|
|
479
|
-
check("JSON dict 会话检索", len(r2["matches"]) == 1
|
|
480
|
-
and r2["matches"][0]["session"] == "s2", str(r2))
|
|
481
|
-
ex = YL.extract_all([src], "convo")
|
|
482
|
-
check("JSON extract", len(ex["messages"]) == 2
|
|
483
|
-
and ex["messages"][0]["role"] == "user", str(ex))
|
|
484
|
-
st = YL.stats_all([src])
|
|
485
|
-
check("JSON stats 消息 5", st["messages"] == 5, str(st["messages"]))
|
|
486
|
-
check("JSON stats 角色", st["roles"].get("user") == 3, str(st["roles"]))
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def test_sqlite_opencode_reader():
|
|
490
|
-
with tempfile.TemporaryDirectory() as td:
|
|
491
|
-
p = Path(td) / "opencode.db"
|
|
492
|
-
build_sqlite_opencode(p)
|
|
493
|
-
src = YL.sniff_source(str(p))
|
|
494
|
-
check("SQLite 文件嗅探", src["format"] == "sqlite", str(src))
|
|
495
|
-
reader = YL.SQLiteReader()
|
|
496
|
-
rows, tm, inv = reader.iter_sessions(src)
|
|
497
|
-
check("opencode scan 2 会话", len(rows) == 2, str(rows))
|
|
498
|
-
by = {r["session"]: r for r in rows}
|
|
499
|
-
check("opencode ses_a 消息 2", by["ses_a"]["messages"] == 2,
|
|
500
|
-
str(by["ses_a"]))
|
|
501
|
-
check("opencode 毫秒日期", by["ses_a"]["date"].startswith("20"),
|
|
502
|
-
by["ses_a"]["date"])
|
|
503
|
-
r = YL.search_all([src], "灰度")
|
|
504
|
-
check("opencode search 命中", len(r["matches"]) == 1
|
|
505
|
-
and r["matches"][0]["session"] == "ses_a", str(r))
|
|
506
|
-
r2 = YL.search_all([src], "隐藏推理")
|
|
507
|
-
check("opencode reasoning 不进文本", r2["matches"] == [], str(r2))
|
|
508
|
-
ex = YL.extract_all([src], "ses_a")
|
|
509
|
-
check("opencode extract 消息 2", len(ex["messages"]) == 2, str(ex))
|
|
510
|
-
msg2 = ex["messages"][1]
|
|
511
|
-
check("opencode 工具标注", msg2["tools"] == ["read_file"], str(msg2))
|
|
512
|
-
check("opencode role assistant", msg2["role"] == "assistant", str(msg2))
|
|
513
|
-
st = YL.stats_all([src])
|
|
514
|
-
check("opencode stats 消息 3", st["messages"] == 3, str(st["messages"]))
|
|
515
|
-
check("opencode stats 角色", st["roles"] == {"user": 2, "assistant": 1},
|
|
516
|
-
str(st["roles"]))
|
|
517
|
-
items = YL.tools_all([src])
|
|
518
|
-
check("opencode tools read_file 1", dict(items).get("read_file") == 1,
|
|
519
|
-
str(items))
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def test_sqlite_generic_reader():
|
|
523
|
-
with tempfile.TemporaryDirectory() as td:
|
|
524
|
-
p = Path(td) / "app.db"
|
|
525
|
-
build_sqlite_generic(p)
|
|
526
|
-
src = YL._mk_source("generic-test", "session", "sqlite", p,
|
|
527
|
-
extra={"table": "messages"})
|
|
528
|
-
reader = YL.SQLiteReader()
|
|
529
|
-
rows, tm, inv = reader.iter_sessions(src)
|
|
530
|
-
by = {r["session"]: r for r in rows}
|
|
531
|
-
check("generic scan 2 会话", len(rows) == 2 and tm == 3, str(rows))
|
|
532
|
-
check("generic g1 消息 2", by["g1"]["messages"] == 2, str(by["g1"]))
|
|
533
|
-
r = YL.search_all([src], "甲")
|
|
534
|
-
check("generic search 命中 2", len(r["matches"]) == 2, str(r))
|
|
535
|
-
ex = YL.extract_all([src], "g1")
|
|
536
|
-
check("generic extract 2 条", len(ex["messages"]) == 2, str(ex))
|
|
537
|
-
st = YL.stats_all([src])
|
|
538
|
-
check("generic stats 角色", st["roles"] == {"user": 2, "assistant": 1},
|
|
539
|
-
str(st["roles"]))
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def test_markdown_reader():
|
|
543
|
-
with tempfile.TemporaryDirectory() as td:
|
|
544
|
-
facts, notes = build_md_fixture(Path(td))
|
|
545
|
-
fsrc = YL._mk_source("yottamemory-facts", "memory", "markdown", facts)
|
|
546
|
-
nsrc = YL._mk_source("codex-notes", "note", "markdown", notes)
|
|
547
|
-
fr = list(YL.MarkdownReader().iter_records(fsrc))
|
|
548
|
-
check("md memory 1 条", len(fr) == 1, str(fr))
|
|
549
|
-
rec = fr[0]
|
|
550
|
-
check("md role FACT", rec["role"] == "FACT", str(rec["role"]))
|
|
551
|
-
check("md title subject", rec["meta"].get("title") == "共享记忆引擎接入指南",
|
|
552
|
-
str(rec["meta"]))
|
|
553
|
-
check("md text statement", "yotta-memory" in rec["text"], rec["text"][:50])
|
|
554
|
-
check("md created 时间", rec["time"] == "2026-08-25", rec["time"])
|
|
555
|
-
nr = list(YL.MarkdownReader().iter_records(nsrc))
|
|
556
|
-
check("md note 1 条", len(nr) == 1 and nr[0]["kind"] == "note", str(nr))
|
|
557
|
-
check("md note 标题", nr[0]["meta"].get("title") == "推送闸门红线",
|
|
558
|
-
str(nr[0]["meta"]))
|
|
559
|
-
check("md note 正文", "测试通过" in nr[0]["text"], nr[0]["text"][:40])
|
|
560
|
-
rows, tm, inv = YL.MarkdownReader().iter_sessions(fsrc)
|
|
561
|
-
check("md memory scan", len(rows) == 1 and tm == 1, str(rows))
|
|
562
|
-
# 结构化 md 文件单独 --dir → kind memory
|
|
563
|
-
fp = facts / "2026-08-25-0002.md"
|
|
564
|
-
s = YL.sniff_source(str(fp))
|
|
565
|
-
check("md 文件嗅探 memory", s["format"] == "markdown" and s["kind"] == "memory",
|
|
566
|
-
str(s))
|
|
567
|
-
np = notes / "note.md"
|
|
568
|
-
s2 = YL.sniff_source(str(np))
|
|
569
|
-
check("md 文件嗅探 note", s2["kind"] == "note", str(s2))
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def test_binary_reader():
|
|
573
|
-
with tempfile.TemporaryDirectory() as td:
|
|
574
|
-
p = Path(td) / "conv.pbtxt"
|
|
575
|
-
p.write_bytes(b"\x00\x01Conversation WindSurf Title\x00\x02more")
|
|
576
|
-
src = YL._mk_source("windsurf-conv", "log", "binary", p, default_on=False)
|
|
577
|
-
recs = list(YL.BinaryReader().iter_records(src))
|
|
578
|
-
check("binary 1 条且不崩", len(recs) == 1, str(recs))
|
|
579
|
-
check("binary title 提取", "WindSurf" in recs[0]["text"], recs[0]["text"])
|
|
580
|
-
check("binary kind log", recs[0]["kind"] == "log", str(recs[0]["kind"]))
|
|
581
|
-
rows, tm, inv = YL.BinaryReader().iter_sessions(src)
|
|
582
|
-
check("binary scan", len(rows) == 1 and tm == 1, str(rows))
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def test_sniff_and_discover():
|
|
586
|
-
with tempfile.TemporaryDirectory() as td:
|
|
587
|
-
base = Path(td)
|
|
588
|
-
# discover:JSONL + SQLite(opencode) + 记忆 md + 自由笔记
|
|
589
|
-
jd = base / ".codex" / "sessions"
|
|
590
|
-
jd.mkdir(parents=True, exist_ok=True)
|
|
591
|
-
(jd / "a.jsonl").write_text('{"type":"message","message":{"role":"user","content":"hi"}}\n',
|
|
592
|
-
encoding="utf-8")
|
|
593
|
-
db = base / ".local" / "share" / "opencode" / "opencode.db"
|
|
594
|
-
db.parent.mkdir(parents=True, exist_ok=True)
|
|
595
|
-
build_sqlite_opencode(db)
|
|
596
|
-
facts, notes = build_md_fixture(base)
|
|
597
|
-
jsrcs = YL.JSONLReader.discover(base)
|
|
598
|
-
check("discover JSONL 命中 codex", any(s["name"] == "codex-sessions"
|
|
599
|
-
for s in jsrcs), str(jsrcs))
|
|
600
|
-
ssrcs = YL.SQLiteReader.discover(base)
|
|
601
|
-
check("discover SQLite 命中 opencode", any(s["name"] == "opencode-db"
|
|
602
|
-
for s in ssrcs), str(ssrcs))
|
|
603
|
-
msrcs = YL.MarkdownReader.discover(base)
|
|
604
|
-
names = {s["name"] for s in msrcs}
|
|
605
|
-
check("discover md 命中 yottamemory-facts", "yottamemory-facts" in names,
|
|
606
|
-
str(msrcs))
|
|
607
|
-
check("discover md 命中 codex-notes", "codex-notes" in names, str(msrcs))
|
|
608
|
-
for s in msrcs:
|
|
609
|
-
if s["name"] == "codex-notes":
|
|
610
|
-
check("自由笔记默认关", s["default_on"] is False, str(s))
|
|
611
|
-
if s["name"] == "yottamemory-facts":
|
|
612
|
-
check("结构化记忆默认开", s["default_on"] is True, str(s))
|
|
613
|
-
# 配置兜底
|
|
614
|
-
cfg = {"sources": [{"name": "myapp", "path": str(base / "app.db"),
|
|
615
|
-
"format": "sqlite", "kind": "session",
|
|
616
|
-
"table": "messages", "col_text": "content"}]}
|
|
617
|
-
srcs = YL.discover_sources(cfg)
|
|
618
|
-
check("配置源登记", any(s["name"] == "myapp" for s in srcs), str(srcs))
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
def test_filters_and_scope():
|
|
622
|
-
srcs = [
|
|
623
|
-
YL._mk_source("sess", "session", "jsonl", "/x", True),
|
|
624
|
-
YL._mk_source("mem", "memory", "markdown", "/y", True),
|
|
625
|
-
YL._mk_source("note", "note", "markdown", "/z", False),
|
|
626
|
-
]
|
|
627
|
-
|
|
628
|
-
class A:
|
|
629
|
-
source = None
|
|
630
|
-
kind = None
|
|
631
|
-
format = None
|
|
632
|
-
|
|
633
|
-
a = A()
|
|
634
|
-
out = YL.filter_sources(srcs, a)
|
|
635
|
-
check("默认范围排除 note", [s["name"] for s in out] == ["sess", "mem"],
|
|
636
|
-
str(out))
|
|
637
|
-
a.kind = "note"
|
|
638
|
-
out = YL.filter_sources(srcs, a)
|
|
639
|
-
check("--kind note 显式开", [s["name"] for s in out] == ["note"], str(out))
|
|
640
|
-
a.kind = None
|
|
641
|
-
a.format = "markdown"
|
|
642
|
-
out = YL.filter_sources(srcs, a)
|
|
643
|
-
check("--format markdown 显式含 note", {s["name"] for s in out} == {"mem", "note"},
|
|
644
|
-
str(out))
|
|
645
|
-
a.format = None
|
|
646
|
-
a.source = ["sess"]
|
|
647
|
-
out = YL.filter_sources(srcs, a)
|
|
648
|
-
check("--source 过滤", [s["name"] for s in out] == ["sess"], str(out))
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
def test_cross_source_scope():
|
|
652
|
-
with tempfile.TemporaryDirectory() as td:
|
|
653
|
-
base = Path(td)
|
|
654
|
-
# 用自己的 fixture:jsonl 会话 + 记忆 md + 自由笔记 都含关键词
|
|
655
|
-
jd = base / "sessions"
|
|
656
|
-
jd.mkdir(parents=True, exist_ok=True)
|
|
657
|
-
(jd / "s1.jsonl").write_text(
|
|
658
|
-
'{"type":"message","timestamp":"2026-08-26T03:00:00+08:00","message":{"role":"user","content":"部署方案定了吗"}}\n',
|
|
659
|
-
encoding="utf-8")
|
|
660
|
-
facts, notes = build_md_fixture(base)
|
|
661
|
-
(facts / "m1.md").write_text(
|
|
662
|
-
"---\ntype: FACT\nsubject: 部署\nstatement: 部署方案已拍板。\ncreated: 2026-08-25\n---\n",
|
|
663
|
-
encoding="utf-8")
|
|
664
|
-
(notes / "n1.md").write_text("# 部署笔记\n\n部署草稿。\n", encoding="utf-8")
|
|
665
|
-
jsrc = YL.sniff_source(str(jd))
|
|
666
|
-
fsrc = YL._mk_source("facts", "memory", "markdown", facts)
|
|
667
|
-
nsrc = YL._mk_source("notes", "note", "markdown", notes, default_on=False)
|
|
668
|
-
srcs = [jsrc, fsrc, nsrc]
|
|
669
|
-
# 默认:会话 + 记忆,排除自由笔记
|
|
670
|
-
default = YL.filter_sources(srcs, type("A", (), {"source": None,
|
|
671
|
-
"kind": None,
|
|
672
|
-
"format": None})())
|
|
673
|
-
res = YL.search_all(default, "部署")
|
|
674
|
-
names = {(m["source"], m["session"]) for m in res["matches"]}
|
|
675
|
-
check("默认范围不含自由笔记", ("notes", "n1") not in names, str(names))
|
|
676
|
-
check("默认范围含会话与记忆",
|
|
677
|
-
("sessions", "s1") in names and ("facts", "m1") in names, str(names))
|
|
678
|
-
# 显式开 note
|
|
679
|
-
res2 = YL.search_all([nsrc], "部署")
|
|
680
|
-
check("显式开 note 可检索", ("notes", "n1") in
|
|
681
|
-
{(m["source"], m["session"]) for m in res2["matches"]}, str(res2))
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
def test_cli_v020(fx):
|
|
685
|
-
# --kind / --format / --source 参数存在且 --dir 行为不变
|
|
686
|
-
r = _run(["search", "部署", "--dir", str(fx), "--format", "jsonl"])
|
|
687
|
-
check("CLI --format jsonl", r.returncode == 0 and "部署方案" in r.stdout,
|
|
688
|
-
"rc=%d" % r.returncode)
|
|
689
|
-
r = _run(["search", "部署", "--dir", str(fx), "--kind", "session"])
|
|
690
|
-
check("CLI --kind session", r.returncode == 0, "rc=%d" % r.returncode)
|
|
691
|
-
r = _run(["scan", "--dir", str(fx), "--source", "nope"])
|
|
692
|
-
check("CLI --source 无命中退出码 1", r.returncode == 1,
|
|
693
|
-
"rc=%d" % r.returncode)
|
|
694
|
-
# locate --json 结构
|
|
695
|
-
r = _run(["locate", "--json"])
|
|
696
|
-
try:
|
|
697
|
-
obj = json.loads(r.stdout)
|
|
698
|
-
check("CLI locate --json", r.returncode == 0
|
|
699
|
-
and "sources" in obj and "default_scope" in obj, r.stdout[:120])
|
|
700
|
-
except Exception as e: # noqa: BLE001
|
|
701
|
-
check("CLI locate --json", False, str(e))
|
|
702
|
-
# 单文件 md --dir(自由笔记显式可查)
|
|
703
|
-
with tempfile.TemporaryDirectory() as td:
|
|
704
|
-
np = Path(td) / "note.md"
|
|
705
|
-
np.write_text("# 标题甲\n\n正文含关键词乙。\n", encoding="utf-8")
|
|
706
|
-
r = _run(["search", "关键词乙", "--dir", str(np)])
|
|
707
|
-
check("CLI 单 md 文件检索", r.returncode == 0 and "关键词乙" in r.stdout,
|
|
708
|
-
"rc=%d out=%s" % (r.returncode, r.stdout[:80]))
|
|
709
|
-
r = _run(["scan", "--dir", str(np), "--json"])
|
|
710
|
-
try:
|
|
711
|
-
obj = json.loads(r.stdout)
|
|
712
|
-
check("CLI 单 md scan", r.returncode == 0
|
|
713
|
-
and obj["total_sessions"] == 1, r.stdout[:120])
|
|
714
|
-
except Exception as e: # noqa: BLE001
|
|
715
|
-
check("CLI 单 md scan", False, str(e))
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
def main():
|
|
720
|
-
print("元史(yotta-logs)测试开始…")
|
|
721
|
-
with tempfile.TemporaryDirectory() as td:
|
|
722
|
-
fx = build_fixture(Path(td))
|
|
723
|
-
test_parse_jsonl()
|
|
724
|
-
test_list_sessions()
|
|
725
|
-
test_load_index()
|
|
726
|
-
test_rec_parsing()
|
|
727
|
-
test_redact()
|
|
728
|
-
test_scan(fx)
|
|
729
|
-
test_search(fx)
|
|
730
|
-
test_extract(fx)
|
|
731
|
-
test_stats(fx)
|
|
732
|
-
test_tools(fx)
|
|
733
|
-
test_cli(fx)
|
|
734
|
-
test_gbk_console(fx)
|
|
735
|
-
test_readonly(fx)
|
|
736
|
-
test_norm_time()
|
|
737
|
-
test_json_reader()
|
|
738
|
-
test_sqlite_opencode_reader()
|
|
739
|
-
test_sqlite_generic_reader()
|
|
740
|
-
test_markdown_reader()
|
|
741
|
-
test_binary_reader()
|
|
742
|
-
test_sniff_and_discover()
|
|
743
|
-
test_filters_and_scope()
|
|
744
|
-
test_cross_source_scope()
|
|
745
|
-
test_cli_v020(fx)
|
|
746
|
-
print("")
|
|
747
|
-
print("通过 %d 项,失败 %d 项" % (PASS, FAIL))
|
|
748
|
-
if FAILED:
|
|
749
|
-
print("失败清单:")
|
|
750
|
-
for name in FAILED:
|
|
751
|
-
print(" - " + name)
|
|
752
|
-
sys.exit(1 if FAIL else 0)
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if __name__ == "__main__":
|
|
757
|
-
main()
|