测试:随机名字不带数字——新增 tests/fixtures/names.letters(uuid4 的十六进制里数字换成字母,同 test_run_data_versions 的写法),dual_check_shop 的数据源名、盲算的调用编号,评测、应用说明检查、导入、遮罩、通知几处测试里拼进文本的随机名字都改用它(以前带着「330」「300」「48」这类数时撞上「文本里不许出现这个数」的断言);test_eval_schedule 查通知里有没有数时先去掉 32 位编号和发送时间(随机的编号带过「310」);test_null_not_null_values_fall_back_to_the_model_default 显式导入 app.db.models(单独跑时 schedules 还没登记进元数据,KeyError)。deploy.sh 注明内置库不用 copy-to-postgres 的 --direct-url

This commit is contained in:
ThearchyHelios
2026-10-19 15:16:54 +08:00
parent 27237623c5
commit 39bc76494e
12 changed files with 49 additions and 23 deletions
+3 -3
View File
@@ -19,7 +19,6 @@ from __future__ import annotations
import json
import re
import sqlite3
import uuid
from pathlib import Path
from langchain_core.messages import AIMessage, ToolMessage
@@ -27,6 +26,7 @@ from langchain_core.messages import AIMessage, ToolMessage
from app.data import introspect
from app.db.base import SessionLocal
from app.db.models import DataSource
from tests.fixtures.names import letters
ROWS = [
(1, "华东", 101, 100.0, "paid", "2026-08-31", "2026-09-01", "online", 0),
@@ -67,7 +67,7 @@ def build(path: Path, *, date_type: str = "DATE") -> Path:
async def add_source(path: Path, *, options: dict | None = None) -> tuple[str, str]:
"""登记成只读的手工源,探查好表结构。返回 (数据源名, id)。"""
name = f"dc_shop_{uuid.uuid4().hex[:8]}"
name = f"dc_shop_{letters(8)}"
async with SessionLocal() as session:
row = DataSource(name=name, kind="sqlite", database=str(path), readonly=True, enabled=True,
options=dict(options or {}))
@@ -135,7 +135,7 @@ class Blind:
hook, blind.on_query = blind.on_query, None
hook()
call = {"name": f"db_query__{blind.source}", "args": {"sql": sqls[done]},
"id": f"blind-{uuid.uuid4().hex[:6]}"}
"id": f"blind-{letters(6)}"}
return AIMessage(content="查一下。", tool_calls=[call])
return AIMessage(content="查完了。")
blind.writer_prompts.append(text)
+19
View File
@@ -0,0 +1,19 @@
"""测试里随机起的名字不带数字。
名字会拼进导出的文件、通知、任务说明这类文本,而不少断言写的是「文本里不许出现某个数」(不许出现 330、300、48……):
随机的十六进制里带着那几个数字的概率不大但不是零(8 位里出现「330」约千分之一点五),一撞上就无缘无故地失败。
十六进制里的数字换成字母(同 test_run_data_versions 的写法),随机性来自 uuid4(不受 pytest-randomly 每个测试重设
random 种子的影响,不会在不同测试里撞名)。
"""
from __future__ import annotations
import uuid
#: 十六进制里的数字换成字母:名字里不带数字,数字的断言才只看得见数据
NO_DIGITS = str.maketrans("0123456789", "ghijklmnop")
def letters(n: int = 8) -> str:
"""n 个随机小写字母(a–p),n 最多 32。"""
return uuid.uuid4().hex[:n].translate(NO_DIGITS)
+3 -3
View File
@@ -6,7 +6,6 @@
from __future__ import annotations
import copy
import uuid
import pytest
@@ -21,6 +20,7 @@ from app.apps.spec import empty_spec, parse_spec
from app.core.artifact_store import graph_hash
from app.db.base import SessionLocal, utcnow
from app.db.models import DataSource, Schedule, Workflow, WorkflowVersion
from tests.fixtures.names import letters
def node(nid, ntype, label=None, **config):
@@ -57,7 +57,7 @@ def approval():
async def make_workflow(graph, *, versions=((1, "governed"),), current=1, status="governed") -> tuple[str, dict]:
async with SessionLocal() as session:
wf = Workflow(name=f"周报-{uuid.uuid4().hex[:6]}", graph=graph, status=status, published_version=current,
wf = Workflow(name=f"周报-{letters(6)}", graph=graph, status=status, published_version=current,
version=max(v for v, _ in versions))
session.add(wf)
await session.flush()
@@ -281,7 +281,7 @@ async def test_process_pins(monkeypatch):
async def test_on_demand_views_on_live_sources_need_a_ttl(tmp_path):
name = f"live_{uuid.uuid4().hex[:6]}"
name = f"live_{letters(6)}"
async with SessionLocal() as session:
session.add(DataSource(name=name, kind="sqlite", database=str(tmp_path / "x.db"), enabled=True))
await session.commit()
+3 -2
View File
@@ -23,6 +23,7 @@ from app.db.base import SessionLocal
from app.db.models import DataSource
from app.engine import masking
from app.engine.nodes import challenge as challenge_mod
from tests.fixtures.names import letters
from tests.test_challenge_node import Models, Play, engine_up, finish, graph, output_of # noqa: F401 - engine_up 是自动用上的夹具
#: 四笔订单,买家手机号设了遮罩;备注没设遮罩,其中一条写着换了写法的号码
@@ -42,7 +43,7 @@ async def masked_shop(tmp_path):
db.executescript(SCRIPT)
db.commit()
db.close()
name = f"shop_{uuid.uuid4().hex[:8]}"
name = f"shop_{letters(8)}"
async with SessionLocal() as session:
row = DataSource(name=name, kind="sqlite", database=str(path), readonly=True, enabled=True,
options={"mask_columns": ["buyer_phone"]})
@@ -139,7 +140,7 @@ async def big_shop(tmp_path):
db.executescript(BIG)
db.commit()
db.close()
name = f"shop_{uuid.uuid4().hex[:8]}"
name = f"shop_{letters(8)}"
async with SessionLocal() as session:
row = DataSource(name=name, kind="sqlite", database=str(path), readonly=True, enabled=True,
options={"mask_columns": ["amount", "buyer_name"]})
+1
View File
@@ -409,6 +409,7 @@ async def test_an_old_database_with_nulls_and_orphans_imports_cleanly(tmp_path):
def test_null_not_null_values_fall_back_to_the_model_default():
from app.db import models # noqa: F401 表登记进元数据(这个进程里之前没有测试导入过模型时 schedules 还不在)
from app.db.base import Base
schedules = Base.metadata.tables["schedules"]
+4 -1
View File
@@ -12,6 +12,7 @@ from __future__ import annotations
import asyncio
import json
import re
import uuid
from datetime import timedelta, timezone
@@ -218,8 +219,10 @@ async def test_a_regression_is_pushed_without_numbers(client, monkeypatch, recei
assert message["event"] == "eval.regressed" and message["title"] == "定时评测「虚构周回归」有退步"
text = message["text"]
assert "通过:0 / 1 题" in text and "退步:1 题" in text
# 消息里的编号(32 位十六进制)和发送时间是随机的、可能正好带着「310」:去掉再查,查的是写出来的文字
readable = re.sub(r"[0-9a-f]{32}", "<id>", json.dumps({**message, "sent_at": ""}, ensure_ascii=False))
for secret in ("330", "310", "gmv", "orders", "paid_date"):
assert secret not in json.dumps(message, ensure_ascii=False), f"通知里不该有 {secret}"
assert secret not in readable, f"通知里不该有 {secret}"
from app.db.models import NotifyDelivery
async with SessionLocal() as session:
+4 -4
View File
@@ -7,7 +7,6 @@ from __future__ import annotations
import json
import sqlite3
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import quote
@@ -27,6 +26,7 @@ from app.main import app
from tests.fixtures import dual_check_shop as shop
from tests.fixtures.acceptance import retail
from tests.fixtures.catalog.fake_model import FakeModel
from tests.fixtures.names import letters
from tests.fixtures.sources import drop_source
SUITE_FILE = Path(__file__).parent / "fixtures" / "eval" / "retail_suite.json"
@@ -57,7 +57,7 @@ async def source(tmp_path):
async def new_suite(client, **body) -> dict:
res = await client.post("/api/evals/suites", json={"name": f"虚构题集_{uuid.uuid4().hex[:6]}", **body}, headers=ACTOR)
res = await client.post("/api/evals/suites", json={"name": f"虚构题集_{letters(6)}", **body}, headers=ACTOR)
assert res.status_code == 201, res.text
return res.json()
@@ -264,7 +264,7 @@ def _weekly(source_name: str, *, refunds: bool = False) -> dict:
async def test_caliber_cases_list_and_dispose_upgrades(client, source):
async with SessionLocal() as session:
wf = Workflow(name=f"周销售_{uuid.uuid4().hex[:6]}", graph=_weekly(source["name"]), status="published",
wf = Workflow(name=f"周销售_{letters(6)}", graph=_weekly(source["name"]), status="published",
published_version=1, version=1)
session.add(wf)
await session.flush()
@@ -329,7 +329,7 @@ async def test_caliber_cases_list_and_dispose_upgrades(client, source):
async def test_caliber_cases_pin_only_published_versions(client, source):
async with SessionLocal() as session:
wf = Workflow(name=f"草稿口径_{uuid.uuid4().hex[:6]}", graph=_weekly(source["name"]), status="draft", version=1)
wf = Workflow(name=f"草稿口径_{letters(6)}", graph=_weekly(source["name"]), status="draft", version=1)
session.add(wf)
await session.flush()
session.add(WorkflowVersion(workflow_id=wf.id, version=1, graph=_weekly(source["name"])))
+2 -2
View File
@@ -8,7 +8,6 @@ from __future__ import annotations
import asyncio
import json
import uuid
import httpx
import pytest
@@ -19,6 +18,7 @@ from app.db.base import SessionLocal
from app.db.models import App, AppVersion, NotifyChannel, NotifyDelivery, Run, Setting
from app.engine.runner import run_manager
from app.notify import core, service
from tests.fixtures.names import letters
URL = "https://hooks.example.com/agentlab/inbox-7f3a9c2e1b"
@@ -141,7 +141,7 @@ async def test_task_opened_is_pushed_to_the_app_subscription_without_numbers(rec
async with SessionLocal() as session:
channel = NotifyChannel(name="财务群", format="generic", url=core_encrypt(URL), events=[])
session.add(channel)
app = App(name=f"报销-{uuid.uuid4().hex[:6]}", published_version=1)
app = App(name=f"报销-{letters(6)}", published_version=1)
session.add(app)
await session.flush()
session.add(AppVersion(app_id=app.id, version=1, spec_hash="h" * 64, bindings={},
+5 -5
View File
@@ -10,7 +10,6 @@ from __future__ import annotations
import asyncio
import json
import uuid
import httpx
import pytest
@@ -21,6 +20,7 @@ from app.db.base import SessionLocal
from app.db.models import App, AppVersion, NotifyChannel, NotifyDelivery, Run, User
from app.engine.runner import run_manager
from app.notify import core, service
from tests.fixtures.names import letters
URL = "https://hooks.example.com/agentlab/run-3b8e1f"
@@ -68,9 +68,9 @@ def flow(**human):
async def _app(events: list[str]) -> tuple[str, str]:
async with SessionLocal() as session:
channel = NotifyChannel(name=f"群-{uuid.uuid4().hex[:6]}", format="generic", url=encrypt(URL), events=[])
channel = NotifyChannel(name=f"群-{letters(6)}", format="generic", url=encrypt(URL), events=[])
session.add(channel)
app = App(name=f"报销-{uuid.uuid4().hex[:6]}", published_version=1)
app = App(name=f"报销-{letters(6)}", published_version=1)
session.add(app)
await session.flush()
session.add(AppVersion(app_id=app.id, version=1, spec_hash="h" * 64, bindings={},
@@ -180,7 +180,7 @@ async def _finished_instance(app_id: str, *, started_by: str, verified: bool) ->
async def test_the_initiator_s_personal_channel_receives_my_instance_finished(received):
"""B1:个人渠道订阅了 run.finished,发起人自己发起的实例结束了收得到;应用自己没订阅也一样。"""
app_id, _ = await _app([])
name = f"uma{uuid.uuid4().hex[:6]}"
name = f"uma{letters(6)}"
mine = await _personal(name, ["run.finished"])
run_id = await _finished_instance(app_id, started_by=name, verified=True)
assert await service.notify_run(run_id, "run.finished") == 1
@@ -192,7 +192,7 @@ async def test_the_initiator_s_personal_channel_receives_my_instance_finished(re
async def test_a_self_declared_initiator_does_not_reach_a_personal_channel(received):
"""关登录时署名是自己填的,谁都能填别人的名字:不按它推给那个人的个人渠道。"""
app_id, _ = await _app([])
name = f"vic{uuid.uuid4().hex[:6]}"
name = f"vic{letters(6)}"
mine = await _personal(name, ["run.finished"])
run_id = await _finished_instance(app_id, started_by=name, verified=False)
assert await service.notify_run(run_id, "run.finished") == 0
+2 -1
View File
@@ -42,6 +42,7 @@ from app.data.xlsx_scan import Bounds, SheetScan, UnsupportedTable, WorkbookScan
from app.db.base import SessionLocal
from app.db.models import DataSource, ImportStaging, SourceSnapshot, TableImport, TableRecipe
from app.main import app
from tests.fixtures.names import letters
FLOW_RECIPE = json.loads((Path(__file__).parent / "fixtures" / "recipes" / "flow_recipe.json").read_text("utf-8"))
FLOW_FULL = Recipe.model_validate(FLOW_RECIPE).model_dump(mode="json")
@@ -294,7 +295,7 @@ async def client():
def unique(prefix: str = "wp5c") -> str:
return f"{prefix}_{uuid.uuid4().hex[:10]}"
return f"{prefix}_{letters(10)}"
def raw_bytes() -> bytes:
+2 -2
View File
@@ -11,7 +11,6 @@ from __future__ import annotations
import datetime as dt
import json
import uuid
from pathlib import Path
from typing import Any
@@ -26,6 +25,7 @@ from app.data.engine import engines, run_query
from app.db.base import SessionLocal
from app.db.models import DataSource, ImportStaging, SourceSnapshot, TableImport, TableRecipe
from app.main import app
from tests.fixtures.names import letters
from tests.fixtures.xlsx.flow import FLOW_EXPECT, flow_workbook
from tests.test_recipe_imports import ( # noqa: F401 - fake 是夹具,导入即可用
FLOW_FULL, api_stage, fake, raw_bytes, recipe_source, staged,
@@ -52,7 +52,7 @@ async def client():
def unique(prefix: str = "wp5c") -> str:
return f"{prefix}_{uuid.uuid4().hex[:10]}"
return f"{prefix}_{letters(10)}"
async def stage(client, name: str, raw: bytes, filename: str, **form: str):
+1
View File
@@ -901,6 +901,7 @@ migrate_rollback() {
copy_to_pg() {
# --assume-stopped:服务是这里刚停的(强杀留下的 -wal 也不挡);快照放数据目录里(和库同一块盘,空间够)。
# 内置库是直连(没有 PgBouncer),断点跟着目标地址走,不用 --direct-url。
# 逐表的结果里来源是空表的几十行不列(没什么可核对的),别的原样打出来;退出码照 copy-to-postgres 的(pipefail)
docker run --rm --network "$DOCKER_NETWORK" -v "$DATA_ABS:/data" -e "PGPASSFILE=$PG_PASSFILE" \
--entrypoint python "$IMAGE" -m app.db copy-to-postgres /data \