Enforce bounded operation guardrails
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
This commit is contained in:
parent
c384f60530
commit
26934e25b9
51 changed files with 1843 additions and 472 deletions
|
|
@ -263,7 +263,7 @@ def test_execute_instruction_with_audit_returns_metadata():
|
|||
assert len(result.prompt_hash) == 64
|
||||
assert result.model == "test-model"
|
||||
assert result.output_validated is True
|
||||
assert result.review_required is True
|
||||
assert result.review_advisory is True
|
||||
|
||||
|
||||
def test_execute_instruction_forwards_llm_connect_run_config():
|
||||
|
|
@ -443,7 +443,7 @@ def test_resilient_report_recovers_valid_prefix_and_quarantines_truncated_tail()
|
|||
result = execute_instruction_with_audit(instr, _Event(), {}, llm)
|
||||
|
||||
assert result.output_validated is True
|
||||
assert result.review_required is True
|
||||
assert result.review_advisory is True
|
||||
assert result.report is not None
|
||||
assert result.report["partial"] is True
|
||||
assert len(result.report["recommendations"]) == 7
|
||||
|
|
@ -593,7 +593,7 @@ class _MetadataBadLLM:
|
|||
return ("x" * 9000) + "{"
|
||||
|
||||
|
||||
def test_invalid_report_preserves_response_metadata_and_long_preview():
|
||||
def test_invalid_report_keeps_metadata_but_discards_provider_output():
|
||||
llm = _MetadataBadLLM()
|
||||
instr = _instr(
|
||||
id="daily-triage-report",
|
||||
|
|
@ -611,10 +611,11 @@ def test_invalid_report_preserves_response_metadata_and_long_preview():
|
|||
"usage": {"input_tokens": 1100, "output_tokens": 1200},
|
||||
}
|
||||
assert result.report["llm_response_metadata"] == result.llm_response_metadata
|
||||
assert len(result.report["raw_output_preview"]) > 4000
|
||||
assert "raw_output_preview" not in result.report
|
||||
assert "partial_report" not in result.report
|
||||
|
||||
|
||||
def test_execute_instruction_with_audit_preserves_invalid_report_with_sinks(
|
||||
def test_execute_instruction_with_audit_discards_invalid_report_body(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
):
|
||||
|
|
@ -657,12 +658,13 @@ def test_execute_instruction_with_audit_preserves_invalid_report_with_sinks(
|
|||
|
||||
assert result.tasks == []
|
||||
assert result.output_validated is False
|
||||
assert result.review_required is True
|
||||
assert result.review_advisory is True
|
||||
assert result.validation_error == "$.recommendations[0]: missing required property 'action'"
|
||||
assert result.report is not None
|
||||
assert result.report["status"] == "validation_failed"
|
||||
assert result.report["partial_summary"] == "Generated partial triage."
|
||||
assert result.report["partial_report"] == report_data
|
||||
assert "partial_summary" not in result.report
|
||||
assert "partial_report" not in result.report
|
||||
assert "Generated partial triage." not in str(result.report)
|
||||
assert llm.call_count == 2
|
||||
|
||||
|
||||
|
|
@ -678,7 +680,7 @@ def test_execute_instruction_with_audit_preserves_execution_failure_with_sinks()
|
|||
|
||||
assert result.tasks == []
|
||||
assert result.output_validated is False
|
||||
assert result.review_required is True
|
||||
assert result.review_advisory is True
|
||||
assert result.validation_error == "LLM_CONNECT_URL is not configured"
|
||||
assert result.report == {
|
||||
"summary": (
|
||||
|
|
@ -726,8 +728,7 @@ def test_condition_true_calls_llm():
|
|||
|
||||
# ── review_required field ─────────────────────────────────────────────────────
|
||||
|
||||
def test_review_required_field_on_instruction_def():
|
||||
"""review_required is a declared field on InstructionDef."""
|
||||
def test_legacy_review_required_maps_to_advisory():
|
||||
defn = InstructionDef(
|
||||
id="test",
|
||||
trusted_fields=["event.attributes.x"],
|
||||
|
|
@ -737,6 +738,7 @@ def test_review_required_field_on_instruction_def():
|
|||
review_required=True,
|
||||
)
|
||||
assert defn.review_required is True
|
||||
assert defn.review_advisory is True
|
||||
|
||||
|
||||
def test_instruction_def_accepts_llm_connect_depth_config():
|
||||
|
|
@ -755,7 +757,7 @@ def test_instruction_def_accepts_llm_connect_depth_config():
|
|||
assert defn.model_params == {"reasoning_effort": "medium"}
|
||||
|
||||
|
||||
def test_review_required_defaults_to_false():
|
||||
def test_review_advisory_defaults_to_false():
|
||||
defn = InstructionDef(
|
||||
id="test",
|
||||
trusted_fields=[],
|
||||
|
|
@ -763,7 +765,21 @@ def test_review_required_defaults_to_false():
|
|||
prompt="p",
|
||||
output_schema="schema.json",
|
||||
)
|
||||
assert defn.review_required is False
|
||||
assert defn.review_required is None
|
||||
assert defn.review_advisory is False
|
||||
|
||||
|
||||
def test_conflicting_review_names_are_rejected():
|
||||
with pytest.raises(ValueError, match="review_required conflicts"):
|
||||
InstructionDef(
|
||||
id="test",
|
||||
trusted_fields=[],
|
||||
model="claude-sonnet-4-6",
|
||||
prompt="p",
|
||||
output_schema="schema.json",
|
||||
review_required=True,
|
||||
review_advisory=False,
|
||||
)
|
||||
|
||||
|
||||
def test_unknown_root_in_field_path_raises():
|
||||
|
|
|
|||
28
tests/test_api_admission.py
Normal file
28
tests/test_api_admission.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from activity_core.api import _validate_context_admission
|
||||
|
||||
|
||||
def test_basic_rest_administration_refuses_unknown_shell_query() -> None:
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_validate_context_admission(
|
||||
[{"type": "shell", "query": "run_from_request", "params": {}}]
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert "not registered" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
def test_basic_rest_administration_accepts_registered_read_only_query() -> None:
|
||||
_validate_context_admission(
|
||||
[
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "discover_kaizen_projects",
|
||||
"params": {},
|
||||
}
|
||||
]
|
||||
)
|
||||
40
tests/test_audit_projection.py
Normal file
40
tests/test_audit_projection.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from activity_core.audit_projection import bounded_audit_projection
|
||||
|
||||
|
||||
def test_audit_projection_drops_raw_and_credential_shaped_fields() -> None:
|
||||
raw = {
|
||||
"definition_id": "daily-triage",
|
||||
"prompt_hash": "a" * 64,
|
||||
"context": {
|
||||
"summary": "bounded",
|
||||
"rendered_prompt": "do not persist",
|
||||
"messages": [{"content": "do not persist"}],
|
||||
"provider_response": {"body": "do not persist"},
|
||||
"tool_output": "do not persist",
|
||||
"credential": "do not persist",
|
||||
"api_key": "do not persist",
|
||||
"nested": {"password": "do not persist", "safe": True},
|
||||
},
|
||||
}
|
||||
|
||||
projected = bounded_audit_projection(raw)
|
||||
|
||||
assert projected == {
|
||||
"definition_id": "daily-triage",
|
||||
"prompt_hash": "a" * 64,
|
||||
"context": {"summary": "bounded", "nested": {"safe": True}},
|
||||
}
|
||||
assert "do not persist" not in str(projected)
|
||||
|
||||
|
||||
def test_audit_projection_bounds_depth_items_and_strings() -> None:
|
||||
projected = bounded_audit_projection(
|
||||
{"text": "x" * 20, "items": list(range(5)), "nested": {"value": 1}},
|
||||
max_depth=1,
|
||||
max_items=2,
|
||||
max_string=5,
|
||||
)
|
||||
|
||||
assert projected == {"text": "xxxxx", "items": ["<depth-limit>", "<depth-limit>"]}
|
||||
167
tests/test_bounded_operations.py
Normal file
167
tests/test_bounded_operations.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from activity_core.bounded_operations import (
|
||||
BOUNDED_OPERATION_REGISTRY,
|
||||
normalize_operation_result,
|
||||
operation_sources,
|
||||
validate_bounded_operations,
|
||||
)
|
||||
|
||||
|
||||
def _evidence_sinks(event_type: str) -> list[dict[str, str]]:
|
||||
return [{"type": "state-hub-progress", "event_type": event_type}]
|
||||
|
||||
|
||||
def test_registry_names_only_the_three_accepted_operations() -> None:
|
||||
assert set(BOUNDED_OPERATION_REGISTRY) == {
|
||||
"sbom_nexus_ingest",
|
||||
"forgejo_package_prune",
|
||||
"cnpg_option_a_backup",
|
||||
}
|
||||
|
||||
|
||||
def test_operation_result_projection_drops_subprocess_output() -> None:
|
||||
projected = normalize_operation_result(
|
||||
"cnpg_option_a_backup",
|
||||
{
|
||||
"overall": "pass",
|
||||
"dumped": 3,
|
||||
"uploaded": 3,
|
||||
"failed": 0,
|
||||
"script_exit_code": 0,
|
||||
"log_tail": "must not persist",
|
||||
"tool_output": "must not persist",
|
||||
"credential": "must not persist",
|
||||
},
|
||||
)
|
||||
|
||||
assert projected == {
|
||||
"kind": "cnpg_option_a_backup",
|
||||
"overall": "pass",
|
||||
"dry_run": False,
|
||||
"dumped": 3,
|
||||
"uploaded": 3,
|
||||
"failed": 0,
|
||||
"script_exit_code": 0,
|
||||
}
|
||||
assert "must not persist" not in str(projected)
|
||||
|
||||
|
||||
def test_valid_prune_operation_is_admitted() -> None:
|
||||
sources = [
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"operation": "forgejo_package_prune",
|
||||
"params": {
|
||||
"prune_script": "/opt/railiance-platform/tools/cmd/forgejo-package-prune",
|
||||
"live_images_file": "/opt/railiance-platform/live-images.txt",
|
||||
"apply": True,
|
||||
"max_versions": 3,
|
||||
"evidence_sinks": _evidence_sinks("forgejo_package_prune"),
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
validate_bounded_operations(sources, [])
|
||||
assert operation_sources(sources)[0][1].temporal_max_attempts == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "error"),
|
||||
[
|
||||
(
|
||||
{"type": "shell", "query": "run_whatever", "params": {}},
|
||||
"not registered",
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"params": {},
|
||||
},
|
||||
"must declare operation",
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"operation": "forgejo_package_prune",
|
||||
"params": {
|
||||
"prune_script": "/tmp/arbitrary-command",
|
||||
"apply": False,
|
||||
"max_versions": 3,
|
||||
"evidence_sinks": _evidence_sinks("forgejo_package_prune"),
|
||||
},
|
||||
},
|
||||
"canonical path",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_shell_admission_fails_closed(source: dict, error: str) -> None:
|
||||
with pytest.raises(ValueError, match=error):
|
||||
validate_bounded_operations([source], [])
|
||||
|
||||
|
||||
def test_sbom_requires_bounded_limit_and_report_evidence() -> None:
|
||||
source = {
|
||||
"type": "sbom-nexus",
|
||||
"query": "catch_up",
|
||||
"operation": "sbom_nexus_ingest",
|
||||
"params": {"apply": True, "limit": 4},
|
||||
}
|
||||
instruction = {
|
||||
"report_sinks": _evidence_sinks("sbom_catchup"),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="limit must be in 1..3"):
|
||||
validate_bounded_operations([source], [instruction])
|
||||
|
||||
source["params"]["limit"] = 3
|
||||
with pytest.raises(ValueError, match="requires an instruction report"):
|
||||
validate_bounded_operations([source], [])
|
||||
|
||||
validate_bounded_operations([source], [instruction])
|
||||
|
||||
|
||||
def test_backup_requires_explicit_safe_targets_and_evidence() -> None:
|
||||
source = {
|
||||
"type": "shell",
|
||||
"query": "cnpg_option_a_backup",
|
||||
"operation": "cnpg_option_a_backup",
|
||||
"params": {
|
||||
"backup_script": "/opt/railiance-platform/tools/cmd/cnpg-option-a-backup",
|
||||
"dry_run": False,
|
||||
"timeout_seconds": 7200,
|
||||
"targets": "r01-forgejo-db,r01-state-hub-db",
|
||||
"evidence_sinks": _evidence_sinks("cnpg_option_a_backup"),
|
||||
},
|
||||
}
|
||||
|
||||
validate_bounded_operations([source], [])
|
||||
source["params"]["targets"] = "ok,$(unsafe)"
|
||||
with pytest.raises(ValueError, match="safe names"):
|
||||
validate_bounded_operations([source], [])
|
||||
|
||||
source["params"]["targets"] = "r01-forgejo-db"
|
||||
source["params"]["evidence_sinks"] = [{"type": "state-hub-progress"}]
|
||||
with pytest.raises(ValueError, match="event_type: cnpg_option_a_backup"):
|
||||
validate_bounded_operations([source], [])
|
||||
|
||||
|
||||
def test_definition_cannot_combine_two_bounded_operations() -> None:
|
||||
prune = {
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"operation": "forgejo_package_prune",
|
||||
}
|
||||
backup = {
|
||||
"type": "shell",
|
||||
"query": "cnpg_option_a_backup",
|
||||
"operation": "cnpg_option_a_backup",
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="at most one bounded operation"):
|
||||
validate_bounded_operations([prune, backup], [])
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
from activity_core import activities
|
||||
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY
|
||||
from activity_core.context_resolvers.forgejo_prune import forgejo_package_prune
|
||||
|
||||
|
||||
class _Completed:
|
||||
|
|
@ -13,7 +15,9 @@ class _Completed:
|
|||
self.returncode = returncode
|
||||
|
||||
|
||||
def test_shell_resolver_runs_forgejo_package_prune(tmp_path, monkeypatch) -> None:
|
||||
def test_bounded_operation_implementation_runs_forgejo_package_prune(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
script = tmp_path / "forgejo-package-prune"
|
||||
script.write_text("#!/usr/bin/env bash\necho '{}'\n", encoding="utf-8")
|
||||
script.chmod(0o755)
|
||||
|
|
@ -29,9 +33,7 @@ def test_shell_resolver_runs_forgejo_package_prune(tmp_path, monkeypatch) -> Non
|
|||
lambda *args, **kwargs: _Completed(payload),
|
||||
)
|
||||
|
||||
result = CONTEXT_RESOLVER_REGISTRY["shell"]().resolve(
|
||||
"forgejo_package_prune",
|
||||
None,
|
||||
result = forgejo_package_prune(
|
||||
{"prune_script": str(script), "max_versions": 3, "apply": False},
|
||||
)
|
||||
|
||||
|
|
@ -46,9 +48,7 @@ def test_apply_without_live_images_file_is_rejected(tmp_path) -> None:
|
|||
script.chmod(0o755)
|
||||
|
||||
try:
|
||||
CONTEXT_RESOLVER_REGISTRY["shell"]().resolve(
|
||||
"forgejo_package_prune",
|
||||
None,
|
||||
forgejo_package_prune(
|
||||
{"prune_script": str(script), "max_versions": 3, "apply": True},
|
||||
)
|
||||
raise AssertionError("expected RuntimeError")
|
||||
|
|
@ -64,9 +64,7 @@ def test_apply_with_empty_live_images_file_is_rejected(tmp_path) -> None:
|
|||
empty.write_text("", encoding="utf-8")
|
||||
|
||||
try:
|
||||
CONTEXT_RESOLVER_REGISTRY["shell"]().resolve(
|
||||
"forgejo_package_prune",
|
||||
None,
|
||||
forgejo_package_prune(
|
||||
{
|
||||
"prune_script": str(script),
|
||||
"apply": True,
|
||||
|
|
@ -75,4 +73,43 @@ def test_apply_with_empty_live_images_file_is_rejected(tmp_path) -> None:
|
|||
)
|
||||
raise AssertionError("expected RuntimeError")
|
||||
except RuntimeError as exc:
|
||||
assert "non-empty" in str(exc) or "empty" in str(exc)
|
||||
assert "non-empty" in str(exc) or "empty" in str(exc)
|
||||
|
||||
|
||||
def test_shell_resolver_refuses_mutating_prune_query() -> None:
|
||||
with pytest.raises(RuntimeError, match="bounded-operation stage"):
|
||||
CONTEXT_RESOLVER_REGISTRY["shell"]().resolve(
|
||||
"forgejo_package_prune", None, {}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_bounded_operation_dispatches_prune(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"activity_core.context_resolvers.forgejo_prune.forgejo_package_prune",
|
||||
lambda params: {"kind": "forgejo_package_prune", "deleted_count": 2},
|
||||
)
|
||||
|
||||
result = await activities.execute_bounded_operation(
|
||||
{
|
||||
"run_id": "run-1",
|
||||
"source": {
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"operation": "forgejo_package_prune",
|
||||
"params": {},
|
||||
"bind_to": "context.prune",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"prune": {
|
||||
"kind": "forgejo_package_prune",
|
||||
"apply": False,
|
||||
"candidate_count": 0,
|
||||
"deleted_count": 2,
|
||||
"skipped_protected_count": 0,
|
||||
"error_count": 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ async def test_evaluate_instructions_returns_task_specs_with_audit(monkeypatch)
|
|||
"model": "test-model",
|
||||
"prompt": "Open tasks: {context.summary.open_tasks}",
|
||||
"output_schema": "",
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
}
|
||||
],
|
||||
"event": {},
|
||||
|
|
@ -58,7 +58,9 @@ async def test_evaluate_instructions_returns_task_specs_with_audit(monkeypatch)
|
|||
assert spec["source_id"] == "daily-triage"
|
||||
assert spec["model"] == "test-model"
|
||||
assert spec["output_validated"] is True
|
||||
assert spec["review_required"] is False
|
||||
assert spec["review_advisory"] is False
|
||||
assert spec["review_gate_applied"] is False
|
||||
assert "review_required" not in spec
|
||||
assert spec["prompt_hash"] is not None
|
||||
assert len(spec["prompt_hash"]) == 64
|
||||
assert result["reports"] == []
|
||||
|
|
@ -90,7 +92,7 @@ async def test_evaluate_instructions_returns_report_payload(monkeypatch) -> None
|
|||
"model": "test-model",
|
||||
"prompt": "Run report.",
|
||||
"output_schema": "activity-core://schemas/daily-triage-report.json",
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
}
|
||||
],
|
||||
"event": {},
|
||||
|
|
@ -145,7 +147,7 @@ async def test_evaluate_instructions_returns_invalid_report_for_report_sinks(
|
|||
"model": "test-model",
|
||||
"prompt": "Run report.",
|
||||
"output_schema": "schemas/daily-triage-report.json",
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
"report_sinks": [{"type": "working-memory", "path": "/tmp"}],
|
||||
}
|
||||
],
|
||||
|
|
@ -157,10 +159,13 @@ async def test_evaluate_instructions_returns_invalid_report_for_report_sinks(
|
|||
assert len(result["reports"]) == 1
|
||||
report = result["reports"][0]
|
||||
assert report["output_validated"] is False
|
||||
assert report["review_required"] is True
|
||||
assert report["review_advisory"] is True
|
||||
assert report["review_gate_applied"] is False
|
||||
assert report["validation_error"] == "$.recommendations[0]: missing required property 'wsjf'"
|
||||
assert report["report"]["status"] == "validation_failed"
|
||||
assert report["report"]["partial_summary"] == "Partial triage."
|
||||
assert "partial_summary" not in report["report"]
|
||||
assert "partial_report" not in report["report"]
|
||||
assert "Partial triage." not in str(report)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -210,7 +215,7 @@ async def test_evaluate_instructions_forwards_llm_connect_depth_config(monkeypat
|
|||
"model_params": {"reasoning_effort": "medium"},
|
||||
"prompt": "Run report.",
|
||||
"output_schema": "schemas/daily-triage-report.json",
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
}
|
||||
],
|
||||
"event": {},
|
||||
|
|
|
|||
|
|
@ -225,6 +225,54 @@ def test_state_hub_progress_prefers_workplan_id(monkeypatch) -> None:
|
|||
assert "workstream_id" not in body
|
||||
|
||||
|
||||
def test_bounded_operation_progress_drops_raw_and_credential_fields(monkeypatch) -> None:
|
||||
posts: list[dict[str, Any]] = []
|
||||
|
||||
monkeypatch.setattr(httpx, "get", lambda *args, **kwargs: DummyResponse([]))
|
||||
|
||||
def fake_post(url: str, **kwargs: Any) -> DummyResponse:
|
||||
posts.append({"url": url, **kwargs})
|
||||
return DummyResponse({"id": "progress-prune-1"})
|
||||
|
||||
monkeypatch.setattr(httpx, "post", fake_post)
|
||||
persist_ops_inventory_evidence(
|
||||
{
|
||||
"activity_id": "weekly-forgejo-package-prune",
|
||||
"run_id": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"context_sources": [
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"bind_to": "context.prune",
|
||||
"params": {
|
||||
"evidence_sinks": [
|
||||
{
|
||||
"type": "state-hub-progress",
|
||||
"state_hub_url": "http://state-hub.test",
|
||||
"event_type": "forgejo_package_prune",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"prune": {
|
||||
"kind": "forgejo_package_prune",
|
||||
"apply": True,
|
||||
"deleted_count": 2,
|
||||
"tool_output": "must not persist",
|
||||
"credential": "must not persist",
|
||||
"provider_response": {"raw": "must not persist"},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
body = posts[0]["json"]
|
||||
assert body["detail"]["probe"]["deleted_count"] == 2
|
||||
assert "must not persist" not in str(body)
|
||||
|
||||
|
||||
def test_core_hub_stabilization_sink_posts_progress(monkeypatch) -> None:
|
||||
posts: list[dict[str, Any]] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ def _payload(sinks: list[dict[str, Any]]) -> dict[str, Any]:
|
|||
"prompt_hash": "abc123",
|
||||
"model": "test-model",
|
||||
"output_validated": True,
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
"review_gate_applied": False,
|
||||
"validation_error": None,
|
||||
"llm_response_metadata": {
|
||||
"finish_reason": "stop",
|
||||
|
|
@ -66,7 +67,9 @@ def test_working_memory_sink_writes_idempotently(tmp_path) -> None:
|
|||
text = note.read_text(encoding="utf-8")
|
||||
assert "activity_core_run_id: 12345678-aaaa-bbbb-cccc-123456789abc" in text
|
||||
assert "output_validated: true" in text
|
||||
assert "review_required: false" in text
|
||||
assert "review_advisory: false" in text
|
||||
assert "review_gate_applied: false" in text
|
||||
assert "review_required:" not in text
|
||||
assert "model: test-model" in text
|
||||
assert "LLM response metadata:" in text
|
||||
assert '"finish_reason": "stop"' in text
|
||||
|
|
@ -121,7 +124,8 @@ def test_state_hub_progress_sink_posts(monkeypatch) -> None:
|
|||
assert "workstream_id" not in posts[0]["json"]
|
||||
assert posts[0]["json"]["detail"]["activity_core_run_id"] == payload_run_id()
|
||||
assert posts[0]["json"]["detail"]["output_validated"] is True
|
||||
assert posts[0]["json"]["detail"]["review_required"] is False
|
||||
assert posts[0]["json"]["detail"]["review_advisory"] is False
|
||||
assert posts[0]["json"]["detail"]["review_gate_applied"] is False
|
||||
assert posts[0]["json"]["detail"]["llm_response_metadata"] == {
|
||||
"finish_reason": "stop",
|
||||
"usage": {"output_tokens": 50},
|
||||
|
|
|
|||
|
|
@ -158,3 +158,37 @@ async def test_event_payload_context_fails_when_required_envelope_missing() -> N
|
|||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_context_does_not_execute_bounded_shell_operation() -> None:
|
||||
snapshot = await resolve_context(
|
||||
[
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "forgejo_package_prune",
|
||||
"operation": "forgejo_package_prune",
|
||||
"params": {},
|
||||
"bind_to": "context.prune",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert snapshot == {
|
||||
"prune": {"operation": "forgejo_package_prune", "status": "pending"}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_context_refuses_unknown_shell_query() -> None:
|
||||
with pytest.raises(ApplicationError, match="not registered as read-only"):
|
||||
await resolve_context(
|
||||
[
|
||||
{
|
||||
"type": "shell",
|
||||
"query": "arbitrary_command",
|
||||
"params": {},
|
||||
"bind_to": "context.bad",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from activity_core.run_artifacts import (
|
|||
artifacts_from_ops_result,
|
||||
build_forgejo_blob_url,
|
||||
match_ops_runs_to_activity_run,
|
||||
public_context_keys,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -39,6 +40,17 @@ def test_build_forgejo_blob_url_rejects_traversal() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_public_context_keys_hide_credential_and_raw_payload_names() -> None:
|
||||
assert public_context_keys(
|
||||
{
|
||||
"safe_summary": {},
|
||||
"credential": "must-drop",
|
||||
"tool_output": "must-drop",
|
||||
"provider_response": {},
|
||||
}
|
||||
) == ["safe_summary"]
|
||||
|
||||
|
||||
def test_artifacts_from_ops_result() -> None:
|
||||
arts = artifacts_from_ops_result(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ def _instruction():
|
|||
"max_tokens": 1,
|
||||
"prompt": "Deterministic SBOM catch-up report.",
|
||||
"output_schema": "",
|
||||
"review_required": False,
|
||||
"review_advisory": False,
|
||||
"report_sinks": [
|
||||
{
|
||||
"type": "state-hub-progress",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue