argo-search 2.9.7 → 2.9.9
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/README.en.md +73 -150
- package/README.es.md +73 -150
- package/README.ja.md +79 -156
- package/README.ko.md +78 -155
- package/README.md +80 -159
- package/SKILL.md +3 -3
- package/assets/readme/hero.svg +2 -2
- package/backends/domain_profiles.json +48 -0
- package/backends/engine_registry.yaml +118 -4
- package/backends/quota_profiles.json +92 -12
- package/backends/source_types_cn.json +27 -1
- package/bin/argo +78 -5
- package/bin/argo.js +33 -2
- package/config.yaml +269 -63
- package/docs/ARGO_INTRO.md +5 -3
- package/docs/ENGINE_CATALOG.md +50 -39
- package/docs/MCP_SETUP.md +3 -1
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/docs/RELEASE_NOTES_v2.9.9.md +78 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/firecrawl.yaml +1 -1
- package/engines/specs/fxtwitter.yaml +5 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/stackexchange.yaml +2 -2
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/mcp/clients.yaml +32 -0
- package/package.json +7 -2
- package/packages/dsh-plugin/README.md +25 -6
- package/packages/dsh-plugin/dsh/client.js +402 -47
- package/packages/dsh-plugin/dsh/diagnostics.js +97 -0
- package/packages/dsh-plugin/dsh/index.js +135 -1876
- package/packages/dsh-plugin/dsh/native-tools.mjs +7 -1
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +3 -3
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +177 -2
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +126 -43
- package/scripts/article_meta.py +131 -0
- package/scripts/bench_tail_ab.py +281 -0
- package/scripts/bounded_run.py +91 -49
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/cache.py +60 -72
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/clock_bridge.py +22 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +119 -4
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +105 -0
- package/scripts/engine_add.py +602 -0
- package/scripts/engine_dispatch.py +58 -86
- package/scripts/engine_env.py +5 -0
- package/scripts/engine_families.py +6 -0
- package/scripts/engine_policy.py +7 -7
- package/scripts/engines.py +21 -0
- package/scripts/engines_base.py +3 -3
- package/scripts/engines_builders.py +12 -0
- package/scripts/engines_builders_batch10.py +48 -3
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_tech.py +0 -1
- package/scripts/evidence.py +31 -3
- package/scripts/evidence_loop.py +58 -22
- package/scripts/fetch_quality.py +13 -0
- package/scripts/fetch_v3.py +28 -3
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/http_client.py +24 -38
- package/scripts/install.ps1 +18 -0
- package/scripts/install.sh +16 -1
- package/scripts/job.py +3 -3
- package/scripts/local_seek.py +41 -5
- package/scripts/macro_countries.py +15 -0
- package/scripts/matrix_search_eval.py +192 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_handlers.py +60 -642
- package/scripts/mcp_launch.sh +10 -8
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +19 -14
- package/scripts/mcp_setup.py +33 -9
- package/scripts/mcp_tools.py +1 -0
- package/scripts/mcp_transport.py +1 -1
- package/scripts/mcp_warm.py +127 -0
- package/scripts/net_proxy.py +27 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery.py +7 -3
- package/scripts/recovery_runner.py +150 -0
- package/scripts/replay_eval.py +18 -0
- package/scripts/research.py +37 -21
- package/scripts/research_expand.py +24 -1
- package/scripts/retry_policy.py +76 -0
- package/scripts/route.py +6 -6
- package/scripts/route_cache.py +18 -0
- package/scripts/route_combo.py +26 -5
- package/scripts/route_domains.py +170 -31
- package/scripts/safe_conn.py +52 -0
- package/scripts/search.py +15 -27
- package/scripts/search_attribution.py +100 -3
- package/scripts/search_cli.py +24 -11
- package/scripts/search_entry.py +8 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +62 -195
- package/scripts/search_rank.py +6 -0
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +138 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/tail_completion.py +212 -0
- package/scripts/url_safety.py +126 -3
- package/scripts/usage_log.py +45 -0
- package/scripts/wide_research.py +45 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/local-search/bm25.py +27 -3
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +31 -674
- package/sub-skills/local-seek/scripts/seek_rank.py +7 -3
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/sub-skills/ego-search/references/browser-runtime.md +0 -43
- package/sub-skills/ego-search/references/install.md +0 -67
- package/sub-skills/ego-search/references/original-ego-upgrade.md +0 -51
- package/sub-skills/ego-search/tests/test_query_normalize.py +0 -68
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +0 -27
- package/sub-skills/js-run/tests/test_jsrun.py +0 -165
- package/sub-skills/local-seek/references/auto-behaviors.md +0 -24
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +0 -62
- package/sub-skills/local-seek/references/git-integration.md +0 -54
- package/sub-skills/local-seek/references/remote-handoff.md +0 -46
- package/sub-skills/local-seek/references/rg-recipes.md +0 -96
- package/sub-skills/local-seek/references/strategies.md +0 -52
- package/sub-skills/local-seek/references/structural-search.md +0 -59
- package/sub-skills/local-seek/tests/test_content_rank.py +0 -222
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +0 -145
- package/sub-skills/local-seek/tests/test_noise_tier.py +0 -101
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +0 -136
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +0 -77
- package/sub-skills/local-seek/tests/test_regression_20261003.py +0 -169
- package/sub-skills/local-seek/tests/test_regression_20261006.py +0 -146
- package/sub-skills/local-seek/tests/test_seek_degradation.py +0 -111
- package/sub-skills/local-seek/tests/test_seek_fts.py +0 -313
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
"""seek_fts 单元测试:索引生命周期、CJK 短语、降级契约与结果 parity。
|
|
2
|
-
|
|
3
|
-
契约基线见 seek_fts 模块 docstring;行抽取口径对齐 rg 恒定 -i。
|
|
4
|
-
"""
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
|
|
14
|
-
sys.path.insert(0, str(SCRIPTS))
|
|
15
|
-
|
|
16
|
-
import seek_fts # noqa: E402
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@pytest.fixture()
|
|
20
|
-
def fts_env(tmp_path, monkeypatch):
|
|
21
|
-
"""独立索引库 + 开关打开;返回 (状态目录, 搜索树)。"""
|
|
22
|
-
state = tmp_path / "state"
|
|
23
|
-
tree = tmp_path / "tree"
|
|
24
|
-
state.mkdir()
|
|
25
|
-
tree.mkdir()
|
|
26
|
-
monkeypatch.setenv("ARGO_STATE_DIR", str(state))
|
|
27
|
-
monkeypatch.setenv("ARGO_LOCAL_FTS", "1")
|
|
28
|
-
return state, tree
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _write(tree: Path, rel: str, text: str) -> Path:
|
|
32
|
-
p = tree / rel
|
|
33
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
-
p.write_text(text, encoding="utf-8")
|
|
35
|
-
return p
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _build(tree):
|
|
39
|
-
info = seek_fts.refresh(str(tree))
|
|
40
|
-
assert info["complete"] == 1, f"首建必须完成: {info}"
|
|
41
|
-
return info
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# ── 建库与查询 ────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
def test_build_and_english_query(fts_env):
|
|
47
|
-
_state, tree = fts_env
|
|
48
|
-
_write(tree, "a.py", "def alpha(x):\n return StateRoot(x)\n")
|
|
49
|
-
_write(tree, "b.md", "plain text without the term\n")
|
|
50
|
-
_build(tree)
|
|
51
|
-
rows = seek_fts.content_candidates("stateroot", str(tree))
|
|
52
|
-
assert rows == [(str(tree / "a.py"), 2, "return StateRoot(x)")]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_cjk_two_char_phrase(fts_env):
|
|
56
|
-
"""wechat-digger 实战坑回归:2 字中文必须以逐字短语命中。"""
|
|
57
|
-
_state, tree = fts_env
|
|
58
|
-
_write(tree, "n.md", "前面若干字,迁移完成于此。\n英文行 migration done\n")
|
|
59
|
-
_build(tree)
|
|
60
|
-
rows = seek_fts.content_candidates("迁移", str(tree))
|
|
61
|
-
assert rows == [(str(tree / "n.md"), 1, "前面若干字,迁移完成于此。")]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_case_insensitive_matches_rg(fts_env):
|
|
65
|
-
"""rg 恒定 -i:小写查询必须命中大写原文。"""
|
|
66
|
-
_state, tree = fts_env
|
|
67
|
-
_write(tree, "c.py", "USE UPPER CASE HERE\n")
|
|
68
|
-
_build(tree)
|
|
69
|
-
assert seek_fts.content_candidates("upper case", str(tree)) == [
|
|
70
|
-
(str(tree / "c.py"), 1, "USE UPPER CASE HERE")]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_line_truncation_matches_seek_truncate(fts_env):
|
|
74
|
-
_state, tree = fts_env
|
|
75
|
-
long_line = "x" * 200 + " needle " + "y" * 200
|
|
76
|
-
_write(tree, "l.txt", long_line + "\n")
|
|
77
|
-
_build(tree)
|
|
78
|
-
rows = seek_fts.content_candidates("needle", str(tree))
|
|
79
|
-
assert rows[0][2] == "x" * 119 + "…"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# ── 降级契约 ─────────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
def test_disabled_flag_returns_none(fts_env, monkeypatch):
|
|
85
|
-
_state, tree = fts_env
|
|
86
|
-
_write(tree, "a.py", "needle here\n")
|
|
87
|
-
_build(tree)
|
|
88
|
-
monkeypatch.setenv("ARGO_LOCAL_FTS", "0")
|
|
89
|
-
assert seek_fts.content_candidates("needle", str(tree)) is None
|
|
90
|
-
assert not seek_fts.query_suitable("needle")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_regex_metachars_rejected(fts_env):
|
|
94
|
-
_state, tree = fts_env
|
|
95
|
-
assert not seek_fts.query_suitable("foo.*bar")
|
|
96
|
-
assert not seek_fts.query_suitable("interface{}")
|
|
97
|
-
assert not seek_fts.query_suitable("a|b")
|
|
98
|
-
assert seek_fts.query_suitable("plain_words")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_incomplete_index_falls_back(fts_env):
|
|
102
|
-
"""未建成(complete=0)宁可慢不可漏:返回 None 走 rg。"""
|
|
103
|
-
_state, tree = fts_env
|
|
104
|
-
_write(tree, "a.py", "needle here\n")
|
|
105
|
-
seek_fts.refresh(str(tree), budget_s=0.0) # 0 预算:清单都不走完
|
|
106
|
-
assert seek_fts.content_candidates("needle", str(tree)) is None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def test_zero_hit_on_complete_index_is_truthy_empty(fts_env):
|
|
110
|
-
"""索引完整时零命中是真没有([]),不再回退 rg 空扫。"""
|
|
111
|
-
_state, tree = fts_env
|
|
112
|
-
_write(tree, "a.py", "something\n")
|
|
113
|
-
_build(tree)
|
|
114
|
-
assert seek_fts.content_candidates("zzzznothing", str(tree)) == []
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def test_subdir_query_uses_parent_index(fts_env):
|
|
118
|
-
_state, tree = fts_env
|
|
119
|
-
_write(tree, "sub/deep.py", "needle in subdir\n")
|
|
120
|
-
_write(tree, "top.py", "needle at top\n")
|
|
121
|
-
_build(tree)
|
|
122
|
-
rows = seek_fts.content_candidates("needle", str(tree / "sub"))
|
|
123
|
-
assert rows == [(str(tree / "sub" / "deep.py"), 1, "needle in subdir")]
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# ── 增量生命周期 ─────────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
def test_incremental_add_modify_delete(fts_env):
|
|
129
|
-
_state, tree = fts_env
|
|
130
|
-
f = _write(tree, "a.py", "old content needle\n")
|
|
131
|
-
_write(tree, "gone.py", "needle will vanish\n")
|
|
132
|
-
_build(tree)
|
|
133
|
-
f.write_text("changed entirely\n", encoding="utf-8")
|
|
134
|
-
_write(tree, "new.py", "fresh needle lands\n", )
|
|
135
|
-
(tree / "gone.py").unlink()
|
|
136
|
-
_build(tree) # 再次 refresh 收齐增删改
|
|
137
|
-
assert seek_fts.content_candidates("needle", str(tree)) == [
|
|
138
|
-
(str(tree / "new.py"), 1, "fresh needle lands")]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def test_gitignore_respected_via_rg_listing(fts_env):
|
|
142
|
-
"""覆盖面 parity:git 仓里 .gitignore 的文件不进索引(rg --files 的语义)。
|
|
143
|
-
|
|
144
|
-
非 git 目录 rg 本来就不应用 .gitignore(搜索与清单同一行为,parity 成立),
|
|
145
|
-
所以这里必须建真仓。
|
|
146
|
-
"""
|
|
147
|
-
_state, tree = fts_env
|
|
148
|
-
subprocess.run(["git", "init", "-q", str(tree)], check=True, timeout=30)
|
|
149
|
-
(tree / ".gitignore").write_text("ignored*\n", encoding="utf-8")
|
|
150
|
-
_write(tree, "kept.py", "needle kept\n")
|
|
151
|
-
_write(tree, "ignored.py", "needle hidden by gitignore\n")
|
|
152
|
-
_build(tree)
|
|
153
|
-
assert seek_fts.content_candidates("needle", str(tree)) == [
|
|
154
|
-
(str(tree / "kept.py"), 1, "needle kept")]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def test_binary_files_skipped(fts_env):
|
|
158
|
-
_state, tree = fts_env
|
|
159
|
-
(tree / "bin.dat").write_bytes(b"needle\x00binary")
|
|
160
|
-
_write(tree, "text.py", "needle text\n")
|
|
161
|
-
_build(tree)
|
|
162
|
-
assert seek_fts.content_candidates("needle", str(tree)) == [
|
|
163
|
-
(str(tree / "text.py"), 1, "needle text")]
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def test_large_body_capped(fts_env):
|
|
167
|
-
"""正文截到 _MAX_BODY_BYTES:更深行不在索引(已知偏差),截内行可命中。"""
|
|
168
|
-
_state, tree = fts_env
|
|
169
|
-
head = "needle in head\n"
|
|
170
|
-
body = "x" * seek_fts._MAX_BODY_BYTES + "\n" # 超界大行,挤掉 tail
|
|
171
|
-
tail = "needle in tail\n"
|
|
172
|
-
_write(tree, "big.log", head + body + tail)
|
|
173
|
-
_build(tree)
|
|
174
|
-
rows = seek_fts.content_candidates("needle", str(tree))
|
|
175
|
-
assert [r[2] for r in rows] == ["needle in head"]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def test_oversized_file_skipped_entirely(fts_env, monkeypatch):
|
|
179
|
-
"""超过 _MAX_FILE_BYTES 的文件整体不入索引(2026-10-11:992MB 索引主因
|
|
180
|
-
是 9-33MB 会话转写被全文入库);限额调小便于测试,正文行 rg 兜底。"""
|
|
181
|
-
_state, tree = fts_env
|
|
182
|
-
monkeypatch.setattr(seek_fts, "_MAX_FILE_BYTES", 64)
|
|
183
|
-
_write(tree, "small.py", "needle small\n")
|
|
184
|
-
_write(tree, "rollout.jsonl", "x" * 200 + "\nneedle hidden\n")
|
|
185
|
-
_build(tree)
|
|
186
|
-
assert seek_fts.content_candidates("needle", str(tree)) == [
|
|
187
|
-
(str(tree / "small.py"), 1, "needle small")]
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def test_machine_output_dirs_excluded(fts_env):
|
|
191
|
-
"""rollout/logs/backups 目录不入索引(机器输出非知识文件,rg 主路仍在)。"""
|
|
192
|
-
_state, tree = fts_env
|
|
193
|
-
_write(tree, "keep.md", "needle kept\n")
|
|
194
|
-
_write(tree, "cli/rollout/sess.jsonl", "needle rollout\n")
|
|
195
|
-
_write(tree, "cli/log/app.jsonl", "needle log\n")
|
|
196
|
-
_write(tree, "v2/logs/run.txt", "needle logs\n")
|
|
197
|
-
_write(tree, "backups/dump.sql", "needle backup\n")
|
|
198
|
-
_build(tree)
|
|
199
|
-
assert seek_fts.content_candidates("needle", str(tree)) == [
|
|
200
|
-
(str(tree / "keep.md"), 1, "needle kept")]
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
# ── 一致性与健壮性 ───────────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
def test_determinism_two_runs_same_order(fts_env):
|
|
206
|
-
_state, tree = fts_env
|
|
207
|
-
for i in range(6):
|
|
208
|
-
_write(tree, f"f{i}.py", f"needle {i}\n")
|
|
209
|
-
_build(tree)
|
|
210
|
-
first = seek_fts.content_candidates("needle", str(tree))
|
|
211
|
-
assert first == seek_fts.content_candidates("needle", str(tree))
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def test_corrupt_db_falls_back_to_rg(fts_env):
|
|
215
|
-
_state, tree = fts_env
|
|
216
|
-
_write(tree, "a.py", "needle here\n")
|
|
217
|
-
_build(tree)
|
|
218
|
-
seek_fts.index_db_path().write_bytes(b"not a sqlite file at all")
|
|
219
|
-
assert seek_fts.content_candidates("needle", str(tree)) is None
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def test_mtime_window_filters_files(fts_env):
|
|
223
|
-
import time as _t
|
|
224
|
-
_state, tree = fts_env
|
|
225
|
-
p = _write(tree, "a.py", "needle here\n")
|
|
226
|
-
_build(tree)
|
|
227
|
-
future = _t.time() + 3600
|
|
228
|
-
os.utime(p, (future, future))
|
|
229
|
-
seek_fts.refresh(str(tree)) # 把新 mtime 收进索引
|
|
230
|
-
rows = seek_fts.content_candidates("needle", str(tree),
|
|
231
|
-
until_ts=_t.time())
|
|
232
|
-
assert rows == [] # 文件 mtime 在窗口外 → 不命中
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def test_cli_rebuild_and_status(fts_env):
|
|
236
|
-
_state, tree = fts_env
|
|
237
|
-
_write(tree, "a.py", "needle\n")
|
|
238
|
-
env = dict(os.environ)
|
|
239
|
-
r = subprocess.run(
|
|
240
|
-
[sys.executable, str(SCRIPTS / "seek_fts.py"), "rebuild", str(tree)],
|
|
241
|
-
capture_output=True, text=True, env=env, timeout=60)
|
|
242
|
-
assert r.returncode == 0, r.stderr
|
|
243
|
-
assert json.loads(r.stdout)["complete"] == 1
|
|
244
|
-
r = subprocess.run(
|
|
245
|
-
[sys.executable, str(SCRIPTS / "seek_fts.py"), "status", str(tree)],
|
|
246
|
-
capture_output=True, text=True, env=env, timeout=60)
|
|
247
|
-
assert json.loads(r.stdout)["indexed"] == 1
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def test_seek_integration_fts_and_rg_parity(fts_env):
|
|
251
|
-
"""端到端:同一条查询,FTS 开与关(rg 主路)结果集一致。"""
|
|
252
|
-
_state, tree = fts_env
|
|
253
|
-
_write(tree, "x.py", "alpha does stateroot things\nsecond line\n")
|
|
254
|
-
_write(tree, "y.md", "另一段:状态根相关\nstateroot mentioned\n")
|
|
255
|
-
_build(tree)
|
|
256
|
-
env = dict(os.environ)
|
|
257
|
-
cmd = [sys.executable, str(SCRIPTS / "seek.py"), "stateroot",
|
|
258
|
-
"--path", str(tree), "--json", "--include-noise"]
|
|
259
|
-
on = json.loads(subprocess.run(cmd, capture_output=True, text=True,
|
|
260
|
-
env={**env, "ARGO_LOCAL_FTS": "1"},
|
|
261
|
-
timeout=60).stdout)["results"]
|
|
262
|
-
off = json.loads(subprocess.run(cmd, capture_output=True, text=True,
|
|
263
|
-
env={**env, "ARGO_LOCAL_FTS": "0"},
|
|
264
|
-
timeout=60).stdout)["results"]
|
|
265
|
-
assert [(r["path"], r["line"]) for r in on] == \
|
|
266
|
-
[(r["path"], r["line"]) for r in off]
|
|
267
|
-
assert on, "两条路都必须命中"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def test_type_filter_bypasses_fts_fastpath(fts_env):
|
|
271
|
-
"""带 --type 时不得走 FTS 快路(快路不做扩展名过滤,会静默混入范围外结果)。
|
|
272
|
-
|
|
273
|
-
回归(2026-10-12):此前 --type py 在 ARGO_LOCAL_FTS=1 下仍命中 .md 文件。
|
|
274
|
-
"""
|
|
275
|
-
_state, tree = fts_env
|
|
276
|
-
_write(tree, "hit.py", "stateroot in python\n")
|
|
277
|
-
_write(tree, "noise.md", "stateroot in markdown\n")
|
|
278
|
-
_build(tree)
|
|
279
|
-
env = dict(os.environ)
|
|
280
|
-
cmd = [sys.executable, str(SCRIPTS / "seek.py"), "stateroot",
|
|
281
|
-
"--path", str(tree), "--type", "py", "--json", "--include-noise"]
|
|
282
|
-
r = subprocess.run(cmd, capture_output=True, text=True,
|
|
283
|
-
env={**env, "ARGO_LOCAL_FTS": "1"}, timeout=60)
|
|
284
|
-
results = json.loads(r.stdout)["results"]
|
|
285
|
-
assert results, "py 文件必须命中"
|
|
286
|
-
assert all(x["path"].endswith(".py") for x in results), \
|
|
287
|
-
f"--type py 下混入了非 py 结果: {[x['path'] for x in results]}"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def test_exclude_filter_bypasses_fts_fastpath(fts_env):
|
|
291
|
-
"""带 --exclude 时不得走 FTS 快路(快路不做排除,会绕掉自污染守卫)。"""
|
|
292
|
-
_state, tree = fts_env
|
|
293
|
-
_write(tree, "keep.py", "stateroot keep\n")
|
|
294
|
-
_write(tree, "skip.py", "stateroot skip\n")
|
|
295
|
-
_build(tree)
|
|
296
|
-
env = dict(os.environ)
|
|
297
|
-
cmd = [sys.executable, str(SCRIPTS / "seek.py"), "stateroot",
|
|
298
|
-
"--path", str(tree), "--exclude", "skip.py", "--json",
|
|
299
|
-
"--include-noise"]
|
|
300
|
-
r = subprocess.run(cmd, capture_output=True, text=True,
|
|
301
|
-
env={**env, "ARGO_LOCAL_FTS": "1"}, timeout=60)
|
|
302
|
-
results = json.loads(r.stdout)["results"]
|
|
303
|
-
assert results and all(not x["path"].endswith("skip.py") for x in results)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def test_like_prefix_dir_boundary(tmp_path):
|
|
307
|
-
"""_like_prefix 目录语义:/a/b 不得匹配 /a/bc(兄弟树误配回归)。"""
|
|
308
|
-
(tmp_path / "a" / "b").mkdir(parents=True)
|
|
309
|
-
(tmp_path / "a" / "bc").mkdir(parents=True)
|
|
310
|
-
pat = seek_fts._like_prefix(str(tmp_path / "a" / "b"))
|
|
311
|
-
assert pat.endswith(os.sep + "%")
|
|
312
|
-
# 根路径特判://% 永不匹配(静默假空回归)
|
|
313
|
-
assert seek_fts._like_prefix("/") == "/%"
|