"""「对象定位」评测套件也要真的在跑 ## 为什么需要这条迁移 新套件的用例首播就是 `active`,但**没有任何东西会去跑它**:每周那道 `assistant.eval_regression` 任务只跑**一个**套件 —— `config["suite"]`,默认 `"regression"`。所以少了这一行,新加的 6 条用例是一套永远不会亮红灯的闸, 而那比没有更糟:它会让人以为这一类已经被守住了。 ## 为什么是第二行,而不是第二个 handler `scheduled_jobs` 把 `name`(行的身份、历史 run 挂在它上面)和 `handler_key` (注册表查找键)**分成了两列**,注释里写明「usually matches name but kept separate」。所以第二个套件 = 第二行 + 同一个 `handler_key` + 不同的 `config`, 零行新代码。 已核对过这样不会串味: - `previous_terminal_run` 的基线查询带 `suite_id == run.suite_id`,两个套件各自 和自己的上一跑比 —— 否则一次「不可比」会把真回退静默吞掉; - 告警的 `related_resource_id` 是 run.id(每跑唯一),两个套件的告警不会互相去重。 ## 时间 周一 04:40(上海),排在回归评测(03:30)之后而不是同时:两跑都占用**员工同一个 模型后端**,并排跑等于自己给自己限速。6 条 × 3 遍比回归那套小得多,04:40 起跑 仍然在上班之前跑完。 """ from __future__ import annotations import json import uuid import sqlalchemy as sa from alembic import op revision = "630" down_revision = "629" branch_labels = None depends_on = None JOB_NAME = "assistant.eval_object_lookup" #: 复用回归评测的 handler —— 它本来就按 ``config["suite"]`` 选套件。 HANDLER_KEY = "assistant.eval_regression" _DESC = ( "每周跑一遍「对象定位」套件:员工点名了一个具体对象(账号号码 / 实例 id / " "IP / 域名 / 序列号)时,答案必须是那个对象的信息。这一类的失败方式独一无二 —— " "回答看起来完全合理,只是说的是另一个东西(生产上「380838443567 这个云账号谁" "负责」被答成了两条同名审批流程的负责人),所以每条用例都带一条确定性的禁止" "短语,不全靠模型评委。与上一次同套件的终态运行对比,掉超过 10 个百分点告警 IT。" ) def _has_table(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.tables " "WHERE table_schema = DATABASE() AND table_name = :n" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if not _has_table(bind, "scheduled_jobs"): return exists = bind.execute( sa.text("SELECT 1 FROM scheduled_jobs WHERE name = :name LIMIT 1"), {"name": JOB_NAME}, ).scalar() if exists: return bind.execute( sa.text( """ INSERT INTO scheduled_jobs (id, name, display_name, description, handler_key, trigger_type, interval_seconds, cron_expr, timezone, enabled, config, run_count, failure_count, created_at, updated_at) VALUES (:id, :name, :display_name, :description, :handler_key, 'cron', NULL, '40 4 * * 1', 'Asia/Shanghai', 1, :config, 0, 0, UTC_TIMESTAMP(), UTC_TIMESTAMP()) """ ), { # 必须是带横线的 uuid4:存成 32 位无横线的话,GUID TypeDecorator # 读回来永远匹配不上,handler 在 run 包装器外面就抛 —— # scheduled_job_runs 一行都不写,run_count / last_run_at 永远 # 是 0 / NULL。本仓踩过四次(059 / 162 / 248 / 592 的注释)。 "id": str(uuid.uuid4()), "name": JOB_NAME, "handler_key": HANDLER_KEY, "display_name": "助手 · 每周对象定位评测", "description": _DESC, "config": json.dumps( {"suite": "object_lookup", "repeats": 3, "concurrency": 3}, ), }, ) def downgrade() -> None: bind = op.get_bind() if _has_table(bind, "scheduled_jobs"): bind.execute( sa.text("DELETE FROM scheduled_jobs WHERE name = :name"), {"name": JOB_NAME}, )