feat: add prompt-first batches and pausable task guidance
This commit is contained in:
1 parent
cec9898e4c
commit
057f0c3050
44 files changed
+1080
-191
No files matched your search
@@ -64,9 +64,9 @@ def workbook_bytes(rows):
|
||||
def test_template_preview_rejects_formula_and_bad_json():
|
||||
data = workbook_bytes(
|
||||
[
|
||||
["正常", "打开设置", "", "", "", '{"name":"测试"}', "冒烟"],
|
||||
["公式", "=SUM(1,2)"],
|
||||
["参数错误", "测试", "", "", "", "{"],
|
||||
["正常", "设置", "冒烟", "打开设置", "", "观察设置", '{"profile":"flash"}'],
|
||||
["公式", "", "", "", "", "=SUM(1,2)"],
|
||||
["参数错误", "", "", "", "", "测试", "{"],
|
||||
]
|
||||
)
|
||||
result = task_import.parse_workbook(data)
|
||||
@@ -75,6 +75,30 @@ def test_template_preview_rejects_formula_and_bad_json():
|
||||
assert not result["valid"]
|
||||
|
||||
|
||||
def test_prompt_only_and_per_row_config_validation():
|
||||
goal = "先查看屏幕,再按实际画面判断。"
|
||||
data = workbook_bytes(
|
||||
[
|
||||
[
|
||||
"CASE-7",
|
||||
"检查项目",
|
||||
"子项",
|
||||
"用例名称",
|
||||
"仅作索引的约束",
|
||||
goal,
|
||||
'{"profile":"pro","model":"mimo-v2.6-flash","record_video":true}',
|
||||
],
|
||||
["CASE-8", "", "", "", "", "观察屏幕", '{"OPENAI_API_KEY":"never"}'],
|
||||
]
|
||||
)
|
||||
preview = task_import.parse_workbook(data)
|
||||
assert [e["row"] for e in preview["errors"]] == [3]
|
||||
case = preview["tasks"][0]
|
||||
assert task_import.case_goal(case) == goal
|
||||
assert case["preconditions"] == "仅作索引的约束"
|
||||
assert case["task_config"]["record_video"] is True
|
||||
|
||||
|
||||
def test_invalid_archive_and_empty_template():
|
||||
with pytest.raises(ValueError):
|
||||
task_import.parse_workbook(b"not a zip")
|
||||
@@ -84,7 +108,9 @@ def test_invalid_archive_and_empty_template():
|
||||
def test_preview_does_not_enqueue(client, isolated):
|
||||
response = client.post(
|
||||
"/api/automation/import/preview",
|
||||
files={"file": ("tasks.xlsx", workbook_bytes([["案例", "观察页面"]]))},
|
||||
files={
|
||||
"file": ("tasks.xlsx", workbook_bytes([["案例", "", "", "观察页面", "", "观察页面"]]))
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["valid"]
|
||||
@@ -137,6 +163,30 @@ async def test_batch_admission_idempotent_and_bound_device(isolated, monkeypatch
|
||||
assert isolated.jobs(batch["id"])[1]["status"] == "pending"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_paused_case_yields_batch_slot_to_next_case(isolated, monkeypatch):
|
||||
from apps.admin_console import server
|
||||
from artemis.runtime.device_pool import DeviceStatus
|
||||
|
||||
batch = service.create_batch(payload())
|
||||
service.batch_action(batch["id"], "resume")
|
||||
first = isolated.jobs(batch["id"])[0]
|
||||
isolated.save_job({**first, "status": "task_paused", "started_at": time.time()})
|
||||
monkeypatch.setattr(
|
||||
service, "state", type("State", (), {"is_paused": False, "active_runs": {}})()
|
||||
)
|
||||
enqueue = AsyncMock(return_value={"tasks": [{"device_serial": "test-device"}]})
|
||||
monkeypatch.setattr(server.task_queue_service, "enqueue_tasks", enqueue)
|
||||
monkeypatch.setattr(
|
||||
"artemis.runtime.device_pool.try_list_devices_async",
|
||||
AsyncMock(return_value=[DeviceStatus(serial="test-device", state="device")]),
|
||||
)
|
||||
await service.tick_batches()
|
||||
assert enqueue.await_count == 1
|
||||
assert enqueue.call_args.args[0] == ["观察首页"]
|
||||
assert [job["status"] for job in isolated.jobs(batch["id"])] == ["task_paused", "queued"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_outage_pauses_without_consuming_pending_case(isolated, monkeypatch):
|
||||
batch = service.create_batch(payload())
|
||||
@@ -193,6 +243,38 @@ def test_reports_do_not_execute_formulas_or_invent_pass(isolated):
|
||||
assert "未验证".encode() in doc.read("word/document.xml")
|
||||
|
||||
|
||||
def test_task_material_zip_contains_ordered_evidence_and_reports(
|
||||
client, isolated, tmp_path, monkeypatch
|
||||
):
|
||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||
evidence = tmp_path / "evidence"
|
||||
evidence.mkdir()
|
||||
first = evidence / "first.png"
|
||||
second = evidence / "second.png"
|
||||
video = evidence / "recording.mp4"
|
||||
for path in (first, second, video):
|
||||
path.write_bytes(path.name.encode())
|
||||
monkeypatch.setattr(
|
||||
reports,
|
||||
"session_screenshots",
|
||||
lambda report: [(first, "0001_步骤0001_操作前.png"), (second, "0002_步骤0002_操作后.png")],
|
||||
)
|
||||
monkeypatch.setattr(reports, "session_videos", lambda report: [(video, "0001_recording.mp4")])
|
||||
|
||||
response = client.get(f"/api/automation/reports/{job['session_id']}/all")
|
||||
assert response.status_code == 200
|
||||
with zipfile.ZipFile(BytesIO(response.content)) as archive:
|
||||
files = archive.namelist()
|
||||
assert any(name.endswith(".docx") for name in files)
|
||||
assert any(name.endswith(".xlsx") for name in files)
|
||||
assert files.index("步骤截图/0001_步骤0001_操作前.png") < files.index(
|
||||
"步骤截图/0002_步骤0002_操作后.png"
|
||||
)
|
||||
assert archive.read("任务录屏/0001_recording.mp4") == b"recording.mp4"
|
||||
manifest = json.loads(archive.read("文件清单.json"))
|
||||
assert manifest["session_id"] == job["session_id"]
|
||||
|
||||
|
||||
def test_history_filters_all_persisted_rows_and_escapes_like(client, isolated):
|
||||
batch = service.create_batch(payload())
|
||||
first, second = isolated.jobs(batch["id"])
|
||||
|
||||
@@ -159,7 +159,7 @@ async def test_shutdown_marks_only_ui_owned_running_sessions():
|
||||
):
|
||||
await on_shutdown()
|
||||
|
||||
update_status.assert_called_once_with("ui-running", "cancelled")
|
||||
update_status.assert_called_once_with("ui-running", "interrupted")
|
||||
assert state.queue_items == []
|
||||
state.active_session_id = None
|
||||
state.is_shutting_down = False
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_startup_cleanup_keeps_live_cross_process_session(tmp_path):
|
||||
assert repository.cleanup_orphans_on_startup() == 1
|
||||
|
||||
assert repository.get_session_status("live") == "running"
|
||||
assert repository.get_session_status("dead") == "failed"
|
||||
assert repository.get_session_status("dead") == "interrupted"
|
||||
|
||||
|
||||
def test_session_list_harvest_rechecks_worker_liveness(tmp_path):
|
||||
|
||||
@@ -987,6 +987,7 @@ async def test_manual_stop_of_one_run_does_not_pollute_concurrent_run():
|
||||
on different devices: device A's task is stopped manually, device B's task
|
||||
then finishes normally and must still be reported as completed.
|
||||
"""
|
||||
state.is_shutting_down = False
|
||||
ended_payloads: dict[str, dict] = {}
|
||||
|
||||
def capture(event_type, data):
|
||||
|
||||
Reference in new issue
Block a user