mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
e10c4f23fc
来自 2026-09 的 UI/UX 审查(142 条)。本提交是前两波,第三波(记录页接入时间轴、跨页互链、check-ui 更新、全站扫尾)另行提交。 地基 - 设计令牌:全局规则和 .btn/.field/.chip 放进 CSS 层,Tailwind 工具类重新生效,选中态、开启态不再被吞掉;字号阶梯; 辅助文字在所有底色上 >= 4.5:1;状态色专用,节点类型色与之拉开色相;「跟随系统」真的跟随,首帧不闪深色;新字标与 favicon - 基础组件:confirmDialog / promptDialog 取代原生对话框;toast 出错不自动消失、可复制、读屏可播报;离线横幅与断线恢复; 九种剪影的状态徽标;唯一一份状态文案(lib/status)、格式化(lib/format)、术语(lib/terms)、报错人话(lib/errors) - 运行态内核:事件折成航迹(run/trace.ts),可投影到任意时刻;取消 / 失败 / 服务重启后统一收尾,不再永远转圈; run.status 随事件回写;token 事件不再堆进 events - 后端:协作成员逐个完成即发事件,调度者决策有事件;时长分执行 / 墙钟 / 等人;API 时间带时区; 中断的运行也发 stream.end;运行列表可筛选翻页;删除运行 / 工作流有保护;运行默认值接通;可测未保存的配置; 报错说清发生了什么、为什么、怎么办 功能 - 画布:状态四重编码、固定高度遥测槽、按缩放三档、运行胶囊与 HUD、航迹时间轴与回放、执行轨迹边、 等待闸门、跟随执行、正式运行只读、一次性时刻(开场点亮、回扫、阻断级联、出具印章) - 编辑:撤销重做、版本历史、问题面板可定位、三栏可收起、模板与表达式字段区分、变量血缘、快捷键、带搜索的选择器 - 回答流 / 问数据:贴底才跟随滚动、工件下钻、出具横幅列出缺口、答案可复制导出、按会话的忙闲、 答案不再在 2000 字处截断、重启后这一轮会收尾 - 记录页:待审批 / 运行中 / 失败页签、筛选翻页、实时接流、排错路径指到节点、封存凭证常驻 - 管理页:新的「数据」页、写操作失败都有反馈、弹窗里先测连接、统一持久的连接状态 - 外壳:导航改版、⌘K 命令面板、? 快捷键帮助、404、页面标题与 favicon 状态点、后台提醒 验证(沙箱):tsc 干净,pytest 579 passed,14 个检查脚本中 13 个全绿;check-ui 剩 1 项旧断言(按旧日期格式找运行行),第三波更新。
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
"""删除工作流时的两道口子:还在跑的运行、以及中文署名。
|
|
|
|
- 运行记录的 workflow_id 是 ON DELETE SET NULL:删掉工作流,运行记录本身留着,
|
|
封存核对用的是运行自带的图快照,不受影响。但还在跑的那次运行会失去归属,
|
|
界面上再也回不到它的画布——和删除运行记录一样,先停再删。
|
|
- 发布署名从 X-Actor 请求头来。浏览器请求头只能是 Latin-1,前端按
|
|
encodeURIComponent 编码后发,发布时要解回来,否则中文署名存成 %E5...。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from urllib.parse import quote
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import Run, Workflow
|
|
from app.main import app
|
|
|
|
GRAPH = {
|
|
"nodes": [
|
|
{"id": "start", "type": "input", "position": {"x": 0, "y": 0},
|
|
"data": {"label": "输入", "config": {"fields": [{"name": "q"}]}}},
|
|
{"id": "out", "type": "output", "position": {"x": 300, "y": 0},
|
|
"data": {"label": "成果", "config": {"fields": [{"name": "r", "value": "{{ input.q }}"}]}}},
|
|
],
|
|
"edges": [{"source": "start", "target": "out"}],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
|
|
async def _workflow_with_run(status: str) -> tuple[str, str]:
|
|
async with SessionLocal() as session:
|
|
workflow = Workflow(name="删除保护", graph=GRAPH)
|
|
session.add(workflow)
|
|
await session.flush()
|
|
run = Run(workflow_id=workflow.id, workflow_name=workflow.name, graph=GRAPH,
|
|
input={}, status=status)
|
|
session.add(run)
|
|
await session.commit()
|
|
return workflow.id, run.id
|
|
|
|
|
|
@pytest.mark.parametrize("status", ["running", "queued"])
|
|
async def test_a_workflow_with_a_live_run_cannot_be_deleted(client, status):
|
|
workflow_id, _ = await _workflow_with_run(status)
|
|
r = await client.delete(f"/api/workflows/{workflow_id}")
|
|
assert r.status_code == 409
|
|
assert "停止" in r.json()["detail"]
|
|
assert (await client.get(f"/api/workflows/{workflow_id}")).status_code == 200
|
|
|
|
|
|
async def test_finished_runs_survive_their_workflow(client):
|
|
workflow_id, run_id = await _workflow_with_run("succeeded")
|
|
assert (await client.delete(f"/api/workflows/{workflow_id}")).status_code == 204
|
|
r = await client.get(f"/api/runs/{run_id}")
|
|
assert r.status_code == 200
|
|
assert r.json()["workflow_id"] is None
|
|
|
|
|
|
async def test_a_chinese_publisher_name_is_stored_decoded(client):
|
|
r = await client.post("/api/workflows", json={"name": "署名", "graph": GRAPH})
|
|
workflow_id = r.json()["id"]
|
|
r = await client.post(f"/api/workflows/{workflow_id}/publish",
|
|
json={"level": "published"}, headers={"X-Actor": quote("张工")})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["published_by"] == "张工"
|
|
assert (await client.get(f"/api/workflows/{workflow_id}")).json()["published_by"] == "张工"
|