mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:21:58 +00:00
3e311ec551
整合(3c-integrate) - 公共层:跨天时长(formatSpan / formatOffset / formatLapse)、本机署名(lib/actor)、检索分类色令牌、 数据源目录(useDatasources)收进公共层;失败说明能一次列出多个写坏的自定义工具,查询超时指到数据源的查询时限 - 管理页收尾:设置页接入统一的离开前确认;检查脚本不再把沙箱里的真实值打进输出 - 画布 ↔ 右栏:点出具印章、点节点能跳到右栏对应位置;「定位」直接聚焦到出问题的字段 - 回答流 / 问数据 / 记录页:失败信息带「去哪修」且不再被翻译两遍;协作团队的结局按执行截取; 等审批时断流重连、而运行已在空档里跑完的,补全事件 扫尾(3c-sweep) - 术语统一:开始运行 / 继续运行、工作流最大步数;未保存的运行统一叫「未保存的工作流」(后端新运行同步) - 沙箱报错不再带 Python 异常类名;新增 check-tokens,未定义的 CSS 变量为 0,两个旧别名迁移后删除 检查脚本(3c-checks) - check-ui 跟上新界面(运行列表按 data 属性找行),加上令牌与选中态的回归断言,输出里的名字打码 - 新增 check-all:按顺序跑全部检查、汇总退出码,--repeat 列出偶发项 - 单节出错只记这一节失败、不再带崩整个脚本;外壳遥测浮层「重新检测」时焦点不再掉到 body 网页工具与 fake-ip - Clash / mihomo 一类代理开 TUN + fake-ip 时,查什么都先拿到 198.18 段的假地址,被当成内网拦截。 新增 AGENTLAB_HTTP_TOOL_FAKE_IP_RANGES(默认空):落在段里的解析结果改向公共 DNS(DoH)核实真实地址, 公网放行、内网照拦、核实不了按拦截处理。DoH 默认先问境外的,境内公共 DNS 会对被封锁的域名返回污染结果 - 列表型环境变量接受逗号分隔(原来注释说逗号分隔、实际只认 JSON 数组);域名解析改为异步,不再阻塞事件循环 验证(沙箱):tsc 干净,pytest 866 passed(两遍),检查脚本全绿。
141 lines
6.4 KiB
Python
141 lines
6.4 KiB
Python
"""换个 schema 看看,不擅自改缓存;表结构给结构化的列;卡片知道缓存是按哪个 schema 探的。
|
|
|
|
「换个 schema 看看」以前一探就把缓存换成那个 schema 的结构——哪怕用户随后选了
|
|
「只看看,不改」,助手此刻看到的已经是另一套表了。前端只好再探一次配置里的
|
|
schema 把缓存换回来,而那一次要是失败,缓存和配置就对不上,只剩一条会消失的 toast。
|
|
|
|
表结构以前只有一段给模型看的文本,前端照着文本格式去解析列,格式一改就全乱。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture
|
|
async def source(client, tmp_path):
|
|
path = tmp_path / "preview.db"
|
|
db = sqlite3.connect(path)
|
|
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, store TEXT NOT NULL, amount REAL)")
|
|
db.execute("CREATE TABLE sales_daily (day TEXT, total REAL)")
|
|
db.execute("CREATE VIEW v_big_orders AS SELECT * FROM orders WHERE amount > 100")
|
|
db.commit()
|
|
db.close()
|
|
r = await client.post("/api/datasources", json={"name": "preview_src", "kind": "sqlite",
|
|
"database": str(path)})
|
|
assert r.status_code == 201, r.text
|
|
yield r.json()
|
|
await client.delete(f"/api/datasources/{r.json()['id']}")
|
|
|
|
|
|
async def _cache(source_id: str) -> dict:
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import DataSource
|
|
|
|
async with SessionLocal() as session:
|
|
row = await session.get(DataSource, source_id)
|
|
return {"cache": dict(row.schema_cache or {}), "synced": row.schema_synced_at}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-7:缓存是按哪个 schema 探的
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_cached_schema_says_which_schema_the_cache_came_from(client, source):
|
|
assert source["cached_schema"] is None, "没探过就是没有缓存"
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
assert r.json()["cached_schema"] == "", "按默认 schema 探的写空串,和「没探过」分开"
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect", params={"schema": "main"})
|
|
assert r.json()["cached_schema"] == "main"
|
|
listed = next(s for s in (await client.get("/api/datasources")).json() if s["id"] == source["id"])
|
|
assert listed["cached_schema"] == "main"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-6 ①:只看看,不改缓存
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_a_dry_run_shows_what_is_there_without_touching_the_cache(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
before = await _cache(source["id"])
|
|
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect",
|
|
params={"schema": "main", "dry_run": "true"})
|
|
assert r.status_code == 200, r.text
|
|
preview = r.json()
|
|
assert preview["dry_run"] is True
|
|
assert preview["schema"] == "main"
|
|
assert preview["table_count"] == 3
|
|
assert set(preview["tables"]) == {"main.orders", "main.sales_daily", "main.v_big_orders"}
|
|
assert preview["schema_error"] == ""
|
|
|
|
after = await _cache(source["id"])
|
|
assert after == before, "只看看也把缓存换掉了——助手此刻看到的就不是配置里那个 schema 了"
|
|
|
|
|
|
async def test_a_dry_run_that_finds_nothing_says_why_and_still_leaves_the_cache(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
before = await _cache(source["id"])
|
|
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect",
|
|
params={"schema": "no_such_schema", "dry_run": "true"})
|
|
preview = r.json()
|
|
assert preview["dry_run"] is True and preview["table_count"] == 0
|
|
assert preview["schema_error"], "探失败了要说出来,不能看起来像「这个 schema 是空的」"
|
|
# 这句话原样画在数据源卡片上:不带驱动的异常类名(OperationalError: …)
|
|
assert not re.search(r"\b[A-Z]\w*(Error|Exception)\b", preview["schema_error"]), preview["schema_error"]
|
|
assert "main" in preview["available_schemas"]
|
|
assert await _cache(source["id"]) == before
|
|
|
|
|
|
async def test_without_dry_run_the_cache_is_still_replaced(client, source):
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect", params={"schema": "main"})
|
|
assert r.json()["table_count"] == 3
|
|
assert (await _cache(source["id"]))["cache"]["schema"] == "main"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-6 ②:表结构的结构化列
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_table_schema_comes_with_structured_columns(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
body = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "orders"})).json()
|
|
assert body["found"] is True
|
|
assert body["kind"] == "table" and body["qualified"] == "orders"
|
|
assert body["detail"].startswith("表 orders"), "给模型看的那段文本照旧"
|
|
assert body["columns"] == [
|
|
{"name": "id", "type": "INTEGER", "pk": True, "not_null": False, "comment": None},
|
|
{"name": "store", "type": "TEXT", "pk": False, "not_null": True, "comment": None},
|
|
{"name": "amount", "type": "REAL", "pk": False, "not_null": False, "comment": None},
|
|
]
|
|
|
|
view = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "V_BIG_ORDERS"})).json()
|
|
assert view["found"] is True and view["kind"] == "view", "和给模型的那份一样,大小写不敏感"
|
|
|
|
|
|
async def test_an_unknown_table_has_no_columns_but_keeps_the_explanation(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
body = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "nope"})).json()
|
|
assert body["found"] is False and body["columns"] == []
|
|
assert "没有 nope" in body["detail"]
|