assistant/tests/test_trial.py

219 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""试用制:草稿也注入(降一级),Agent 的回执当裁判,被证实的升级、被证伪的降级。"""
from __future__ import annotations
import pytest
from backend.orchestrator import orchestrator as orc
from backend.orchestrator.orchestrator import parse_trial_receipt
class TestLearnedBlock:
KNOWLEDGE = {"learned": {"门户结构": "有 8 个一级模块"}}
TRIALS = [{"id": 42, "key": "提交入口", "value": "提交按钮在表单底部,叫「提交填报」"}]
def test_verified_and_trial_are_separate_sections(self):
block = orc._learned_block(self.KNOWLEDGE, {}, self.TRIALS)
assert "已知信息(已确认" in block and "待验证线索" in block
def test_trial_entries_carry_their_id_for_the_receipt(self):
assert "[42]" in orc._learned_block(self.KNOWLEDGE, {}, self.TRIALS)
def test_page_always_wins_over_a_trial_hint(self):
# 错的线索会主动帮倒忙,所以必须写死「以页面为准」
block = orc._learned_block(self.KNOWLEDGE, {}, self.TRIALS)
assert "一律以页面为准" in block
def test_receipt_format_is_spelled_out(self):
block = orc._learned_block(self.KNOWLEDGE, {}, self.TRIALS)
assert "线索回执" in block and "相符" in block and "不符" in block and "没用上" in block
def test_no_trials_means_no_trial_section(self):
assert "待验证线索" not in orc._learned_block(self.KNOWLEDGE, {}, [])
def test_no_knowledge_at_all_produces_nothing(self):
assert orc._learned_block({}, {}, []) == ""
def test_trials_alone_still_render(self):
assert "待验证线索" in orc._learned_block({}, {}, self.TRIALS)
class TestReceiptParsing:
ALLOWED = {7, 42, 99}
def test_parses_the_documented_format(self):
text = "任务完成。\n\n线索回执:\n线索[42] 不符 提交按钮实际叫「保存并提交」\n线索[7] 相符 班级确实是下拉框"
assert parse_trial_receipt(text, self.ALLOWED) == [
{"id": 42, "outcome": "contradicted", "note": "提交按钮实际叫「保存并提交」"},
{"id": 7, "outcome": "confirmed", "note": "班级确实是下拉框"},
]
@pytest.mark.parametrize("line,outcome", [
("线索[7] 相符 对的", "confirmed"),
("线索[7]:相符,对的", "confirmed"),
("线索7 不符 错的", "contradicted"),
("线索[7] 没用上 没去到那页", "unused"),
("线索[7] 未用上 没去到那页", "unused"),
])
def test_tolerates_punctuation_and_wording_variants(self, line, outcome):
parsed = parse_trial_receipt(line, self.ALLOWED)
assert parsed and parsed[0]["outcome"] == outcome
def test_ids_that_were_never_injected_are_ignored(self):
# 防止模型凭空编一个编号出来,把没注入过的条目改掉
assert parse_trial_receipt("线索[500] 相符 编的", self.ALLOWED) == []
def test_later_lines_win_for_the_same_id(self):
text = "线索[7] 相符 先说对\n线索[7] 不符 后来发现不对"
assert parse_trial_receipt(text, self.ALLOWED)[0]["outcome"] == "contradicted"
@pytest.mark.parametrize("text", ["", "任务完成,没有回执", "线索很有用但没按格式写"])
def test_no_receipt_is_not_an_error(self, text):
assert parse_trial_receipt(text, self.ALLOWED) == []
def test_receipt_mixed_into_a_long_summary(self):
text = "【任务结果】已填未提交。\n\n一、操作过程\n1. 打开系统\n2. 填表\n\n【线索回执】\n线索[42] 相符 确实如此\n"
assert len(parse_trial_receipt(text, self.ALLOWED)) == 1
@pytest.mark.db
class TestTrialLifecycle:
KID, BID = "pytest_trial", "b1"
def _draft(self, db, key: str, value: str, category: str = "control_tip"):
db.add_learned_drafts(self.KID, self.BID, [{"key": key, "value": value, "category": category}], "t0")
return db.list_learned(knowledge_id=self.KID, business_id=self.BID)[0]
def test_drafts_are_offered_as_trials(self, test_school):
from backend.db import database as db
self._draft(db, "提交入口", "提交按钮在表单底部,叫「提交填报」,不要点保存草稿")
trials = db.trial_learned(self.KID, self.BID, 8)
assert len(trials) == 1 and trials[0]["status"] == "draft"
def test_verified_entries_are_not_offered_again(self, test_school):
from backend.db import database as db
entry = self._draft(db, "提交入口", "提交按钮在表单底部,叫「提交填报」,不要点保存草稿")
db.set_learned_status(entry["id"], "verified")
assert db.trial_learned(self.KID, self.BID, 8) == []
# 内容必须彼此差异够大,否则会被内容判重合并成一条(那是 add_learned_drafts 的正常行为)
DISTINCT = [
("班级控件", "班级是下拉框,点开后从选项里选择对应班级"),
("提交入口", "表单底部有「提交填报」按钮,旁边是保存草稿"),
("日期输入", "听课时间支持直接键入 yyyy-mm-dd 后回车确认"),
("菜单路径", "门户左侧导航进入辅导员管理再到工作日志"),
("附件上传", "相关附件一栏支持拖拽,也可点击选择文件"),
("审批状态", "列表最右列展示当前审批进度和处理人"),
]
def test_injection_is_capped(self, test_school):
from backend.db import database as db
for i, (key, value) in enumerate(self.DISTINCT):
db.add_learned_drafts(self.KID, self.BID, [{"key": key, "value": value, "category": "path"}], f"t{i}")
assert len(db.list_learned(knowledge_id=self.KID, business_id=self.BID)) == len(self.DISTINCT)
assert len(db.trial_learned(self.KID, self.BID, 3)) == 3
def test_contradicted_entries_sort_last(self, test_school):
from backend.db import database as db
good = self._draft(db, "班级控件", self.DISTINCT[0][1])
db.add_learned_drafts(self.KID, self.BID,
[{"key": "菜单路径", "value": self.DISTINCT[3][1], "category": "path"}], "t2")
bad = next(r for r in db.list_learned(knowledge_id=self.KID, business_id=self.BID) if r["key"] == "菜单路径")
db.record_trials("task_x", [bad["id"]])
db.apply_trial_outcomes("task_x", [{"id": bad["id"], "outcome": "contradicted", "note": "页面上不是这样"}], 2, 9)
order = [r["id"] for r in db.trial_learned(self.KID, self.BID, 8)]
assert order.index(good["id"]) < order.index(bad["id"])
def test_two_confirmations_promote(self, test_school):
from backend.db import database as db
entry = self._draft(db, "提交入口", "提交按钮在表单底部,叫「提交填报」,不要点保存草稿")
for task in ("task_1", "task_2"):
db.record_trials(task, [entry["id"]])
stats = db.apply_trial_outcomes(task, [{"id": entry["id"], "outcome": "confirmed", "note": "页面确实如此"}], 2, 1)
assert stats["promoted"] == 1
row = db.get_learned(entry["id"])
assert row["status"] == "verified" and row["verified_by"] == "trial" and row["confirmed"] == 2
assert "提交入口" in db.verified_learned(self.KID, self.BID)
def test_one_contradiction_demotes(self, test_school):
from backend.db import database as db
entry = self._draft(db, "错的经验", "这条说法与页面实际不符,应该被降级")
db.record_trials("task_1", [entry["id"]])
stats = db.apply_trial_outcomes("task_1", [{"id": entry["id"], "outcome": "contradicted", "note": "实际叫别的名字"}], 2, 1)
assert stats["demoted"] == 1
assert db.get_learned(entry["id"])["status"] == "rejected"
assert db.trial_learned(self.KID, self.BID, 8) == [] # 不再注入
def test_a_contradiction_blocks_promotion_even_with_confirmations(self, test_school):
from backend.db import database as db
entry = self._draft(db, "时灵时不灵", "这条有时对有时不对,不该升级")
db.apply_trial_outcomes("t1", [{"id": entry["id"], "outcome": "confirmed", "note": "对"}], 2, 9)
db.apply_trial_outcomes("t2", [{"id": entry["id"], "outcome": "contradicted", "note": "不对"}], 2, 9)
db.apply_trial_outcomes("t3", [{"id": entry["id"], "outcome": "confirmed", "note": "又对了"}], 2, 9)
row = db.get_learned(entry["id"])
assert row["status"] == "draft" and row["confirmed"] == 2 and row["contradicted"] == 1
def test_unused_changes_nothing(self, test_school):
from backend.db import database as db
entry = self._draft(db, "没用上的", "这次任务没走到相关页面,不算证据")
db.apply_trial_outcomes("t1", [{"id": entry["id"], "outcome": "unused", "note": "没去到那页"}], 2, 1)
row = db.get_learned(entry["id"])
assert row["status"] == "draft" and row["confirmed"] == 0 and row["contradicted"] == 0
def test_trial_history_is_traceable(self, test_school):
from backend.db import database as db
entry = self._draft(db, "可追溯", "要能查出这条是被哪次任务证实的")
db.apply_trial_outcomes("task_abc", [{"id": entry["id"], "outcome": "confirmed", "note": "页面确实如此"}], 2, 1)
history = db.trial_history(entry["id"])
assert history and history[0]["task_id"] == "task_abc" and history[0]["outcome"] == "confirmed"
def test_same_task_reporting_twice_does_not_double_count(self, test_school):
from backend.db import database as db
entry = self._draft(db, "重复回执", "同一个任务重复给回执不该算两次证据")
db.apply_trial_outcomes("t1", [{"id": entry["id"], "outcome": "confirmed", "note": "对"}], 9, 9)
db.apply_trial_outcomes("t1", [{"id": entry["id"], "outcome": "confirmed", "note": "对"}], 9, 9)
history = db.trial_history(entry["id"])
assert len(history) == 1
def test_unknown_entry_ids_are_ignored(self, test_school):
from backend.db import database as db
stats = db.apply_trial_outcomes("t1", [{"id": 99999999, "outcome": "confirmed"}], 2, 1)
assert stats["promoted"] == 0 and stats["confirmed"] == 0
def test_malformed_outcomes_are_ignored(self, test_school):
from backend.db import database as db
entry = self._draft(db, "格式乱的", "回执格式不对时不能改坏数据")
db.apply_trial_outcomes("t1", [{"id": entry["id"], "outcome": "编的状态"}, {"id": "abc"}, {}], 2, 1)
row = db.get_learned(entry["id"])
assert row["confirmed"] == 0 and row["contradicted"] == 0
class TestTrialSwitch:
def test_can_be_turned_off(self, monkeypatch, knowledge, business):
from backend.config import settings
from backend.orchestrator.orchestrator import Orchestrator
monkeypatch.setattr(settings, "trial_enabled", False)
assert Orchestrator._pick_trials(knowledge, business) == []
def test_database_failure_does_not_break_execution(self, monkeypatch, knowledge, business):
from backend.config import settings
from backend.db import database as db
from backend.orchestrator.orchestrator import Orchestrator
monkeypatch.setattr(settings, "trial_enabled", True)
monkeypatch.setattr(db, "trial_learned", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("库挂了")))
assert Orchestrator._pick_trials(knowledge, business) == []