Files
AgentLab/backend/tests/test_maintenance_fire_history.py
T
ThearchyHelios 7f5578c094 定时任务页认得数据源维护:触发历史列出建了的探查、剖析任务和没执行的步骤;「去数据源页设置」改链 #source-<id>(复核 W4R-05、W4R-04)
W4R-05 为什么:数据源维护的一次触发建的是探查、剖析任务,没有运行;api/schedules._fire_outs 不交触发记录的 inputs,
FireRow 只认 run_id 和 eval_run_id,于是每一次建了任务的维护都写成「已发起 · 未发起运行」,和数据源页「定时维护」的
「最近一次」对不上;「立即运行」一次维护还弹「未能发起运行」。

- 后端 api/schedules._fire_outs:目标是数据源维护的触发另带 inputs({steps, jobs, skipped},app/data/source_maintenance
  记在触发上的那一份,和数据源页「定时维护」读的同一个形状);别的目标给 null(工作流触发的 inputs 是按模板渲染的工作流
  输入,触发历史不列)。触发历史、页头「上次触发」、列表、立即运行的答复都走它。test_schedules_api 的 FIRE_KEYS 加 inputs。
- 前端 FireHistory:维护的那一行逐个列建了的任务(步骤、任务种类、编号、表数),探查链到数据源页那张卡片、剖析链到数据
  目录里那个任务的进度(?pjob=,同「最近一次」);没执行的步骤逐条写原因,服务端连成一句的原因不再重复;没建任务写
  「没有建任务」(刚触发、任务还在建写「正在建维护任务」),不写「未发起运行」。
- 立即运行(parts.runNow):维护开始了说「已开始维护」、详情逐行写建了什么;跳过照实说上一次维护还没做完;没开始说
  「没有开始维护」和原因。状态小标签的悬停同理。
- types.ts 加 ScheduleMaintenanceStep、ScheduleMaintenanceInputs(和 W4-d 的 MaintenanceFireInputs 结构一致,合并后
  交叉类型照样过 tsc);叫法进 SCHEDULE_TEXT(步骤叫法同服务端 STEP_LABEL,测试逐个对上)。

W4R-04 为什么:维护定时详情里「去数据源页设置」链到 /data/databases?source=<id>,数据源页(W4-d)只认 #source-<id>
(目录页后台任务条「查看」的约定),点过去落在列表顶上。统一成 #source-<id>:链接都经 model.sourceCardPath 出,
探查任务的链接同用它。

检查:check-schedules 新增「纯函数:数据源维护链到哪里、建了什么怎么写」「数据源维护的触发历史」两段(假接口:四种
维护触发、立即运行开始与跳过),改「链到数据源页」的断言;394 项全过。去掉 FireRow 的维护分支、链接改回 ?source= 回跑:
4 项失败。后端新文件 tests/test_maintenance_fire_history.py(立即运行一次维护后触发历史、页头、列表都带 inputs;工作流
触发不列渲染的输入;步骤和叫法前后端对上)。
2026-10-16 22:02:09 +08:00

93 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""定时任务页的触发历史认得数据源维护(复核 W4R-05)。
数据源维护的一次触发建的是探查、剖析任务,没有运行:触发记录的 inputs({steps, jobs, skipped},app/data/source_maintenance)
要交到触发历史、页头的「上次触发」和「立即运行」的答复里,界面才写得出建了哪几个任务、哪几步没执行,不再写「未发起运行」。
工作流目标的触发不给 inputs(那是按模板渲染的工作流输入,触发历史不列)。
库、表、名字都是虚构的。
"""
from __future__ import annotations
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import delete
from app.db.base import SessionLocal, utcnow
from app.db.models import Schedule, ScheduleFire
from app.main import app
from app.scheduler import service
from tests.fixtures.sources import drop_source
from tests.test_source_maintenance import _schedule, _source, _wait_jobs
@pytest.fixture
async def client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
@pytest.fixture
async def made():
ids: list[str] = []
yield ids
async with SessionLocal() as session:
await session.execute(delete(Schedule).where(Schedule.id.in_(ids)))
await session.commit()
async def test_a_maintenance_fire_carries_the_jobs_it_started_and_the_steps_not_run(client, tmp_path, made):
sid = await _source(client, tmp_path / "shop.db", profile=False)
try:
schedule_id = await _schedule(sid)
made.append(schedule_id)
ran = (await client.post(f"/api/schedules/{schedule_id}/run")).json()
fire = ran["fire"]
assert ran["run_id"] is None and fire["status"] == "started" and fire["run_id"] is None
jobs = fire["inputs"]["jobs"]
assert [j["kind"] for j in jobs] == ["introspect"] and jobs[0]["id"]
assert {s["step"] for s in fire["inputs"]["skipped"]} == {"profile_needed", "observe"}
async with SessionLocal() as session:
await _wait_jobs(await session.get(ScheduleFire, fire["id"]))
listed = (await client.get(f"/api/schedules/{schedule_id}/fires")).json()["items"]
assert [f["id"] for f in listed] == [fire["id"]]
assert listed[0]["inputs"] == fire["inputs"]
assert set(listed[0]["inputs"]) == {"steps", "jobs", "skipped"}
head = (await client.get(f"/api/schedules/{schedule_id}")).json()
assert head["last_fire"]["inputs"]["jobs"] == jobs
rows = (await client.get("/api/schedules")).json()["items"]
assert [s["last_fire"]["inputs"]["jobs"] for s in rows if s["id"] == schedule_id] == [jobs]
finally:
await drop_source(sid)
async def test_a_workflow_fire_does_not_list_its_rendered_inputs(client, made):
async with SessionLocal() as session:
row = Schedule(name="虚构周报 · 每周", cron="0 9 * * 1", timezone="Asia/Shanghai",
**service.target_columns("workflow", "wf-fictional"), input_template={"date": "{{ schedule.yesterday }}"})
session.add(row)
await session.flush()
session.add(ScheduleFire(schedule_id=row.id, fire_at=utcnow().replace(microsecond=0), kind="manual",
status="failed_to_start", reason="虚构的原因", inputs={"date": "2026-10-15"}))
await session.commit()
made.append(row.id)
[got] = (await client.get(f"/api/schedules/{row.id}/fires")).json()["items"]
assert got["inputs"] is None
def test_the_steps_and_their_names_on_the_schedules_page_match_the_server():
"""定时任务页照服务端的步骤和叫法写(types.ts 的 ScheduleMaintenanceStep、SCHEDULE_TEXT.maintenanceStep):服务端加了一步,
界面没跟上就写出英文原值。直接读两份 .ts 比(同 test_frontend_type_unions 的写法)。"""
import re
from pathlib import Path
from app.data.source_maintenance import STEP_LABEL, STEPS
src = Path(__file__).resolve().parents[2] / "frontend" / "src"
types = (src / "types.ts").read_text(encoding="utf-8")
union = re.search(r"export type ScheduleMaintenanceStep =([^\n]*)", types)
assert union is not None and re.findall(r"'([^']+)'", union.group(1)) == list(STEPS)
terms = (src / "lib" / "terms.ts").read_text(encoding="utf-8")
labels = re.search(r"\n\s*maintenanceStep: \{([^}]*)\}", terms)
assert labels is not None and dict(re.findall(r"(\w+): '([^']+)'", labels.group(1))) == STEP_LABEL