feat: add guided exploration and share user-started MCP backend

This commit is contained in:
chenmingxuan committed 2026-10-16 18:32:46 +08:00
1 parent f86db1ced6
commit ee256ad7ce
144 files changed
+8625 -2075

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
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()