2026-09-08 01:07:59 +08:00
|
|
|
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
|
2026-09-15 10:31:50 +08:00
|
|
|
from tools.skills_scanner import discover_skills, install_skill, scan_skills
|
2026-09-08 01:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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"])
|
2026-09-15 10:31:50 +08:00
|
|
|
def test_archived_native_skills_are_hidden_and_folder_import_is_managed(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as root:
|
|
|
|
|
base=Path(root)
|
|
|
|
|
archived=base/"skills"/"old"/"SKILL.md";archived.parent.mkdir(parents=True)
|
|
|
|
|
archived.write_text("---\nname: old-skill\ndescription: Old unrelated method\nstatus: archived\n---\nOld",encoding="utf-8")
|
|
|
|
|
content="---\nname: leasing-check\ndisplay_name: 金租核查\ndescription: Review leasing evidence\ninputs: [lease.total_value]\n---\nRead evidence first."
|
|
|
|
|
installed=install_skill(base,content)
|
|
|
|
|
self.assertEqual(installed["id"],"imported/leasing-check/SKILL.md")
|
|
|
|
|
self.assertEqual([s["name"] for s in discover_skills(base)],["leasing-check"])
|
|
|
|
|
with self.assertRaises(FileExistsError):install_skill(base,content)
|
2026-09-08 01:07:59 +08:00
|
|
|
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()
|