feat: release 2.4.0 with original cases and automatic human review
This commit is contained in:
1 parent
baf069854b
commit
4dc2057d43
66 files changed
+1390
-206
No files matched your search
@@ -908,6 +908,85 @@ def test_reports_do_not_execute_formulas_or_invent_pass(isolated):
|
||||
assert "未验证".encode() in doc.read("word/document.xml")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("result_status", ["passed", "failed", "blocked", "inconclusive"])
|
||||
def test_report_without_output_body_uses_effective_verdict_and_evidence(isolated, result_status):
|
||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||
isolated.save_job({**job, "status": "completed"})
|
||||
folder = reports.TRACES_PATH / job["session_id"]
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "run_outcome.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"task_status": "completed",
|
||||
"test_result": {
|
||||
"status": result_status,
|
||||
"reason": "实际验收依据",
|
||||
"evidence": ["记录中的证据"],
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
report = reports.session_report(job["session_id"])
|
||||
assert report["verdict"] in report["content"]
|
||||
assert "实际验收依据" in report["content"]
|
||||
assert "记录中的证据" in report["content"]
|
||||
assert "未保存独立输出正文" in report["content"]
|
||||
assert "没有保存最终结论" not in report["content"]
|
||||
assert report["test_result"]["status"] == result_status
|
||||
|
||||
|
||||
def test_report_without_output_body_keeps_human_review_authoritative(isolated):
|
||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||
isolated.save_job({**job, "status": "completed"})
|
||||
folder = reports.TRACES_PATH / job["session_id"]
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "run_outcome.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"test_result": {"status": "passed", "reason": "AI 原判通过"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
isolated.save_case_review(job["session_id"], "failed", "用户确认实际结果不符合预期")
|
||||
report = reports.session_report(job["session_id"])
|
||||
assert "人工确认" in report["content"]
|
||||
assert "用户确认实际结果不符合预期" in report["content"]
|
||||
assert "AI 原判通过" not in report["content"]
|
||||
assert report["test_result"]["status"] == "failed"
|
||||
|
||||
|
||||
def test_report_projects_stored_android_receipts_without_rewriting_history(isolated):
|
||||
from tests.unit.data_engine.test_evidence_sources import android_receipt
|
||||
|
||||
sid = isolated.jobs(service.create_batch(payload())["id"])[0]["session_id"]
|
||||
receipt = android_receipt()["payload"]["result"]
|
||||
raw_payload = json.dumps({"args": {"action": "screenshot"}, "result": json.dumps(receipt)})
|
||||
with sqlite3.connect(isolated.path) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO sessions(session_id,initial_goal,start_time,status) VALUES (?,?,?,?)",
|
||||
(sid, "跨端截图", 1, "completed"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO steps(step_id,session_id,step_number) VALUES (?,?,?)", ("s", sid, 1)
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO traces(trace_id,session_id,step_id,type,name,timestamp,status,payload) VALUES (?,?,?,?,?,?,?,?)",
|
||||
("capture", sid, "s", "tool", "android_file", 2, "success", raw_payload),
|
||||
)
|
||||
report = reports.session_report(sid)
|
||||
source = report["evidence_sources"]["sources"][0]
|
||||
assert source["kind"] == "android"
|
||||
assert source["references"][0]["trace_id"] == "capture"
|
||||
assert report["evidence_sources"]["unattributed_step_count"] == 0
|
||||
with sqlite3.connect(isolated.path) as conn:
|
||||
assert (
|
||||
conn.execute("SELECT payload FROM traces WHERE trace_id='capture'").fetchone()[0]
|
||||
== raw_payload
|
||||
)
|
||||
|
||||
|
||||
def test_failed_report_exposes_logged_reason_and_formats_chinese_markdown(isolated):
|
||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||
isolated.save_job({**job, "status": "failed"})
|
||||
|
||||
@@ -110,6 +110,8 @@ def test_strip_trace_status_suffix_handles_every_terminal_marker():
|
||||
assert MediaService.strip_trace_status_suffix("web_1_PASS_2026") == "web_1"
|
||||
assert MediaService.strip_trace_status_suffix("web_1_FAIL_2026") == "web_1"
|
||||
assert MediaService.strip_trace_status_suffix("web_1_TESTFAIL_2026") == "web_1"
|
||||
assert MediaService.strip_trace_status_suffix("web_1_BLOCKED_2026") == "web_1"
|
||||
assert MediaService.strip_trace_status_suffix("web_1_UNVERIFIED_2026") == "web_1"
|
||||
assert MediaService.strip_trace_status_suffix("web_1") == "web_1"
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ async def test_list_sessions_uses_batched_history_metadata(monkeypatch):
|
||||
repo.get_llm_traces_for_profiles_map.return_value = {
|
||||
"session-3": ['{"model": "gemini-3.7-pro"}']
|
||||
}
|
||||
repo.get_recorded_model_info_map.return_value = {
|
||||
"session-1": {"id": "gpt-6.1-terra", "provider": "openai"},
|
||||
"session-2": {"id": "gpt-6-luna", "provider": "openai"},
|
||||
}
|
||||
repo.get_agent_trace_names_map.return_value = {}
|
||||
|
||||
monkeypatch.setattr(sessions_router, "session_repo", repo, raising=False)
|
||||
@@ -57,6 +61,10 @@ async def test_list_sessions_uses_batched_history_metadata(monkeypatch):
|
||||
result = await sessions_router.list_sessions()
|
||||
|
||||
assert len(result) == 3
|
||||
assert [row["model_info"]["id"] for row in result] == ["gpt-6.1-terra", "gpt-6-luna", ""]
|
||||
repo.get_recorded_model_info_map.assert_called_once_with(
|
||||
["session-1", "session-2", "session-3"]
|
||||
)
|
||||
repo.get_latest_video_recordings_map.assert_called_once_with()
|
||||
repo.get_llm_traces_for_profiles_map.assert_called_once_with(["session-3"])
|
||||
repo.get_agent_trace_names_map.assert_called_once_with()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.admin_console.database.connection import db_session
|
||||
@@ -37,3 +38,31 @@ def test_session_list_harvest_rechecks_worker_liveness(tmp_path):
|
||||
assert repository.harvest_orphaned_sessions(["external-live"]) == 0
|
||||
|
||||
assert repository.get_session_status("external-live") == "running"
|
||||
|
||||
|
||||
def test_recorded_model_metadata_is_per_session_and_prefers_operator(tmp_path):
|
||||
db_path = tmp_path / "sessions.db"
|
||||
repository = SessionRepository(db_path)
|
||||
for session_id in ("one", "two", "no-record"):
|
||||
_insert_running_session(db_path, session_id, 1)
|
||||
with db_session(db_path) as conn:
|
||||
for trace_id, session_id, timestamp, payload in (
|
||||
("invalid", "one", 0, "{"),
|
||||
("planner", "one", 1, json.dumps({"role": "planner", "model": "other-model"})),
|
||||
(
|
||||
"operator",
|
||||
"one",
|
||||
2,
|
||||
json.dumps({"role": "operator", "model": "gpt-6.1-terra", "provider": "openai"}),
|
||||
),
|
||||
("flash", "two", 1, json.dumps({"role": "FlashRunner", "model": "gpt-6-luna"})),
|
||||
):
|
||||
conn.execute(
|
||||
"INSERT INTO traces(trace_id,session_id,type,timestamp,payload) VALUES (?,?,?,?,?)",
|
||||
(trace_id, session_id, "llm_diagnostic", timestamp, payload),
|
||||
)
|
||||
conn.commit()
|
||||
assert repository.get_recorded_model_info_map(["one", "two", "no-record"]) == {
|
||||
"one": {"id": "gpt-6.1-terra", "provider": "openai"},
|
||||
"two": {"id": "gpt-6-luna", "provider": ""},
|
||||
}
|
||||
@@ -99,6 +99,34 @@ def test_usage_flash_runner_counts_as_operator_and_has_no_tuning(tmp_path):
|
||||
assert usage["run_tuning"] is None
|
||||
|
||||
|
||||
def test_usage_uses_its_recorded_model_and_supports_legacy_endpoint_keys(tmp_path):
|
||||
db_path = tmp_path / "sessions.db"
|
||||
repo = SessionRepository(db_path)
|
||||
_insert_session(db_path, "models", {"profile": "flash"})
|
||||
_insert_usage(db_path, "models", "new", 1.0, {**_usage(100, 10), "model": "terra"})
|
||||
_insert_usage(
|
||||
db_path,
|
||||
"models",
|
||||
"legacy",
|
||||
2.0,
|
||||
{**_usage(200, 20), "source": "openai:terra:https://example.test:high"},
|
||||
)
|
||||
_insert_usage(
|
||||
db_path, "models", "lens", 3.0, {**_usage(300, 30), "source": "lens:step_capsule:terra"}
|
||||
)
|
||||
_insert_usage(db_path, "models", "unknown", 4.0, {**_usage(300, 30), "source": "other"})
|
||||
_insert_usage(
|
||||
db_path,
|
||||
"models",
|
||||
"fallback",
|
||||
5.0,
|
||||
{**_usage(300, 30), "source": "lens:step_capsule:fallback"},
|
||||
)
|
||||
models = {item["model"]: item for item in repo.get_session_usage("models")["by_node_model"]}
|
||||
assert models["terra"]["llm_calls"] == 3
|
||||
assert models["unknown"]["llm_calls"] == 2
|
||||
|
||||
|
||||
def test_usage_for_unknown_or_empty_session_is_zeroed(tmp_path):
|
||||
db_path = tmp_path / "sessions.db"
|
||||
repo = SessionRepository(db_path)
|
||||
@@ -112,6 +140,26 @@ def test_usage_for_unknown_or_empty_session_is_zeroed(tmp_path):
|
||||
assert usage["run_tuning"] is None
|
||||
|
||||
|
||||
def test_legacy_usage_preserves_model_colons_and_keeps_models_separate(tmp_path):
|
||||
db_path = tmp_path / "sessions.db"
|
||||
repo = SessionRepository(db_path)
|
||||
_insert_session(db_path, "models", {"profile": "flash"})
|
||||
sources = {
|
||||
"team/model:stable": "openai:team/model:stable:https://example.test:8443/v1:high",
|
||||
"team/model:preview": "openai:team/model:preview:http://[::1]:8080/v1:high",
|
||||
"unknown": "openai:team/model:preview:unparseable:high",
|
||||
}
|
||||
for index, source in enumerate(sources.values()):
|
||||
_insert_usage(
|
||||
db_path, "models", str(index), float(index), {**_usage(100, 10), "source": source}
|
||||
)
|
||||
|
||||
groups = {item["model"]: item for item in repo.get_session_usage("models")["by_node_model"]}
|
||||
assert groups.keys() == sources.keys()
|
||||
assert all(item["llm_calls"] == 1 for item in groups.values())
|
||||
assert all(item["prompt_tokens"] == 100 for item in groups.values())
|
||||
|
||||
|
||||
def test_usage_route_is_registered():
|
||||
from apps.admin_console.routers import sessions
|
||||
|
||||
|
||||
@@ -3,6 +3,30 @@ import json
|
||||
from apps.admin_console.database.repositories.step_repository import StepRepository
|
||||
|
||||
|
||||
def test_android_capture_receipt_survives_cleaning_without_becoming_image_store_id():
|
||||
receipt = {
|
||||
"ok": True,
|
||||
"action": "screenshot",
|
||||
"verified": True,
|
||||
"device_serial": "device-under-test",
|
||||
"trace_path": "artifacts/android/screen.png",
|
||||
"sha256": "a" * 64,
|
||||
"captured_at": "2026-10-28T00:00:00Z",
|
||||
"content": "large file content must not be copied into the task stream",
|
||||
}
|
||||
cleaned = StepRepository()._clean_tool_payload(
|
||||
{"args": {"action": "screenshot"}, "result": json.dumps(receipt)},
|
||||
"tool",
|
||||
"android_file",
|
||||
)
|
||||
assert cleaned["result"] == {key: value for key, value in receipt.items() if key != "content"}
|
||||
assert "post_image_name" not in cleaned
|
||||
wrapped = StepRepository()._clean_tool_payload(
|
||||
{"result": json.dumps({"outcome": json.dumps(receipt)})}, "tool", "android_file"
|
||||
)
|
||||
assert wrapped["result"] == cleaned["result"]
|
||||
|
||||
|
||||
def test_legacy_pause_log_is_normalized_to_failed_llm_trace():
|
||||
repository = StepRepository()
|
||||
trace = {
|
||||
|
||||
@@ -57,10 +57,13 @@ def manifest_payload():
|
||||
}
|
||||
|
||||
|
||||
def install(plugins):
|
||||
def install(plugins, original_case=False):
|
||||
package = BytesIO()
|
||||
manifest = manifest_payload()
|
||||
if original_case:
|
||||
manifest["workspace"]["panels"][0]["kind"] = "original_case"
|
||||
with zipfile.ZipFile(package, "w") as archive:
|
||||
archive.writestr("plugin.json", json.dumps(manifest_payload()))
|
||||
archive.writestr("plugin.json", json.dumps(manifest))
|
||||
install_archive(plugins, package.getvalue())
|
||||
plugins.set_enabled("review", True)
|
||||
|
||||
@@ -105,6 +108,86 @@ def save(client, values, revision=0, request_id=None):
|
||||
)
|
||||
|
||||
|
||||
def test_original_case_import_requires_enabled_plugin_and_keeps_execution_prompt(context):
|
||||
from apps.admin_console.services import task_import
|
||||
|
||||
store, plugins, client = context
|
||||
legacy = load_workbook(BytesIO(task_import.template()))
|
||||
assert legacy["任务"].max_column == 7
|
||||
legacy["任务"].append(["OLD", "", "", "旧用例", "", "原样执行", ""])
|
||||
legacy_bytes = BytesIO()
|
||||
legacy.save(legacy_bytes)
|
||||
assert "original_case" not in task_import.parse_workbook(legacy_bytes.getvalue())["tasks"][0]
|
||||
|
||||
install(plugins, original_case=True)
|
||||
workbook = load_workbook(BytesIO(task_import.template()))
|
||||
assert [cell.value for cell in workbook["任务"][1]] == [
|
||||
*task_import.HEADERS,
|
||||
*task_import.CASE_HEADERS,
|
||||
]
|
||||
workbook["任务"].append(
|
||||
[
|
||||
"CASE-001",
|
||||
"检测项目",
|
||||
"测试子项",
|
||||
"测试标题",
|
||||
"提取的约束",
|
||||
"只执行此提示词",
|
||||
"{}",
|
||||
"原始预置条件",
|
||||
"1. 第一步\n2. 第二步",
|
||||
"原始预期结果",
|
||||
]
|
||||
)
|
||||
stream = BytesIO()
|
||||
workbook.save(stream)
|
||||
case = task_import.parse_workbook(stream.getvalue())["tasks"][0]
|
||||
assert task_import.case_goal(case) == "只执行此提示词"
|
||||
assert case["preconditions"] == "提取的约束"
|
||||
assert case["original_case"] == {
|
||||
"preconditions": "原始预置条件",
|
||||
"steps": "1. 第一步\n2. 第二步",
|
||||
"expected": "原始预期结果",
|
||||
}
|
||||
assert task_import.parse_workbook(legacy_bytes.getvalue())["valid"]
|
||||
store.save_job({"session_id": "task-one", "status": "completed", "case": case})
|
||||
result = client.get("/api/task-extensions/task-one").json()
|
||||
assert "CASE-001" in result["case_title"] and "测试标题" in result["case_title"]
|
||||
assert result["original_case"] == case["original_case"]
|
||||
assert result["initial_prompt"] == "只执行此提示词"
|
||||
assert report_export.session_report("task-one")["original_case"] == case["original_case"]
|
||||
|
||||
plugins.set_enabled("review", False)
|
||||
assert load_workbook(BytesIO(task_import.template()))["任务"].max_column == 7
|
||||
with pytest.raises(ValueError, match="列名或顺序"):
|
||||
task_import.parse_workbook(stream.getvalue())
|
||||
|
||||
|
||||
def test_review_navigation_uses_batch_order_and_excludes_unfinished_jobs(context):
|
||||
store, _, client = context
|
||||
store.create_batch({"id": "review-batch", "status": "completed"}, [], {})
|
||||
for position, status in [(3, "completed"), (1, "failed"), (2, "pending")]:
|
||||
store.save_job(
|
||||
{
|
||||
"session_id": f"batch-task-{position}",
|
||||
"batch_id": "review-batch",
|
||||
"position": position,
|
||||
"status": status,
|
||||
"case": {"name": f"用例{position}", "goal": "测试"},
|
||||
}
|
||||
)
|
||||
first = client.get("/api/task-extensions/batch-task-1").json()["navigation"]
|
||||
assert first == {
|
||||
"scope": "batch",
|
||||
"position": 1,
|
||||
"total": 2,
|
||||
"previous": None,
|
||||
"next": "batch-task-3",
|
||||
}
|
||||
last = client.get("/api/task-extensions/batch-task-3").json()["navigation"]
|
||||
assert last["previous"] == "batch-task-1" and last["next"] is None
|
||||
|
||||
|
||||
def test_workspace_requires_v3_and_valid_bounded_declarations():
|
||||
for change in (
|
||||
{"schema_version": 2},
|
||||
|
||||
@@ -601,6 +601,45 @@ async def test_status_reports_external_global_owner_without_ipc_connection():
|
||||
assert result["goal"] == "CLI task: inspect settings"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("recorded", [True, False])
|
||||
async def test_status_uses_the_active_sessions_model(recorded):
|
||||
owner = DeviceLockOwner(
|
||||
pid=24680,
|
||||
process_created_at=1234.5,
|
||||
token="active-owner-token",
|
||||
device_id="emulator-5554",
|
||||
description="active task",
|
||||
acquired_at="2026-08-24T00:00:00+00:00",
|
||||
session_id="active",
|
||||
ingress="web",
|
||||
)
|
||||
state.queue_items.append(
|
||||
{"session_id": "active", "status": "running", "runtime_overrides": {"model": "terra"}}
|
||||
)
|
||||
with (
|
||||
patch.object(TaskQueueService, "ensure_worker_running"),
|
||||
patch(
|
||||
"apps.admin_console.routers.tasks.DeviceExecutionLock.get_active_owner",
|
||||
return_value=owner,
|
||||
),
|
||||
patch(
|
||||
"apps.admin_console.routers.tasks.DeviceExecutionLock.get_active_owners",
|
||||
return_value={},
|
||||
),
|
||||
patch("apps.admin_console.routers.tasks.session_repo") as repo,
|
||||
patch("apps.admin_console.routers.tasks.model_service") as models,
|
||||
):
|
||||
repo.get_latest_session.return_value = None
|
||||
repo.get_session_by_id.return_value = None
|
||||
repo.get_recorded_model_info_map.return_value = (
|
||||
{"active": {"id": "terra-recorded", "provider": "openai"}} if recorded else {}
|
||||
)
|
||||
models.get_active_model_info.return_value = {"id": "global", "provider": "openai"}
|
||||
result = await get_status()
|
||||
assert result["model_info"]["id"] == ("terra-recorded" if recorded else "terra")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_task_triggers_next_pending_task():
|
||||
executed_goals = []
|
||||
|
||||
Reference in new issue
Block a user