From cca7071dbdf48371852a0603214b88f923aad1fa Mon Sep 17 00:00:00 2001 From: iomgaa Date: Tue, 21 Jul 2026 01:17:46 -0400 Subject: [PATCH] feat: add soak corpus loaders and scenario generators --- tests/unit/test_soak_corpus.py | 131 +++++++++++++++++++++++ tools/soak/__init__.py | 4 + tools/soak/corpus.py | 124 +++++++++++++++++++++ tools/soak/scenarios.py | 190 +++++++++++++++++++++++++++++++++ 4 files changed, 449 insertions(+) create mode 100644 tests/unit/test_soak_corpus.py create mode 100644 tools/soak/__init__.py create mode 100644 tools/soak/corpus.py create mode 100644 tools/soak/scenarios.py diff --git a/tests/unit/test_soak_corpus.py b/tests/unit/test_soak_corpus.py new file mode 100644 index 0000000..64a8c1a --- /dev/null +++ b/tests/unit/test_soak_corpus.py @@ -0,0 +1,131 @@ +"""soak harness 纯函数层测试(M2 设计 §8;T10/T11)。 + +语料还原器与不变量断言函数是纯函数,用真实 schema 的二次构造微型库测试 +(真实场景优先: schema 与 data/soak 实库逐字一致)。 +""" + +import json +import sqlite3 + +import pytest + +from tools.soak.corpus import ( + assemble_frame_messages, + chs_image_messages, + load_replay_payloads, + load_trace_chains, +) +from tools.soak.scenarios import weighted_mix + + +def _mini_harness_db(path): + """predictions 表 schema 与 data/soak/harness.db 逐字一致的微型库。""" + conn = sqlite3.connect(path) + conn.execute( + "CREATE TABLE predictions (run_id TEXT, timestamp TEXT, video_id TEXT," + " question_id TEXT, task_type TEXT, prediction TEXT, answer TEXT, evidence TEXT," + " reasoning TEXT, steps_used INTEGER, prompt_tokens INTEGER," + " completion_tokens INTEGER, stop_reason TEXT, steps_json TEXT)" + ) + steps = [ + {"thought": f"思考{i}", "tool_call": {"tool": "view_node", "args": {"i": i}}, + "tool_output": f"输出{i}" * 50} + for i in range(3) + ] + conn.execute( + "INSERT INTO predictions VALUES ('r1','t','vid1','q1','synopsis','A','A','','',3,1,1,'stop',?)", + (json.dumps(steps, ensure_ascii=False),), + ) + conn.execute( + "INSERT INTO predictions VALUES ('r1','t','vid2','q2','synopsis','B','B','','',0,1,1,'stop','[]')" + ) + conn.commit() + conn.close() + + +def _mini_telemetry_db(path): + conn = sqlite3.connect(path) + conn.execute( + "CREATE TABLE llm_calls (call_id TEXT PRIMARY KEY, parent_call_id TEXT," + " session_id TEXT, model_name TEXT, provider TEXT, messages TEXT, response TEXT," + " thinking TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, latency_ms INTEGER," + " ttft_ms REAL, max_inter_token_ms REAL, cache_hit INTEGER, error TEXT, created_at TEXT)" + ) + msgs = json.dumps([{"role": "user", "content": "回放负载"}]) + conn.execute( + "INSERT INTO llm_calls VALUES ('c1',NULL,'s1','m','p',?,'r','',1,1,1,NULL,NULL,0,NULL,'t')", + (msgs,), + ) + conn.execute( + "INSERT INTO llm_calls VALUES ('c2',NULL,'s1','m','p','not-json','r','',1,1,1,NULL,NULL,0,NULL,'t')" + ) + conn.commit() + conn.close() + + +class TestTraceChains: + def test_cumulative_chain_shapes(self, tmp_path): + db = tmp_path / "h.db" + _mini_harness_db(db) + chains = load_trace_chains(db) + assert len(chains) == 1 # steps_used=0 的行剔除 + chain = chains[0] + assert len(chain) == 3 # 每步一个累积快照 + # 快照单调增长: system+user 起步,每步追加 assistant+user + assert [len(snap) for snap in chain] == [4, 6, 8] + first = chain[0] + assert first[0]["role"] == "system" and first[1]["role"] == "user" + assert first[2]["role"] == "assistant" and "思考0" in first[2]["content"] + assert first[3]["role"] == "user" and "输出0" in first[3]["content"] + # 后续快照是前缀扩展(累积语义) + assert chain[1][:4] == chain[0] + + def test_max_steps_cap(self, tmp_path): + db = tmp_path / "h.db" + _mini_harness_db(db) + assert [len(s) for s in load_trace_chains(db, max_steps=2)[0]] == [4, 6] + + +class TestReplayPayloads: + def test_loads_and_skips_unparseable(self, tmp_path): + db = tmp_path / "t.db" + _mini_telemetry_db(db) + payloads = load_replay_payloads(db) + assert len(payloads) == 1 # 坏 JSON 行剔除 + assert payloads[0][0]["content"] == "回放负载" + + +class TestImageAssembly: + def test_frame_messages_two_tiers(self, tmp_path): + vid = tmp_path / "vid1" / "frames" + vid.mkdir(parents=True) + for i in range(6): + (vid / f"f{i}.jpg").write_bytes(b"\xff\xd8fakejpeg" + bytes([i])) + msgs = assemble_frame_messages(tmp_path, n_frames=4, rng=lambda: 0.0) + parts = msgs[0]["content"] + images = [p for p in parts if p["type"] == "image_url"] + assert len(images) == 4 + assert images[0]["image_url"]["url"].startswith("data:image/jpeg;base64,") + + def test_chs_single_image_with_instruction(self, tmp_path): + (tmp_path / "chs_0001.jpg").write_bytes(b"\xff\xd8fake") + msgs = chs_image_messages(tmp_path / "chs_0001.jpg") + parts = msgs[0]["content"] + assert any(p["type"] == "image_url" for p in parts) + text = next(p["text"] for p in parts if p["type"] == "text") + assert "JSON" in text # 固定结构化指令 + + +class TestWeightedMix: + def test_ratio_approximation(self): + import random + + rng = random.Random(42).random + weights = {"P1": 0.1, "P2": 0.2, "P3": 0.5, "P5": 0.2} + picks = [weighted_mix(weights, rng) for _ in range(10_000)] + for key, w in weights.items(): + assert picks.count(key) / 10_000 == pytest.approx(w, abs=0.05) + + def test_invalid_weights_rejected(self): + with pytest.raises(ValueError): + weighted_mix({"P1": 0.0}, lambda: 0.5) diff --git a/tools/soak/__init__.py b/tools/soak/__init__.py new file mode 100644 index 0000000..af5a637 --- /dev/null +++ b/tools/soak/__init__.py @@ -0,0 +1,4 @@ +"""真实数据压测 harness(M2 设计 §8;findings/2026-07-20-m2-soak-workload.md)。 + +独立工具,不被 src/ import、不入 pytest 门(纯函数部分有 unit 测试)。 +""" diff --git a/tools/soak/corpus.py b/tools/soak/corpus.py new file mode 100644 index 0000000..26d008e --- /dev/null +++ b/tools/soak/corpus.py @@ -0,0 +1,124 @@ +"""压测语料装载(纯函数): 真实库还原请求负载,不做任何网络调用。 + +数据源(data/soak/,不入 git): +- harness.db predictions.steps_json: VT agent 完整轨迹 → P1 长上下文累积链 +- generate_questions_telemetry.db llm_calls.messages: 376 条真实重载 → P2 回放 +- vt_frames/