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:
1 parent
2328a4df0c
commit
e1ea099592
128 files changed
+4944
-700
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user