chore: snapshot in-progress question-gen work before preflight fixes
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"""core/agent/protocols.py 单元测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
@@ -18,9 +19,11 @@ class _FakeDispatcher:
|
||||
def test_fake_dispatcher_satisfies_protocol() -> None:
|
||||
assert isinstance(_FakeDispatcher(), ToolDispatcher)
|
||||
|
||||
|
||||
def test_plain_object_not_dispatcher() -> None:
|
||||
assert not isinstance(object(), ToolDispatcher)
|
||||
|
||||
|
||||
def test_hookspec_can_register() -> None:
|
||||
pm = pluggy.PluginManager("agent_loop")
|
||||
pm.add_hookspecs(AgentLoopSpec)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""core/agent/types.py 单元测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.agent.types import LoopResult, Step
|
||||
@@ -31,12 +32,18 @@ class TestLoopResult:
|
||||
|
||||
def test_with_steps(self) -> None:
|
||||
step = Step(
|
||||
thought="t", reflect={}, plan={},
|
||||
thought="t",
|
||||
reflect={},
|
||||
plan={},
|
||||
tool_call={"tool": "t", "args": {}},
|
||||
tool_output="o", raw_content="r", call_id="c",
|
||||
tool_output="o",
|
||||
raw_content="r",
|
||||
call_id="c",
|
||||
)
|
||||
lr = LoopResult(
|
||||
result={"answer": "42"}, steps=[step], steps_used=1,
|
||||
result={"answer": "42"},
|
||||
steps=[step],
|
||||
steps_used=1,
|
||||
token_usage={"prompt_tokens": 100, "completion_tokens": 50},
|
||||
stop_reason="finished",
|
||||
)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""core/protocols.py 单元测试 — 验证 Protocol 可 runtime_checkable。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from core.protocols import LLMProvider, TelemetryRecorder, VLMProvider
|
||||
from core.types import LLMResponse
|
||||
|
||||
@@ -19,9 +18,17 @@ class _FakeLLM:
|
||||
parent_call_id: str | None = None,
|
||||
) -> LLMResponse:
|
||||
return LLMResponse(
|
||||
content="ok", thinking="", model="m", provider="p",
|
||||
prompt_tokens=1, completion_tokens=1, latency_ms=1,
|
||||
ttft_ms=None, max_inter_token_ms=None, cache_hit=False, call_id="c",
|
||||
content="ok",
|
||||
thinking="",
|
||||
model="m",
|
||||
provider="p",
|
||||
prompt_tokens=1,
|
||||
completion_tokens=1,
|
||||
latency_ms=1,
|
||||
ttft_ms=None,
|
||||
max_inter_token_ms=None,
|
||||
cache_hit=False,
|
||||
call_id="c",
|
||||
)
|
||||
|
||||
|
||||
@@ -35,19 +42,39 @@ class _FakeVLM:
|
||||
parent_call_id: str | None = None,
|
||||
) -> LLMResponse:
|
||||
return LLMResponse(
|
||||
content="ok", thinking="", model="m", provider="p",
|
||||
prompt_tokens=1, completion_tokens=1, latency_ms=1,
|
||||
ttft_ms=None, max_inter_token_ms=None, cache_hit=False, call_id="c",
|
||||
content="ok",
|
||||
thinking="",
|
||||
model="m",
|
||||
provider="p",
|
||||
prompt_tokens=1,
|
||||
completion_tokens=1,
|
||||
latency_ms=1,
|
||||
ttft_ms=None,
|
||||
max_inter_token_ms=None,
|
||||
cache_hit=False,
|
||||
call_id="c",
|
||||
)
|
||||
|
||||
|
||||
class _FakeTelemetry:
|
||||
async def record_llm_call(
|
||||
self, *, call_id: str, parent_call_id: str | None, session_id: str | None,
|
||||
model_name: str, provider: str, messages: str, response: str, thinking: str,
|
||||
prompt_tokens: int, completion_tokens: int, latency_ms: int,
|
||||
ttft_ms: float | None, max_inter_token_ms: float | None,
|
||||
cache_hit: bool, error: str | None,
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
parent_call_id: str | None,
|
||||
session_id: str | None,
|
||||
model_name: str,
|
||||
provider: str,
|
||||
messages: str,
|
||||
response: str,
|
||||
thinking: str,
|
||||
prompt_tokens: int,
|
||||
completion_tokens: int,
|
||||
latency_ms: int,
|
||||
ttft_ms: float | None,
|
||||
max_inter_token_ms: float | None,
|
||||
cache_hit: bool,
|
||||
error: str | None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@@ -55,12 +82,15 @@ class _FakeTelemetry:
|
||||
def test_fake_llm_satisfies_protocol() -> None:
|
||||
assert isinstance(_FakeLLM(), LLMProvider)
|
||||
|
||||
|
||||
def test_fake_vlm_satisfies_protocol() -> None:
|
||||
assert isinstance(_FakeVLM(), VLMProvider)
|
||||
|
||||
|
||||
def test_fake_telemetry_satisfies_protocol() -> None:
|
||||
assert isinstance(_FakeTelemetry(), TelemetryRecorder)
|
||||
|
||||
|
||||
def test_plain_object_does_not_satisfy() -> None:
|
||||
assert not isinstance(object(), LLMProvider)
|
||||
assert not isinstance(object(), VLMProvider)
|
||||
@@ -80,8 +110,11 @@ class TestPoolStrategyProtocol:
|
||||
class FakeStrategy:
|
||||
def build(self, questions, correctness, config):
|
||||
return Pools(
|
||||
diagnosis=[], validation=[], test=[],
|
||||
baseline_run_id="", baseline_val_accuracy=0.0,
|
||||
diagnosis=[],
|
||||
validation=[],
|
||||
test=[],
|
||||
baseline_run_id="",
|
||||
baseline_val_accuracy=0.0,
|
||||
)
|
||||
|
||||
def build_incremental(self, new_task_types, questions, correctness, config):
|
||||
|
||||
@@ -14,21 +14,21 @@ from app.question_gen.families import (
|
||||
get_family_for_slot,
|
||||
)
|
||||
|
||||
# 12 种任务类型(来自 harness config)
|
||||
# Video-MME 12 种任务类型
|
||||
ALL_TASK_TYPES: frozenset[str] = frozenset(
|
||||
[
|
||||
"Action Recognition",
|
||||
"Action Reasoning",
|
||||
"Action Prediction",
|
||||
"Action Sequence",
|
||||
"Attribute Perception",
|
||||
"Counting Problem",
|
||||
"Information Synopsis",
|
||||
"Object Recognition",
|
||||
"Object Reasoning",
|
||||
"Object Interaction",
|
||||
"Scene Understanding",
|
||||
"Event Reasoning",
|
||||
"Causal Reasoning",
|
||||
"Temporal Reasoning",
|
||||
"OCR Problems",
|
||||
"Spatial Perception",
|
||||
"Spatial Reasoning",
|
||||
"Temporal Perception",
|
||||
"Temporal Reasoning",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -163,7 +163,7 @@ class TestGetFamilyForSlot:
|
||||
def test_no_legal_family_raises(self) -> None:
|
||||
"""所有 family 权重为 0 时合法族为空,应抛出 ValueError。"""
|
||||
rng = random.Random(0)
|
||||
# Spatial Reasoning 合法族: RETRIEVAL, REASONING, VISUAL, SPATIAL
|
||||
# Spatial Reasoning 合法族: SPATIAL
|
||||
# 如果 ratios 中只含不合法的族名,应抛错
|
||||
with pytest.raises(ValueError, match="合法"):
|
||||
get_family_for_slot(
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
的核心路径与边界条件。
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from core.evolution.gate import compute_e_value, gate_decision, probation_verdict
|
||||
from core.evolution.types import GateParams, GateVerdict
|
||||
from core.evolution.types import GateParams
|
||||
|
||||
_PARAMS = GateParams(
|
||||
e_confirm=20.0,
|
||||
|
||||
@@ -11,7 +11,6 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# 确保项目根目录在 sys.path 中
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
@@ -112,9 +112,7 @@ class TestHarnessLog:
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(
|
||||
"SELECT status FROM _runs WHERE run_id = ?", (run_id,)
|
||||
).fetchone()
|
||||
row = conn.execute("SELECT status FROM _runs WHERE run_id = ?", (run_id,)).fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row["status"] == "failed"
|
||||
@@ -130,9 +128,7 @@ class TestHarnessLog:
|
||||
log.insert("t", {"x": 42})
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
count = conn.execute(
|
||||
"SELECT COUNT(*) FROM _runs WHERE run_id = ?", (run_id,)
|
||||
).fetchone()[0]
|
||||
count = conn.execute("SELECT COUNT(*) FROM _runs WHERE run_id = ?", (run_id,)).fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
assert count == 1, "ON CONFLICT DO UPDATE 应保证 _runs 只有一行"
|
||||
@@ -151,9 +147,7 @@ class TestHarnessLog:
|
||||
with HarnessLog(db_path, run_id) as log:
|
||||
log.create_table("batch", {"epoch": "INTEGER", "loss": "REAL"})
|
||||
log.insert_many("batch", records)
|
||||
rows = log.query(
|
||||
"SELECT * FROM batch WHERE run_id = ? ORDER BY epoch", (run_id,)
|
||||
)
|
||||
rows = log.query("SELECT * FROM batch WHERE run_id = ? ORDER BY epoch", (run_id,))
|
||||
|
||||
assert len(rows) == 5
|
||||
assert [r["epoch"] for r in rows] == [0, 1, 2, 3, 4]
|
||||
@@ -163,9 +157,7 @@ class TestHarnessLog:
|
||||
with HarnessLog(db_path, run_id) as log:
|
||||
log.log_event("train_start", {"epoch": 1, "lr": 0.001})
|
||||
log.log_event("train_end", {"epoch": 1, "loss": 0.42})
|
||||
rows = log.query(
|
||||
"SELECT * FROM _events WHERE run_id = ? ORDER BY id", (run_id,)
|
||||
)
|
||||
rows = log.query("SELECT * FROM _events WHERE run_id = ? ORDER BY id", (run_id,))
|
||||
|
||||
assert len(rows) == 2
|
||||
assert rows[0]["event_type"] == "train_start"
|
||||
|
||||
@@ -9,7 +9,6 @@ import pytest
|
||||
|
||||
from app.harness.store import (
|
||||
_parse_version,
|
||||
_write_meta,
|
||||
advance_version,
|
||||
extract_run_db,
|
||||
init_seed,
|
||||
@@ -21,7 +20,6 @@ from app.harness.store import (
|
||||
read_seed,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_version
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -52,7 +50,7 @@ class TestParseVersion:
|
||||
class TestListVersions:
|
||||
"""list_versions 按数字排序,v10 排在 v2 后。"""
|
||||
|
||||
def test_list_versions_numeric_sort(self, tmp_path: "Path") -> None:
|
||||
def test_list_versions_numeric_sort(self, tmp_path: Path) -> None:
|
||||
"""v10 必须排在 v2 后面(非字典序)。"""
|
||||
store = tmp_path / "store"
|
||||
resource = store / "skills"
|
||||
@@ -62,11 +60,11 @@ class TestListVersions:
|
||||
result = list_versions(store, "skills")
|
||||
assert result == ["v1", "v2", "v3", "v10", "v20"]
|
||||
|
||||
def test_list_versions_empty(self, tmp_path: "Path") -> None:
|
||||
def test_list_versions_empty(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
assert list_versions(store, "skills") == []
|
||||
|
||||
def test_list_versions_ignores_non_version_dirs(self, tmp_path: "Path") -> None:
|
||||
def test_list_versions_ignores_non_version_dirs(self, tmp_path: Path) -> None:
|
||||
"""非 v\\d+ 格式的目录被忽略。"""
|
||||
store = tmp_path / "store"
|
||||
resource = store / "skills"
|
||||
@@ -85,11 +83,11 @@ class TestListVersions:
|
||||
class TestNextVersion:
|
||||
"""next_version 返回下一个可用版本号。"""
|
||||
|
||||
def test_next_version_empty(self, tmp_path: "Path") -> None:
|
||||
def test_next_version_empty(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
assert next_version(store, "skills") == "v1"
|
||||
|
||||
def test_next_version_after_existing(self, tmp_path: "Path") -> None:
|
||||
def test_next_version_after_existing(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
resource = store / "skills"
|
||||
resource.mkdir(parents=True)
|
||||
@@ -97,7 +95,7 @@ class TestNextVersion:
|
||||
(resource / "v2").mkdir()
|
||||
assert next_version(store, "skills") == "v3"
|
||||
|
||||
def test_next_version_with_gap(self, tmp_path: "Path") -> None:
|
||||
def test_next_version_with_gap(self, tmp_path: Path) -> None:
|
||||
"""v1 和 v10 之间有 gap,next 应为 v11。"""
|
||||
store = tmp_path / "store"
|
||||
resource = store / "skills"
|
||||
@@ -115,7 +113,7 @@ class TestNextVersion:
|
||||
class TestAdvanceVersion:
|
||||
"""advance_version copytree + _write_meta。"""
|
||||
|
||||
def test_advance_version(self, tmp_path: "Path") -> None:
|
||||
def test_advance_version(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
resource = store / "skills"
|
||||
resource.mkdir(parents=True)
|
||||
@@ -149,7 +147,7 @@ class TestAdvanceVersion:
|
||||
class TestInitStore:
|
||||
"""init_store 初始化 Store 目录结构。"""
|
||||
|
||||
def test_init_store(self, tmp_path: "Path") -> None:
|
||||
def test_init_store(self, tmp_path: Path) -> None:
|
||||
videos = tmp_path / "videos_src"
|
||||
videos.mkdir()
|
||||
(videos / "v001").mkdir()
|
||||
@@ -172,13 +170,11 @@ class TestInitStore:
|
||||
assert (store / "skills" / "v1" / "search.md").read_text() == "skill"
|
||||
assert (store / "prompts" / "v1" / "system.md").read_text() == "prompt"
|
||||
|
||||
skills_meta = json.loads(
|
||||
(store / "skills" / "v1" / "meta.json").read_text()
|
||||
)
|
||||
skills_meta = json.loads((store / "skills" / "v1" / "meta.json").read_text())
|
||||
assert skills_meta["version"] == "v1"
|
||||
assert skills_meta["source"] == "manual"
|
||||
|
||||
def test_init_store_exists_raises(self, tmp_path: "Path") -> None:
|
||||
def test_init_store_exists_raises(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
store.mkdir()
|
||||
with pytest.raises(FileExistsError, match="Store 已存在"):
|
||||
@@ -205,13 +201,9 @@ def _make_seed_fixtures(tmp_path):
|
||||
|
||||
baseline_db = tmp_path / "base.db"
|
||||
conn = sqlite3.connect(baseline_db)
|
||||
conn.execute(
|
||||
"CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)"
|
||||
)
|
||||
conn.execute("CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)")
|
||||
conn.execute("INSERT INTO _runs VALUES ('r1', 'done')")
|
||||
conn.execute(
|
||||
"CREATE TABLE predictions (run_id TEXT, question_id TEXT, answer TEXT)"
|
||||
)
|
||||
conn.execute("CREATE TABLE predictions (run_id TEXT, question_id TEXT, answer TEXT)")
|
||||
conn.execute("INSERT INTO predictions VALUES ('r1', 'q1', 'A')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -222,7 +214,7 @@ def _make_seed_fixtures(tmp_path):
|
||||
class TestInitSeed:
|
||||
"""init_seed 创建种子目录。"""
|
||||
|
||||
def test_init_seed(self, tmp_path: "Path") -> None:
|
||||
def test_init_seed(self, tmp_path: Path) -> None:
|
||||
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
|
||||
seed_dir = init_seed(
|
||||
store,
|
||||
@@ -245,25 +237,23 @@ class TestInitSeed:
|
||||
assert meta["description"] == "初始种子"
|
||||
assert "created_at" in meta
|
||||
|
||||
def test_init_seed_exists_raises(self, tmp_path: "Path") -> None:
|
||||
def test_init_seed_exists_raises(self, tmp_path: Path) -> None:
|
||||
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
|
||||
init_seed(store, "dup", skills_dir, prompts_dir, baseline_db, "r1", None, "first")
|
||||
with pytest.raises(FileExistsError, match="种子已存在"):
|
||||
init_seed(
|
||||
store, "dup", skills_dir, prompts_dir, baseline_db, "r1", None, "second"
|
||||
)
|
||||
init_seed(store, "dup", skills_dir, prompts_dir, baseline_db, "r1", None, "second")
|
||||
|
||||
|
||||
class TestListSeeds:
|
||||
"""list_seeds 列出所有种子。"""
|
||||
|
||||
def test_list_seeds(self, tmp_path: "Path") -> None:
|
||||
def test_list_seeds(self, tmp_path: Path) -> None:
|
||||
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
|
||||
init_seed(store, "beta", skills_dir, prompts_dir, baseline_db, "r1", None, "b")
|
||||
init_seed(store, "alpha", skills_dir, prompts_dir, baseline_db, "r1", None, "a")
|
||||
assert list_seeds(store) == ["alpha", "beta"]
|
||||
|
||||
def test_list_seeds_empty(self, tmp_path: "Path") -> None:
|
||||
def test_list_seeds_empty(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
assert list_seeds(store) == []
|
||||
|
||||
@@ -271,14 +261,14 @@ class TestListSeeds:
|
||||
class TestReadSeed:
|
||||
"""read_seed 读取 seed.json。"""
|
||||
|
||||
def test_read_seed(self, tmp_path: "Path") -> None:
|
||||
def test_read_seed(self, tmp_path: Path) -> None:
|
||||
store, skills_dir, prompts_dir, baseline_db = _make_seed_fixtures(tmp_path)
|
||||
init_seed(store, "s1", skills_dir, prompts_dir, baseline_db, "r1", None, "desc")
|
||||
meta = read_seed(store, "s1")
|
||||
assert meta["baseline_run_id"] == "r1"
|
||||
assert meta["description"] == "desc"
|
||||
|
||||
def test_read_seed_not_found(self, tmp_path: "Path") -> None:
|
||||
def test_read_seed_not_found(self, tmp_path: Path) -> None:
|
||||
store = tmp_path / "store"
|
||||
store.mkdir()
|
||||
with pytest.raises(FileNotFoundError, match="种子不存在"):
|
||||
@@ -296,22 +286,17 @@ class TestExtractRunDb:
|
||||
def _make_src_db(self, path):
|
||||
"""创建带 _runs + predictions 表的源 db。"""
|
||||
conn = sqlite3.connect(path)
|
||||
conn.execute(
|
||||
"CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)"
|
||||
)
|
||||
conn.execute("CREATE TABLE _runs (run_id TEXT PRIMARY KEY, status TEXT)")
|
||||
conn.execute("INSERT INTO _runs VALUES ('r1', 'done')")
|
||||
conn.execute("INSERT INTO _runs VALUES ('r2', 'done')")
|
||||
conn.execute(
|
||||
"CREATE TABLE predictions "
|
||||
"(run_id TEXT, question_id TEXT, answer TEXT)"
|
||||
)
|
||||
conn.execute("CREATE TABLE predictions (run_id TEXT, question_id TEXT, answer TEXT)")
|
||||
conn.execute("INSERT INTO predictions VALUES ('r1', 'q1', 'A')")
|
||||
conn.execute("INSERT INTO predictions VALUES ('r1', 'q2', 'B')")
|
||||
conn.execute("INSERT INTO predictions VALUES ('r2', 'q1', 'C')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def test_extract_run_db_preserves_pk(self, tmp_path: "Path") -> None:
|
||||
def test_extract_run_db_preserves_pk(self, tmp_path: Path) -> None:
|
||||
"""原始 CREATE 保留主键约束。"""
|
||||
src = tmp_path / "src.db"
|
||||
dst = tmp_path / "dst.db"
|
||||
@@ -334,7 +319,7 @@ class TestExtractRunDb:
|
||||
assert len(preds) == 2
|
||||
conn.close()
|
||||
|
||||
def test_extract_run_db_missing_table(self, tmp_path: "Path") -> None:
|
||||
def test_extract_run_db_missing_table(self, tmp_path: Path) -> None:
|
||||
"""源 db 无目标表时报错。"""
|
||||
src = tmp_path / "src.db"
|
||||
dst = tmp_path / "dst.db"
|
||||
@@ -345,7 +330,7 @@ class TestExtractRunDb:
|
||||
with pytest.raises(RuntimeError, match="源 db 无表"):
|
||||
extract_run_db(src, dst, "r1")
|
||||
|
||||
def test_extract_run_db_no_rows(self, tmp_path: "Path") -> None:
|
||||
def test_extract_run_db_no_rows(self, tmp_path: Path) -> None:
|
||||
"""目标 run_id 不存在时报错。"""
|
||||
src = tmp_path / "src.db"
|
||||
dst = tmp_path / "dst.db"
|
||||
@@ -382,9 +367,7 @@ def _make_promote_fixtures(tmp_path):
|
||||
prompts_version TEXT
|
||||
)
|
||||
""")
|
||||
conn.execute(
|
||||
"INSERT INTO _runs VALUES ('eval_001', 'v2', 'v2')"
|
||||
)
|
||||
conn.execute("INSERT INTO _runs VALUES ('eval_001', 'v2', 'v2')")
|
||||
conn.execute("""
|
||||
CREATE TABLE predictions (
|
||||
run_id TEXT, question_id TEXT, answer TEXT
|
||||
@@ -400,7 +383,7 @@ def _make_promote_fixtures(tmp_path):
|
||||
class TestPromoteToSeed:
|
||||
"""promote_to_seed 固化 workspace 版本为种子。"""
|
||||
|
||||
def test_promote_to_seed_success(self, tmp_path: "Path") -> None:
|
||||
def test_promote_to_seed_success(self, tmp_path: Path) -> None:
|
||||
ws, store = _make_promote_fixtures(tmp_path)
|
||||
seed_dir = promote_to_seed(ws, store, "v2", "eval_001", "evolved-seed", "good")
|
||||
assert seed_dir == store / "seeds" / "evolved-seed"
|
||||
@@ -411,13 +394,13 @@ class TestPromoteToSeed:
|
||||
assert meta["baseline_run_id"] == "eval_001"
|
||||
assert meta["parent"] == "ws:v2"
|
||||
|
||||
def test_promote_to_seed_version_mismatch(self, tmp_path: "Path") -> None:
|
||||
def test_promote_to_seed_version_mismatch(self, tmp_path: Path) -> None:
|
||||
"""eval run 的 skills_version 与 --version 不符时报错。"""
|
||||
ws, store = _make_promote_fixtures(tmp_path)
|
||||
with pytest.raises(ValueError, match="版本.*不符"):
|
||||
promote_to_seed(ws, store, "v3", "eval_001", "bad", "mismatch")
|
||||
|
||||
def test_promote_to_seed_null_version(self, tmp_path: "Path") -> None:
|
||||
def test_promote_to_seed_null_version(self, tmp_path: Path) -> None:
|
||||
"""eval run 的版本为 NULL 时报错。"""
|
||||
ws = tmp_path / "ws2"
|
||||
ws.mkdir()
|
||||
@@ -438,20 +421,20 @@ class TestPromoteToSeed:
|
||||
with pytest.raises(ValueError, match="NULL"):
|
||||
promote_to_seed(ws, store, "v1", "eval_null", "bad", "null ver")
|
||||
|
||||
def test_promote_to_seed_run_not_found(self, tmp_path: "Path") -> None:
|
||||
def test_promote_to_seed_run_not_found(self, tmp_path: Path) -> None:
|
||||
"""eval run 不存在时报错。"""
|
||||
ws, store = _make_promote_fixtures(tmp_path)
|
||||
with pytest.raises(ValueError, match="eval run 不存在"):
|
||||
promote_to_seed(ws, store, "v1", "nonexistent", "bad", "no run")
|
||||
|
||||
def test_promote_cleanup_tmp_db(self, tmp_path: "Path") -> None:
|
||||
def test_promote_cleanup_tmp_db(self, tmp_path: Path) -> None:
|
||||
"""finally 清理临时 db 文件。"""
|
||||
ws, store = _make_promote_fixtures(tmp_path)
|
||||
promote_to_seed(ws, store, "v2", "eval_001", "clean-test", "cleanup")
|
||||
# 临时 db 应已清理
|
||||
assert not (ws / "_promote_tmp.db").exists()
|
||||
|
||||
def test_promote_cleanup_tmp_db_on_error(self, tmp_path: "Path") -> None:
|
||||
def test_promote_cleanup_tmp_db_on_error(self, tmp_path: Path) -> None:
|
||||
"""即使 init_seed 失败(同名种子),临时 db 也应被清理。"""
|
||||
ws, store = _make_promote_fixtures(tmp_path)
|
||||
promote_to_seed(ws, store, "v2", "eval_001", "first", "first time")
|
||||
|
||||
@@ -259,9 +259,7 @@ class TestApplyPatch:
|
||||
def test_insert_after_protected_skip(self) -> None:
|
||||
content = "# Title\n\nFROZEN BLOCK\n\nrest"
|
||||
edits = [{"op": "insert_after", "target": "FROZEN BLOCK", "content": "nope"}]
|
||||
out, reports = apply_patch_with_report(
|
||||
content, edits, protected_spans=["FROZEN BLOCK"]
|
||||
)
|
||||
out, reports = apply_patch_with_report(content, edits, protected_spans=["FROZEN BLOCK"])
|
||||
assert out == content
|
||||
assert reports[0]["status"] == "skipped_protected"
|
||||
|
||||
@@ -276,9 +274,7 @@ class TestApplyPatch:
|
||||
def test_replace_protected_skip(self) -> None:
|
||||
content = "# Title\n\nprotected\n\nrest"
|
||||
edits = [{"op": "replace", "target": "protected", "content": "nope"}]
|
||||
out, reports = apply_patch_with_report(
|
||||
content, edits, protected_spans=["protected"]
|
||||
)
|
||||
out, reports = apply_patch_with_report(content, edits, protected_spans=["protected"])
|
||||
assert "protected" in out
|
||||
assert reports[0]["status"] == "skipped_protected"
|
||||
|
||||
@@ -335,9 +331,7 @@ class TestApplyPatch:
|
||||
{"op": "append", "target": "", "content": "prefix text"},
|
||||
{"op": "replace", "target": protected, "content": "nope"},
|
||||
]
|
||||
out, reports = apply_patch_with_report(
|
||||
content, edits, protected_spans=[protected]
|
||||
)
|
||||
out, reports = apply_patch_with_report(content, edits, protected_spans=[protected])
|
||||
# append 在 FREEZE 之前插入,坐标右移后 replace 仍能检测冻结区
|
||||
assert reports[1]["status"] == "skipped_protected"
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ async def test_cache_miss_returns_none(fake_redis: object) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_roundtrip(
|
||||
fake_redis: object, sample_response: LLMResponse
|
||||
) -> None:
|
||||
async def test_cache_roundtrip(fake_redis: object, sample_response: LLMResponse) -> None:
|
||||
"""set 后 get 应返回相同内容。"""
|
||||
cache = RedisResponseCache(redis=fake_redis, ttl_s=300)
|
||||
await cache.set("gpt-4o", MESSAGES, sample_response)
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.tree.index import (
|
||||
L3Node,
|
||||
TreeIndex,
|
||||
)
|
||||
from app.tree.repair.detector import NodeIssue, detect_issues
|
||||
from app.tree.repair.detector import detect_issues
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""修复管线断点续跑 progress 管理测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -10,6 +11,7 @@ import pytest
|
||||
def test_load_progress_missing_file(tmp_path):
|
||||
"""progress 文件不存在时返回空集合。"""
|
||||
from tools.repair_trees import load_progress
|
||||
|
||||
result = load_progress(tmp_path / "nonexistent.json")
|
||||
assert result == set()
|
||||
|
||||
@@ -17,6 +19,7 @@ def test_load_progress_missing_file(tmp_path):
|
||||
def test_load_progress_valid_file(tmp_path):
|
||||
"""正常读取已有 progress 文件。"""
|
||||
from tools.repair_trees import load_progress
|
||||
|
||||
path = tmp_path / "progress.json"
|
||||
path.write_text(json.dumps({"finished_video_ids": ["vid_a", "vid_b"]}))
|
||||
result = load_progress(path)
|
||||
@@ -26,6 +29,7 @@ def test_load_progress_valid_file(tmp_path):
|
||||
def test_load_progress_corrupted_file(tmp_path):
|
||||
"""损坏的 JSON 文件返回空集合(不抛异常)。"""
|
||||
from tools.repair_trees import load_progress
|
||||
|
||||
path = tmp_path / "progress.json"
|
||||
path.write_text("{invalid json")
|
||||
result = load_progress(path)
|
||||
@@ -36,6 +40,7 @@ def test_load_progress_corrupted_file(tmp_path):
|
||||
async def test_save_progress_atomic(tmp_path):
|
||||
"""save_progress 原子写入,并发调用不丢失更新。"""
|
||||
from tools.repair_trees import save_progress
|
||||
|
||||
path = tmp_path / "progress.json"
|
||||
lock = asyncio.Lock()
|
||||
await save_progress(path, lock, "vid_a")
|
||||
@@ -48,6 +53,7 @@ async def test_save_progress_atomic(tmp_path):
|
||||
async def test_save_progress_concurrent(tmp_path):
|
||||
"""16 路并发 save_progress 不丢失更新。"""
|
||||
from tools.repair_trees import save_progress
|
||||
|
||||
path = tmp_path / "progress.json"
|
||||
lock = asyncio.Lock()
|
||||
tasks = [save_progress(path, lock, f"vid_{i}") for i in range(16)]
|
||||
@@ -59,6 +65,7 @@ async def test_save_progress_concurrent(tmp_path):
|
||||
def test_should_skip_finished():
|
||||
"""已在 finished 集合中的视频应跳过。"""
|
||||
from tools.repair_trees import should_skip_video
|
||||
|
||||
finished = {"vid_a", "vid_b"}
|
||||
assert should_skip_video("vid_a", finished, reaggregate_all=False) is True
|
||||
assert should_skip_video("vid_c", finished, reaggregate_all=False) is False
|
||||
@@ -67,5 +74,6 @@ def test_should_skip_finished():
|
||||
def test_should_skip_reaggregate_all_forces_rerun():
|
||||
"""--reaggregate-all 标志强制不跳过。"""
|
||||
from tools.repair_trees import should_skip_video
|
||||
|
||||
finished = {"vid_a"}
|
||||
assert should_skip_video("vid_a", finished, reaggregate_all=True) is False
|
||||
|
||||
@@ -138,7 +138,7 @@ class TestQuestionGenStore:
|
||||
slot_id="slot-0",
|
||||
video_id="v002",
|
||||
family="reasoning",
|
||||
task_type="Action Sequence",
|
||||
task_type="Action Reasoning",
|
||||
skill_target="M2",
|
||||
attempt=1,
|
||||
question_text="为什么这样做?",
|
||||
|
||||
@@ -219,11 +219,7 @@ class TestCheckAnchors:
|
||||
def test_no_info_statement_not_counted(self) -> None:
|
||||
"""声明句"未包含…相关…信息"不计入 n_assertions。"""
|
||||
anchor_map = {"s1": "行1"}
|
||||
summary = (
|
||||
"[相关信息]\n"
|
||||
"- 该节点未包含与问题直接相关的信息\n"
|
||||
"- 关键发现(s1)"
|
||||
)
|
||||
summary = "[相关信息]\n- 该节点未包含与问题直接相关的信息\n- 关键发现(s1)"
|
||||
_, stats = check_anchors(summary, anchor_map)
|
||||
assert stats["n_assertions"] == 1 # 声明句不计
|
||||
assert stats["n_anchored"] == 1
|
||||
@@ -271,9 +267,7 @@ class TestAssembleAnchoredOutput:
|
||||
"""ids_expand 模式:保留行号 + 附加引文段。"""
|
||||
anchor_map = {"s1": "第一行内容", "s2": "第二行内容"}
|
||||
summary = "关键发现(s1,s2)"
|
||||
result, stats = assemble_anchored_output(
|
||||
summary, anchor_map, "ids_expand"
|
||||
)
|
||||
result, stats = assemble_anchored_output(summary, anchor_map, "ids_expand")
|
||||
assert "(s1,s2)" in result
|
||||
assert "[引文]" in result
|
||||
assert 's1: "第一行内容"' in result
|
||||
@@ -284,9 +278,7 @@ class TestAssembleAnchoredOutput:
|
||||
"""expand_only 模式:剥除行号 + 附加引文段。"""
|
||||
anchor_map = {"s1": "第一行内容"}
|
||||
summary = "关键发现(s1)"
|
||||
result, stats = assemble_anchored_output(
|
||||
summary, anchor_map, "expand_only"
|
||||
)
|
||||
result, stats = assemble_anchored_output(summary, anchor_map, "expand_only")
|
||||
assert "(s1)" not in result
|
||||
assert "[引文]" in result
|
||||
assert 's1: "第一行内容"' in result
|
||||
@@ -297,21 +289,15 @@ class TestAssembleAnchoredOutput:
|
||||
anchor_map = {f"s{i}": f"行{i}" for i in range(1, 10)}
|
||||
refs = ",".join(f"s{i}" for i in range(1, 10))
|
||||
summary = f"发现({refs})"
|
||||
result, stats = assemble_anchored_output(
|
||||
summary, anchor_map, "ids_expand"
|
||||
)
|
||||
result, stats = assemble_anchored_output(summary, anchor_map, "ids_expand")
|
||||
assert stats["n_expanded"] == 5
|
||||
|
||||
def test_max_chars_cap(self) -> None:
|
||||
"""总字符超过 800 时截断。"""
|
||||
anchor_map = {
|
||||
f"s{i}": "A" * 300 for i in range(1, 6)
|
||||
}
|
||||
anchor_map = {f"s{i}": "A" * 300 for i in range(1, 6)}
|
||||
refs = ",".join(f"s{i}" for i in range(1, 6))
|
||||
summary = f"发现({refs})"
|
||||
result, stats = assemble_anchored_output(
|
||||
summary, anchor_map, "ids_expand"
|
||||
)
|
||||
result, stats = assemble_anchored_output(summary, anchor_map, "ids_expand")
|
||||
# 300 字符原文 + 前缀 ≈ 310+ 每条,800 / 310 ≈ 2 条
|
||||
assert stats["n_expanded"] < 5
|
||||
|
||||
@@ -319,9 +305,7 @@ class TestAssembleAnchoredOutput:
|
||||
"""单行超 200 字符截断并标记 n_trunc。"""
|
||||
anchor_map = {"s1": "A" * 250}
|
||||
summary = "发现(s1)"
|
||||
result, stats = assemble_anchored_output(
|
||||
summary, anchor_map, "ids_expand"
|
||||
)
|
||||
result, stats = assemble_anchored_output(summary, anchor_map, "ids_expand")
|
||||
assert stats["n_trunc"] == 1
|
||||
assert "…" in result
|
||||
|
||||
@@ -388,10 +372,12 @@ class TestSummarizeNode:
|
||||
async def test_anchor_mode(self, prompts_dir: Path) -> None:
|
||||
"""锚模式:check_anchors + assemble。"""
|
||||
anchor_map = {"s1": "第一行", "s2": "第二行"}
|
||||
llm = FakeLLMProvider([
|
||||
"[相关信息]\n- 关键发现(s1)\n- 补充(s2)",
|
||||
"核实通过",
|
||||
])
|
||||
llm = FakeLLMProvider(
|
||||
[
|
||||
"[相关信息]\n- 关键发现(s1)\n- 补充(s2)",
|
||||
"核实通过",
|
||||
]
|
||||
)
|
||||
result = await summarize_node(
|
||||
llm,
|
||||
"带行号的内容",
|
||||
@@ -409,10 +395,12 @@ class TestSummarizeNode:
|
||||
"""锚模式 stats_sink 回调接收完整统计。"""
|
||||
anchor_map = {"s1": "第一行"}
|
||||
collected: list[dict] = []
|
||||
llm = FakeLLMProvider([
|
||||
"[相关信息]\n- 关键发现(s1)",
|
||||
"核实通过",
|
||||
])
|
||||
llm = FakeLLMProvider(
|
||||
[
|
||||
"[相关信息]\n- 关键发现(s1)",
|
||||
"核实通过",
|
||||
]
|
||||
)
|
||||
await summarize_node(
|
||||
llm,
|
||||
"内容",
|
||||
@@ -471,9 +459,7 @@ class TestSummarizeChildren:
|
||||
{"id": "n2", "time_range": (30.0, 60.0), "summary": "中间"},
|
||||
]
|
||||
llm = FakeLLMProvider(["相关性标注结果", "核实通过"])
|
||||
result = await summarize_children(
|
||||
llm, children_info, "问题", prompts_dir
|
||||
)
|
||||
result = await summarize_children(llm, children_info, "问题", prompts_dir)
|
||||
assert "相关性标注结果" in result
|
||||
assert "[核实] 核实通过" in result
|
||||
|
||||
@@ -484,25 +470,19 @@ class TestSummarizeChildren:
|
||||
{"id": "n1", "time_range": (0.0, 30.0), "summary": "开头"},
|
||||
]
|
||||
llm = FailingLLMProvider("网络错误")
|
||||
result = await summarize_children(
|
||||
llm, children_info, "问题", prompts_dir
|
||||
)
|
||||
result = await summarize_children(llm, children_info, "问题", prompts_dir)
|
||||
assert "n1" in result
|
||||
assert "0-30s" in result
|
||||
assert "开头" in result
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_verify_failure_returns_extract_only(
|
||||
self, prompts_dir: Path
|
||||
) -> None:
|
||||
async def test_verify_failure_returns_extract_only(self, prompts_dir: Path) -> None:
|
||||
"""核实轮失败仍返回提取结果。"""
|
||||
children_info = [
|
||||
{"id": "n1", "time_range": (0.0, 30.0), "summary": "开头"},
|
||||
]
|
||||
llm = FailOnNthLLMProvider(["标注结果"], fail_on=2)
|
||||
result = await summarize_children(
|
||||
llm, children_info, "问题", prompts_dir
|
||||
)
|
||||
result = await summarize_children(llm, children_info, "问题", prompts_dir)
|
||||
assert "标注结果" in result
|
||||
|
||||
|
||||
@@ -513,19 +493,22 @@ class TestSummarizeNodesBatch:
|
||||
async def test_batch_normal(self, prompts_dir: Path) -> None:
|
||||
"""并发三个节点,结果顺序与输入一致。"""
|
||||
# 每个节点需要 2 轮 LLM 调用(提取 + 核实)
|
||||
llm = FakeLLMProvider([
|
||||
"摘要A", "核实A",
|
||||
"摘要B", "核实B",
|
||||
"摘要C", "核实C",
|
||||
])
|
||||
llm = FakeLLMProvider(
|
||||
[
|
||||
"摘要A",
|
||||
"核实A",
|
||||
"摘要B",
|
||||
"核实B",
|
||||
"摘要C",
|
||||
"核实C",
|
||||
]
|
||||
)
|
||||
items = [
|
||||
("n1", "内容1", "extra1"),
|
||||
("n2", "内容2", "extra2"),
|
||||
("n3", "内容3", "extra3"),
|
||||
]
|
||||
results = await summarize_nodes_batch(
|
||||
llm, items, "问题", prompts_dir
|
||||
)
|
||||
results = await summarize_nodes_batch(llm, items, "问题", prompts_dir)
|
||||
assert len(results) == 3
|
||||
assert results[0][0] == "n1"
|
||||
assert results[1][0] == "n2"
|
||||
@@ -538,7 +521,5 @@ class TestSummarizeNodesBatch:
|
||||
async def test_batch_empty(self, prompts_dir: Path) -> None:
|
||||
"""空列表返回空结果。"""
|
||||
llm = FakeLLMProvider([])
|
||||
results = await summarize_nodes_batch(
|
||||
llm, [], "问题", prompts_dir
|
||||
)
|
||||
results = await summarize_nodes_batch(llm, [], "问题", prompts_dir)
|
||||
assert results == []
|
||||
|
||||
@@ -126,9 +126,7 @@ def prompts_dir(tmp_path: Path) -> Path:
|
||||
class TestObserveFrameNormal:
|
||||
"""两轮正常执行(verify=True)。"""
|
||||
|
||||
def test_two_round_normal(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_two_round_normal(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["raw evidence", "verified ok"])
|
||||
@@ -158,9 +156,7 @@ class TestObserveFrameNormal:
|
||||
class TestObserveFrameExtractOnly:
|
||||
"""verify=False 仅执行提取轮。"""
|
||||
|
||||
def test_extract_only(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_extract_only(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["only extract"])
|
||||
@@ -183,9 +179,7 @@ class TestObserveFrameExtractOnly:
|
||||
class TestObserveFrameOCRInjection:
|
||||
"""OCR 注入:文本非空时并置于问题前。"""
|
||||
|
||||
def test_ocr_injected(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_ocr_injected(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["evidence with ocr"])
|
||||
@@ -216,9 +210,7 @@ class TestObserveFrameOCRInjection:
|
||||
class TestObserveFrameOCRFailDegrades:
|
||||
"""OCR 转录抛出异常时降级:不注入 OCR、ocr_failed=1。"""
|
||||
|
||||
def test_ocr_failure_degrades(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_ocr_failure_degrades(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["evidence no ocr"])
|
||||
@@ -245,9 +237,7 @@ class TestObserveFrameOCRFailDegrades:
|
||||
class TestObserveFrameOCRNone:
|
||||
"""ocr=None 时不执行转录。"""
|
||||
|
||||
def test_ocr_none(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_ocr_none(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["no ocr"])
|
||||
@@ -274,9 +264,7 @@ class TestObserveFrameOCRNone:
|
||||
class TestObserveFrameVLMExtractFailure:
|
||||
"""VLM 提取轮失败 → 返回 [VL错误]。"""
|
||||
|
||||
def test_vlm_extract_failure(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_vlm_extract_failure(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(raises=[RuntimeError("VLM timeout")])
|
||||
@@ -302,9 +290,7 @@ class TestObserveFrameVLMExtractFailure:
|
||||
class TestObserveFrameVLMVerifyFailureDegrades:
|
||||
"""VLM 验证轮失败 → 降级:保留提取结果 + [验证] 跳过。"""
|
||||
|
||||
def test_vlm_verify_failure_degrades(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_vlm_verify_failure_degrades(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(
|
||||
@@ -359,9 +345,7 @@ class TestObserveFrameFileMissing:
|
||||
class TestObserveFrameStatsKeys:
|
||||
"""stats 包含全部五个预期键。"""
|
||||
|
||||
def test_stats_keys_complete(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_stats_keys_complete(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["evidence"])
|
||||
@@ -386,9 +370,7 @@ class TestObserveFrameStatsKeys:
|
||||
class TestObserveFrameDiscrepancyAndAbstain:
|
||||
"""VLM 返回含 '分歧' 或 '[证据不存在]' 时对应 stats 标记。"""
|
||||
|
||||
def test_discrepancy_flag(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_discrepancy_flag(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["发现分歧:OCR 与画面不一致"])
|
||||
@@ -408,9 +390,7 @@ class TestObserveFrameDiscrepancyAndAbstain:
|
||||
|
||||
assert collected[0]["discrepancy"] == 1
|
||||
|
||||
def test_abstain_flag(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_abstain_flag(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["[证据不存在] 无法判断"])
|
||||
@@ -434,9 +414,7 @@ class TestObserveFrameDiscrepancyAndAbstain:
|
||||
class TestObserveFrameTelemetryPassthrough:
|
||||
"""session_id 和 parent_call_id 透传到 VLM 调用。"""
|
||||
|
||||
def test_telemetry_passthrough(
|
||||
self, frame_files: list[Path], prompts_dir: Path
|
||||
) -> None:
|
||||
def test_telemetry_passthrough(self, frame_files: list[Path], prompts_dir: Path) -> None:
|
||||
from app.search.vision import observe_frame
|
||||
|
||||
vlm = FakeVLMProvider(responses=["evidence"])
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
def test_core_importable():
|
||||
"""core 包可导入。"""
|
||||
import core
|
||||
|
||||
assert core is not None
|
||||
|
||||
|
||||
def test_app_importable():
|
||||
"""app 包可导入。"""
|
||||
import app
|
||||
|
||||
assert app is not None
|
||||
|
||||
|
||||
def test_adapters_importable():
|
||||
"""adapters 包可导入。"""
|
||||
import adapters
|
||||
|
||||
assert adapters is not None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""adapters/telemetry.py 单元测试 — SQLiteTelemetryRecorder。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
@@ -9,11 +10,11 @@ import pytest
|
||||
from adapters.telemetry import SQLiteTelemetryRecorder
|
||||
from core.protocols import TelemetryRecorder
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db_path(tmp_path):
|
||||
"""返回临时数据库路径。"""
|
||||
@@ -51,6 +52,7 @@ def _make_call_kwargs(*, cache_hit: bool = False, error: str | None = None):
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_satisfies_protocol(recorder):
|
||||
"""SQLiteTelemetryRecorder 满足 TelemetryRecorder Protocol。"""
|
||||
assert isinstance(recorder, TelemetryRecorder)
|
||||
@@ -86,7 +88,9 @@ async def test_record_with_error(recorder, db_path):
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute("SELECT error FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT error FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row["error"] == "RateLimitError: 429"
|
||||
@@ -100,7 +104,9 @@ async def test_record_cache_hit(recorder, db_path):
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute("SELECT cache_hit FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT cache_hit FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
|
||||
assert row["cache_hit"] == 1
|
||||
@@ -130,6 +136,7 @@ async def test_db_error_does_not_propagate(tmp_path):
|
||||
async def test_concurrent_writes_no_lock_error(recorder, db_path):
|
||||
"""16 路并发 record_llm_call 应全部成功,无 database is locked 错误。"""
|
||||
import asyncio
|
||||
|
||||
tasks = []
|
||||
for _ in range(16):
|
||||
kwargs = _make_call_kwargs()
|
||||
|
||||
Reference in New Issue
Block a user