mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:31:59 +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 项旧断言(按旧日期格式找运行行),第三波更新。
128 lines
5.0 KiB
Python
128 lines
5.0 KiB
Python
"""设置页的「运行默认值」必须真的被读到。
|
||
|
||
以前这三项只存在于 DEFAULT_SETTINGS 里:发起运行永远落在 "default",
|
||
「危险工具默认需要人工确认」勾不勾都一样。用户改了默认知识库,以为以后的
|
||
运行都会用它——这比没有这个开关更糟,它给的是虚假的确定感。
|
||
|
||
这里守发起运行这一侧:请求没带的 memory_scope / collection 取设置里的值;
|
||
审批默认值的换算给引擎一个唯一的出处。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Run, Setting
|
||
from app.main import app
|
||
|
||
GRAPH = {
|
||
"nodes": [
|
||
{"id": "a", "type": "input", "data": {"label": "入口", "config": {"fields": [{"name": "q"}]}}},
|
||
{"id": "b", "type": "output", "data": {"label": "成果",
|
||
"config": {"fields": [{"name": "x", "value": "{{ input.q }}"}]}}},
|
||
],
|
||
"edges": [{"source": "a", "target": "b"}],
|
||
}
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def _clean_run_settings():
|
||
async def drop() -> None:
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Setting, "run")
|
||
if row:
|
||
await session.delete(row)
|
||
await session.commit()
|
||
|
||
await drop()
|
||
yield
|
||
await drop()
|
||
|
||
|
||
@pytest.fixture
|
||
def started(monkeypatch):
|
||
"""截下交给 run_manager.start 的参数,不真的起执行任务(测试里没有 checkpointer)。"""
|
||
from app.api import runs as runs_api
|
||
|
||
captured: dict = {}
|
||
|
||
async def fake_start(**kw):
|
||
captured.update(kw)
|
||
async with SessionLocal() as session:
|
||
run = Run(workflow_name=kw.get("workflow_name", ""), status="queued",
|
||
graph=kw["graph"], input=kw["input_payload"])
|
||
session.add(run)
|
||
await session.commit()
|
||
await session.refresh(run)
|
||
return run
|
||
|
||
monkeypatch.setattr(runs_api.run_manager, "start", fake_start)
|
||
return captured
|
||
|
||
|
||
async def _put_run_settings(client, **values) -> None:
|
||
base = {"default_memory_scope": "default", "default_collection": "default",
|
||
"stream_tokens": True, "confirm_dangerous_tools": True}
|
||
r = await client.put("/api/settings", json={"values": {"run": {**base, **values}}})
|
||
assert r.status_code == 200
|
||
|
||
|
||
async def test_a_run_without_scope_uses_the_settings_defaults(client, started):
|
||
await _put_run_settings(client, default_memory_scope="ops", default_collection="manuals")
|
||
|
||
r = await client.post("/api/runs", json={"graph": GRAPH, "input": {"q": "x"}})
|
||
assert r.status_code == 201, r.text
|
||
assert started["memory_scope"] == "ops"
|
||
assert started["collection"] == "manuals"
|
||
|
||
|
||
async def test_what_the_request_says_wins_over_the_defaults(client, started):
|
||
await _put_run_settings(client, default_memory_scope="ops", default_collection="manuals")
|
||
|
||
r = await client.post("/api/runs", json={"graph": GRAPH, "memory_scope": "team",
|
||
"collection": "specs"})
|
||
assert r.status_code == 201, r.text
|
||
assert (started["memory_scope"], started["collection"]) == ("team", "specs")
|
||
|
||
|
||
async def test_blank_or_missing_settings_fall_back_to_default(client, started):
|
||
r = await client.post("/api/runs", json={"graph": GRAPH})
|
||
assert r.status_code == 201, r.text
|
||
assert (started["memory_scope"], started["collection"]) == ("default", "default")
|
||
|
||
# 设置页把输入框清空再保存,存下来的是空串——不能拿空串当记忆域
|
||
await _put_run_settings(client, default_memory_scope="", default_collection=" ")
|
||
r = await client.post("/api/runs", json={"graph": GRAPH})
|
||
assert (started["memory_scope"], started["collection"]) == ("default", "default")
|
||
|
||
|
||
async def test_run_defaults_reads_through_to_the_stored_group(client):
|
||
from app.api.settings import run_defaults
|
||
|
||
await _put_run_settings(client, default_collection="manuals", confirm_dangerous_tools=False)
|
||
async with SessionLocal() as session:
|
||
values = await run_defaults(session)
|
||
assert values["default_collection"] == "manuals"
|
||
assert values["confirm_dangerous_tools"] is False
|
||
# 没存过的键回落到内置默认,而不是 KeyError
|
||
assert values["stream_tokens"] is True
|
||
|
||
|
||
def test_confirm_dangerous_tools_maps_onto_an_approval_policy():
|
||
"""引擎那边只认 approval 的取值,这里是唯一的换算出处。"""
|
||
from app.api.settings import tool_approval_default
|
||
|
||
assert tool_approval_default({"confirm_dangerous_tools": True}) == "dangerous"
|
||
assert tool_approval_default({"confirm_dangerous_tools": False}) == "never"
|
||
# 缺省、存坏了都按开着算:关审批必须是一次明确的选择
|
||
assert tool_approval_default({}) == "dangerous"
|
||
assert tool_approval_default({"confirm_dangerous_tools": None}) == "dangerous"
|