feat: prepare 2.2.0 with UI and execution fixes

Add dark theme, richer Markdown and readable plugin results.
Improve Android input evidence, response integrity and diagnostics.
Unify lifecycle, guidance receipts and evidence sources across clients.
Keep direct action dispatch without an added AI quota approval layer.
Align version metadata at 2.2.0 and bundle Helper 1.2.2.

Validation: 3063 backend tests before the version bump; 44 after.
Frontend: 317 tests and production build passed.
This commit is contained in:
chenmingxuan committed 2026-10-26 00:44:13 +08:00
1 parent 83bcfd54d5
commit fac4ccb760
146 files changed
+5689 -2033

No files matched your search

@@ -19,6 +19,7 @@ import time
import pytest
from artemis.runtime import cancel_requests as cr
from artemis.runtime.cancellation import CancellationReason
@pytest.fixture(autouse=True)
@@ -91,3 +92,27 @@ async def test_watch_delivers_once_and_clears_marker():
assert await asyncio.wait_for(watcher, timeout=2) is True
assert delivered == [1]
assert not cr.is_cancel_requested(session_id="w-1")
@pytest.mark.asyncio
@pytest.mark.parametrize("cause", [CancellationReason.USER_STOP, CancellationReason.UNKNOWN])
async def test_watch_forwards_only_the_structured_cause(cause):
path = cr.request_cancel(
session_id="with-reason", reason="private stop details", cancel_reason=cause
)[0]
delivered = []
assert await cr.watch_for_cancel_request(
delivered.append, session_id="with-reason", pid=0, pass_reason=True
)
assert delivered == [cause]
assert not path.exists()
def test_legacy_or_unrecognized_marker_reasons_remain_unknown():
path = cr.request_cancel(session_id="legacy", reason="stop from UI")[0]
payload = json.loads(path.read_text(encoding="utf-8"))
for value in [None, "private user instruction", ["user_stop"]]:
payload["cancel_reason"] = value
path.write_text(json.dumps(payload), encoding="utf-8")
assert cr.get_cancel_reason(session_id="legacy") is CancellationReason.UNKNOWN
assert cr.is_cancel_requested(session_id="legacy")
+63
View File
@@ -14,6 +14,47 @@ import pytest
import artemis.runtime.task_control as task_control
def test_transition_timing_keeps_repeated_states_and_separates_pause_and_resource_wait(
tmp_path, monkeypatch
):
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
clock = [10]
monkeypatch.setattr(
task_control,
"time",
SimpleNamespace(**{**vars(task_control.time), "time": lambda: clock[0]}),
)
for at, status in (
(10, "queued"),
(20, "running"),
(25, "running"),
(30, "task_paused"),
(40, "resuming"),
(50, "running"),
):
clock[0] = at
task_control.write_control_state("timed-task", status)
assert task_control.read_state("timed-task")["timing"] == {
"started_at": 10,
"state_started_at": 50,
"seconds": {"queued": 10, "running": 10, "task_paused": 10, "resuming": 10},
}
def test_non_object_control_state_and_invalid_timing_can_be_replaced(tmp_path, monkeypatch):
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
path = task_control.control_dir("broken-state") / "task_control_state.json"
path.parent.mkdir(parents=True)
path.write_text("[]", encoding="utf-8")
assert task_control.read_state("broken-state") == {}
task_control.write_control_state("broken-state", "running")
state = task_control.read_state("broken-state")
state["timing"] = "invalid"
task_control._write_atomic(path, state)
task_control.write_control_state("broken-state", "task_paused")
assert task_control.read_state("broken-state")["timing"]["seconds"] == {}
def _windows_access_error(code: int) -> PermissionError:
error = PermissionError(errno.EACCES, "state file is in use")
error.winerror = code
@@ -423,6 +464,28 @@ def test_guidance_history_distinguishes_submission_from_runner_consumption(tmp_p
assert all(item["status"] == "consumed" and item["consumed_at"] for item in history)
def test_terminal_guidance_reports_no_consumption_and_keeps_original_receipt(tmp_path, monkeypatch):
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
receipt = task_control.add_instruction("task-123", "检查新入口", instruction_id="guidance")
history = task_control.list_instructions("task-123", task_status="cancelled")
assert history[0]["status"] == "unapplied"
assert history[0]["accepted_at"] == receipt["timestamp"]
assert "consumed_at" not in history[0]
assert task_control.list_instructions("task-123")[0]["status"] == "pending"
def test_claimed_guidance_is_visible_and_not_falsely_reported_as_consumed(tmp_path, monkeypatch):
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
task_control.add_instruction("task-123", "检查新入口", instruction_id="guidance")
directory = task_control.control_dir("task-123") / "instructions"
(directory / "guidance.json").rename(directory / "guidance.reading")
assert task_control.list_instructions("task-123")[0]["status"] == "reading"
assert (
task_control.list_instructions("task-123", task_status="completed")[0]["status"]
== "unconfirmed"
)
def test_guidance_history_endpoint_returns_pending_and_consumed(tmp_path, monkeypatch):
from fastapi import FastAPI
from fastapi.testclient import TestClient
+96
View File
@@ -0,0 +1,96 @@
from artemis.runtime.task_progress import progress_snapshot
def test_queued_time_is_not_execution_time_even_with_legacy_started_at():
value = progress_snapshot(job={"status": "queued", "created_at": 10, "started_at": 10}, now=50)
assert value["phase"] == "queued"
assert value["durations"]["queued"] == 40
assert value["durations"]["execution"] is None
def test_progress_splits_wait_pause_review_and_export_without_double_counting():
value = progress_snapshot(
job={
"status": "completed",
"created_at": 10,
"worker_started_at": 20,
"ended_at": 120,
"finalized_at": 125,
},
control={
"status": "running",
"timing": {
"started_at": 20,
"state_started_at": 80,
"seconds": {"queued": 10, "task_paused": 20, "resuming": 10, "running": 20},
},
},
reviews=[{"started_at": 90, "ended_at": 110}, {"started_at": 100, "ended_at": 115}],
now=500,
)
assert value["phase"] == "finished"
assert value["durations"] == {
"total": 115,
"queued": 10,
"resource_wait": 20,
"paused": 20,
"final_review": 25,
"execution": 35,
"report_export": 5,
}
def test_active_review_has_its_own_phase_and_terminal_review_is_not_still_active():
review = {"started_at": 30, "status": "running", "updated_at": 35}
live = progress_snapshot(job={"status": "running"}, reviews=[review], now=40)
assert live["phase"] == "final_review"
assert live["durations"]["final_review"] == 10
stopped = progress_snapshot(
job={"status": "cancelled", "ended_at": 45}, reviews=[review], now=500
)
assert stopped["phase"] == "finished"
assert stopped["durations"]["final_review"] == 5
assert not stopped["review_timing_complete"]
def test_legacy_missing_intervals_are_unknown_rather_than_zero_or_model_speed():
value = progress_snapshot(
session={"status": "completed", "start_time": 20, "end_time": 100}, now=500
)
assert value["durations"]["queued"] is None
assert value["durations"]["execution"] is None
assert value["durations"]["paused"] is None
assert value["durations"]["total"] is None
assert value["observed_seconds"] == 80
def test_waiting_for_resource_and_pause_are_not_running():
for status, phase in (
("queued", "waiting_resources"),
("resuming", "waiting_resources"),
("task_paused", "paused"),
):
value = progress_snapshot(
job={"status": "running", "worker_started_at": 10}, control={"status": status}, now=50
)
assert value["phase"] == phase
def test_terminal_without_end_timestamp_does_not_keep_increasing_or_guess_duration():
job = {"status": "completed", "created_at": 10, "worker_started_at": 20}
first = progress_snapshot(job=job, now=100)
assert first == progress_snapshot(job=job, now=500)
assert first["observed_at"] is None
assert all(first["durations"][key] is None for key in ("total", "execution", "report_export"))
def test_export_in_progress_keeps_total_current_without_extending_execution():
value = progress_snapshot(
job={"status": "finalizing", "created_at": 10, "worker_started_at": 100, "ended_at": 200},
now=230,
)
assert value["phase"] == "report_export"
assert value["observed_at"] == 230
assert value["durations"]["total"] == 220
assert value["durations"]["report_export"] == 30
assert value["worker_ended_at"] == 200