refactor: simplify task and history services
Remove retired query paths and repository compatibility branches, and share task test fixtures. Cancel pending questions and resolve their notifications when recovering expired runs.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from tests.support.operational_task_repository import TaskRepository, _context, _task_document, _active_run
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
@@ -114,11 +115,6 @@ async def test_access_request_read_uses_table_after_migration():
|
||||
assert connection.execute.await_count == 2
|
||||
|
||||
|
||||
def _context(user_id=None, *, system_role="user"):
|
||||
return ProjectContext(
|
||||
project_id=uuid4(), project_code="test", user_id=user_id or uuid4(),
|
||||
project_role="member", system_role=system_role,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -232,208 +228,12 @@ async def test_targeted_notification_cannot_be_acknowledged_without_permission()
|
||||
assert exc.value.code == "PERMISSION_DENIED"
|
||||
|
||||
|
||||
def _task_document(context, now, *, lease=None):
|
||||
task_id = uuid4()
|
||||
return OperationalTaskSummary(
|
||||
execution_scope="production",
|
||||
project_id=context.project_id,
|
||||
task_id=task_id,
|
||||
task_type="event_investigation",
|
||||
workflow="water_quality_complaint",
|
||||
status="running",
|
||||
priority="normal",
|
||||
revision=2,
|
||||
execution_generation=1,
|
||||
execution_request_id=uuid4(),
|
||||
title="调查",
|
||||
summary="验证状态机",
|
||||
source={"kind": "operator", "event_kind": "water_quality_complaint"},
|
||||
trigger={"kind": "manual", "occurred_at": now, "detail": "test"},
|
||||
scheduled_for=None,
|
||||
agent_session_id="session-1",
|
||||
agent_lease_expires_at=lease,
|
||||
current_plan_revision=None,
|
||||
current_report_revision=None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
class TaskRepository:
|
||||
def __init__(self, task, *, action=None):
|
||||
self.task = task
|
||||
self.action = action
|
||||
self.requests = {}
|
||||
self.evidence = {}
|
||||
self.feedback = {}
|
||||
self.plans = []
|
||||
self.reports = []
|
||||
self.progress = {}
|
||||
self.notifications = []
|
||||
self.locks = []
|
||||
self.access_requests = {}
|
||||
|
||||
@asynccontextmanager
|
||||
async def transaction(self):
|
||||
yield self
|
||||
|
||||
async def lock(self, key):
|
||||
self.locks.append(key)
|
||||
|
||||
async def get_request(self, scope, request_id):
|
||||
return self.requests.get((scope, request_id))
|
||||
|
||||
async def save_request(self, scope, request_id, request_digest, response):
|
||||
self.requests[(scope, request_id)] = {
|
||||
"request_digest": request_digest,
|
||||
"response": response,
|
||||
}
|
||||
|
||||
async def get_task(self, task_id):
|
||||
return self.task if task_id == self.task["task_id"] else None
|
||||
|
||||
async def update_task(self, task):
|
||||
self.task = task
|
||||
|
||||
async def get_latest_plan(self, _task_id):
|
||||
return self.plans[-1] if self.plans else None
|
||||
|
||||
async def get_latest_report(self, _task_id):
|
||||
return self.reports[-1] if self.reports else None
|
||||
|
||||
async def get_latest_progress(self, task_id, generation):
|
||||
return self.progress.get((task_id, generation))
|
||||
|
||||
async def save_progress(self, task_id, snapshot):
|
||||
self.progress[(task_id, snapshot["execution_generation"])] = snapshot
|
||||
|
||||
async def get_report(self, _task_id, revision):
|
||||
return next((item for item in self.reports if item["revision"] == revision), None)
|
||||
|
||||
async def list_reports(self, _task_id, limit, offset):
|
||||
return list(reversed(self.reports))[offset:offset + limit]
|
||||
|
||||
async def insert_plan(self, _task_id, snapshot):
|
||||
self.plans.append(snapshot)
|
||||
|
||||
async def insert_report(self, _task_id, snapshot):
|
||||
self.reports.append(snapshot)
|
||||
|
||||
async def list_evidence(self, _task_id):
|
||||
return list(self.evidence.values())
|
||||
|
||||
async def get_evidence(self, _task_id, evidence_id):
|
||||
return self.evidence.get(evidence_id)
|
||||
|
||||
async def insert_evidence(self, _task_id, evidence):
|
||||
self.evidence[evidence["evidence_id"]] = evidence
|
||||
|
||||
async def list_access_requests(self, _task_id):
|
||||
return list(self.access_requests.values())
|
||||
|
||||
async def get_access_request(self, _task_id, access_request_id):
|
||||
return self.access_requests.get(access_request_id)
|
||||
|
||||
async def insert_access_request(self, _task_id, document):
|
||||
self.access_requests[document["access_request_id"]] = document
|
||||
|
||||
async def update_access_request(self, _task_id, document, expected_version):
|
||||
assert document["version"] == expected_version + 1
|
||||
self.access_requests[document["access_request_id"]] = document
|
||||
|
||||
async def list_actions(self, _task_id):
|
||||
return [self.action] if self.action else []
|
||||
|
||||
async def get_action(self, _task_id, action_id):
|
||||
if self.action and self.action["action_id"] == action_id:
|
||||
return self.action
|
||||
return None
|
||||
|
||||
async def update_action(self, _task_id, action):
|
||||
self.action = action
|
||||
|
||||
async def insert_action(self, _task_id, action):
|
||||
self.action = action
|
||||
|
||||
async def insert_notification(self, notification):
|
||||
self.notifications.append(notification)
|
||||
|
||||
async def resolve_notifications(
|
||||
self, task_id, *, kinds, subject_type, subject_id, resolved_at, resolution_reason
|
||||
):
|
||||
resolved = []
|
||||
for notification in self.notifications:
|
||||
if (
|
||||
notification["task_id"] == task_id
|
||||
and notification["kind"] in kinds
|
||||
and notification.get("lifecycle_status", "active") == "active"
|
||||
and notification.get("subject_type") == subject_type
|
||||
and notification.get("subject_id") == subject_id
|
||||
):
|
||||
notification.update(
|
||||
lifecycle_status="resolved",
|
||||
resolved_at=resolved_at.isoformat(),
|
||||
resolution_reason=resolution_reason,
|
||||
)
|
||||
resolved.append(notification.copy())
|
||||
return resolved
|
||||
|
||||
async def get_feedback(self, task_id, source_event_id):
|
||||
return self.feedback.get((task_id, source_event_id))
|
||||
|
||||
async def upsert_feedback(self, task_id, feedback, accepted_at):
|
||||
self.feedback[(task_id, feedback["source_event_id"])] = {
|
||||
"source_revision": feedback["source_revision"],
|
||||
"document": feedback,
|
||||
"accepted_at": accepted_at,
|
||||
}
|
||||
|
||||
|
||||
class RunTaskRepository(TaskRepository):
|
||||
def __init__(self, task, run):
|
||||
super().__init__(task)
|
||||
self.run = run
|
||||
self.events = []
|
||||
|
||||
async def get_active_run(self, task_id):
|
||||
if task_id == self.task["task_id"] and self.run["status"] in {"queued", "claimed", "running"}:
|
||||
return self.run
|
||||
return None
|
||||
|
||||
async def get_run(self, run_id):
|
||||
return self.run if run_id == self.run["run_id"] else None
|
||||
|
||||
async def require_execution_storage(self):
|
||||
return None
|
||||
|
||||
async def update_run(self, run, expected_revision):
|
||||
assert run["revision"] == expected_revision + 1
|
||||
self.run = run
|
||||
|
||||
async def insert_run(self, run):
|
||||
self.run = run
|
||||
|
||||
async def append_event(self, **event):
|
||||
self.events.append(event)
|
||||
return len(self.events)
|
||||
|
||||
async def current_event_cursor(self):
|
||||
return len(self.events)
|
||||
|
||||
async def list_presentation_events(self, task_id, after, through, limit):
|
||||
return [{"event_seq": index, **event} for index, event in enumerate(self.events, 1)
|
||||
if after < index <= through and event["topic"] == "operational.presentation.requested"
|
||||
and event["aggregate_id"] == task_id][:limit]
|
||||
|
||||
|
||||
def _active_run(task, now):
|
||||
return OperationalTaskRun(
|
||||
run_id=uuid4(), task_id=task["task_id"], generation=task["execution_generation"],
|
||||
execution_request_id=task["execution_request_id"], status="running", revision=2,
|
||||
agent_session_id="session-1", lease_expires_at=now + timedelta(minutes=1),
|
||||
heartbeat_at=now, started_at=now, dispatch_attempts=0,
|
||||
next_dispatch_at=now, created_at=now, updated_at=now,
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -457,7 +257,7 @@ async def test_run_claim_persists_agent_instance_ownership(task_scope, worker_sc
|
||||
run = _active_run(task, now)
|
||||
run.update(status="queued", revision=1, agent_session_id=None,
|
||||
lease_expires_at=None, heartbeat_at=None, started_at=None)
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now,
|
||||
execution_scope=worker_scope)
|
||||
|
||||
@@ -488,7 +288,7 @@ async def test_progress_can_publish_without_conclusion_and_replaces_the_complete
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
first = AgentArtifactsCommand.model_validate({
|
||||
"request_id": str(uuid4()), "source_session_id": "session-1", "run_id": repository.run["run_id"],
|
||||
@@ -535,7 +335,7 @@ async def test_reading_publications_reject_stale_execution_ownership(kind, inval
|
||||
run["lease_expires_at"] = now.isoformat()
|
||||
elif invalid == "finished":
|
||||
run["status"] = "succeeded"
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
identity = {"request_id": uuid4(), "run_id": uuid4() if invalid == "run" else run["run_id"],
|
||||
"source_session_id": "other-session" if invalid == "session" else "session-1"}
|
||||
@@ -556,7 +356,7 @@ async def test_progress_unknown_evidence_does_not_publish_or_authorize_actions()
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
with pytest.raises(DomainError):
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
@@ -582,7 +382,7 @@ async def test_presentation_cursor_and_event_are_read_only_and_idempotent():
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
baseline = await service.presentation_cursor(task["task_id"])
|
||||
assert baseline.event_cursor == 0 and str(baseline.active_run.run_id) == repository.run["run_id"]
|
||||
@@ -608,7 +408,7 @@ async def test_replay_investigation_can_publish_chart_and_propose_work_order():
|
||||
task["source"]["replay_context"] = {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": now.isoformat(), "batch_sequence": 1,
|
||||
}
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot(
|
||||
source="query_scada_readings", source_ref="mcp:frozen-scada", queried_at=now,
|
||||
@@ -645,7 +445,7 @@ async def test_comparison_chart_publication_is_validated_at_both_write_boundarie
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot(
|
||||
source="query_scada_readings", source_ref="mcp:comparison", queried_at=now,
|
||||
@@ -715,7 +515,7 @@ async def test_replay_cannot_propose_approve_or_submit_device_commands(operation
|
||||
required_execution_permission="command.execute", approval_status="pending",
|
||||
execution_status="not_submitted", proposed_by="principal:test", proposed_at=now,
|
||||
)
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.action = action.model_dump(mode="json")
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
dispatcher = SimpleNamespace(dispatch=AsyncMock())
|
||||
@@ -743,7 +543,7 @@ async def test_unified_commands_record_canonical_events_without_mutating_task(pa
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
command = TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=repository.run["run_id"], source_session_id="session-1", command=payload,
|
||||
@@ -762,7 +562,7 @@ async def test_saved_mcp_coverage_envelope_publishes_groups_without_agent_geomet
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
coverage = build_node_coverage(
|
||||
[{"group_id": "one", "title": "第一组", "node_ids": ["A"]},
|
||||
@@ -795,7 +595,7 @@ async def test_overlay_event_uses_stored_coordinates_and_rejects_untrusted_or_ov
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
snapshot = TaskDataSnapshot(source="scada_devices", source_ref="verified-points", queried_at=now,
|
||||
origin="system_data", truncated=False,
|
||||
@@ -844,7 +644,7 @@ async def test_completed_investigation_resumes_once_for_linked_new_field_feedbac
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
_seed_concluded_report(repository, now)
|
||||
action = await service.propose_action(task["task_id"], ActionProposalCommand(
|
||||
@@ -891,7 +691,7 @@ async def test_completed_investigation_rejects_unlinked_or_initial_child_feedbac
|
||||
task.update(status="completed", lifecycle_status="completed")
|
||||
run = _active_run(task, now)
|
||||
run["status"] = "succeeded"
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
with pytest.raises(DomainError) as unlinked:
|
||||
await service.external_feedback(task["task_id"], ExternalFeedbackCommand(
|
||||
@@ -923,7 +723,7 @@ async def test_original_complaint_feedback_advances_its_source_revision():
|
||||
source={"kind": "external_system", "external_id": source_id, "external_revision": 1})
|
||||
run = _active_run(task, now)
|
||||
run["status"] = "succeeded"
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
command = ExternalFeedbackCommand(request_id=uuid4(), source_event_id=f"peripheral_sample:{source_id}",
|
||||
source_revision=2, status="in_progress", occurred_at=now, detail="新投诉信息")
|
||||
@@ -940,7 +740,7 @@ async def test_run_heartbeat_does_not_change_task_business_revision():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
updated = await service.heartbeat_run(
|
||||
@@ -962,7 +762,7 @@ async def test_run_heartbeat_rejects_a_different_agent_instance_with_same_sessio
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
run = _active_run(task, now)
|
||||
run["agent_instance_id"] = "agent-a"
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
with pytest.raises(DomainError) as mismatch:
|
||||
@@ -982,7 +782,7 @@ async def test_complete_without_same_generation_artifacts_fails_run_explicitly()
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
completed = await service.complete_run(
|
||||
@@ -1014,7 +814,7 @@ async def test_external_feedback_coalesces_into_an_already_queued_run():
|
||||
status="queued", revision=1, agent_session_id=None,
|
||||
lease_expires_at=None, heartbeat_at=None, started_at=None,
|
||||
)
|
||||
repository = RunTaskRepository(task, run)
|
||||
repository = TaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
result = await service.external_feedback(
|
||||
@@ -1069,7 +869,7 @@ async def test_scada_final_report_requires_complete_current_input_not_selected_d
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
task["workflow"] = "scada_anomaly"
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
data = {"devices": [{"device_id": "P1"}], "readings": [], "coverage": {
|
||||
"mode": "all_scada", "query_complete": True,
|
||||
@@ -1124,7 +924,7 @@ async def test_insufficient_data_exits_successfully_and_blocks_actions_until_new
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
command = AgentArtifactsCommand(
|
||||
request_id=uuid4(), source_session_id="session-1",
|
||||
@@ -1179,7 +979,7 @@ async def test_insufficient_report_rejects_inconsistent_context(defect, expected
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
report = _insufficient_report()
|
||||
node = {"id": "read", "title": "查询", "owner": "agent", "status": "skipped", "detail": "关键数据不足"}
|
||||
@@ -1229,7 +1029,7 @@ async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(
|
||||
task["source"]["replay_context"] = {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": incident.isoformat(), "batch_sequence": 3,
|
||||
}
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.connection = object()
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
monkeypatch.setattr(service, "_resolve_investigation_alerts", AsyncMock())
|
||||
@@ -1308,8 +1108,10 @@ async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(
|
||||
assert repository.evidence[revised_id]["data_snapshot"]["data"]["verified_through"] == (returned + timedelta(minutes=30)).isoformat()
|
||||
evidence_id = revised_id
|
||||
|
||||
repository.task.update(status="running", agent_session_id="session-1")
|
||||
repository.run = _active_run(repository.task, now)
|
||||
await service.claim_run(task["task_id"], repository.run["run_id"], AgentRunCommand(
|
||||
request_id=repository.run["execution_request_id"], expected_revision=repository.run["revision"],
|
||||
source_session_id="session-1",
|
||||
))
|
||||
_seed_concluded_report(repository, now)
|
||||
with pytest.raises(DomainError) as rejected:
|
||||
await service.complete_run(task["task_id"], repository.run["run_id"], AgentRunCommand(
|
||||
@@ -1334,7 +1136,7 @@ async def test_plan_only_keeps_lease_until_same_generation_report_closes_run():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
planned = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
@@ -1369,7 +1171,7 @@ async def test_interim_report_cannot_finish_even_with_a_completed_plan():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
plan = _waiting_plan()
|
||||
plan["nodes"][0]["status"] = "completed"
|
||||
@@ -1410,7 +1212,11 @@ async def test_report_is_not_blocked_by_a_historical_plan():
|
||||
request_id=uuid4(), source_session_id="session-1", report=_current_report(),
|
||||
))
|
||||
assert result.task.latest_report.execution_generation == 2
|
||||
assert result.task.status == "completed"
|
||||
assert result.task.status == "running"
|
||||
completed = await service.complete_run(task["task_id"], repository.run["run_id"], AgentRunCommand(
|
||||
request_id=uuid4(), expected_revision=repository.run["revision"], source_session_id="session-1",
|
||||
))
|
||||
assert completed.status == "completed"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -1418,7 +1224,7 @@ async def test_report_and_action_without_plan_finish_with_real_approval_pending(
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
request_id=uuid4(), source_session_id="session-1", report=_current_report(),
|
||||
@@ -1441,7 +1247,7 @@ async def test_insufficient_data_without_graph_or_node_ends_the_round():
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
report = _insufficient_report()
|
||||
report["data_gaps"][0].pop("node_id")
|
||||
@@ -1463,7 +1269,7 @@ async def test_report_history_and_notifications_keep_selected_and_current_refere
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
for conclusion in ["第一次判断", "更新后的判断"]:
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
@@ -1499,7 +1305,7 @@ async def test_final_report_archives_its_progress_diagram_across_new_rounds():
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
diagram = {"title": "调查过程", "mermaid": "flowchart LR\nA[核查] --> B[形成判断]",
|
||||
"node_refs": [{"node_id": "B", "label": "形成判断", "status": "running"}]}
|
||||
@@ -1526,7 +1332,7 @@ async def test_legacy_report_diagram_reads_only_matching_prepublication_progress
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
repository.reports.append({
|
||||
"revision": 1, "execution_generation": 1, "source_session_id": "session-1",
|
||||
@@ -1559,7 +1365,7 @@ async def test_optional_diagram_cannot_forge_business_references(node_ref, code)
|
||||
now = datetime(2026, 10, 9, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
with pytest.raises(DomainError) as invalid:
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
@@ -1575,7 +1381,7 @@ async def test_action_proposal_keeps_running_lease_until_plan_and_report_close_r
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
request_id=uuid4(), source_session_id="session-1", plan=_waiting_plan(),
|
||||
@@ -1624,7 +1430,7 @@ async def test_rejected_action_requeues_task_in_a_new_execution_generation():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now).copy())
|
||||
repository = TaskRepository(task, _active_run(task, now).copy())
|
||||
service = OperationalTaskService(
|
||||
repository,
|
||||
context,
|
||||
@@ -1673,7 +1479,7 @@ async def test_action_decision_rejects_a_stale_expected_version():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(
|
||||
repository,
|
||||
context,
|
||||
@@ -1737,7 +1543,7 @@ async def test_task_access_grant_is_project_scoped_reviewable_and_requeues():
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
initial_revision = task["revision"]
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
agent = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
request = await agent.request_access(task["task_id"], TaskAccessRequestCreate(
|
||||
request_id=uuid4(), source_session_id="session-1", node_id="read-scada",
|
||||
@@ -1771,7 +1577,7 @@ async def test_task_access_request_rejects_a_second_pending_request():
|
||||
now = datetime(2026, 9, 24, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
await service.request_access(task["task_id"], TaskAccessRequestCreate(
|
||||
request_id=uuid4(), source_session_id="session-1", node_id="read-scada",
|
||||
@@ -1794,7 +1600,7 @@ async def test_task_access_request_fails_closed_before_project_migration():
|
||||
now = datetime(2026, 9, 24, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.access_requests_available = AsyncMock(return_value=False)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
@@ -1813,7 +1619,7 @@ async def test_completed_gate_requires_access_request_decision():
|
||||
now = datetime(2026, 9, 24, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
request = await service.request_access(task["task_id"], TaskAccessRequestCreate(
|
||||
request_id=uuid4(), source_session_id="session-1", node_id="read-scada",
|
||||
@@ -1850,7 +1656,7 @@ async def test_work_order_draft_edit_creates_new_review_version(replay):
|
||||
task["source"]["replay_context"] = {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": now.isoformat(), "batch_sequence": 1,
|
||||
}
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, {OPERATIONAL_ACTION_WORK_ORDER_APPROVE}, clock=lambda: now)
|
||||
_seed_concluded_report(repository, now)
|
||||
action = await service.propose_action(task["task_id"], ActionProposalCommand(
|
||||
@@ -1881,7 +1687,7 @@ async def test_one_click_work_order_confirmation_replays_without_duplicate_dispa
|
||||
task["source"]["replay_context"] = {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": now.isoformat(), "batch_sequence": 1,
|
||||
}
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, {
|
||||
OPERATIONAL_ACTION_WORK_ORDER_APPROVE, OPERATIONAL_ACTION_WORK_ORDER_EXECUTE,
|
||||
}, clock=lambda: now)
|
||||
@@ -2125,7 +1931,7 @@ async def test_expired_owned_lease_is_rejected_with_stable_code():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now - timedelta(seconds=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.run["lease_expires_at"] = now - timedelta(seconds=1)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
|
||||
@@ -2253,7 +2059,7 @@ async def test_interactive_report_publication_is_idempotent_and_versioned_with_e
|
||||
now = datetime(2026, 10, 20, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
report = {**rich_report(), "stage": "final"}
|
||||
command = AgentArtifactsCommand(
|
||||
@@ -2290,7 +2096,7 @@ async def test_presentation_history_is_task_scoped_paginated_and_readable_after_
|
||||
now = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now)
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.run["status"] = "succeeded"
|
||||
for index in range(4):
|
||||
await repository.append_event(
|
||||
@@ -2322,7 +2128,7 @@ async def test_final_report_rejected_while_original_run_has_pending_calculations
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
pending_job = str(uuid4())
|
||||
repository.pending_calculation_jobs = AsyncMock(return_value=[pending_job])
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
@@ -2346,7 +2152,7 @@ async def test_completion_rejected_until_calculations_reach_terminal_state():
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.pending_calculation_jobs = AsyncMock(return_value=[str(uuid4())])
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
command = AgentRunCommand(request_id=uuid4(), expected_revision=repository.run['revision'], source_session_id='session-1')
|
||||
@@ -2380,7 +2186,7 @@ async def test_interim_report_can_explain_progress_while_calculation_is_pending(
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
repository.pending_calculation_jobs = AsyncMock(return_value=[str(uuid4())])
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
result = await service.publish_artifacts(task['task_id'], AgentArtifactsCommand(
|
||||
|
||||
Reference in New Issue
Block a user