"""Read-only meta-skill run history RPC handlers.""" from __future__ import annotations import asyncio from pathlib import Path import pytest from opensquilla.gateway.auth import Principal from opensquilla.gateway.protocol import ERROR_UNAUTHORIZED from opensquilla.gateway.rpc import get_dispatcher from opensquilla.gateway.rpc.registry import RpcContext from opensquilla.gateway.rpc_meta_runs import ( _bounded_limit, _handle_meta_runs_confirm_preflight, _handle_meta_runs_cost, _handle_meta_runs_diff, _handle_meta_runs_draft, _handle_meta_runs_eval_baseline, _handle_meta_runs_failures, _handle_meta_runs_list, _handle_meta_runs_replay, _handle_meta_runs_show, _handle_meta_runs_validate, ) from opensquilla.gateway.scopes import ADMIN_SCOPE, METHOD_SCOPES, READ_SCOPE from opensquilla.persistence.meta_run_writer import open_meta_run_writer from opensquilla.persistence.migrator import apply_pending from opensquilla.skills.meta.inputs import make_meta_inputs from opensquilla.skills.meta.scheduler import _preflight_missing_fields from opensquilla.skills.meta.types import MetaPlan, MetaResult, MetaStep MIGRATIONS_DIR = Path(__file__).resolve().parents[1].parent / "migrations" def _seed_writer( tmp_path: Path, *, final_status: str = "ok", final_result: MetaResult | None = None, ): db = str(tmp_path / "runs.db") apply_pending(db, MIGRATIONS_DIR) writer = open_meta_run_writer(db) plan = MetaPlan( name="alpha-skill", triggers=("alpha request",), priority=10, steps=(MetaStep(id="s1", skill="writer", kind="agent", label="Write"),), request_template={ "outcome": "Brief", "fields": [ {"name": "audience", "required": True}, {"name": "language", "required": True}, ], }, output_contract={"required_sections": ["Summary"]}, eval_prompts=[{ "name": "brief", "prompt": "Write an alpha brief", "rubric": ["Summary"], }], ) run_id = writer.begin_run_sync( meta_skill_name="alpha-skill", meta_plan=plan, triggered_by="soft_meta_invoke", inputs={"user_message": "Write an alpha brief"}, session_key="sess-1", turn_id="turn-1", ) writer.begin_step_sync( run_id=run_id, step=plan.steps[0], effective_skill="writer", rendered_inputs={"task": "Write an alpha brief"}, ) writer.finish_step_sync( run_id=run_id, step_id="s1", status="ok", output_text="done", ) # finish_run_sync is status-guarded (running → terminal), so the seed # helper finalizes exactly once with the caller's desired terminal state. writer.finish_run_sync( run_id=run_id, status=final_status, # type: ignore[arg-type] result=final_result or MetaResult(ok=True, final_text="done"), ) return writer, run_id def test_meta_runs_list_rpc_returns_summary(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_list({"limit": 5}, ctx)) finally: writer.close() assert payload["runs"][0]["run_id"] == run_id assert payload["runs"][0]["summary"]["step_count"] == 1 assert payload["runs"][0]["summary"]["usage"]["available"] is False assert "inputs_json" not in payload["runs"][0] assert "plan_snapshot_json" not in payload["runs"][0] assert "final_text" not in payload["runs"][0] assert "steps" not in payload["runs"][0] assert "output_text" not in payload["runs"][0]["summary"]["steps"][0] assert "rendered_inputs_json" not in payload["runs"][0]["summary"]["steps"][0] assert payload["runs"][0]["validation"] == { "available": True, "request_template": True, "output_contract": True, "eval_baseline": True, "field_count": 2, "required_field_count": 2, "eval_prompt_count": 1, } def test_meta_runs_failures_rpc_returns_summary_only(tmp_path: Path) -> None: writer, run_id = _seed_writer( tmp_path, final_status="failed", final_result=MetaResult( ok=False, error="raw secret failure detail", failed_step_id="s1", ), ) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_failures({"limit": 500}, ctx)) finally: writer.close() run = payload["runs"][0] assert run["run_id"] == run_id assert run["error_present"] is True assert "error" not in run assert "inputs_json" not in run assert "final_text" not in run def test_meta_runs_show_rpc_returns_steps(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_show({"runId": run_id}, ctx)) finally: writer.close() run = payload["run"] assert run["run_id"] == run_id assert run["steps"][0]["step_id"] == "s1" assert run["summary"]["steps"][0]["output_chars"] == 4 def test_meta_runs_draft_rpc_returns_author_seed(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_draft({"runId": run_id}, ctx)) finally: writer.close() draft = payload["draft"] assert draft["source_run"]["run_id"] == run_id assert draft["name"] == "alpha-skill-draft" assert draft["request_template"]["outcome"] == "Brief" assert draft["eval_prompts"][0]["name"] == "brief" def test_meta_runs_confirm_preflight_requires_template_fields(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_confirm_preflight({ "runId": run_id, "fields": {"audience": "engineers", "language": "zh-CN"}, "interpretedRequest": "Write an alpha brief for engineers in zh-CN.", }, ctx)) finally: writer.close() assert payload["confirmed"] is True assert payload["run_id"] == run_id assert payload["fields"]["audience"] == "engineers" assert "opensquilla:meta_preflight_confirmed=1" in payload["message"] assert "opensquilla:meta_preflight_run_id=" in payload["message"] assert "opensquilla:meta_preflight_fields=" in payload["message"] replay_inputs = make_meta_inputs(user_message=payload["message"]) assert replay_inputs["meta_preflight_fields"] == { "audience": "engineers", "language": "zh-CN", } assert replay_inputs["collected"]["preflight"]["audience"] == "engineers" assert _preflight_missing_fields( { "fields": [ {"name": "audience", "required": True}, {"name": "language", "required": True}, ], }, replay_inputs, ) == [] def test_meta_runs_confirm_preflight_rejects_missing_fields(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) with pytest.raises(Exception) as exc_info: asyncio.run(_handle_meta_runs_confirm_preflight({ "runId": run_id, "fields": {"audience": "engineers"}, }, ctx)) finally: writer.close() assert "language" in str(exc_info.value) def test_meta_runs_replay_rpc_returns_bounded_replay_message(tmp_path: Path) -> None: writer, run_id = _seed_writer( tmp_path, final_status="failed", final_result=MetaResult(ok=False, error="failed at writer", failed_step_id="s1"), ) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_replay({ "runId": run_id, "mode": "failed-step", }, ctx)) finally: writer.close() replay = payload["replay"] assert replay["run_id"] == run_id assert replay["mode"] == "failed-step" assert replay["failed_step_id"] == "s1" assert "prior failed step" in replay["message"] assert replay["request"]["user_message"] == "Write an alpha brief" def test_meta_runs_replay_keeps_original_request_before_large_outputs(tmp_path: Path) -> None: writer, run_id = _seed_writer( tmp_path, final_status="failed", final_result=MetaResult(ok=False, error="failed late", failed_step_id="s1"), ) writer.finish_step_sync( run_id=run_id, step_id="s1", status="ok", output_text="x" * 10_000, ) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_replay({ "runId": run_id, "mode": "partial-context", }, ctx)) finally: writer.close() message = payload["replay"]["message"] assert "Original request: Write an alpha brief" in message assert message.index("Original request") < message.index("Prior successful outputs") assert "...[truncated for replay]" in message def test_meta_runs_diff_rpc_compares_runs(tmp_path: Path) -> None: writer, left_run_id = _seed_writer(tmp_path) plan = MetaPlan( name="alpha-skill", triggers=("alpha request",), priority=10, steps=(MetaStep(id="s1", skill="writer", kind="agent", label="Write"),), ) right_run_id = writer.begin_run_sync( meta_skill_name="alpha-skill", meta_plan=plan, triggered_by="soft_meta_invoke", inputs={"user_message": "Write a revised alpha brief"}, session_key="sess-1", turn_id="turn-2", ) writer.finish_run_sync( run_id=right_run_id, status="failed", result=MetaResult(ok=False, error="boom", failed_step_id="s1"), ) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_diff({ "leftRunId": left_run_id, "rightRunId": right_run_id, }, ctx)) finally: writer.close() diff = payload["diff"] assert diff["left"]["run_id"] == left_run_id assert diff["right"]["run_id"] == right_run_id assert diff["status_changed"] is True assert diff["final_text_chars_delta"] == -4 assert diff["steps"][0]["step_id"] == "s1" def test_meta_runs_cost_rpc_aggregates_persisted_step_usage(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) writer.finish_step_sync( run_id=run_id, step_id="s1", status="ok", output_text="done", usage={ "input_tokens": 50, "output_tokens": 10, "total_tokens": 60, "cost_usd": 0.0123, "billed_cost_usd": 0.0123, "cost_source": "provider_billed", "model": "gpt-test", }, ) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_cost({"limit": 5}, ctx)) finally: writer.close() assert payload["aggregate"]["run_count"] == 1 assert payload["aggregate"]["usage"]["available"] is True assert payload["aggregate"]["usage"]["input_tokens"] == 50 assert payload["aggregate"]["usage"]["total_tokens"] == 60 assert payload["aggregate"]["usage"]["cost_usd"] == pytest.approx(0.0123) assert payload["aggregate"]["usage"]["cost_source"] == "provider_billed" assert payload["runs"][0]["run_id"] == run_id assert payload["runs"][0]["usage"]["available"] is True assert payload["runs"][0]["steps"][0]["usage"]["model"] == "gpt-test" def test_meta_runs_validate_rpc_exposes_spec_metadata(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_validate({"runId": run_id}, ctx)) finally: writer.close() validation = payload["validation"] assert validation["run_id"] == run_id assert validation["request_template"]["field_names"] == ["audience", "language"] assert validation["output_contract"]["required_sections"] == ["Summary"] assert validation["eval_prompts"][0]["name"] == "brief" def test_meta_runs_eval_baseline_rpc_returns_deterministic_rubric(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) try: ctx = RpcContext(conn_id="test", meta_run_writer=writer) payload = asyncio.run(_handle_meta_runs_eval_baseline({"runId": run_id}, ctx)) finally: writer.close() baseline = payload["baseline"] assert baseline["run_id"] == run_id assert baseline["available"] is True assert baseline["items"][0]["name"] == "brief" assert baseline["items"][0]["judge"]["mode"] == "deterministic_metadata" def test_meta_runs_rpc_scope_contract() -> None: assert METHOD_SCOPES["meta.runs.list"] == READ_SCOPE assert METHOD_SCOPES["meta.runs.failures"] == READ_SCOPE assert METHOD_SCOPES["meta.runs.show"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.draft"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.confirm_preflight"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.diff"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.replay"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.cost"] == READ_SCOPE assert METHOD_SCOPES["meta.runs.validate"] == ADMIN_SCOPE assert METHOD_SCOPES["meta.runs.eval_baseline"] == ADMIN_SCOPE @pytest.mark.asyncio async def test_meta_runs_show_and_draft_deny_read_only_dispatch(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) read_only = Principal( role="operator", scopes=frozenset({READ_SCOPE}), is_owner=False, authenticated=True, ) try: ctx = RpcContext(conn_id="test", principal=read_only, meta_run_writer=writer) dispatcher = get_dispatcher() for method in ("meta.runs.show", "meta.runs.draft"): res = await dispatcher.dispatch("r1", method, {"runId": run_id}, ctx) assert res.error is not None assert res.error.code == ERROR_UNAUTHORIZED finally: writer.close() @pytest.mark.asyncio async def test_meta_runs_read_only_requires_session_key_for_history(tmp_path: Path) -> None: writer, _run_id = _seed_writer(tmp_path) read_only = Principal( role="operator", scopes=frozenset({READ_SCOPE}), is_owner=False, authenticated=True, ) try: ctx = RpcContext(conn_id="test", principal=read_only, meta_run_writer=writer) dispatcher = get_dispatcher() for method in ("meta.runs.list", "meta.runs.failures"): res = await dispatcher.dispatch("r1", method, {"limit": 5}, ctx) assert res.error is not None assert res.error.code == ERROR_UNAUTHORIZED finally: writer.close() @pytest.mark.asyncio async def test_meta_runs_read_only_allows_session_scoped_history(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) other_plan = MetaPlan( name="beta-skill", triggers=("beta request",), priority=10, steps=(MetaStep(id="s1", skill="writer", kind="agent", label="Write"),), ) other_run_id = writer.begin_run_sync( meta_skill_name="beta-skill", meta_plan=other_plan, triggered_by="soft_meta_invoke", inputs={"user_message": "Write a beta brief"}, session_key="sess-2", turn_id="turn-2", ) writer.finish_run_sync( run_id=other_run_id, status="ok", result=MetaResult(ok=True, final_text="done"), ) read_only = Principal( role="operator", scopes=frozenset({READ_SCOPE}), is_owner=False, authenticated=True, ) try: ctx = RpcContext(conn_id="test", principal=read_only, meta_run_writer=writer) res = await get_dispatcher().dispatch( "r1", "meta.runs.list", {"sessionKey": "sess-1", "limit": 5}, ctx, ) assert res.error is None, res.error assert [run["run_id"] for run in res.payload["runs"]] == [run_id] assert all(run["session_key"] == "sess-1" for run in res.payload["runs"]) finally: writer.close() @pytest.mark.asyncio async def test_meta_runs_failures_read_only_allows_session_scoped_history( tmp_path: Path, ) -> None: writer, run_id = _seed_writer( tmp_path, final_status="failed", final_result=MetaResult(ok=False, error="failed", failed_step_id="s1"), ) other_plan = MetaPlan( name="beta-skill", triggers=("beta request",), priority=10, steps=(MetaStep(id="s1", skill="writer", kind="agent", label="Write"),), ) other_run_id = writer.begin_run_sync( meta_skill_name="beta-skill", meta_plan=other_plan, triggered_by="soft_meta_invoke", inputs={"user_message": "Write a beta brief"}, session_key="sess-2", turn_id="turn-2", ) writer.finish_run_sync( run_id=other_run_id, status="failed", result=MetaResult(ok=False, error="other failed", failed_step_id="s1"), ) read_only = Principal( role="operator", scopes=frozenset({READ_SCOPE}), is_owner=False, authenticated=True, ) try: ctx = RpcContext(conn_id="test", principal=read_only, meta_run_writer=writer) res = await get_dispatcher().dispatch( "r1", "meta.runs.failures", {"sessionKey": "sess-1", "limit": 5}, ctx, ) assert res.error is None, res.error assert [run["run_id"] for run in res.payload["runs"]] == [run_id] assert all(run["session_key"] == "sess-1" for run in res.payload["runs"]) finally: writer.close() @pytest.mark.asyncio async def test_meta_runs_owner_read_scope_allows_session_history(tmp_path: Path) -> None: writer, run_id = _seed_writer(tmp_path) owner_read = Principal( role="operator", scopes=frozenset({READ_SCOPE}), is_owner=True, authenticated=False, ) try: ctx = RpcContext(conn_id="test", principal=owner_read, meta_run_writer=writer) res = await get_dispatcher().dispatch( "r1", "meta.runs.list", {"sessionKey": "sess-1", "limit": 5}, ctx, ) assert res.error is None, res.error assert res.payload["runs"][0]["run_id"] == run_id finally: writer.close() def test_meta_runs_rpc_limit_is_bounded() -> None: assert _bounded_limit(None) == 50 assert _bounded_limit(-1) == 50 assert _bounded_limit("5000") == 100 assert _bounded_limit("12") == 12 def test_meta_runs_rpc_does_not_import_cli_private_helpers() -> None: source = Path("src/opensquilla/gateway/rpc_meta_runs.py").read_text() assert "opensquilla.cli.skills_meta_cmd" not in source assert "_meta_run_writer" not in source def test_meta_runs_cli_uses_neutral_report_helpers() -> None: source = Path("src/opensquilla/cli/skills_meta_cmd.py").read_text() assert "opensquilla.gateway.rpc_meta_runs" not in source assert "opensquilla.skills.meta.run_reports" in source