初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
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)