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:
chenmingxuan committed 2026-10-23 12:36:56 +08:00
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
+18
View File
@@ -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
+19
View File
@@ -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, "环境初始化失败")