feat: add official Codex skill and harden task execution

Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
This commit is contained in:
chenmingxuan committed 2026-10-18 14:18:37 +08:00
1 parent 2328a4df0c
commit e1ea099592
128 files changed
+4944 -700

No files matched your search

+101 -4
View File
@@ -17,6 +17,7 @@ settlement timeout, when-semantics at the exit, run outcome, and the four
switch-combination matrix."""
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -83,7 +84,7 @@ async def test_exploration_question_prevents_completed_plan_from_starting_final_
repo.ask(run["id"], "下一步做什么?", "返回主页", "ask")
with patch("artemis.graph.graph.run_final_check", new=AsyncMock()) as reviewer:
assert convergence_gate(_make_state(), ctx) == "continue"
result = await exit_settlement_node(_make_state(), ctx)
result = await exit_settlement_node(_make_state(execution_report="本次调查报告"), ctx)
assert result == {"exit_settlement_route": "continue"}
reviewer.assert_not_called()
assert repo.get(session["id"])["pending_question"]
@@ -100,7 +101,7 @@ async def test_exploration_late_return_home_guidance_invalidates_final_review(ex
return CheckReport(verdicts=[], unmet_subgoals=[])
with patch("artemis.graph.graph.run_final_check", new=review):
result = await exit_settlement_node(_make_state(), ctx)
result = await exit_settlement_node(_make_state(execution_report="本次调查报告"), ctx)
assert result == {"exit_settlement_route": "continue"}
assert repo.run(run["id"])["status"] == "running"
assert repo.run(run["id"])["user_guidance_count"] == 1
@@ -118,12 +119,99 @@ async def test_review_stops_capture_before_waiting_and_seals_exploration(explora
return CheckReport(verdicts=[], unmet_subgoals=[])
with patch("artemis.graph.graph.run_final_check", new=review):
result = await exit_settlement_node(_make_state(), ctx)
result = await exit_settlement_node(_make_state(execution_report="本次调查报告"), ctx)
assert result["exit_settlement_route"] == "end"
assert repo.get(session["id"])["status"] == "finalizing"
ctx.on_execution_resume.assert_not_called()
@pytest.mark.asyncio
async def test_completed_exploration_waits_for_report_before_review(exploration_exit):
ctx, repo, session, run = exploration_exit
state = State.initial("保存并交付完整报告")
with patch("artemis.graph.graph.run_final_check", new=AsyncMock()) as review:
assert convergence_gate(state, ctx) == "continue"
assert await exit_settlement_node(state, ctx) == {"exit_settlement_route": "continue"}
review.assert_not_called()
ctx.on_execution_review.assert_not_called()
assert repo.run(run["id"])["status"] == "running"
@pytest.mark.asyncio
@pytest.mark.parametrize("verdict_status", ["passed", "failed", "inconclusive"])
async def test_report_is_saved_and_fully_delivered_before_independent_review(
exploration_exit, verdict_status
):
from artemis.runtime.case_result import CaseTestResult
ctx, repo, session, run = exploration_exit
body = "# 本次调查报告\n" + "实际证据及未测范围。" * 2400
state = State(initial_goal="交付完整报告", execution_report=body)
_write_plan(ctx.data_engine.base_dir, "- [x] 调查并交付报告\n - assert: 实际页面结果\n")
async def review(*args, **kwargs):
directory = ctx.data_engine.base_dir
assert (directory / "notes" / "output.md").read_text(encoding="utf-8") == body
assert json.loads((directory / "result.json").read_text(encoding="utf-8")) == {
"result": body
}
delivered = [m for m in repo.messages(session["id"]) if m.get("kind") == "execution_report"]
assert len(delivered) == 3
assert "".join(m["text"] for m in delivered) == body
assert kwargs["execution_report"] == body
assert kwargs["report_delivery"]["exploration_message_sequences"] == [
m["sequence"] for m in delivered
]
return CheckReport(
verdicts=[
CheckVerdict(
item_text="实际页面结果",
kind="assert",
status=verdict_status,
evidence="步骤 1",
)
],
test_result=CaseTestResult(
status=verdict_status, reason="按本次实际证据裁定", evidence=["步骤 1 实际页面"]
),
)
with patch("artemis.graph.graph.run_final_check", new=review):
result = await exit_settlement_node(state, ctx)
assert result["run_outcome"]["test_result"]["status"] == verdict_status
assert repo.run(run["id"])["status"] == "finalizing"
@pytest.mark.asyncio
async def test_report_delivery_is_idempotent_with_review_disabled(exploration_exit):
ctx, repo, session, run = exploration_exit
ctx.execution_setup.disable_checker = True
state = State(initial_goal="调查", execution_report="完整报告正文")
assert convergence_gate(state, ctx) == "exit_settlement"
with patch("artemis.graph.graph.run_final_check", new=AsyncMock()) as review:
await exit_settlement_node(state, ctx)
await exit_settlement_node(state, ctx)
review.assert_not_called()
delivered = [m for m in repo.messages(session["id"]) if m.get("kind") == "execution_report"]
assert len(delivered) == 1
assert delivered[0]["text"] == "完整报告正文"
@pytest.mark.asyncio
async def test_report_write_failure_cannot_release_as_completed(exploration_exit, monkeypatch):
from pathlib import Path
ctx, repo, session, run = exploration_exit
state = State(initial_goal="调查", execution_report="完整报告正文")
monkeypatch.setattr(Path, "replace", MagicMock(side_effect=OSError("disk full")))
with patch("artemis.graph.graph.run_final_check", new=AsyncMock()) as review:
with pytest.raises(OSError, match="disk full"):
await exit_settlement_node(state, ctx)
review.assert_not_called()
assert repo.run(run["id"])["status"] == "running"
assert not list(ctx.data_engine.base_dir.rglob("*.tmp"))
def _make_ctx(tmp_path, **setup_kwargs):
setup_kwargs.setdefault("disable_checker", False)
ctx = MagicMock(spec=ArtemisContext)
@@ -311,7 +399,16 @@ async def test_final_check_receives_ledger_and_at_end_items(tmp_path):
captured = {}
async def fake_final(ctx_arg, goal, plan_text, ledger, check_items, attempt_id=None):
async def fake_final(
ctx_arg,
goal,
plan_text,
ledger,
check_items,
attempt_id=None,
execution_report=None,
report_delivery=None,
):
captured["goal"] = goal
captured["ledger"] = ledger
captured["items"] = list(check_items)