250 lines
8.8 KiB
Python
250 lines
8.8 KiB
Python
from sqlmodel import Session, SQLModel, create_engine, select
|
|
|
|
from backend.app.api import routes_jobs
|
|
from backend.app.db.models import AppSetting, AuditEvent, JobRun
|
|
from backend.app.services import action_execution, mcp_operations
|
|
from backend.app.services import privatemuse_deployment_recovery as recovery
|
|
from backend.app.services.action_execution import approval_execution_supported
|
|
from backend.app.services.action_registry import (
|
|
ACTION_RISK,
|
|
ACTIONS,
|
|
action_default_target,
|
|
)
|
|
|
|
|
|
def test_privatemuse_target_registration_is_a_typed_high_risk_action():
|
|
action = "deployment.target.register_privatemuse"
|
|
assert action in ACTIONS
|
|
assert ACTION_RISK[action] == "high"
|
|
assert action_default_target(action) == "jens/privatemuse"
|
|
assert approval_execution_supported(action) is True
|
|
|
|
|
|
def test_privatemuse_target_registration_rejects_any_other_target():
|
|
result = action_execution.execute_governed_action(
|
|
None, # type: ignore[arg-type]
|
|
action="deployment.target.register_privatemuse",
|
|
target="jens/not-privatemuse",
|
|
source="chatgpt-mcp",
|
|
risk="high",
|
|
reason="Regression test for exact-target enforcement.",
|
|
original_policy_decision="allowed",
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert result["result"] == "blocked"
|
|
assert result["policyDecision"] == "blocked"
|
|
assert result["message"] == "PrivateMuse target registration accepts only jens/privatemuse."
|
|
|
|
|
|
def test_privatemuse_target_registration_dispatches_only_fixed_executor(monkeypatch):
|
|
observed = {}
|
|
|
|
def fake_executor(session, **kwargs):
|
|
observed["session"] = session
|
|
observed.update(kwargs)
|
|
return {
|
|
"success": True,
|
|
"status": "success",
|
|
"result": "success",
|
|
"message": "enrolled",
|
|
"jobId": "job-private-muse",
|
|
"auditId": "audit-private-muse",
|
|
"policyDecision": "allowed",
|
|
"originalPolicyDecision": kwargs["original_policy_decision"],
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
action_execution,
|
|
"_execute_privatemuse_target_enrollment",
|
|
fake_executor,
|
|
)
|
|
|
|
sentinel_session = object()
|
|
result = action_execution.execute_governed_action(
|
|
sentinel_session, # type: ignore[arg-type]
|
|
action="deployment.target.register_privatemuse",
|
|
target="JENS/PrivateMuse",
|
|
source="chatgpt-mcp",
|
|
risk="high",
|
|
reason="Enroll the fixed PrivateMuse deployment target.",
|
|
original_policy_decision="allowed",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert observed == {
|
|
"session": sentinel_session,
|
|
"source": "chatgpt-mcp",
|
|
"risk": "high",
|
|
"reason": "Enroll the fixed PrivateMuse deployment target.",
|
|
"original_policy_decision": "allowed",
|
|
}
|
|
|
|
|
|
def test_privatemuse_unknown_outcome_keeps_job_running_and_retains_exec_evidence(
|
|
tmp_path, monkeypatch
|
|
):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'unknown-action.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
|
|
def uncertain_run(self, *, session, job_id):
|
|
return {
|
|
"status": "outcome_unknown",
|
|
"reason": "Detached recovery outcome is unknown; do not retry.",
|
|
"execution_id": "exec-action-unknown",
|
|
"original_job_id": job_id,
|
|
}
|
|
|
|
monkeypatch.setattr(recovery.PrivateMuseDeploymentRecovery, "run", uncertain_run)
|
|
|
|
with Session(engine) as session:
|
|
result = action_execution.execute_governed_action(
|
|
session,
|
|
action="deployment.target.register_privatemuse",
|
|
target="jens/privatemuse",
|
|
source="chatgpt-mcp",
|
|
risk="high",
|
|
reason="Exercise durable unknown-outcome reporting.",
|
|
original_policy_decision="allowed",
|
|
)
|
|
job = session.get(JobRun, result["jobId"])
|
|
audit = session.get(AuditEvent, result["auditId"])
|
|
|
|
assert result["success"] is False
|
|
assert result["status"] == "outcome_unknown"
|
|
assert result["result"] == "outcome_unknown"
|
|
assert result["executionId"] == "exec-action-unknown"
|
|
assert job is not None
|
|
assert job.status == "running"
|
|
assert "exec-action-unknown" in str(job.result_summary)
|
|
assert audit is not None
|
|
assert audit.result == "outcome_unknown"
|
|
assert audit.after_state["executionId"] == "exec-action-unknown"
|
|
assert audit.after_state["originalJobId"] == result["jobId"]
|
|
|
|
|
|
def test_unknown_privatemuse_recovery_job_cannot_be_cancelled(tmp_path):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'cancel-guard.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
session.add(
|
|
JobRun(
|
|
id="job-unknown-recovery",
|
|
type="deployment.target.register_privatemuse",
|
|
status="running",
|
|
source="chatgpt-mcp",
|
|
action_id="deployment.target.register_privatemuse",
|
|
target="jens/privatemuse",
|
|
risk="high",
|
|
policy_decision="allowed",
|
|
result_summary="outcome_unknown; executionId=exec-cancel-guard",
|
|
)
|
|
)
|
|
session.add(
|
|
AppSetting(
|
|
key=recovery._RECOVERY_STATE_SETTING,
|
|
value_json={
|
|
"state": "outcome_unknown",
|
|
"executionId": "exec-cancel-guard",
|
|
"controllerId": "controller-id",
|
|
"jobId": "job-unknown-recovery",
|
|
},
|
|
is_secret=False,
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
result = mcp_operations.cancel_operation_job(
|
|
session, "job-unknown-recovery"
|
|
)
|
|
route_result = routes_jobs.cancel("job-unknown-recovery", session=session)
|
|
persisted = session.exec(
|
|
select(JobRun).where(JobRun.id == "job-unknown-recovery")
|
|
).one()
|
|
|
|
assert result == {
|
|
"jobId": "job-unknown-recovery",
|
|
"status": "running",
|
|
"cancelled": False,
|
|
"changed": False,
|
|
"reason": (
|
|
"This job owns an unresolved detached PrivateMuse recovery execution; "
|
|
"reconcile that execution before changing the job record."
|
|
),
|
|
}
|
|
assert route_result == {
|
|
"success": False,
|
|
"message": (
|
|
"This job owns an unresolved detached PrivateMuse recovery execution; "
|
|
"reconcile that execution before changing the job record."
|
|
),
|
|
"id": "job-unknown-recovery",
|
|
"status": "running",
|
|
}
|
|
assert persisted.status == "running"
|
|
|
|
|
|
def test_privatemuse_job_is_cancellation_guarded_at_admission(tmp_path):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'admission-cancel.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
_state, admitted = recovery.PrivateMuseDeploymentRecovery.admit_job(
|
|
session,
|
|
JobRun(
|
|
id="job-admitted-recovery",
|
|
type="deployment.target.register_privatemuse",
|
|
status="queued",
|
|
source="chatgpt-mcp",
|
|
action_id="deployment.target.register_privatemuse",
|
|
target="jens/privatemuse",
|
|
risk="high",
|
|
policy_decision="allowed",
|
|
),
|
|
)
|
|
result = mcp_operations.cancel_operation_job(
|
|
session, "job-admitted-recovery"
|
|
)
|
|
|
|
assert admitted is True
|
|
assert result["cancelled"] is False
|
|
assert result["changed"] is False
|
|
assert result["status"] == "queued"
|
|
assert "unresolved detached PrivateMuse recovery" in result["reason"]
|
|
|
|
|
|
def test_privatemuse_cancellation_guard_applies_only_to_the_owner(tmp_path):
|
|
engine = create_engine(f"sqlite:///{tmp_path / 'owner-cancel.db'}")
|
|
SQLModel.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
recovery.PrivateMuseDeploymentRecovery.admit_job(
|
|
session,
|
|
JobRun(
|
|
id="job-owner",
|
|
type="deployment.target.register_privatemuse",
|
|
status="queued",
|
|
source="chatgpt-mcp",
|
|
action_id="deployment.target.register_privatemuse",
|
|
target="jens/privatemuse",
|
|
risk="high",
|
|
policy_decision="allowed",
|
|
),
|
|
)
|
|
unrelated = JobRun(
|
|
id="job-unrelated",
|
|
type="deployment.target.register_privatemuse",
|
|
status="running",
|
|
source="chatgpt-mcp",
|
|
action_id="deployment.target.register_privatemuse",
|
|
target="jens/privatemuse",
|
|
risk="high",
|
|
policy_decision="allowed",
|
|
)
|
|
session.add(unrelated)
|
|
session.commit()
|
|
|
|
result = mcp_operations.cancel_operation_job(session, unrelated.id)
|
|
|
|
assert result["cancelled"] is True
|
|
assert result["changed"] is True
|
|
assert result["status"] == "cancelled"
|