feat(workbench): keep only actionable notifications
This commit is contained in:
@@ -498,7 +498,9 @@ class OperationalTaskNotification(StrictModel):
|
|||||||
task_id: UUID
|
task_id: UUID
|
||||||
kind: Literal[
|
kind: Literal[
|
||||||
"task_created", "action_pending", "report_updated", "task_completed",
|
"task_created", "action_pending", "report_updated", "task_completed",
|
||||||
"task_failed", "run_delivery_degraded"
|
"task_failed", "run_delivery_degraded", "access_approval_pending",
|
||||||
|
"action_approval_pending", "human_input_required",
|
||||||
|
"external_coordination_failed"
|
||||||
]
|
]
|
||||||
title: NonEmptyStr
|
title: NonEmptyStr
|
||||||
description: NonEmptyStr
|
description: NonEmptyStr
|
||||||
|
|||||||
@@ -179,9 +179,6 @@ class OperationalTaskService:
|
|||||||
run = self._new_run(task_id, 1, now)
|
run = self._new_run(task_id, 1, now)
|
||||||
await self.repo.insert_run(_json(run))
|
await self.repo.insert_run(_json(run))
|
||||||
await self._run_event(run, now, queued=True)
|
await self._run_event(run, now, queued=True)
|
||||||
await self._notify(
|
|
||||||
task_id, "task_created", command.title, "工况任务已进入待执行队列", now
|
|
||||||
)
|
|
||||||
await self.repo.save_request(
|
await self.repo.save_request(
|
||||||
scope, request_id, digest(body), {"task_id": str(task_id)}
|
scope, request_id, digest(body), {"task_id": str(task_id)}
|
||||||
)
|
)
|
||||||
@@ -602,10 +599,6 @@ class OperationalTaskService:
|
|||||||
occurred_at=now,
|
occurred_at=now,
|
||||||
metadata={"task_id": str(task_id), "run_id": str(active_run["run_id"])},
|
metadata={"task_id": str(task_id), "run_id": str(active_run["run_id"])},
|
||||||
)
|
)
|
||||||
await self._notify(
|
|
||||||
task_id, "report_updated", command.report.title,
|
|
||||||
command.report.current_conclusion, now,
|
|
||||||
)
|
|
||||||
if current_plan is None:
|
if current_plan is None:
|
||||||
stored_plan = await self.repo.get_latest_plan(str(task_id))
|
stored_plan = await self.repo.get_latest_plan(str(task_id))
|
||||||
if stored_plan is not None:
|
if stored_plan is not None:
|
||||||
@@ -704,7 +697,7 @@ class OperationalTaskService:
|
|||||||
)
|
)
|
||||||
await self._task_event(task, now)
|
await self._task_event(task, now)
|
||||||
await self._notify(
|
await self._notify(
|
||||||
task_id, "action_pending", task["title"], command.reason, now,
|
task_id, "action_approval_pending", task["title"], command.reason, now,
|
||||||
required_permission=approval_permission,
|
required_permission=approval_permission,
|
||||||
)
|
)
|
||||||
await self._save_request(
|
await self._save_request(
|
||||||
@@ -783,9 +776,27 @@ class OperationalTaskService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
task = await self.detail(task_id)
|
task = await self.detail(task_id)
|
||||||
outcome = await dispatcher.dispatch(
|
try:
|
||||||
context=self.context, task=task, action=action, credential=credential
|
outcome = await dispatcher.dispatch(
|
||||||
)
|
context=self.context, task=task, action=action, credential=credential
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
async with self.repo.transaction():
|
||||||
|
await self.repo.lock(f"action:{action_id}")
|
||||||
|
current = await self._action(task_id, action_id)
|
||||||
|
if (
|
||||||
|
current.version == action.version
|
||||||
|
and current.execution_status == "unknown"
|
||||||
|
):
|
||||||
|
await self._notify(
|
||||||
|
task_id,
|
||||||
|
"external_coordination_failed",
|
||||||
|
task.title,
|
||||||
|
"外部系统提交中断,执行结果未知,请核对回执",
|
||||||
|
self._now(),
|
||||||
|
required_permission=action.required_execution_permission,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
async with self.repo.transaction():
|
async with self.repo.transaction():
|
||||||
await self.repo.lock(f"request:{scope}:{command.request_id}")
|
await self.repo.lock(f"request:{scope}:{command.request_id}")
|
||||||
@@ -801,14 +812,24 @@ class OperationalTaskService:
|
|||||||
"execution_detail": outcome.detail,
|
"execution_detail": outcome.detail,
|
||||||
})
|
})
|
||||||
await self.repo.update_action(str(task_id), _json(current))
|
await self.repo.update_action(str(task_id), _json(current))
|
||||||
await self._action_event(current, self._now())
|
outcome_recorded_at = self._now()
|
||||||
|
await self._action_event(current, outcome_recorded_at)
|
||||||
|
if outcome.status in {"unknown", "failed", "unsupported"}:
|
||||||
|
await self._notify(
|
||||||
|
task_id,
|
||||||
|
"external_coordination_failed",
|
||||||
|
task.title,
|
||||||
|
outcome.detail,
|
||||||
|
outcome_recorded_at,
|
||||||
|
required_permission=action.required_execution_permission,
|
||||||
|
)
|
||||||
if outcome.status in {"succeeded", "failed", "unsupported"}:
|
if outcome.status in {"succeeded", "failed", "unsupported"}:
|
||||||
task_document = await self._task(task_id)
|
task_document = await self._task(task_id)
|
||||||
await self._requeue_task(task_document, self._now())
|
await self._requeue_task(task_document, outcome_recorded_at)
|
||||||
await self.repo.update_task(
|
await self.repo.update_task(
|
||||||
_task_json(OperationalTaskSummary.model_validate(task_document))
|
_task_json(OperationalTaskSummary.model_validate(task_document))
|
||||||
)
|
)
|
||||||
await self._task_event(task_document, self._now())
|
await self._task_event(task_document, outcome_recorded_at)
|
||||||
await self._save_request(scope, command.request_id, body, {"action": _json(current)})
|
await self._save_request(scope, command.request_id, body, {"action": _json(current)})
|
||||||
return current
|
return current
|
||||||
|
|
||||||
@@ -842,19 +863,34 @@ class OperationalTaskService:
|
|||||||
current = await self._action(task_id, action_id)
|
current = await self._action(task_id, action_id)
|
||||||
require(current.version == action.version, "VERSION_CONFLICT",
|
require(current.version == action.version, "VERSION_CONFLICT",
|
||||||
"Action changed while reconciling", status=409)
|
"Action changed while reconciling", status=409)
|
||||||
|
previous_execution_status = current.execution_status
|
||||||
current = current.model_copy(update={
|
current = current.model_copy(update={
|
||||||
"version": current.version + 1, "execution_status": outcome.status,
|
"version": current.version + 1, "execution_status": outcome.status,
|
||||||
"receipt": outcome.receipt, "execution_detail": outcome.detail,
|
"receipt": outcome.receipt, "execution_detail": outcome.detail,
|
||||||
})
|
})
|
||||||
await self.repo.update_action(str(task_id), _json(current))
|
await self.repo.update_action(str(task_id), _json(current))
|
||||||
await self._action_event(current, self._now())
|
outcome_recorded_at = self._now()
|
||||||
|
await self._action_event(current, outcome_recorded_at)
|
||||||
|
if (
|
||||||
|
outcome.status in {"unknown", "failed", "unsupported"}
|
||||||
|
and outcome.status != previous_execution_status
|
||||||
|
):
|
||||||
|
task = await self._task(task_id)
|
||||||
|
await self._notify(
|
||||||
|
task_id,
|
||||||
|
"external_coordination_failed",
|
||||||
|
task["title"],
|
||||||
|
outcome.detail,
|
||||||
|
outcome_recorded_at,
|
||||||
|
required_permission=action.required_execution_permission,
|
||||||
|
)
|
||||||
if outcome.status in {"succeeded", "failed", "unsupported"}:
|
if outcome.status in {"succeeded", "failed", "unsupported"}:
|
||||||
task_document = await self._task(task_id)
|
task_document = await self._task(task_id)
|
||||||
await self._requeue_task(task_document, self._now())
|
await self._requeue_task(task_document, outcome_recorded_at)
|
||||||
await self.repo.update_task(
|
await self.repo.update_task(
|
||||||
_task_json(OperationalTaskSummary.model_validate(task_document))
|
_task_json(OperationalTaskSummary.model_validate(task_document))
|
||||||
)
|
)
|
||||||
await self._task_event(task_document, self._now())
|
await self._task_event(task_document, outcome_recorded_at)
|
||||||
await self._save_request(
|
await self._save_request(
|
||||||
scope, command.request_id, body, {"action": _json(current)}
|
scope, command.request_id, body, {"action": _json(current)}
|
||||||
)
|
)
|
||||||
@@ -961,9 +997,9 @@ class OperationalTaskService:
|
|||||||
occurred_at=now,
|
occurred_at=now,
|
||||||
metadata={"task_id": str(task_id), "namespace": "operational-task"},
|
metadata={"task_id": str(task_id), "namespace": "operational-task"},
|
||||||
)
|
)
|
||||||
if task["status"] in {"completed", "failed"}:
|
if task["status"] == "failed":
|
||||||
await self._notify(
|
await self._notify(
|
||||||
task_id, "task_completed" if task["status"] == "completed" else "task_failed",
|
task_id, "task_failed",
|
||||||
task["title"], command.detail, now,
|
task["title"], command.detail, now,
|
||||||
)
|
)
|
||||||
response = {
|
response = {
|
||||||
@@ -1195,9 +1231,6 @@ class OperationalTaskService:
|
|||||||
"version": schedule.version + 1, "updated_at": now,
|
"version": schedule.version + 1, "updated_at": now,
|
||||||
})
|
})
|
||||||
await self.repo.advance_schedule(_json(next_schedule))
|
await self.repo.advance_schedule(_json(next_schedule))
|
||||||
await self._notify(
|
|
||||||
task_id, "task_created", task.title, "定时流程已进入待执行队列", now
|
|
||||||
)
|
|
||||||
await self._run_event(run, now, queued=True)
|
await self._run_event(run, now, queued=True)
|
||||||
await self._task_event(_task_json(task), now)
|
await self._task_event(_task_json(task), now)
|
||||||
task_ids.append(task_id)
|
task_ids.append(task_id)
|
||||||
@@ -1283,14 +1316,33 @@ class OperationalTaskService:
|
|||||||
await self.repo.update_task(_task_json(OperationalTaskSummary.model_validate(task)))
|
await self.repo.update_task(_task_json(OperationalTaskSummary.model_validate(task)))
|
||||||
await self._run_event(run_model, now)
|
await self._run_event(run_model, now)
|
||||||
await self._task_event(task, now)
|
await self._task_event(task, now)
|
||||||
if lifecycle in {"completed", "failed"}:
|
if lifecycle == "failed":
|
||||||
await self._notify(
|
await self._notify(
|
||||||
task_id,
|
task_id,
|
||||||
"task_completed" if lifecycle == "completed" else "task_failed",
|
"task_failed",
|
||||||
task["title"],
|
task["title"],
|
||||||
"工况任务流程状态已更新",
|
failure_detail or "工况任务执行失败",
|
||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
|
elif lifecycle == "waiting_human":
|
||||||
|
pending_actions = any(
|
||||||
|
action.approval_status == "pending" for action in actions
|
||||||
|
)
|
||||||
|
pending_access = any(
|
||||||
|
request.get("status") == "pending"
|
||||||
|
for request in (
|
||||||
|
await self.repo.list_access_requests(str(task_id))
|
||||||
|
if hasattr(self.repo, "list_access_requests") else []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not pending_actions and not pending_access:
|
||||||
|
await self._notify(
|
||||||
|
task_id,
|
||||||
|
"human_input_required",
|
||||||
|
task["title"],
|
||||||
|
"任务等待人工补充信息或业务确认",
|
||||||
|
now,
|
||||||
|
)
|
||||||
await self._save_request(scope, command.request_id, body, {"task_id": str(task_id)})
|
await self._save_request(scope, command.request_id, body, {"task_id": str(task_id)})
|
||||||
return await self.detail(task_id)
|
return await self.detail(task_id)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"contracts": {
|
"contracts": {
|
||||||
"server": {
|
"server": {
|
||||||
"file": "server-v1.openapi.json",
|
"file": "server-v1.openapi.json",
|
||||||
"sha256": "c37f5626398e1762584a844bb908ca4f4fb68e203f4d37a489561669a434312b"
|
"sha256": "4a7ef16dc9ebf9731359c314ab600730dc9b06dd4b0396c0e4e847d57063ee12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3624,7 +3624,11 @@
|
|||||||
"report_updated",
|
"report_updated",
|
||||||
"task_completed",
|
"task_completed",
|
||||||
"task_failed",
|
"task_failed",
|
||||||
"run_delivery_degraded"
|
"run_delivery_degraded",
|
||||||
|
"access_approval_pending",
|
||||||
|
"action_approval_pending",
|
||||||
|
"human_input_required",
|
||||||
|
"external_coordination_failed"
|
||||||
],
|
],
|
||||||
"title": "Kind",
|
"title": "Kind",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 异常处置通知路由
|
||||||
|
|
||||||
|
“异常处置”只展示需要当前用户判断、授权或处置的活动事项。通知表仍是用户收件箱,不替代 `domain_event_log`;列表按当前用户权限和已读状态过滤。
|
||||||
|
|
||||||
|
## 进入异常处置的类型
|
||||||
|
|
||||||
|
| 分类 | 通知类型 | 严重度 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 任务异常 | `task_failed`、`run_delivery_degraded` | 严重 | 任务失败、连续超时、产物不完整或 Agent 投递持续失败 |
|
||||||
|
| 人工审核 | `access_approval_pending`、`action_approval_pending` | 警告 | 数据访问授权、工单派发或设备动作审批 |
|
||||||
|
| 业务告警 | 活动的 `workbench.alerts` | 告警定义 | 压力、流量、水质、设备或通信等仍未解除的业务异常 |
|
||||||
|
| 人工补充 | `human_input_required` | 警告 | 计划存在等待人工处理的节点,且没有更具体的待审批动作或访问申请 |
|
||||||
|
| 外部协同异常 | `external_coordination_failed` | 严重 | 外部动作回执未知、提交失败或适配器不支持,需要人工核对 |
|
||||||
|
|
||||||
|
兼容读取旧 `action_pending`,并按“人工审核”展示。它不再由新代码写入。
|
||||||
|
|
||||||
|
## 不进入异常处置的类型
|
||||||
|
|
||||||
|
- `task_created`:任务创建后直接进入工况列表。
|
||||||
|
- `report_updated`:报告修订只更新任务详情。
|
||||||
|
- `task_completed`:正常完成不产生异常事项。
|
||||||
|
- queued、running 等普通状态变化和短暂 SSE 重连不创建用户通知。
|
||||||
|
|
||||||
|
新写入不再产生上述三类普通生命周期通知。`020_prune_non_actionable_notifications.sql` 只删除历史 `task_created`、`report_updated` 和 `task_completed` 通知及其级联已读记录,并为受影响项目写入一次批量失效事件。任务、报告、动作、业务告警和事件历史均不删除。
|
||||||
|
|
||||||
|
## 数据库应用范围
|
||||||
|
|
||||||
|
2026-09-24 本轮只允许在 `tjwater_v2` 执行 `020`。执行前必须确认 `current_database()`、完成备份,并在事务后核对:普通生命周期通知为零,审批、任务异常和外部协同异常仍保留。其他项目数据库待 `tjwater_v2` 验证通过后再单独评估,不批量应用。
|
||||||
|
|
||||||
|
本次实际执行结果:删除 16 条普通生命周期通知,保留 2 条旧 `action_pending` 审批通知,活动业务告警仍为 0 条,并写入 `event_seq=39` 的批量失效事件。通知表和用户已读表的迁移前逻辑备份位于 `/home/zhifu/TJWaterBackups/notification-routing-20260924-gZvjsn/tjwater_v2-notifications-before-020.json`,SHA-256 为 `046d6c3a0888b981791f3f39ee1972f304aab18a7e6af035da742c069b97a369`。系统缺少 `pg_dump` 客户端,因此本轮没有生成新的整库 custom dump;上一轮整库备份未被覆盖。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- Remove historical task lifecycle notices that do not require user action.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TEMP TABLE notification_cleanup_projects ON COMMIT DROP AS
|
||||||
|
SELECT
|
||||||
|
project_id,
|
||||||
|
count(*)::bigint AS removed_count
|
||||||
|
FROM workbench.operational_task_notifications
|
||||||
|
WHERE document->>'kind' IN ('task_created', 'report_updated', 'task_completed')
|
||||||
|
GROUP BY project_id;
|
||||||
|
|
||||||
|
DELETE FROM workbench.operational_task_notifications
|
||||||
|
WHERE document->>'kind' IN ('task_created', 'report_updated', 'task_completed');
|
||||||
|
|
||||||
|
WITH inserted_events AS (
|
||||||
|
INSERT INTO workbench.domain_event_log (
|
||||||
|
project_id,
|
||||||
|
event_id,
|
||||||
|
topic,
|
||||||
|
aggregate_type,
|
||||||
|
aggregate_id,
|
||||||
|
aggregate_revision,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
project_id,
|
||||||
|
(
|
||||||
|
substr(md5(project_id::text || ':notification-routing-v1'), 1, 8)
|
||||||
|
|| '-' || substr(md5(project_id::text || ':notification-routing-v1'), 9, 4)
|
||||||
|
|| '-5' || substr(md5(project_id::text || ':notification-routing-v1'), 14, 3)
|
||||||
|
|| '-a' || substr(md5(project_id::text || ':notification-routing-v1'), 18, 3)
|
||||||
|
|| '-' || substr(md5(project_id::text || ':notification-routing-v1'), 21, 12)
|
||||||
|
)::uuid,
|
||||||
|
'operational.notification.changed',
|
||||||
|
'operational_notification_batch',
|
||||||
|
project_id::text,
|
||||||
|
removed_count,
|
||||||
|
jsonb_build_object(
|
||||||
|
'change', 'batch_pruned',
|
||||||
|
'reason', 'non_actionable_notification_cleanup',
|
||||||
|
'removed_count', removed_count
|
||||||
|
)
|
||||||
|
FROM notification_cleanup_projects
|
||||||
|
ON CONFLICT (project_id, event_id) DO NOTHING
|
||||||
|
RETURNING project_id, event_seq
|
||||||
|
)
|
||||||
|
SELECT pg_notify('workbench_events', project_id::text || ':' || event_seq::text)
|
||||||
|
FROM inserted_events;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -132,11 +132,16 @@ async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back(
|
|||||||
assert member_notifications
|
assert member_notifications
|
||||||
assert all(item.task_id == task.task_id for item in member_notifications)
|
assert all(item.task_id == task.task_id for item in member_notifications)
|
||||||
action_pending = next(
|
action_pending = next(
|
||||||
item for item in member_notifications if item.kind == "action_pending"
|
item for item in member_notifications
|
||||||
|
if item.kind == "action_approval_pending"
|
||||||
)
|
)
|
||||||
task_created = next(
|
human_input = next(
|
||||||
item for item in member_notifications if item.kind == "task_created"
|
item for item in member_notifications
|
||||||
|
if item.kind == "human_input_required"
|
||||||
)
|
)
|
||||||
|
assert not any(item.kind in {
|
||||||
|
"task_created", "report_updated", "task_completed"
|
||||||
|
} for item in member_notifications)
|
||||||
assert action_pending.required_permission == action.required_approval_permission
|
assert action_pending.required_permission == action.required_approval_permission
|
||||||
|
|
||||||
viewer_context = ProjectContext(
|
viewer_context = ProjectContext(
|
||||||
@@ -153,7 +158,7 @@ async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back(
|
|||||||
viewer_notifications, _ = await viewer_service.list_notifications(
|
viewer_notifications, _ = await viewer_service.list_notifications(
|
||||||
unread_only=True, limit=100, offset=0
|
unread_only=True, limit=100, offset=0
|
||||||
)
|
)
|
||||||
assert task_created.notification_id in {
|
assert human_input.notification_id in {
|
||||||
item.notification_id for item in viewer_notifications
|
item.notification_id for item in viewer_notifications
|
||||||
}
|
}
|
||||||
assert action_pending.notification_id not in {
|
assert action_pending.notification_id not in {
|
||||||
@@ -161,9 +166,9 @@ async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back(
|
|||||||
}
|
}
|
||||||
|
|
||||||
acknowledged = await service.acknowledge_notification(
|
acknowledged = await service.acknowledge_notification(
|
||||||
task_created.notification_id,
|
human_input.notification_id,
|
||||||
NotificationAcknowledgeCommand(
|
NotificationAcknowledgeCommand(
|
||||||
request_id=uuid4(), expected_version=task_created.version
|
request_id=uuid4(), expected_version=human_input.version
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert acknowledged.status == "read"
|
assert acknowledged.status == "read"
|
||||||
@@ -179,21 +184,21 @@ async def test_postgres_task_artifacts_approval_and_project_isolation_roll_back(
|
|||||||
)
|
)
|
||||||
persisted_created = next(
|
persisted_created = next(
|
||||||
item for item in persisted_notifications
|
item for item in persisted_notifications
|
||||||
if item.notification_id == task_created.notification_id
|
if item.notification_id == human_input.notification_id
|
||||||
)
|
)
|
||||||
assert persisted_created.status == "read"
|
assert persisted_created.status == "read"
|
||||||
assert persisted_created.version == task_created.version + 1
|
assert persisted_created.version == human_input.version + 1
|
||||||
assert persisted_created.read_by == f"principal:{context.user_id}"
|
assert persisted_created.read_by == f"principal:{context.user_id}"
|
||||||
unread_notifications, _ = await persisted_service.list_notifications(
|
unread_notifications, _ = await persisted_service.list_notifications(
|
||||||
unread_only=True, limit=100, offset=0
|
unread_only=True, limit=100, offset=0
|
||||||
)
|
)
|
||||||
assert task_created.notification_id not in {
|
assert human_input.notification_id not in {
|
||||||
item.notification_id for item in unread_notifications
|
item.notification_id for item in unread_notifications
|
||||||
}
|
}
|
||||||
viewer_after_member_ack, _ = await viewer_service.list_notifications(
|
viewer_after_member_ack, _ = await viewer_service.list_notifications(
|
||||||
unread_only=True, limit=100, offset=0
|
unread_only=True, limit=100, offset=0
|
||||||
)
|
)
|
||||||
assert task_created.notification_id in {
|
assert human_input.notification_id in {
|
||||||
item.notification_id for item in viewer_after_member_ack
|
item.notification_id for item in viewer_after_member_ack
|
||||||
}
|
}
|
||||||
action = await service.decide_action(task.task_id, action.action_id, ActionDecisionCommand(
|
action = await service.decide_action(task.task_id, action.action_id, ActionDecisionCommand(
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ async def test_complete_without_same_generation_artifacts_fails_run_explicitly()
|
|||||||
assert repository.run["failure_code"] == "ARTIFACT_INCOMPLETE"
|
assert repository.run["failure_code"] == "ARTIFACT_INCOMPLETE"
|
||||||
assert completed.lifecycle_status == "failed"
|
assert completed.lifecycle_status == "failed"
|
||||||
assert completed.active_run is None
|
assert completed.active_run is None
|
||||||
|
assert [item["kind"] for item in repository.notifications] == ["task_failed"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -451,6 +452,9 @@ async def test_plan_only_keeps_lease_until_same_generation_report_closes_run():
|
|||||||
)
|
)
|
||||||
assert completed.status == "waiting_human"
|
assert completed.status == "waiting_human"
|
||||||
assert completed.agent_lease_expires_at is None
|
assert completed.agent_lease_expires_at is None
|
||||||
|
assert [item["kind"] for item in repository.notifications] == [
|
||||||
|
"human_input_required"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -521,6 +525,9 @@ async def test_action_proposal_keeps_running_lease_until_plan_and_report_close_r
|
|||||||
)
|
)
|
||||||
assert closed.status == "waiting_human"
|
assert closed.status == "waiting_human"
|
||||||
assert closed.actions[0].action_id == action.action_id
|
assert closed.actions[0].action_id == action.action_id
|
||||||
|
assert [item["kind"] for item in repository.notifications] == [
|
||||||
|
"action_approval_pending"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@@ -786,6 +793,9 @@ async def test_interrupted_submit_replays_unknown_without_dispatching_twice():
|
|||||||
assert dispatcher.calls == 1
|
assert dispatcher.calls == 1
|
||||||
assert replay.execution_status == "unknown"
|
assert replay.execution_status == "unknown"
|
||||||
assert replay.execution_request_id is not None
|
assert replay.execution_request_id is not None
|
||||||
|
assert [item["kind"] for item in repository.notifications] == [
|
||||||
|
"external_coordination_failed"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|||||||
Reference in New Issue
Block a user