feat: add guided exploration and share user-started MCP backend
This commit is contained in:
1 parent
f86db1ced6
commit
ee256ad7ce
144 files changed
+8625
-2075
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user