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.
116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
"""Shared task progress stays read-only and distinguishes receipt from consumption."""
|
|
|
|
import json
|
|
import sqlite3
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
import pytest
|
|
|
|
from apps.admin_console.routers import tasks
|
|
from apps.admin_console.services import task_lifecycle as service
|
|
from artemis.runtime import task_control as control
|
|
|
|
|
|
@pytest.fixture
|
|
def recorded(tmp_path, monkeypatch):
|
|
db = tmp_path / "只读 progress.db"
|
|
sid = "recorded-task"
|
|
with sqlite3.connect(db) as conn:
|
|
conn.executescript(
|
|
"CREATE TABLE sessions (session_id TEXT, status TEXT, pid INTEGER, "
|
|
"start_time REAL, end_time REAL);"
|
|
"CREATE TABLE automation_jobs (session_id TEXT, payload TEXT);"
|
|
"CREATE TABLE traces (session_id TEXT, type TEXT, payload TEXT);"
|
|
)
|
|
conn.execute("INSERT INTO sessions VALUES (?, 'completed', 17, 20, 120)", (sid,))
|
|
conn.execute(
|
|
"INSERT INTO automation_jobs VALUES (?, ?)",
|
|
(
|
|
sid,
|
|
json.dumps(
|
|
{
|
|
"status": "completed",
|
|
"created_at": 10,
|
|
"worker_started_at": 20,
|
|
"ended_at": 120,
|
|
"finalized_at": 125,
|
|
}
|
|
),
|
|
),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO traces VALUES (?, 'final_review_diagnostic', ?)",
|
|
(sid, json.dumps({"started_at": 90, "ended_at": 110, "status": "completed"})),
|
|
)
|
|
monkeypatch.setattr(control, "TRACES_PATH", tmp_path / "traces")
|
|
monkeypatch.setattr(service, "DB_PATH", db)
|
|
control._write_atomic(
|
|
control.control_dir(sid) / "task_control_state.json",
|
|
{
|
|
"pid": 17,
|
|
"status": "running",
|
|
"timing": {
|
|
"started_at": 20,
|
|
"state_started_at": 80,
|
|
"seconds": {"queued": 10, "task_paused": 20, "running": 30},
|
|
},
|
|
},
|
|
)
|
|
return db, sid
|
|
|
|
|
|
def test_finished_projection_is_stable_and_does_not_bootstrap_or_change_database(recorded):
|
|
db, sid = recorded
|
|
before = db.read_bytes()
|
|
first = service.task_lifecycle(sid, db_path=str(db), now=500)
|
|
assert first == service.task_lifecycle(sid, db_path=db, now=900)
|
|
assert first["durations"] == {
|
|
"total": 115,
|
|
"queued": 10,
|
|
"resource_wait": 10,
|
|
"paused": 20,
|
|
"final_review": 20,
|
|
"execution": 50,
|
|
"report_export": 5,
|
|
}
|
|
assert db.read_bytes() == before
|
|
|
|
|
|
def test_legacy_guidance_array_and_detailed_web_receipt_share_terminal_state(recorded):
|
|
_, sid = recorded
|
|
pending = control.add_instruction(sid, "保存现有证据", instruction_id="pending")
|
|
original = control.control_dir(sid) / "instructions" / "pending.json"
|
|
before = original.read_bytes()
|
|
app = FastAPI()
|
|
app.include_router(tasks.router)
|
|
client = TestClient(app)
|
|
details = client.get(f"/api/tasks/{sid}/guidance?details=true").json()
|
|
legacy = client.get(f"/api/tasks/{sid}/guidance").json()
|
|
assert details["instructions"] == legacy
|
|
assert legacy[0]["status"] == "unapplied"
|
|
assert legacy[0]["accepted_at"] == pending["timestamp"]
|
|
assert details["lifecycle"]["guidance_counts"]["unapplied"] == 1
|
|
assert original.read_bytes() == before
|
|
|
|
|
|
def test_state_from_another_worker_does_not_supply_current_timing(recorded):
|
|
db, sid = recorded
|
|
state = control.read_state(sid)
|
|
state["pid"] = 18
|
|
control._write_atomic(control.control_dir(sid) / "task_control_state.json", state)
|
|
value = service.task_lifecycle(sid, db_path=db)
|
|
assert not value["control_timing_recorded"]
|
|
assert value["durations"]["execution"] is None
|
|
assert value["durations"]["resource_wait"] is None
|
|
|
|
|
|
def test_missing_database_is_not_created_and_does_not_hide_guidance(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(control, "TRACES_PATH", tmp_path)
|
|
control.add_instruction("new-task", "等待安全检查点")
|
|
path = tmp_path / "missing.db"
|
|
value = service.task_lifecycle("new-task", db_path=path, now=100)
|
|
assert value["phase"] == "unknown"
|
|
assert value["guidance_counts"]["pending"] == 1
|
|
assert not path.exists()
|