AutoRpa/rpa_yisaiqi/tests/test_agent.py

241 lines
9.2 KiB
Python

from rpa_yisaiqi.agent import JobAgent
class FakeClient:
def __init__(self, job, *, tasks=None):
self.job = dict(job)
self.tasks = list(tasks or [])
self.calls = []
def get_job(self, job_id):
self.calls.append(("get_job", job_id))
return {"job": dict(self.job), "tasks": list(self.tasks)}
def start_node(self, job_id, node_code, **kwargs):
self.calls.append(("start", node_code, kwargs))
return {"ok": True}
def complete_node(self, job_id, node_code, payload):
self.calls.append(("complete", node_code, payload))
return {"ok": True, "next_node": "PRODUCT_MATCH"}
def fail_node(self, job_id, node_code, payload):
self.calls.append(("fail", node_code, payload))
return {"ok": True, "job_status": "FAILED_RETRYABLE"}
def ingest_items(self, job_id, rows, replace=True):
self.calls.append(("ingest_items", job_id, rows))
return {"ok": True, "inserted": len(rows)}
def match_run(self, job_id):
self.calls.append(("match_run", job_id))
return {"ok": True, "next_node": "CONTRACT_CREATE"}
def list_tasks(self, **kwargs):
self.calls.append(("list_tasks", kwargs))
return {"items": list(self.tasks)}
def submit_task(self, task_id, action, payload, submit_token, operator="agent"):
self.calls.append(("submit_task", task_id, action, payload, submit_token))
return {"ok": True, "next_node": "CONTRACT_CREATE"}
def check_contract_result(self, job_id, approved, **kwargs):
self.calls.append(("check_result", job_id, approved))
return {"ok": True, "next_node": "REBATE_HEADER"}
def retry_job(self, job_id):
self.calls.append(("retry", job_id))
return {"ok": True, "node_code": self.job["current_node_code"], "attempt_no": 2}
def test_agent_does_not_hardcode_next_node_after_order_fetch():
client = FakeClient({
"job_id": "JOB-1",
"current_node_code": "ORDER_FETCH",
"status": "PENDING",
"source_type": "YYC",
"supplier_name": "重庆亿昊药业",
"order_date": "2026-08-12",
})
agent = JobAgent(client, "JOB-1", runner_mode="mock")
result = agent.step()
assert result["next_node"] == "PRODUCT_MATCH"
assert ("complete", "ORDER_FETCH") == (client.calls[-1][0], client.calls[-1][1])
assert not any(call[0] == "complete" and call[1] == "OCR_EXTRACT" for call in client.calls)
def test_agent_runs_product_match_via_state_center_not_rpa_callback():
client = FakeClient({
"job_id": "JOB-1",
"current_node_code": "PRODUCT_MATCH",
"status": "PENDING",
"source_type": "YYC",
})
agent = JobAgent(client, "JOB-1", runner_mode="mock")
result = agent.step()
assert result["next_node"] == "CONTRACT_CREATE"
assert client.calls[1][0] == "match_run"
assert not any(call[0] in {"start", "complete"} for call in client.calls)
def test_agent_auto_human_submits_source_add_task_not_node_complete():
job_id = "YYC-重庆亿昊药业-2fa0-20260812-16abff"
client = FakeClient(
{
"job_id": job_id,
"current_node_code": "SOURCE_ADD",
"status": "WAITING_USER",
"source_type": "YYC",
},
tasks=[{"task_id": "T-1", "node_code": "SOURCE_ADD", "status": "OPEN"}],
)
agent = JobAgent(client, job_id, runner_mode="mock", auto_human=True)
result = agent.step()
assert result["next_node"] == "CONTRACT_CREATE"
assert client.calls[-1][:3] == ("submit_task", "T-1", "CONFIRM_AND_CONTINUE")
assert len(client.calls[-1][4]) <= 64
assert not any(call[0] == "complete" for call in client.calls)
def test_agent_stops_on_human_node_when_auto_human_disabled():
client = FakeClient({
"job_id": "JOB-1",
"current_node_code": "SOURCE_ADD",
"status": "WAITING_USER",
"source_type": "YYC",
})
agent = JobAgent(client, "JOB-1", runner_mode="mock", auto_human=False)
result = agent.step()
assert result["stopped"] == "WAITING_USER"
assert not any(call[0] == "submit_task" for call in client.calls)
def test_agent_contract_wait_uses_check_result_not_start_complete():
client = FakeClient({
"job_id": "JOB-1",
"current_node_code": "CONTRACT_APPROVE_WAIT",
"status": "WAITING_EXTERNAL",
"source_type": "YYC",
})
agent = JobAgent(client, "JOB-1", runner_mode="mock")
result = agent.step()
assert result["next_node"] == "REBATE_HEADER"
assert client.calls[-1][:2] == ("check_result", "JOB-1")
assert not any(call[0] in {"start", "complete"} for call in client.calls)
def test_agent_retries_failed_retryable_then_stops_if_still_failed():
client = FakeClient({
"job_id": "JOB-1",
"current_node_code": "ORDER_FETCH",
"status": "FAILED_RETRYABLE",
"source_type": "YYC",
})
agent = JobAgent(client, "JOB-1", runner_mode="mock", auto_retry=True)
result = agent.step()
assert ("retry", "JOB-1") in client.calls
assert result["attempt_no"] == 2
class ScriptedClient(FakeClient):
"""Advance through canned job states after each action. Sequence is not in the agent."""
def __init__(self, frames):
self.frames = list(frames)
super().__init__(self.frames[0]["job"], tasks=self.frames[0].get("tasks") or [])
def _advance(self):
if len(self.frames) > 1:
self.frames.pop(0)
self.job = dict(self.frames[0]["job"])
self.tasks = list(self.frames[0].get("tasks") or [])
def complete_node(self, job_id, node_code, payload):
nxt = self.frames[0].get("next_node")
self._advance()
self.calls.append(("complete", node_code, payload))
return {"ok": True, "next_node": nxt}
def match_run(self, job_id):
nxt = self.frames[0].get("next_node")
self._advance()
self.calls.append(("match_run", job_id))
return {"ok": True, "next_node": nxt}
def submit_task(self, task_id, action, payload, submit_token, operator="agent"):
nxt = self.frames[0].get("next_node")
self._advance()
self.calls.append(("submit_task", task_id, action, payload))
return {"ok": True, "next_node": nxt}
def check_contract_result(self, job_id, approved, **kwargs):
nxt = self.frames[0].get("next_node")
self._advance()
self.calls.append(("check_result", job_id, approved))
return {"ok": True, "next_node": nxt}
def test_agent_run_follows_state_center_until_success():
client = ScriptedClient([
{"job": {"job_id": "JOB-1", "current_node_code": "ORDER_FETCH", "status": "PENDING",
"source_type": "YYC", "supplier_name": "重庆亿昊药业", "order_date": "2026-08-12"},
"next_node": "PRODUCT_MATCH"},
{"job": {"job_id": "JOB-1", "current_node_code": "PRODUCT_MATCH", "status": "PENDING",
"source_type": "YYC"},
"next_node": "CONTRACT_CREATE"},
{"job": {"job_id": "JOB-1", "current_node_code": "CONTRACT_CREATE", "status": "PENDING",
"source_type": "YYC", "order_date": "2026-08-12"},
"next_node": "CONTRACT_APPROVE_WAIT"},
{"job": {"job_id": "JOB-1", "current_node_code": "CONTRACT_APPROVE_WAIT",
"status": "WAITING_EXTERNAL", "source_type": "YYC"},
"next_node": "REBATE_HEADER"},
{"job": {"job_id": "JOB-1", "current_node_code": "REBATE_HEADER", "status": "PENDING",
"source_type": "YYC"},
"next_node": "REBATE_IMPORT"},
{"job": {"job_id": "JOB-1", "current_node_code": "REBATE_IMPORT", "status": "PENDING",
"source_type": "YYC"},
"next_node": "REBATE_SUBMIT"},
{"job": {"job_id": "JOB-1", "current_node_code": "REBATE_SUBMIT", "status": "PENDING",
"source_type": "YYC"},
"next_node": "M5_CREATE_ORDER"},
{"job": {"job_id": "JOB-1", "current_node_code": "M5_CREATE_ORDER", "status": "PENDING",
"source_type": "YYC"},
"next_node": "M5_MANUAL_CONFIRM"},
{"job": {"job_id": "JOB-1", "current_node_code": "M5_MANUAL_CONFIRM", "status": "WAITING_USER",
"source_type": "YYC"},
"tasks": [{"task_id": "T-M5", "node_code": "M5_MANUAL_CONFIRM", "status": "OPEN"}],
"next_node": "M5_RECEIPT_NOTICE"},
{"job": {"job_id": "JOB-1", "current_node_code": "M5_RECEIPT_NOTICE", "status": "WAITING_USER",
"source_type": "YYC"},
"tasks": [{"task_id": "T-SH", "node_code": "M5_RECEIPT_NOTICE", "status": "OPEN"}],
"next_node": None},
{"job": {"job_id": "JOB-1", "current_node_code": "ARCHIVE_JOB", "status": "SUCCESS",
"source_type": "YYC"}},
])
agent = JobAgent(client, "JOB-1", runner_mode="mock", auto_human=True)
history = agent.run()
acted = [call[0] for call in client.calls if call[0] in {
"complete", "match_run", "check_result", "submit_task",
}]
assert acted == [
"complete", "match_run", "complete", "check_result",
"complete", "complete", "complete", "complete",
"submit_task", "submit_task",
]
assert history[-1]["stopped"] == "SUCCESS"
assert not any(call[1] == "OCR_EXTRACT" for call in client.calls if call[0] == "complete")