feat(intelligent-eval): logical delete for terminal evals
All checks were successful
CI / test (push) Successful in 3m24s
All checks were successful
CI / test (push) Successful in 3m24s
Add `deleted` terminal status: completed/cancelled/failed → deleted via
DELETE /api/intelligent-evals/{id} (idempotent, 409 for non-terminal).
Deleted evals are hidden from list, detail, stats, and all sub-resource
endpoints (sessions/report/decision-logs/config-snapshots); child tables
are untouched (audit-safe). Frontend shows a Popconfirm-guarded delete
button for terminal evals only.
This commit is contained in:
parent
d77a363f58
commit
2d2c5a2904
@ -28,7 +28,8 @@ def _log_to_dict(log: IntelligentEvalDecisionLogDB) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _require_eval(eval_id: str, session: Session) -> None:
|
def _require_eval(eval_id: str, session: Session) -> None:
|
||||||
if session.get(IntelligentEvalDB, eval_id) is None:
|
eval_db = session.get(IntelligentEvalDB, eval_id)
|
||||||
|
if eval_db is None or eval_db.status == IntelligentEvalStatus.DELETED.value:
|
||||||
raise LookupError(f"intelligent eval {eval_id} not found")
|
raise LookupError(f"intelligent eval {eval_id} not found")
|
||||||
|
|
||||||
|
|
||||||
@ -118,9 +119,7 @@ def count_decisions(eval_id: str, decision_type: str, session: Session) -> int:
|
|||||||
|
|
||||||
def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
||||||
"""List decision logs for an eval. Raises ``LookupError`` if eval not found."""
|
"""List decision logs for an eval. Raises ``LookupError`` if eval not found."""
|
||||||
eval_db = session.get(IntelligentEvalDB, eval_id)
|
_require_eval(eval_id, session)
|
||||||
if eval_db is None:
|
|
||||||
raise LookupError(f"intelligent eval {eval_id} not found")
|
|
||||||
|
|
||||||
logs = session.exec(
|
logs = session.exec(
|
||||||
select(IntelligentEvalDecisionLogDB)
|
select(IntelligentEvalDecisionLogDB)
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
→ failed
|
→ failed
|
||||||
pending_approval 可打回 → planning(附反馈)
|
pending_approval 可打回 → planning(附反馈)
|
||||||
executing 可取消 → cancelled
|
executing 可取消 → cancelled
|
||||||
|
completed / cancelled / failed 可删除 → deleted(逻辑删除)
|
||||||
|
|
||||||
非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。
|
非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。
|
||||||
"""
|
"""
|
||||||
@ -50,9 +51,10 @@ _TRANSITIONS: dict[IntelligentEvalStatus, set[IntelligentEvalStatus]] = {
|
|||||||
IntelligentEvalStatus.CANCELLED,
|
IntelligentEvalStatus.CANCELLED,
|
||||||
IntelligentEvalStatus.FAILED,
|
IntelligentEvalStatus.FAILED,
|
||||||
},
|
},
|
||||||
IntelligentEvalStatus.COMPLETED: set(),
|
IntelligentEvalStatus.COMPLETED: {IntelligentEvalStatus.DELETED},
|
||||||
IntelligentEvalStatus.CANCELLED: set(),
|
IntelligentEvalStatus.CANCELLED: {IntelligentEvalStatus.DELETED},
|
||||||
IntelligentEvalStatus.FAILED: set(),
|
IntelligentEvalStatus.FAILED: {IntelligentEvalStatus.DELETED},
|
||||||
|
IntelligentEvalStatus.DELETED: set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -197,6 +199,17 @@ def cancel(session: Session, eval_id: str) -> IntelligentEval:
|
|||||||
return _transition(repo, ev, IntelligentEvalStatus.CANCELLED)
|
return _transition(repo, ev, IntelligentEvalStatus.CANCELLED)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_eval(session: Session, eval_id: str) -> IntelligentEval:
|
||||||
|
"""逻辑删除:completed / cancelled / failed → deleted。幂等:已删除直接返回。"""
|
||||||
|
repo = IntelligentEvalRepository(session)
|
||||||
|
ev = repo._get_raw(eval_id)
|
||||||
|
if ev is None:
|
||||||
|
raise IntelligentEvalNotFoundError(f"intelligent eval {eval_id} not found")
|
||||||
|
if ev.status is IntelligentEvalStatus.DELETED:
|
||||||
|
return ev
|
||||||
|
return _transition(repo, ev, IntelligentEvalStatus.DELETED)
|
||||||
|
|
||||||
|
|
||||||
def submit_report(session: Session, eval_id: str, report: dict[str, Any]) -> IntelligentEval:
|
def submit_report(session: Session, eval_id: str, report: dict[str, Any]) -> IntelligentEval:
|
||||||
"""OpenClaw 提交结构化报告:executing → completed。
|
"""OpenClaw 提交结构化报告:executing → completed。
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class IntelligentEvalStatus(str, Enum):
|
|||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
DELETED = "deleted"
|
||||||
|
|
||||||
|
|
||||||
class IntelligentEvalSessionStatus(str, Enum):
|
class IntelligentEvalSessionStatus(str, Enum):
|
||||||
|
|||||||
@ -98,14 +98,25 @@ class IntelligentEvalRepository:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list_all(self) -> list[IntelligentEval]:
|
def list_all(self) -> list[IntelligentEval]:
|
||||||
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
statement = (
|
||||||
|
select(IntelligentEvalDB)
|
||||||
|
.where(IntelligentEvalDB.status != IntelligentEvalStatus.DELETED.value)
|
||||||
|
.order_by(IntelligentEvalDB.created_at.desc())
|
||||||
|
)
|
||||||
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
return [self._from_db(r) for r in self.session.exec(statement).all()]
|
||||||
|
|
||||||
def list_page(
|
def list_page(
|
||||||
self, offset: int, limit: int, status: Optional[str] = None
|
self, offset: int, limit: int, status: Optional[str] = None
|
||||||
) -> list[IntelligentEval]:
|
) -> list[IntelligentEval]:
|
||||||
"""Return one page of evals (created_at desc), optionally filtered by status."""
|
"""Return one page of evals (created_at desc), optionally filtered by status.
|
||||||
statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc())
|
|
||||||
|
Deleted evals are always excluded — the list API has no recycle bin.
|
||||||
|
"""
|
||||||
|
statement = (
|
||||||
|
select(IntelligentEvalDB)
|
||||||
|
.where(IntelligentEvalDB.status != IntelligentEvalStatus.DELETED.value)
|
||||||
|
.order_by(IntelligentEvalDB.created_at.desc())
|
||||||
|
)
|
||||||
if status:
|
if status:
|
||||||
statement = statement.where(IntelligentEvalDB.status == status)
|
statement = statement.where(IntelligentEvalDB.status == status)
|
||||||
statement = statement.offset(offset).limit(limit)
|
statement = statement.offset(offset).limit(limit)
|
||||||
@ -113,21 +124,32 @@ class IntelligentEvalRepository:
|
|||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
"""Total number of evaluations (for pagination metadata)."""
|
"""Total number of evaluations (for pagination metadata)."""
|
||||||
return self.session.exec(select(func.count()).select_from(IntelligentEvalDB)).one()
|
return self.session.exec(
|
||||||
|
select(func.count())
|
||||||
|
.select_from(IntelligentEvalDB)
|
||||||
|
.where(IntelligentEvalDB.status != IntelligentEvalStatus.DELETED.value)
|
||||||
|
).one()
|
||||||
|
|
||||||
def count_by_status(self) -> dict[str, int]:
|
def count_by_status(self) -> dict[str, int]:
|
||||||
"""Count evaluations per status (for the list page stat bar)."""
|
"""Count evaluations per status (for the list page stat bar)."""
|
||||||
rows = self.session.exec(
|
rows = self.session.exec(
|
||||||
select(IntelligentEvalDB.status, func.count(IntelligentEvalDB.id)).group_by(
|
select(IntelligentEvalDB.status, func.count(IntelligentEvalDB.id))
|
||||||
IntelligentEvalDB.status
|
.where(IntelligentEvalDB.status != IntelligentEvalStatus.DELETED.value)
|
||||||
)
|
.group_by(IntelligentEvalDB.status)
|
||||||
).all()
|
).all()
|
||||||
stats = {status.value: 0 for status in IntelligentEvalStatus}
|
stats = {status.value: 0 for status in IntelligentEvalStatus if status != IntelligentEvalStatus.DELETED}
|
||||||
for status, cnt in rows:
|
for status, cnt in rows:
|
||||||
stats[status] = cnt
|
stats[status] = cnt
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
def get(self, eval_id: str) -> Optional[IntelligentEval]:
|
||||||
|
db = self.session.get(IntelligentEvalDB, eval_id)
|
||||||
|
if db is None or db.status == IntelligentEvalStatus.DELETED.value:
|
||||||
|
return None
|
||||||
|
return self._from_db(db)
|
||||||
|
|
||||||
|
def _get_raw(self, eval_id: str) -> Optional[IntelligentEval]:
|
||||||
|
"""包含已删除评估的原样读取(仅供 delete 幂等检查使用)。"""
|
||||||
db = self.session.get(IntelligentEvalDB, eval_id)
|
db = self.session.get(IntelligentEvalDB, eval_id)
|
||||||
return self._from_db(db) if db else None
|
return self._from_db(db) if db else None
|
||||||
|
|
||||||
@ -144,7 +166,10 @@ class IntelligentEvalRepository:
|
|||||||
IntelligentEvalSessionDB.eval_id == IntelligentEvalDB.id,
|
IntelligentEvalSessionDB.eval_id == IntelligentEvalDB.id,
|
||||||
isouter=True,
|
isouter=True,
|
||||||
)
|
)
|
||||||
.where(IntelligentEvalDB.id == eval_id)
|
.where(
|
||||||
|
IntelligentEvalDB.id == eval_id,
|
||||||
|
IntelligentEvalDB.status != IntelligentEvalStatus.DELETED.value,
|
||||||
|
)
|
||||||
.order_by(IntelligentEvalSessionDB.created_at.asc())
|
.order_by(IntelligentEvalSessionDB.created_at.asc())
|
||||||
)
|
)
|
||||||
rows = self.session.exec(statement).all()
|
rows = self.session.exec(statement).all()
|
||||||
|
|||||||
@ -70,6 +70,16 @@ def _translate(exc: Exception) -> HTTPException:
|
|||||||
return HTTPException(status_code=409, detail=exc.reason)
|
return HTTPException(status_code=409, detail=exc.reason)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_eval_exists(session: Session, eval_id: str) -> None:
|
||||||
|
"""404 when the eval is missing or logically deleted."""
|
||||||
|
from agenteval.intelligent_eval.models import IntelligentEvalStatus
|
||||||
|
from agenteval.storage.db import IntelligentEvalDB
|
||||||
|
|
||||||
|
eval_db = session.get(IntelligentEvalDB, eval_id)
|
||||||
|
if eval_db is None or eval_db.status == IntelligentEvalStatus.DELETED.value:
|
||||||
|
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
||||||
|
|
||||||
|
|
||||||
def _eval_response(ev, session: Session) -> dict:
|
def _eval_response(ev, session: Session) -> dict:
|
||||||
"""Serialize one stable intelligent-evaluation read projection."""
|
"""Serialize one stable intelligent-evaluation read projection."""
|
||||||
|
|
||||||
@ -195,6 +205,16 @@ async def cancel(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
|||||||
return _eval_response(ev, session)
|
return _eval_response(ev, session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{eval_id}")
|
||||||
|
async def delete_eval(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||||||
|
"""逻辑删除已终态的评测。非终态返回 409,不存在返回 404,幂等。"""
|
||||||
|
try:
|
||||||
|
lifecycle.delete_eval(session, eval_id)
|
||||||
|
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||||||
|
raise _translate(exc) from exc
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{eval_id}/report")
|
@router.put("/{eval_id}/report")
|
||||||
async def submit_report(eval_id: str, request: SubmitReportRequest, session: Session = Depends(get_db)) -> dict:
|
async def submit_report(eval_id: str, request: SubmitReportRequest, session: Session = Depends(get_db)) -> dict:
|
||||||
try:
|
try:
|
||||||
@ -372,12 +392,8 @@ async def list_decision_logs(eval_id: str, session: Session = Depends(get_db)) -
|
|||||||
async def list_config_snapshots(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
async def list_config_snapshots(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||||||
"""List all config snapshots for an evaluation."""
|
"""List all config snapshots for an evaluation."""
|
||||||
from agenteval.intelligent_eval import config_snapshot
|
from agenteval.intelligent_eval import config_snapshot
|
||||||
from agenteval.storage.db import IntelligentEvalDB
|
|
||||||
|
|
||||||
# Verify eval exists
|
_require_eval_exists(session, eval_id)
|
||||||
eval_db = session.get(IntelligentEvalDB, eval_id)
|
|
||||||
if eval_db is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
|
||||||
|
|
||||||
snapshots = config_snapshot.list_snapshots(eval_id, session)
|
snapshots = config_snapshot.list_snapshots(eval_id, session)
|
||||||
return {
|
return {
|
||||||
@ -404,12 +420,8 @@ async def list_config_snapshots(eval_id: str, session: Session = Depends(get_db)
|
|||||||
async def get_config_snapshot(eval_id: str, snapshot_id: str, session: Session = Depends(get_db)) -> dict:
|
async def get_config_snapshot(eval_id: str, snapshot_id: str, session: Session = Depends(get_db)) -> dict:
|
||||||
"""Get a single config snapshot."""
|
"""Get a single config snapshot."""
|
||||||
from agenteval.intelligent_eval import config_snapshot
|
from agenteval.intelligent_eval import config_snapshot
|
||||||
from agenteval.storage.db import IntelligentEvalDB
|
|
||||||
|
|
||||||
# Verify eval exists
|
_require_eval_exists(session, eval_id)
|
||||||
eval_db = session.get(IntelligentEvalDB, eval_id)
|
|
||||||
if eval_db is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
|
||||||
|
|
||||||
snapshot = config_snapshot.get_snapshot(snapshot_id, session)
|
snapshot = config_snapshot.get_snapshot(snapshot_id, session)
|
||||||
if snapshot is None or snapshot.eval_id != eval_id:
|
if snapshot is None or snapshot.eval_id != eval_id:
|
||||||
@ -443,12 +455,8 @@ async def compare_config_snapshots(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Compare two config snapshots and return differences."""
|
"""Compare two config snapshots and return differences."""
|
||||||
from agenteval.intelligent_eval import config_snapshot
|
from agenteval.intelligent_eval import config_snapshot
|
||||||
from agenteval.storage.db import IntelligentEvalDB
|
|
||||||
|
|
||||||
# Verify eval exists
|
_require_eval_exists(session, eval_id)
|
||||||
eval_db = session.get(IntelligentEvalDB, eval_id)
|
|
||||||
if eval_db is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
|
||||||
|
|
||||||
# Get both snapshots
|
# Get both snapshots
|
||||||
snapshot1 = config_snapshot.get_snapshot(request.snapshot_id_1, session)
|
snapshot1 = config_snapshot.get_snapshot(request.snapshot_id_1, session)
|
||||||
|
|||||||
@ -607,7 +607,7 @@ export const campaignsApi = {
|
|||||||
|
|
||||||
export type IntelligentEvalStatus =
|
export type IntelligentEvalStatus =
|
||||||
| 'draft' | 'planning' | 'pending_approval'
|
| 'draft' | 'planning' | 'pending_approval'
|
||||||
| 'executing' | 'completed' | 'cancelled' | 'failed'
|
| 'executing' | 'completed' | 'cancelled' | 'failed' | 'deleted'
|
||||||
|
|
||||||
export type IntelligentEvalSessionStatus = 'running' | 'completed' | 'failed' | 'expired'
|
export type IntelligentEvalSessionStatus = 'running' | 'completed' | 'failed' | 'expired'
|
||||||
|
|
||||||
@ -826,6 +826,7 @@ export const intelligentEvalsApi = {
|
|||||||
reject: (id: string, feedback: string) =>
|
reject: (id: string, feedback: string) =>
|
||||||
api.post<IntelligentEval>(`/intelligent-evals/${id}/reject`, { feedback }),
|
api.post<IntelligentEval>(`/intelligent-evals/${id}/reject`, { feedback }),
|
||||||
cancel: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/cancel`),
|
cancel: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/cancel`),
|
||||||
|
remove: (id: string) => api.delete<{ ok: boolean }>(`/intelligent-evals/${id}`),
|
||||||
listSessions: (id: string) =>
|
listSessions: (id: string) =>
|
||||||
api.get<{ sessions: IntelligentEvalSession[] }>(`/intelligent-evals/${id}/sessions`),
|
api.get<{ sessions: IntelligentEvalSession[] }>(`/intelligent-evals/${id}/sessions`),
|
||||||
listMessages: (id: string, sessionId: string) =>
|
listMessages: (id: string, sessionId: string) =>
|
||||||
|
|||||||
@ -8,8 +8,14 @@ export const EVAL_STATUS: Record<IntelligentEvalStatus, { label: string; color:
|
|||||||
completed: { label: '已完成', color: 'success' },
|
completed: { label: '已完成', color: 'success' },
|
||||||
cancelled: { label: '已取消', color: 'default' },
|
cancelled: { label: '已取消', color: 'default' },
|
||||||
failed: { label: '失败', color: 'error' },
|
failed: { label: '失败', color: 'error' },
|
||||||
|
deleted: { label: '已删除', color: 'default' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 终态集合(与后端 lifecycle 状态机一致):仅终态评估可被逻辑删除
|
||||||
|
export const TERMINAL_STATUSES: ReadonlySet<IntelligentEvalStatus> = new Set<IntelligentEvalStatus>([
|
||||||
|
'completed', 'cancelled', 'failed',
|
||||||
|
])
|
||||||
|
|
||||||
export const SESSION_STATUS: Record<IntelligentEvalSessionStatus, { label: string; color: string }> = {
|
export const SESSION_STATUS: Record<IntelligentEvalSessionStatus, { label: string; color: string }> = {
|
||||||
running: { label: '进行中', color: 'processing' },
|
running: { label: '进行中', color: 'processing' },
|
||||||
completed: { label: '已完成', color: 'success' },
|
completed: { label: '已完成', color: 'success' },
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
import type { TabsProps } from 'antd'
|
import type { TabsProps } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import {
|
import {
|
||||||
PlusOutlined, ReloadOutlined, StopOutlined, UnorderedListOutlined,
|
DeleteOutlined, PlusOutlined, ReloadOutlined, StopOutlined, UnorderedListOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import FormDrawer from '../components/FormDrawer'
|
import FormDrawer from '../components/FormDrawer'
|
||||||
import PageWrapper from '../components/PageWrapper'
|
import PageWrapper from '../components/PageWrapper'
|
||||||
@ -15,7 +15,7 @@ import ExecutionProcess from '../components/intelligent_eval/ExecutionProcess'
|
|||||||
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
|
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
|
||||||
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
|
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
|
||||||
import EvalReport from '../components/intelligent_eval/EvalReport'
|
import EvalReport from '../components/intelligent_eval/EvalReport'
|
||||||
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
import { EVAL_STATUS, TERMINAL_STATUSES } from '../components/intelligent_eval/status'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
import {
|
import {
|
||||||
intelligentEvalsApi, targetsApi,
|
intelligentEvalsApi, targetsApi,
|
||||||
@ -143,6 +143,12 @@ export default function IntelligentEvalsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
await intelligentEvalsApi.remove(id)
|
||||||
|
message.success('已删除')
|
||||||
|
void reloadList()
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<IntelligentEval> = [
|
const columns: ColumnsType<IntelligentEval> = [
|
||||||
{
|
{
|
||||||
title: '名称', dataIndex: 'name', key: 'name', width: 300, ellipsis: true,
|
title: '名称', dataIndex: 'name', key: 'name', width: 300, ellipsis: true,
|
||||||
@ -184,9 +190,22 @@ export default function IntelligentEvalsPage() {
|
|||||||
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作', key: 'action', width: 88, fixed: 'right' as const,
|
title: '操作', key: 'action', width: 130, fixed: 'right' as const,
|
||||||
render: (_, ev) => (
|
render: (_, ev) => (
|
||||||
|
<Space size={4}>
|
||||||
<Button size="small" onClick={() => openDetail(ev.id)}>查看</Button>
|
<Button size="small" onClick={() => openDetail(ev.id)}>查看</Button>
|
||||||
|
{TERMINAL_STATUSES.has(ev.status) && (
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除"
|
||||||
|
description="删除后不可恢复,确定要删除该评估吗?"
|
||||||
|
onConfirm={() => handleDelete(ev.id)}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Button size="small" danger icon={<DeleteOutlined />} onClick={(e) => e.stopPropagation()} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -598,3 +598,103 @@ class TestExecutionProgress:
|
|||||||
async def test_unknown_eval_returns_404(self, client, seeded_db):
|
async def test_unknown_eval_returns_404(self, client, seeded_db):
|
||||||
resp = await client.get("/api/intelligent-evals/missing/execution-progress")
|
resp = await client.get("/api/intelligent-evals/missing/execution-progress")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeleteEval:
|
||||||
|
async def _create_completed(self, client) -> str:
|
||||||
|
eval_id = await _create_executing_eval(client)
|
||||||
|
resp = await client.put(f"/api/intelligent-evals/{eval_id}/report", json={"report": _report_payload()})
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
return eval_id
|
||||||
|
|
||||||
|
async def _create_cancelled(self, client) -> str:
|
||||||
|
eval_id = await _create_executing_eval(client)
|
||||||
|
resp = await client.post(f"/api/intelligent-evals/{eval_id}/cancel")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
return eval_id
|
||||||
|
|
||||||
|
async def test_delete_completed_eval_hides_from_list_and_stats(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
assert resp.json() == {"ok": True}
|
||||||
|
|
||||||
|
list_resp = await client.get("/api/intelligent-evals", params={"page": 1})
|
||||||
|
data = list_resp.json()
|
||||||
|
assert all(ev["id"] != eval_id for ev in data["intelligent_evals"])
|
||||||
|
assert data["total"] == 0
|
||||||
|
assert data["stats"]["completed"] == 0
|
||||||
|
|
||||||
|
async def test_delete_cancelled_eval(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_cancelled(client)
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
|
||||||
|
async def test_delete_planning_eval_returns_409(self, client, seeded_db):
|
||||||
|
ev = await _create_eval(client) # planning state
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/intelligent-evals/{ev['id']}")
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
async def test_delete_executing_eval_returns_409(self, client, seeded_db):
|
||||||
|
eval_id = await _create_executing_eval(client)
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
async def test_delete_unknown_eval_returns_404(self, client, seeded_db):
|
||||||
|
resp = await client.delete("/api/intelligent-evals/missing")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
async def test_delete_twice_is_idempotent(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
|
||||||
|
await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
resp = await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
|
||||||
|
async def test_deleted_eval_detail_returns_404(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
async def test_deleted_eval_subresources_return_404(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
|
||||||
|
sessions_resp = await client.get(f"/api/intelligent-evals/{eval_id}/sessions")
|
||||||
|
assert sessions_resp.status_code == 404
|
||||||
|
report_resp = await client.get(f"/api/intelligent-evals/{eval_id}/report")
|
||||||
|
assert report_resp.status_code == 404
|
||||||
|
markdown_resp = await client.get(f"/api/intelligent-evals/{eval_id}/report/markdown")
|
||||||
|
assert markdown_resp.status_code == 404
|
||||||
|
logs_resp = await client.get(f"/api/intelligent-evals/{eval_id}/decision-logs")
|
||||||
|
assert logs_resp.status_code == 404
|
||||||
|
snapshots_resp = await client.get(f"/api/intelligent-evals/{eval_id}/config-snapshots")
|
||||||
|
assert snapshots_resp.status_code == 404
|
||||||
|
progress_resp = await client.get(f"/api/intelligent-evals/{eval_id}/execution-progress")
|
||||||
|
assert progress_resp.status_code == 404
|
||||||
|
|
||||||
|
async def test_deleted_eval_rejects_new_decision_logs(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/api/intelligent-evals/{eval_id}/decision-logs",
|
||||||
|
json={"decision_type": "wait", "reason": "late agent report", "cron_id": "w-1"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
async def test_list_with_status_deleted_returns_empty(self, client, seeded_db):
|
||||||
|
eval_id = await self._create_completed(client)
|
||||||
|
await client.delete(f"/api/intelligent-evals/{eval_id}")
|
||||||
|
|
||||||
|
resp = await client.get("/api/intelligent-evals", params={"page": 1, "status": "deleted"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["intelligent_evals"] == []
|
||||||
|
assert data["total"] == 0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user