124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
import os
|
|
import time
|
|
import subprocess
|
|
import sys
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
from apps.admin_console.services.automation_store import AutomationStore
|
|
from apps.admin_console.services.exploration_recovery import reconcile_explorations
|
|
from artemis.config.exploration import ExplorationConfig
|
|
from artemis.exploration.editorial import DiscussionReply, discuss
|
|
from artemis.exploration.store import ExplorationStore
|
|
from artemis.runtime import task_control
|
|
|
|
|
|
def test_worker_can_import_exploration_without_importing_web_server():
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"import sys; from artemis.exploration.runtime import ExplorationControl; assert 'apps.admin_console.server' not in sys.modules",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=30,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_web_and_exploration_share_the_same_queue_state():
|
|
code = "from apps.admin_console import server; from apps.admin_console.services import task_queue_service, exploration_recovery; from apps.admin_console.routers import tasks; assert server.state is task_queue_service.state is exploration_recovery.state is tasks.state"
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", code], capture_output=True, text=True, encoding="utf-8", timeout=30
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
@pytest.fixture
|
|
def records(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path / "traces")
|
|
repo = ExplorationStore(AutomationStore(tmp_path / "history.db"))
|
|
session = repo.create("探索设置", ExplorationConfig(), {}, "create")
|
|
return repo, session
|
|
|
|
|
|
def test_recovery_preserves_live_paused_owner_and_real_verdict(records, monkeypatch):
|
|
repo, session = records
|
|
run = repo.reserve_run(session["id"], "start", kind="exploration", goal="设置", task_config={})
|
|
repo.set_run_state(run["id"], "paused", reason="execution_budget")
|
|
monkeypatch.setattr(
|
|
"apps.admin_console.services.exploration_recovery.read_state",
|
|
lambda _: {"pid": os.getpid()},
|
|
)
|
|
reconcile_explorations(repo)
|
|
assert repo.get(session["id"])["status"] == "paused"
|
|
monkeypatch.setattr(
|
|
"apps.admin_console.services.exploration_recovery.read_worker_outcome",
|
|
lambda _: {
|
|
"task_status": "completed",
|
|
"test_result": {"status": "passed", "reason": "有证据"},
|
|
},
|
|
)
|
|
reconcile_explorations(repo)
|
|
assert repo.get(session["id"])["active_run_id"] is None
|
|
assert repo.run(run["id"])["outcome"]["test_result"]["status"] == "passed"
|
|
|
|
|
|
def test_orphan_editorial_jobs_become_retryable_without_starting_tasks(records):
|
|
repo, session = records
|
|
for kind in ("exploration_generation", "exploration_discussion"):
|
|
repo.database.put(
|
|
kind,
|
|
"dead",
|
|
{
|
|
"id": "dead",
|
|
"status": "running",
|
|
"owner_pid": 0,
|
|
"lease_until": time.time() + 999,
|
|
"exploration_id": session["id"],
|
|
},
|
|
)
|
|
reconcile_explorations(repo)
|
|
assert repo.runs(session["id"]) == []
|
|
assert repo.database.records("exploration_generation")[0]["status"] == "failed"
|
|
assert repo.database.records("exploration_discussion")[0]["status"] == "failed"
|
|
|
|
|
|
def test_validation_status_follows_actual_worker_without_creating_outcome(records, monkeypatch):
|
|
from artemis.exploration.models import CaseDraft
|
|
|
|
repo, session = records
|
|
repo.save_case(session["id"], CaseDraft(name="设置", goal="检查设置"), 0, "save")
|
|
run = repo.reserve_run(
|
|
session["id"], "validate", kind="validation", goal="复跑", task_config={}, case_version=1
|
|
)
|
|
monkeypatch.setattr(
|
|
"apps.admin_console.services.exploration_recovery.read_state",
|
|
lambda _: {"pid": os.getpid(), "status": "running"},
|
|
)
|
|
reconcile_explorations(repo)
|
|
assert repo.run(run["id"])["status"] == "running"
|
|
assert repo.run(run["id"])["outcome"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completed_discussion_is_idempotent_and_never_admits_device_work(
|
|
records, monkeypatch
|
|
):
|
|
repo, session = records
|
|
message = repo.add_message(session["id"], "为什么选这个入口?", "m")
|
|
monkeypatch.setattr("artemis.exploration.editorial.editorial_context", Mock())
|
|
model = Mock()
|
|
model.with_structured_output.return_value.ainvoke = AsyncMock(
|
|
return_value=DiscussionReply(reply="已观察到的入口在右上角;可编辑提示词。")
|
|
)
|
|
monkeypatch.setattr("artemis.exploration.editorial.get_llm", Mock(return_value=model))
|
|
await discuss(repo, session["id"], message)
|
|
await discuss(repo, session["id"], message)
|
|
assert len([m for m in repo.messages(session["id"]) if m["role"] == "assistant"]) == 1
|
|
assert repo.runs(session["id"]) == []
|
|
model.with_structured_output.return_value.ainvoke.assert_awaited_once()
|