import tempfile import unittest from pathlib import Path from unittest.mock import patch from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import AIMessage, ToolMessage from langchain_core.outputs import ChatResult, ChatGeneration from graph.agent import AgentManager from graph.risk_task import invoke_risk_task from tools.risk_task_tools import create_risk_tools from tools.skills_scanner import discover_skills, scan_skills class ToolModel(BaseChatModel): """No network: still exercises the real LangChain agent/tool execution loop.""" skill_id: str = "risk/SKILL.md" card_count: int = 1 @property def _llm_type(self): return "test-tool-model" def bind_tools(self, tools, **kwargs): return self def _generate(self, messages, stop=None, run_manager=None, **kwargs): if not any(isinstance(m,ToolMessage) for m in messages): calls=[("list_data_cards",{}), *[("read_data_card",{"index":i}) for i in range(self.card_count)],("read_checks_and_rules",{}),("read_selected_skill",{"skill_id":self.skill_id})] message=AIMessage(content="",tool_calls=[{"name":name,"args":args,"id":str(i),"type":"tool_call"} for i,(name,args) in enumerate(calls)]) else: message=AIMessage(content="请核查财务负债;依据财务卡 v1 的 liabilities 字段。") return ChatResult(generations=[ChatGeneration(message=message)]) class CoreTests(unittest.TestCase): def context(self): return {"peer_datasets":[{"card":"财务卡","version":"v1","records":[{"values":{"liabilities":20}}]}],"scalar_values":{"metrics.debt_ratio":.8},"human_rules":[],"rule_findings":[]} def test_real_core_tool_cycle_and_isolation(self): manager=AgentManager() tools=create_risk_tools(self.context(),{}, {"risk/SKILL.md":"核实负债"}) output=manager.invoke_scoped("进行研判",model=ToolModel(),tools=tools,system_prompt="读取工具并给建议") self.assertEqual(output["engine"],"mini-openclaw/AgentManager") self.assertEqual(len([m for m in output["messages"] if m["type"]=="tool"]),4) self.assertIsNone(manager._llm) self.assertEqual(manager._tools,[]) self.assertNotIn("terminal",[t.name for t in tools]) def test_native_scanner_and_adapter(self): with tempfile.TemporaryDirectory() as root: base=Path(root);file=base/"skills"/"risk"/"SKILL.md";file.parent.mkdir(parents=True) file.write_text("---\nname: risk\ndescription: Check financial evidence\n---\nOnly suggest checks.",encoding="utf-8") self.assertEqual(discover_skills(base)[0]["id"],"risk/SKILL.md") self.assertIn("risk/SKILL.md",scan_skills(base)) with patch("graph.risk_task.ChatOpenAI",return_value=ToolModel()): output=invoke_risk_task({"native_skills":["risk/SKILL.md"]},self.context(),{},"http://127.0.0.1:11434/v1",base) self.assertIn("Only suggest",output["native_skills"][0]["content"]) self.assertTrue(output["messages"]) def test_tools_cannot_read_unselected_resources(self): tools={t.name:t for t in create_risk_tools(self.context(),{}, {})} self.assertIn("授权",tools["read_data_card"].invoke({"index":99})) self.assertIn("未被",tools["read_selected_skill"].invoke({"skill_id":"../../config.json"})) def test_snapshot_immutable_after_tool_creation(self): context=self.context();tools={t.name:t for t in create_risk_tools(context,{}, {})} context["peer_datasets"].clear() self.assertIn("财务卡",tools["read_data_card"].invoke({"index":0})) def test_api_to_core_and_persisted_trace(self): from fastapi import FastAPI from fastapi.testclient import TestClient from api import risk_agent_config as api with tempfile.TemporaryDirectory() as root, patch.object(api,"DB_PATH",Path(root)/"test.sqlite"), patch.dict("os.environ",{"XINGBANG_LOCAL_MODEL_URL":"http://127.0.0.1:11434/v1","XINGBANG_AUTH_USERS":"","XINGBANG_REQUIRE_AUTH":"0"}): app=FastAPI();app.include_router(api.router) with TestClient(app) as client, patch("graph.risk_task.ChatOpenAI",return_value=ToolModel(skill_id="risk_evidence/SKILL.md",card_count=3)): response=client.post("/api/xingbang/risk-agent/plans/plan-default/run",json={"project_id":"XB-2026-001"}) self.assertEqual(response.status_code,200,response.text) result=response.json() self.assertEqual(result["engine"],"mini-openclaw/AgentManager") trace=result["agent_traces"][0] self.assertEqual(len([m for m in trace["messages"] if m["type"]=="tool"]),6) saved=client.get("/api/xingbang/risk-agent/runs/history").json()[0] self.assertEqual(saved["agent_traces"],result["agent_traces"]) if __name__=="__main__":unittest.main()