初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,177 @@
|
||||
"""AuditEventSink / CompositeEventSink 单元测试。
|
||||
|
||||
审计 sink 把沙箱 tool.completed / tool.failed 转写为 AuditEvent:completed →
|
||||
tool.executed / allow,failed → tool.failed / deny;argv(来自 started)作为
|
||||
input_summary 交 record_audit_event 脱敏。record_audit_event 的数据访问已被 mock,
|
||||
本测试聚焦 sink 的「事件 → 审计字段」映射与脱敏入口。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.integrations.sandbox.audit_sink import AuditEventSink
|
||||
from app.integrations.sandbox.events import (
|
||||
CompositeEventSink,
|
||||
SandboxEvent,
|
||||
completed_event,
|
||||
failed_event,
|
||||
started_event,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _fake_session():
|
||||
yield AsyncMock()
|
||||
|
||||
|
||||
def _sink() -> AuditEventSink:
|
||||
return AuditEventSink(_fake_session, run_id="run_1", character_id="char_1")
|
||||
|
||||
|
||||
async def test_completed_writes_executed_allow() -> None:
|
||||
sink = _sink()
|
||||
await sink.emit(
|
||||
started_event(
|
||||
tool="shell", argv=["ls", "-la"], risk_level="L2", trace_id="t1", run_id="run_1"
|
||||
)
|
||||
)
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(
|
||||
completed_event(
|
||||
tool="shell",
|
||||
trace_id="t1",
|
||||
run_id="run_1",
|
||||
exit_code=0,
|
||||
duration_ms=42,
|
||||
timeout=False,
|
||||
truncated=False,
|
||||
stdout_len=10,
|
||||
stderr_len=0,
|
||||
)
|
||||
)
|
||||
rec.assert_awaited_once()
|
||||
kw = rec.call_args.kwargs
|
||||
assert kw["action"] == "tool.executed"
|
||||
assert kw["decision"] == "allow"
|
||||
assert kw["pivot"] == "tool_runtime"
|
||||
assert kw["risk_level"] == "L2"
|
||||
assert kw["input_summary"] == "ls -la"
|
||||
assert kw["resource_id"] == "shell"
|
||||
assert kw["trace_id"] == "t1"
|
||||
assert kw["character_id"] == "char_1"
|
||||
assert "exit=0" in kw["output_summary"]
|
||||
|
||||
|
||||
async def test_failed_writes_failed_deny() -> None:
|
||||
sink = _sink()
|
||||
await sink.emit(
|
||||
started_event(
|
||||
tool="shell", argv=["sh", "-c", "bad"], risk_level="L3", trace_id="t1", run_id="run_1"
|
||||
)
|
||||
)
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(
|
||||
failed_event(
|
||||
tool="shell",
|
||||
trace_id="t1",
|
||||
run_id="run_1",
|
||||
error_code="exec_timeout",
|
||||
error_message="执行超时",
|
||||
)
|
||||
)
|
||||
rec.assert_awaited_once()
|
||||
kw = rec.call_args.kwargs
|
||||
assert kw["action"] == "tool.failed"
|
||||
assert kw["decision"] == "deny"
|
||||
assert kw["risk_level"] == "L3"
|
||||
assert kw["reason"] == "exec_timeout"
|
||||
assert kw["output_summary"] == "执行超时"
|
||||
|
||||
|
||||
async def test_started_does_not_write_audit() -> None:
|
||||
"""started 只缓存上下文,不写审计。"""
|
||||
sink = _sink()
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(
|
||||
started_event(tool="shell", argv=["ls"], risk_level="L1", trace_id="t1", run_id="run_1")
|
||||
)
|
||||
rec.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_completed_summary_marks_timeout_and_truncation() -> None:
|
||||
sink = _sink()
|
||||
await sink.emit(
|
||||
started_event(tool="shell", argv=["ls"], risk_level="L1", trace_id="t1", run_id="run_1")
|
||||
)
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(
|
||||
completed_event(
|
||||
tool="shell",
|
||||
trace_id="t1",
|
||||
run_id="run_1",
|
||||
exit_code=None,
|
||||
duration_ms=5000,
|
||||
timeout=True,
|
||||
truncated=True,
|
||||
stdout_len=0,
|
||||
stderr_len=0,
|
||||
)
|
||||
)
|
||||
summary = rec.call_args.kwargs["output_summary"]
|
||||
assert "timeout" in summary
|
||||
assert "truncated" in summary
|
||||
|
||||
|
||||
async def test_empty_argv_yields_none_input_summary() -> None:
|
||||
"""executor 路径(fetch_url 等)started argv 为空,input_summary 应为 None。"""
|
||||
sink = _sink()
|
||||
await sink.emit(
|
||||
started_event(tool="fetch_url", argv=[], risk_level="L2", trace_id="t1", run_id="run_1")
|
||||
)
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(
|
||||
completed_event(
|
||||
tool="fetch_url",
|
||||
trace_id="t1",
|
||||
run_id="run_1",
|
||||
exit_code=0,
|
||||
duration_ms=100,
|
||||
timeout=False,
|
||||
truncated=False,
|
||||
stdout_len=5,
|
||||
stderr_len=0,
|
||||
)
|
||||
)
|
||||
assert rec.call_args.kwargs["input_summary"] is None
|
||||
|
||||
|
||||
async def test_unknown_event_type_ignored() -> None:
|
||||
"""非 started/completed/failed 的事件不触发审计写入。"""
|
||||
sink = _sink()
|
||||
with patch(
|
||||
"app.integrations.sandbox.audit_sink.record_audit_event", new_callable=AsyncMock
|
||||
) as rec:
|
||||
await sink.emit(SandboxEvent(type="tool.stdout", payload={"tool": "shell"}))
|
||||
rec.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_composite_sink_fans_out_to_all() -> None:
|
||||
a = AsyncMock()
|
||||
b = AsyncMock()
|
||||
composite = CompositeEventSink([a, b])
|
||||
ev = SandboxEvent(type="tool.started", payload={"tool": "x"})
|
||||
await composite.emit(ev)
|
||||
a.emit.assert_awaited_once_with(ev)
|
||||
b.emit.assert_awaited_once_with(ev)
|
||||
Reference in new issue
Block a user