"""Workspace plugins use isolated persistence and never execute models or devices.""" import json from io import BytesIO import sys from uuid import uuid4 import zipfile from docx import Document from fastapi import FastAPI from fastapi.testclient import TestClient from openpyxl import load_workbook import pytest from apps.admin_console.database.repositories.session_repository import SessionRepository from apps.admin_console.database.repositories.step_repository import StepRepository from apps.admin_console.routers import task_extensions as api from apps.admin_console.services import batch_archive, report_export, task_extensions from apps.admin_console.services.automation_store import AutomationStore from artemis.data_engine.storage import StorageManager from artemis.plugins.installer import install_archive from artemis.plugins.models import PluginManifest, PluginSelection from artemis.plugins.store import PluginStore def manifest_payload(): return { "schema_version": 3, "id": "review", "name": "人工评审", "version": "1.0.0", "platforms": [sys.platform], "targets": [], "capabilities": {"kind": "other"}, "workspace": { "panels": [ {"id": "prompt", "title": "任务需求", "side": "left", "kind": "initial_prompt"}, {"id": "review", "title": "人工评审", "side": "right", "kind": "form"}, ], "fields": [ { "id": "accuracy", "label": "AI执行准确性", "kind": "choice", "options": ["Pass", "Failed"], }, {"id": "accuracy_note", "label": "执行备注", "kind": "text", "max_length": 100}, { "id": "correctness", "label": "测试结果正确性", "kind": "choice", "options": ["Pass", "Failed"], }, {"id": "correctness_note", "label": "结果备注", "kind": "text"}, ], }, } def install(plugins): package = BytesIO() with zipfile.ZipFile(package, "w") as archive: archive.writestr("plugin.json", json.dumps(manifest_payload())) install_archive(plugins, package.getvalue()) plugins.set_enabled("review", True) @pytest.fixture def context(tmp_path, monkeypatch): path = tmp_path / "data.db" StorageManager(db_path=path, base_trace_dir=tmp_path / "traces") store = AutomationStore(path) plugins = PluginStore(tmp_path / "plugins") monkeypatch.setattr(task_extensions, "plugin_store", lambda: plugins) for module in (api, report_export): monkeypatch.setattr(module, "store", store) monkeypatch.setattr(report_export, "SessionRepository", lambda *args: SessionRepository(path)) monkeypatch.setattr(report_export, "StepRepository", lambda *args: StepRepository(path)) monkeypatch.setattr(report_export, "TRACES_PATH", tmp_path / "traces") monkeypatch.setattr(report_export, "EXPORT_ROOT", tmp_path / "exports") store.save_job( { "session_id": "task-one", "status": "completed", "case": { "name": "测试任务", "goal": "# 最初的需求\n\n|项目|要求|\n|---|---|\n|页面|可见|", }, } ) app = FastAPI() app.include_router(api.router) with TestClient(app) as client: yield store, plugins, client def save(client, values, revision=0, request_id=None): return client.put( "/api/task-extensions/task-one/review", json={ "values": values, "expected_revision": revision, "request_id": request_id or str(uuid4()), }, ) def test_workspace_requires_v3_and_valid_bounded_declarations(): for change in ( {"schema_version": 2}, { "workspace": { "panels": [ { "id": "prompt", "title": "需求", "side": "left", "kind": "initial_prompt", "width": {"minimum": 500, "preferred": 300}, } ] } }, { "workspace": { "panels": [{"id": "prompt", "title": "需求", "side": "left", "kind": "javascript"}] } }, ): with pytest.raises(ValueError): PluginManifest.model_validate({**manifest_payload(), **change}) def test_plugin_is_optional_and_not_selected_for_execution(context): store, plugins, client = context assert client.get("/api/task-extensions/task-one").json()["plugins"] == [] assert save(client, {}).status_code == 409 install(plugins) assert plugins.selections() == [] with pytest.raises(ValueError, match="工作区扩展"): plugins.pin([PluginSelection(id="review")], "android") data = client.get("/api/task-extensions/task-one").json() assert data["initial_prompt"].startswith("# 最初的需求") assert set(data["plugins"][0]["values"].values()) == {""} assert save(client, {}).status_code == 200 assert store.get("case_review", "task-one") is None def test_prompt_is_initial_session_request_not_case_or_step_text(context): store, _, client = context with store.connection() as conn: conn.execute( "INSERT INTO sessions(session_id,initial_goal,status,start_time) VALUES (?,?,?,?)", ("task-one", "原始任务需求", "completed", 1), ) conn.commit() assert client.get("/api/task-extensions/task-one").json()["initial_prompt"] == "原始任务需求" def test_partial_clear_cas_and_idempotent_retry_preserve_history(context): store, plugins, client = context install(plugins) request_id = str(uuid4()) assert ( save( client, {"accuracy": "Pass", "accuracy_note": "人工核对"}, request_id=request_id ).status_code == 200 ) assert ( save( client, {"accuracy": "Pass", "accuracy_note": "人工核对"}, request_id=request_id ).status_code == 200 ) assert save(client, {"accuracy": "Failed"}, request_id=request_id).status_code == 409 assert save(client, {"correctness": "Failed"}, revision=0).status_code == 409 assert save(client, {"correctness": "Failed"}, revision=1).status_code == 200 result = save(client, {"accuracy": "", "accuracy_note": ""}, revision=2).json()["plugins"][0] assert result["values"]["accuracy"] == "" assert result["values"]["correctness"] == "Failed" assert len(result["history"]) == 3 assert result["history"][0]["values"]["accuracy"] == "Pass" assert store.get("case_review", "task-one") is None def test_invalid_running_missing_and_disabled_writes_are_rejected(context): store, plugins, client = context install(plugins) for values in ({"unknown": "Pass"}, {"accuracy": "Maybe"}, {"accuracy_note": "x" * 101}): assert save(client, values).status_code == 409 store.save_job({**store.job("task-one"), "status": "running"}) assert not client.get("/api/task-extensions/task-one").json()["editable"] assert save(client, {"accuracy": "Pass"}).status_code == 409 assert client.get("/api/task-extensions/missing").status_code == 404 assert store.get("task_extensions", "task-one") is None def test_disable_uninstall_reinstall_keep_history_and_gate_exports(context): store, plugins, client = context install(plugins) save(client, {"accuracy": "Pass"}) plugins.set_enabled("review", False) assert task_extensions.report_sections("task-one", store) == [] assert save(client, {"accuracy": "Failed"}, revision=1).status_code == 409 plugins.uninstall("review") assert client.get("/api/task-extensions/task-one").json()["plugins"] == [] install(plugins) data = client.get("/api/task-extensions/task-one").json()["plugins"][0] assert data["values"]["accuracy"] == "Pass" assert data["revision"] == 1 store.save_job({"session_id": "rerun", "status": "completed", "case": {"goal": "同样需求"}}) assert client.get("/api/task-extensions/rerun").json()["plugins"][0]["revision"] == 0 store.delete_history("task-one") assert store.get("task_extensions", "task-one") is None def test_word_excel_blank_fields_and_cache_refresh(context): store, plugins, client = context install(plugins) original_result = report_export.session_report("task-one")["test_result"] paths = report_export.export_session("task-one", ["docx", "xlsx"]) wb = load_workbook(paths[1]) assert wb["人工评审"].cell(2, 3).value is None save(client, {"accuracy": "Pass", "accuracy_note": "=1+1"}) paths = report_export.export_session("task-one", ["docx", "xlsx"]) wb = load_workbook(paths[1]) assert wb["人工评审"].cell(2, 3).value == "Pass" assert wb["人工评审"].cell(3, 3).data_type != "f" assert "Pass" in " ".join( cell.text for table in Document(paths[0]).tables for row in table.rows for cell in row.cells ) assert report_export.session_report("task-one")["test_result"] == original_result plugins.set_enabled("review", False) paths = report_export.export_session("task-one", ["docx", "xlsx"]) assert "人工评审" not in load_workbook(paths[1]).sheetnames assert not any("人工评审" in p.text for p in Document(paths[0]).paragraphs) def test_batch_snapshot_signature_changes_on_review_and_plugin_state(context): store, plugins, client = context install(plugins) store.create_batch( { "id": "batch", "name": "批次", "status": "completed", "formats": ["docx", "xlsx"], "report_ready": True, }, [], {}, ) job = store.job("task-one") store.delete_history("task-one") store.save_job({**job, "batch_id": "batch"}) path = report_export.export_batch("batch") assert batch_archive.cached("batch", store.get("batch", "batch")) with zipfile.ZipFile(path) as archive: assert "人工评审" in load_workbook(BytesIO(archive.read("批次结果.xlsx"))).sheetnames assert "人工评审" in " ".join( p.text for p in Document(BytesIO(archive.read("批次汇总.docx"))).paragraphs ) save(client, {"correctness": "Failed"}) # Even a late worker publishing report_ready cannot resurrect an old archive. assert not batch_archive.cached("batch", {"report_ready": True}) report_export.export_batch("batch") plugins.set_enabled("review", False) assert not batch_archive.cached("batch", {"report_ready": True}) store.begin_delete_batch("batch") store.delete_batch("batch") assert store.get("task_extensions", "task-one") is None