241 lines
9.2 KiB
Python
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")
|