"""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()