feat: release 2.1.0 with optional Flash final AI review
Default Flash final review to off and persist the setting across the UI, MCP, SDK and queued tasks. Keep execution separate from test verdicts. Bundle UI fonts and licenses to remove Google Fonts from startup builds. Align backend/frontend versions and document installation and upgrades.
This commit is contained in:
1 parent
b584dae80a
commit
83bcfd54d5
52 files changed
+898
-33
No files matched your search
@@ -951,6 +951,51 @@ def test_human_review_overrides_every_report_preserves_ai_and_invalidates_batch_
|
||||
assert outcome_file.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
def test_skipped_flash_review_is_consistent_in_history_reports_and_human_review(
|
||||
client, isolated, monkeypatch
|
||||
):
|
||||
from artemis.runtime import case_result
|
||||
|
||||
monkeypatch.setattr(case_result, "TRACES_PATH", reports.TRACES_PATH)
|
||||
batch = service.create_batch(payload())
|
||||
job = isolated.jobs(batch["id"])[0]
|
||||
isolated.save_job({**job, "status": "completed"})
|
||||
folder = reports.TRACES_PATH / job["session_id"]
|
||||
folder.mkdir(parents=True)
|
||||
result = {
|
||||
"status": "inconclusive",
|
||||
"reason": "执行完成 · 未做 AI 判定",
|
||||
"evidence": [],
|
||||
"verification_skipped": True,
|
||||
}
|
||||
(folder / "run_outcome.json").write_text(
|
||||
json.dumps({"task_status": "completed", "tests": {}, "test_result": result}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
for item in (
|
||||
client.get("/api/automation/history").json()["items"][0],
|
||||
client.get(f"/api/automation/batches/{batch['id']}").json()["jobs"][0],
|
||||
client.get(f"/api/automation/reports/{job['session_id']}").json(),
|
||||
):
|
||||
assert item["status"] == "completed"
|
||||
assert item["test_result"] == {**result, "source": "system"}
|
||||
assert reports.session_report(job["session_id"])["verdict"] == "未做 AI 判定"
|
||||
paths = reports.export_session(job["session_id"])
|
||||
workbook = load_workbook(next(path for path in paths if path.suffix == ".xlsx"))
|
||||
assert "未做 AI 判定" in [
|
||||
cell.value for row in workbook["任务结果"].iter_rows() for cell in row
|
||||
]
|
||||
doc = Document(next(path for path in paths if path.suffix == ".docx"))
|
||||
assert "未做 AI 判定" in "\n".join(paragraph.text for paragraph in doc.paragraphs)
|
||||
reviewed = client.put(
|
||||
f"/api/automation/reports/{job['session_id']}/review",
|
||||
json={"status": "passed", "reason": "已人工核对"},
|
||||
).json()
|
||||
assert reviewed["test_result"]["source"] == "human"
|
||||
assert reviewed["test_result"]["ai_result"] == {**result, "source": "system"}
|
||||
assert reviewed["verdict"] == "测试通过(人工确认)"
|
||||
|
||||
|
||||
def test_review_rejects_active_missing_invalid_and_packing_tasks(client, isolated, monkeypatch):
|
||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||
url = f"/api/automation/reports/{job['session_id']}/review"
|
||||
|
||||
@@ -31,6 +31,7 @@ def isolated_settings(monkeypatch, tmp_path):
|
||||
"OPENAI_REASONING_EFFORT",
|
||||
"ARTEMIS_VIDEO_ENABLED",
|
||||
"ARTEMIS_OUTPUTTER_ENABLED",
|
||||
"ARTEMIS_FLASH_FINAL_CHECK_ENABLED",
|
||||
"ARTEMIS_STEP_SUMMARIZER_ENABLED",
|
||||
"ARTEMIS_SCREENSHOT_LIMIT_ENABLED",
|
||||
"ARTEMIS_TASK_TIMEOUT_SECONDS",
|
||||
@@ -149,6 +150,36 @@ def test_capabilities_persist_and_apply_to_new_tasks(isolated_settings):
|
||||
assert load_agent_config().video_analyzer.enabled is True
|
||||
|
||||
|
||||
def test_flash_review_defaults_off_and_persists_without_changing_pro(isolated_settings):
|
||||
assert Settings(_env_file=None).ARTEMIS_FLASH_FINAL_CHECK_ENABLED is False
|
||||
assert service.get_capability_settings().flash_final_check_enabled is False
|
||||
assert load_agent_config().checker.final_check is True
|
||||
for enabled in (True, False):
|
||||
saved = service.save_capability_settings(
|
||||
service.CapabilitySettings(flash_final_check_enabled=enabled)
|
||||
)
|
||||
assert saved.flash_final_check_enabled is enabled
|
||||
assert Settings(_env_file=isolated_settings).ARTEMIS_FLASH_FINAL_CHECK_ENABLED is enabled
|
||||
config = load_agent_config()
|
||||
assert config.flash.final_check_enabled is enabled
|
||||
assert config.checker.final_check is True
|
||||
assert config.pro.checker.final_check is True
|
||||
# Older clients omit the new switch; saving another setting keeps it.
|
||||
assert (
|
||||
service.save_capability_settings(
|
||||
service.CapabilitySettings(recording_enabled=False)
|
||||
).flash_final_check_enabled
|
||||
is enabled
|
||||
)
|
||||
|
||||
|
||||
def test_old_checker_config_does_not_enable_flash_review(monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", raising=False)
|
||||
path = tmp_path / "artemis.jsonc"
|
||||
path.write_text('{"agent":{"checker":{"enabled":true,"final_check":true}}}', encoding="utf-8")
|
||||
assert load_agent_config(path).flash.final_check_enabled is False
|
||||
|
||||
|
||||
def test_screenshot_json_can_override_environment(monkeypatch, tmp_path):
|
||||
config_path = tmp_path / "artemis.jsonc"
|
||||
config_path.write_text(
|
||||
|
||||
@@ -767,6 +767,34 @@ async def test_enqueue_tasks_unified_ingress():
|
||||
assert task["goal"] == "Test unified goal"
|
||||
|
||||
|
||||
def test_flash_review_is_refreshed_at_launch_and_frozen_in_worker_environment(monkeypatch):
|
||||
from apps.admin_console.services.execution_config import execution_config
|
||||
|
||||
monkeypatch.setattr("artemis.exploration.store.explorations.run", lambda _sid: None)
|
||||
monkeypatch.setattr("apps.admin_console.services.automation_store.store.job", lambda _sid: None)
|
||||
item = {"profile": "flash", "goal": "queued", "session_id": "flash-config-snapshot"}
|
||||
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "false")
|
||||
old_snapshot = execution_config(item)
|
||||
item["execution_config"] = old_snapshot
|
||||
assert old_snapshot["flash_final_check_enabled"] is False
|
||||
assert "flash_final_check_enabled" not in old_snapshot["runtime_overrides"]
|
||||
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "true")
|
||||
item["execution_config"] = execution_config(item)
|
||||
assert item["execution_config"]["flash_final_check_enabled"] is True
|
||||
# Simulate a later save between this task's snapshot and worker launch.
|
||||
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "false")
|
||||
_cmd, env = TaskQueueService._build_worker_invocation(
|
||||
item,
|
||||
"flash-config-snapshot",
|
||||
item["session_id"],
|
||||
item["goal"],
|
||||
"flash",
|
||||
TaskQueueService._task_target(item),
|
||||
)
|
||||
assert env["ARTEMIS_FLASH_FINAL_CHECK_ENABLED"] == "true"
|
||||
assert os.environ["ARTEMIS_FLASH_FINAL_CHECK_ENABLED"] == "false"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queue_worker_notifies_conversation():
|
||||
"""Verify queue_worker calls notify() when conversation_id is attached to task."""
|
||||
|
||||
@@ -123,6 +123,7 @@ async def test_acceptance_timeout_finishes_with_unknown_verdict(tmp_path, monkey
|
||||
)
|
||||
assert data["task_status"] == "completed"
|
||||
assert data["test_result"]["status"] == "inconclusive"
|
||||
|
||||
assert "总预算已用尽(0.01 秒)" in data["test_result"]["reason"]
|
||||
|
||||
|
||||
@@ -155,6 +156,23 @@ async def test_disabled_acceptance_does_not_call_a_model_or_claim_pass(
|
||||
)
|
||||
check.assert_not_called()
|
||||
assert data["test_result"]["status"] == "inconclusive"
|
||||
assert data["test_result"]["verification_skipped"] is True
|
||||
assert "未做 AI 判定" in data["test_result"]["reason"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skipping_review_does_not_hide_execution_failure(tmp_path, monkeypatch):
|
||||
check = AsyncMock()
|
||||
monkeypatch.setattr(flash, "run_final_check", check)
|
||||
data = await flash.settle_flash_outcome(
|
||||
context(tmp_path, disable_final_check=True),
|
||||
"任务",
|
||||
{"status": "failed", "explanation": "设备断开"},
|
||||
)
|
||||
check.assert_not_called()
|
||||
assert data["task_status"] == "blocked"
|
||||
assert data["test_result"]["status"] == "blocked"
|
||||
assert not data["test_result"].get("verification_skipped")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -74,6 +74,24 @@ async def test_config_requires_explicit_persistence_and_excludes_secrets(monkeyp
|
||||
assert payload["tool_fallback_model"] == "gpt-6-luna"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_can_save_flash_review_without_exposing_task_overrides(monkeypatch):
|
||||
request = AsyncMock(
|
||||
return_value={"flash_final_check_enabled": False, "recording_enabled": False}
|
||||
)
|
||||
monkeypatch.setattr(tools, "_request", request)
|
||||
await tools.mobile_config(
|
||||
section="capabilities",
|
||||
changes={"flash_final_check_enabled": True},
|
||||
persist_defaults=True,
|
||||
)
|
||||
assert request.call_args.args == ("POST", "/api/system/capability-settings")
|
||||
assert request.call_args.kwargs["json"] == {
|
||||
"flash_final_check_enabled": True,
|
||||
"recording_enabled": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_is_read_only_and_create_is_draft(monkeypatch, tmp_path):
|
||||
source = tmp_path / "cases.xlsx"
|
||||
|
||||
@@ -114,11 +114,24 @@ async def test_locked_work_profile_does_not_block_unlocked_device_owner():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monkeypatch):
|
||||
@pytest.mark.parametrize(
|
||||
"profile,enabled,expected",
|
||||
[
|
||||
("flash", False, False),
|
||||
("flash", True, True),
|
||||
("Flash", False, False),
|
||||
("pro", False, True),
|
||||
("pro", True, True),
|
||||
],
|
||||
)
|
||||
async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(
|
||||
monkeypatch, profile, enabled, expected
|
||||
):
|
||||
import uuid
|
||||
|
||||
canonical_sid = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"
|
||||
monkeypatch.setenv("ARTEMIS_SESSION_ID", canonical_sid)
|
||||
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", str(enabled).lower())
|
||||
|
||||
agent = Agent()
|
||||
assert agent._session_id == canonical_sid
|
||||
@@ -127,7 +140,7 @@ async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monk
|
||||
task.id = "fallback-id"
|
||||
task.get_name.return_value = "task-name"
|
||||
task.request.goal = "test goal"
|
||||
task.request.profile = "flash"
|
||||
task.request.profile = profile
|
||||
task.request.task_name = "task-name"
|
||||
task.request.trace_path = "traces"
|
||||
|
||||
@@ -138,6 +151,7 @@ async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monk
|
||||
mock_data_engine_cls.return_value = mock_data_engine
|
||||
|
||||
agent._prepare_tracing(task=task, context=context)
|
||||
assert context.execution_setup.final_check_enabled is expected
|
||||
|
||||
mock_data_engine.start_session.assert_called_once()
|
||||
call_kwargs = mock_data_engine.start_session.call_args.kwargs
|
||||
|
||||
@@ -44,6 +44,25 @@ def test_completed_execution_and_failed_test_are_independent():
|
||||
assert resolve_trace_suffix("completed", outcome("failed")) == "_TESTFAIL"
|
||||
|
||||
|
||||
def test_skipped_review_metadata_survives_reload_without_changing_model_schema(tmp_path):
|
||||
result = CaseTestResult(
|
||||
status="inconclusive", reason="执行完成 · 未做 AI 判定", verification_skipped=True
|
||||
)
|
||||
directory = tmp_path / "task-skipped"
|
||||
directory.mkdir()
|
||||
(directory / "run_outcome.json").write_text(
|
||||
json.dumps({"task_status": "completed", "test_result": result.model_dump()}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loaded = read_case_result("task-skipped", "completed", root=tmp_path)
|
||||
assert loaded == result.model_dump()
|
||||
assert loaded["verification_skipped"] is True
|
||||
assert "verification_skipped" not in CaseTestResult.model_json_schema()["properties"]
|
||||
assert (
|
||||
"verification_skipped" not in CaseTestResult(status="blocked", reason="失败").model_dump()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["failed", "cancelled", "interrupted", "skipped"])
|
||||
def test_incomplete_execution_does_not_inherit_a_pass(status):
|
||||
result = resolve_case_result(outcome(), status, "环境初始化失败")
|
||||
|
||||
Reference in new issue
Block a user