activity-core/tests/test_bounded_operations.py
tegwick f89ebe9c82
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 52s
ACTIVITY-WP-0043-T06: single-repo owner-return pickup caller in the worker
Precondition 3 of the activation plan. New bounded operation
owner_return_pickup (state-hub-owner-return/pickup), executed by the existing
execute_bounded_operation activity and operator-triggered via the ops console
trigger with confirm_side_effect; no schedule, definition stays disabled.

The caller reads an operator-supplied C-25 checker artefact pinned by sha256
(the checker needs a local checkout and cannot run in the worker), requires a
full-SHA source_revision on the report and every C-25 finding, refuses failed
or incomplete collection, binds build_current_tasks to that SHA (Forge HEAD
verified), runs the State Hub planner vendored byte-identically from
state-hub 150cb0b, and submits only reviewable candidates under
ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED. Evidence is IDs/counts only.

build_current_tasks now refuses a report without source_revision and names
source_revision_reason when it is null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 692292@bnt-lap001
Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
2026-10-04 13:40:29 +02:00

183 lines
5.8 KiB
Python

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_accepted_operations() -> None:
assert set(BOUNDED_OPERATION_REGISTRY) == {
"sbom_nexus_ingest",
"forgejo_package_prune",
"cnpg_option_a_backup",
# ACTIVITY-WP-0044 / RMGR-WP-0020-T03 (draft definition, disabled)
"repo_manager_freshness",
# ACTIVITY-WP-0043-T06 (operator-triggered single-repo pickup)
"owner_return_pickup",
}
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], [])
@pytest.mark.parametrize("overall", ["sensitive-value", {"secret": "value"}, ["value"], None])
def test_backup_projection_does_not_copy_untrusted_status(overall):
result = normalize_operation_result("cnpg_option_a_backup", {"overall": overall})
assert result["overall"] == "unknown"
@pytest.mark.parametrize("overall", ["ok", "degraded", "error", "pass", "fail", "unknown"])
def test_backup_projection_preserves_supported_status(overall):
result = normalize_operation_result("cnpg_option_a_backup", {"overall": overall})
assert result["overall"] == overall