Initial import of Shirobana bot backend

Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
This commit is contained in:
RRRosmontis
2026-11-11 00:25:30 +08:00
commit 34089d0042
106 changed files with 28028 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
"""Wardrobe orchestration regressions: fake model, executor and task store only."""
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
BASE = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
import agent_loop
from agent import ToolResult
def call(name="白衬衫", description="柔软", action="add", tool="wardrobe"):
return {"name": tool, "args": {"action": action, "params": {"name": name, "description": description}}}
def request(*calls):
return {"message": [], "tools": list(calls)}
def reply(text):
return {"message": [{"content": text}], "tools": []}
class Channel:
name = "test"
def is_admin(self, uid):
return False
class Store:
last = None
def __init__(self):
self.steps = []
self.finished = None
Store.last = self
def start(self, *args):
return "fake-task"
def record(self, *args):
self.steps.append(args)
def finish(self, task, status, error=""):
self.finished = status
class Engine:
BASE_PROMPT = "GROUP_PERSONA"
user_persona_prompt = staticmethod(lambda uid: "ORIGINAL_PERSONA")
def __init__(self, responses):
self.responses = list(responses)
self.prompts = []
async def call_deepseek(self, prompt, *args, **kwargs):
polishing = "【仅润色回复正文】" in prompt
assert kwargs["reasoning"] is (not polishing)
self.prompts.append(prompt)
response = self.responses.pop(0) if self.responses else RuntimeError("outage")
if isinstance(response, Exception):
raise response
return json.dumps(response, ensure_ascii=False) if polishing and isinstance(response, dict) else response
def parse_json_response(self, raw):
return raw
class WardrobeReplyTest(unittest.IsolatedAsyncioTestCase):
async def run_loop(self, initial, responses, outcomes=None, **limits):
calls = []
outcomes = list(outcomes or [])
class Executor:
async def execute(self, name, args, ctx):
calls.append((name, args))
if outcomes:
return outcomes.pop(0)
item = {"id": len(calls), "uid": 999, "name": args["params"]["name"],
"description": "RAW_PRIVATE_DESCRIPTION", "created_at": "SECRET_TIME"}
return ToolResult(True, "ok", json.dumps(item), data={"item": item})
engine = Engine(responses)
with patch.object(agent_loop, "AgentTaskStore", Store), patch.object(agent_loop, "ToolExecutor", Executor):
with patch.multiple(agent_loop, **({"MAX_CALLS": 12, "MAX_ROUNDS": 8} | limits)):
result = await agent_loop.run_agent_loop(engine, initial, "ORIGINAL_PERSONA", "user", Channel(), "private", 1, 2)
return result, calls, engine
async def test_plain_prose_finalization_does_not_need_json_fields(self):
result, calls, model = await self.run_loop(request(call()), [None, "谢谢主人,这件我记住啦。"])
self.assertEqual(agent_loop._message_text(result), "谢谢主人,这件我记住啦。")
self.assertEqual(len(calls), 1)
self.assertEqual(result['tools'], [])
self.assertNotIn('请仅生成有效JSON', model.prompts[-1])
self.assertNotIn('RAW_PRIVATE_DESCRIPTION', model.prompts[-1])
self.assertNotIn('SECRET_TIME', model.prompts[-1])
self.assertIn('ORIGINAL_PERSONA', model.prompts[-1])
self.assertEqual(Store.last.finished, 'done')
async def test_legacy_text_envelope_can_omit_tools(self):
result, calls, _ = await self.run_loop(request(call()), [None, '{"text":"谢谢主人。"}'])
self.assertEqual(agent_loop._message_text(result), '谢谢主人。')
self.assertEqual(len(calls), 1)
def test_plain_reply_rejects_tools_and_db_dump_without_execution(self):
for raw in ('{"tools":[{"name":"wardrobe"}],"text":"x"}',
'{"id":"abc","uid":42,"name":"dress"}', '', None):
self.assertIsNone(agent_loop._plain_reply(raw))
self.assertEqual(agent_loop._plain_reply('好呀,谢谢你。'), '好呀,谢谢你。')
async def test_outage_never_exposes_raw_records(self):
result, calls, engine = await self.run_loop(request(call()), [RuntimeError("offline")])
self.assertEqual(agent_loop._message_text(result), "好呀,白衬衫已经在衣柜里啦。")
self.assertEqual(result["tools"], [])
self.assertEqual(len(engine.prompts), 2)
self.assertIn("ORIGINAL_PERSONA", engine.prompts[-1])
self.assertIn("实际工具结果", engine.prompts[-1])
async def test_same_normalized_name_paraphrase_replays(self):
result, calls, engine = await self.run_loop(
request(call("A Shirt", "丝绸")),
[request(call("a\tshirt", "轻柔丝质")), reply("记住啦")])
self.assertEqual(len(calls), 1)
self.assertIn('"code": "already_saved"', engine.prompts[-1])
self.assertIn("update", engine.prompts[-1])
self.assertIn('"id": 1', engine.prompts[-1])
async def test_outage_receipt_names_only_completed_adds(self):
result, calls, _ = await self.run_loop(request(call("红裙"), call("蓝裙"), call("未执行绿裙")), [None, None])
text = agent_loop._message_text(result)
self.assertEqual(len(calls), 2)
self.assertEqual("好呀,红裙、蓝裙已经在衣柜里啦。", text)
for forbidden in ("未执行绿裙", "RAW_PRIVATE_DESCRIPTION", "uid", "SECRET_TIME", "{"):
self.assertNotIn(forbidden, text)
async def test_different_names_both_saved(self):
_, calls, _ = await self.run_loop(request(call("红裙"), call("蓝裙")), [reply("好呀")])
self.assertEqual(len(calls), 2)
async def test_update_reversion_is_not_replayed_from_stale_cache(self):
first = call('外套', '蓝色', action='update')
second = call('外套', '红色', action='update')
_, calls, _ = await self.run_loop(request(first), [request(second), request(first), reply('改好了')])
self.assertEqual(len(calls), 3)
async def test_delete_invalidates_same_turn_add_guard(self):
added = ToolResult(True, 'ok', '已保存', data={'item': {'id': 'first', 'name': '外套'}})
removed = ToolResult(True, 'ok', '已删除', data={'item': {'id': 'first', 'name': '外套'}})
readded = ToolResult(True, 'ok', '已保存', data={'item': {'id': 'second', 'name': '外套'}})
_, calls, _ = await self.run_loop(request(call('外套')),
[request(call('外套', action='delete')), request(call('外套')), reply('收好了')],
outcomes=[added, removed, readded])
self.assertEqual(len(calls), 3)
async def test_exact_duplicates_do_not_execute(self):
_, calls, _ = await self.run_loop(request(call(), call()), [reply("好呀")])
self.assertEqual(len(calls), 1)
async def test_failed_add_can_retry(self):
failed = ToolResult(False, "busy", "暂时忙,请重试")
_, calls, _ = await self.run_loop(request(call()), [request(call()), reply("好了")], [failed])
self.assertEqual(len(calls), 2)
async def test_all_prior_successes_survive_in_bounded_ledger(self):
rounds = [request(call(f"衣服{i}")) for i in range(1, 5)]
_, _, engine = await self.run_loop(request(call("衣服0")), rounds + [reply("好呀")])
last = engine.prompts[-1]
ledger = json.loads(last.split("【累计成功操作摘要(内部)】\n")[1].split("\n【你刚才")[0])
self.assertEqual(len(ledger), 5)
for i in range(5):
self.assertIn(f"衣服{i}", " ".join(ledger))
self.assertTrue(all(len(entry) <= 400 for entry in ledger))
self.assertNotIn("RAW_PRIVATE_DESCRIPTION", " ".join(ledger))
self.assertNotIn("SECRET_TIME", " ".join(ledger))
self.assertIn("tool=wardrobe; action=add; code=ok", ledger[0])
self.assertIn('"action": "add"', last)
async def test_ledger_bound_and_no_raw_arguments(self):
rounds = [request(call(f"衣服{i}")) for i in range(1, 14)]
_, _, engine = await self.run_loop(request(call("衣服0", "password=secret")), rounds + [reply("好了")], MAX_ROUNDS=20, MAX_CALLS=20)
ledger = json.loads(engine.prompts[-1].split("【累计成功操作摘要(内部)】\n")[1].split("\n【你刚才")[0])
self.assertEqual(len(ledger), 12)
self.assertNotIn("secret", " ".join(ledger))
self.assertTrue(all(len(entry) <= 400 for entry in ledger))
async def test_read_only_queries_are_not_deduplicated(self):
for tool in ("wardrobe", "mail"):
_, calls, _ = await self.run_loop(request(call(action="list", tool=tool)),
[request(call(action="list", tool=tool)), reply("好了")])
self.assertEqual(len(calls), 2)
async def test_invalid_followup_and_invalid_final_content(self):
result, calls, engine = await self.run_loop(request(call()),
[{"tools": "invalid", "message": []}, {"tools": [], "message": [{"content": {"text": "wrong type"}}]}])
self.assertEqual(agent_loop._message_text(result), "好呀,白衬衫已经在衣柜里啦。")
self.assertEqual(len(engine.prompts), 2)
self.assertEqual(len(calls), 1)
async def test_latest_observation_keeps_all_results(self):
_, _, engine = await self.run_loop(request(call("一"), call("二")), [reply("好了")])
observation = engine.prompts[0].split("【你刚才执行了工具,以下是结构化结果】\n")[1].split("\n请根据")[0]
self.assertEqual(len(json.loads(observation)), 2)
async def test_invalid_model_fallback_status_is_not_done(self):
await self.run_loop(request(call()), [None, None])
self.assertEqual(Store.last.finished, "response_fallback")
async def test_finalization_cannot_execute_proposed_tool(self):
result, calls, engine = await self.run_loop(request(call()), [None, request(call("不该保存"))])
self.assertEqual(len(calls), 1)
self.assertEqual(result["tools"], [])
self.assertEqual(len(engine.prompts), 2)
async def test_leaked_final_response_gets_one_repair(self):
result, calls, engine = await self.run_loop(request(call()), [reply('{"id":1,"description":"private"}'), reply("谢谢你,我记住啦。")])
self.assertEqual(agent_loop._message_text(result), "谢谢你,我记住啦。")
self.assertEqual(len(engine.prompts), 2)
async def test_leaked_repair_rejected(self):
result, _, _ = await self.run_loop(request(call()), [None, reply('{"uid":1,"created_at":"today"}')])
self.assertNotIn("uid", str(result))
self.assertIn("已经在衣柜里", str(result))
async def test_empty_message_and_limits_finalize(self):
for limits, responses in [({}, [reply(""), reply("记住了")]),
({"MAX_ROUNDS": 1}, [request(call("后续")), reply("记住了")]),
({"MAX_CALLS": 1}, [request(call("后续")), reply("记住了")])]:
result, calls, engine = await self.run_loop(request(call()), responses, **limits)
self.assertEqual(agent_loop._message_text(result), "记住了")
self.assertEqual(len(calls), 1)
self.assertEqual(len(engine.prompts), 2)
async def test_mixed_failure_not_all_success(self):
good = ToolResult(True, "ok", '{"id":1,"uid":999}', data={"item": {"name": "裙子"}})
bad = ToolResult(False, "invalid_args", "衣物名称不能为空")
result, _, _ = await self.run_loop(request(call("一"), call("二")), [None], [good, bad])
text = agent_loop._message_text(result)
self.assertIn("还有操作没完成", text)
self.assertIn("衣物名称不能为空", text)
self.assertNotIn("这些衣服我记住啦", text)
self.assertNotIn("uid", text)
def test_list_names_only_and_write_fallbacks(self):
record = {"id": 4, "uid": 42, "name": "绿裙", "description": "SECRET", "created_at": "DATE"}
result = agent_loop._fallback_parsed([{"tool": "wardrobe", "action": "list", "ok": True, "safe_message": json.dumps(record), "data": {"items": [record]}}])
self.assertEqual(agent_loop._message_text(result), "衣柜里有:绿裙。")
for action, word in [("update", "改好"), ("delete", "移除")]:
text = agent_loop._message_text(agent_loop._fallback_parsed([{"tool": "wardrobe", "action": action, "ok": True, "safe_message": json.dumps(record)}]))
self.assertIn(word, text)
self.assertNotIn("SECRET", text)
def test_nonwardrobe_fallback_unchanged(self):
result = agent_loop._fallback_parsed([{"tool": "mail", "ok": True, "safe_message": "邮件已发送"}])
self.assertEqual(agent_loop._message_text(result), "已经处理好了呢。\n邮件已发送")
result = agent_loop._fallback_parsed([{"tool": "pixiv", "ok": False, "code": "send_failed", "safe_message": "发送失败 https://example.test"}])
self.assertEqual(agent_loop._message_text(result), "处理时遇到了具体问题:\npixiv(send_failed):发送失败 https://example.test")
if __name__ == "__main__":
unittest.main()