feat(cybersecurity): auto-commit con 48 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"""Tests para las funciones puras de tee_anthropic_sse.
|
||||
|
||||
Cubre split_sse_events y event_to_ndjson sin necesitar mitmproxy.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from tee_anthropic_sse import split_sse_events, event_to_ndjson
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSE fixture — captura real de la API de Anthropic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RAW_SSE = (
|
||||
b"event: message_start\n"
|
||||
b'data: {"type":"message_start","message":{"model":"claude-opus-4-8","id":"msg_x",'
|
||||
b'"type":"message","role":"assistant","content":[],"stop_reason":null}}\n'
|
||||
b"\n"
|
||||
b"event: content_block_start\n"
|
||||
b'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n'
|
||||
b"\n"
|
||||
b"event: ping\n"
|
||||
b'data: {"type": "ping"}\n'
|
||||
b"\n"
|
||||
b"event: content_block_delta\n"
|
||||
b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"P"}}\n'
|
||||
b"\n"
|
||||
b"event: content_block_delta\n"
|
||||
b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ONG"}}\n'
|
||||
b"\n"
|
||||
b"event: content_block_stop\n"
|
||||
b'data: {"type":"content_block_stop","index":0}\n'
|
||||
b"\n"
|
||||
b"event: message_delta\n"
|
||||
b'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},'
|
||||
b'"usage":{"output_tokens":5}}\n'
|
||||
b"\n"
|
||||
b"event: message_stop\n"
|
||||
b'data: {"type":"message_stop"}\n'
|
||||
b"\n"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# split_sse_events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_split_buffer_completo_devuelve_8_bloques():
|
||||
"""Con el buffer completo devuelve los 8 bloques y leftover vacio."""
|
||||
events, leftover = split_sse_events(_RAW_SSE)
|
||||
assert len(events) == 8
|
||||
assert leftover == b""
|
||||
|
||||
|
||||
def test_split_bloques_contienen_event_y_data():
|
||||
"""Cada bloque contiene las lineas event: y data: esperadas."""
|
||||
events, _ = split_sse_events(_RAW_SSE)
|
||||
assert "event: message_start" in events[0]
|
||||
assert "event: ping" in events[2]
|
||||
assert "event: message_stop" in events[7]
|
||||
|
||||
|
||||
def test_split_buffer_cortado_preserva_incompleto():
|
||||
"""Con un buffer cortado a la mitad de un evento, devuelve solo los completos."""
|
||||
# Encontrar la SEGUNDA aparicion de content_block_delta (quinto evento en total)
|
||||
first_occ = _RAW_SSE.find(b"event: content_block_delta\ndata:")
|
||||
second_occ = _RAW_SSE.find(b"event: content_block_delta\ndata:", first_occ + 1)
|
||||
# Cortar en medio del data: del segundo content_block_delta
|
||||
cut_buf = _RAW_SSE[:second_occ + 20]
|
||||
|
||||
events, leftover = split_sse_events(cut_buf)
|
||||
# Debe haber exactamente 4 eventos completos:
|
||||
# message_start, content_block_start, ping, primer content_block_delta
|
||||
assert len(events) == 4
|
||||
# El leftover no debe estar vacio (el segundo delta queda a medias)
|
||||
assert len(leftover) > 0
|
||||
|
||||
|
||||
def test_split_resto_mas_continuacion_reconstruye_evento():
|
||||
"""Concatenar leftover + continuacion reconstituye el evento cortado."""
|
||||
# Cortar justo antes del \n\n que cierra el primer delta
|
||||
cut_point = _RAW_SSE.find(b"\n\nevent: content_block_delta\n", 100)
|
||||
first_half = _RAW_SSE[:cut_point + 1] # termina dentro del separador
|
||||
second_half = _RAW_SSE[cut_point + 1:]
|
||||
|
||||
events1, leftover1 = split_sse_events(first_half)
|
||||
combined = leftover1 + second_half
|
||||
events2, leftover2 = split_sse_events(combined)
|
||||
|
||||
# La union debe cubrir todos los bloques del segundo tramo
|
||||
all_events = events1 + events2
|
||||
assert len(all_events) == 8
|
||||
assert leftover2 == b""
|
||||
|
||||
|
||||
def test_split_buffer_vacio():
|
||||
"""Buffer vacio devuelve lista vacia y leftover vacio."""
|
||||
events, leftover = split_sse_events(b"")
|
||||
assert events == []
|
||||
assert leftover == b""
|
||||
|
||||
|
||||
def test_split_evento_unico_sin_separador_final():
|
||||
"""Un evento sin separador final queda como leftover."""
|
||||
chunk = b"event: ping\ndata: {\"type\":\"ping\"}"
|
||||
events, leftover = split_sse_events(chunk)
|
||||
assert events == []
|
||||
assert b"ping" in leftover
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# event_to_ndjson
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_text_delta_p():
|
||||
"""content_block_delta text_delta 'P' -> [{type:text_delta, stream_id:1, text:'P'}]."""
|
||||
block = (
|
||||
"event: content_block_delta\n"
|
||||
'data: {"type":"content_block_delta","index":0,'
|
||||
'"delta":{"type":"text_delta","text":"P"}}'
|
||||
)
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == [{"type": "text_delta", "stream_id": 1, "text": "P"}]
|
||||
|
||||
|
||||
def test_text_delta_ong():
|
||||
"""content_block_delta text_delta 'ONG' -> text 'ONG'."""
|
||||
block = (
|
||||
"event: content_block_delta\n"
|
||||
'data: {"type":"content_block_delta","index":0,'
|
||||
'"delta":{"type":"text_delta","text":"ONG"}}'
|
||||
)
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == [{"type": "text_delta", "stream_id": 1, "text": "ONG"}]
|
||||
|
||||
|
||||
def test_message_stop_con_stop_holder_previo():
|
||||
"""message_stop con stop_holder ya cargado -> stop_reason end_turn."""
|
||||
stop_holder: dict = {}
|
||||
|
||||
# Primero simular message_delta para poblar el holder
|
||||
delta_block = (
|
||||
"event: message_delta\n"
|
||||
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn",'
|
||||
'"stop_sequence":null},"usage":{"output_tokens":5}}'
|
||||
)
|
||||
event_to_ndjson(delta_block, 1, stop_holder)
|
||||
assert stop_holder.get("stop_reason") == "end_turn"
|
||||
|
||||
# Ahora message_stop
|
||||
stop_block = (
|
||||
"event: message_stop\n"
|
||||
'data: {"type":"message_stop"}'
|
||||
)
|
||||
result = event_to_ndjson(stop_block, 1, stop_holder)
|
||||
assert result == [{"type": "message_stop", "stream_id": 1, "stop_reason": "end_turn"}]
|
||||
|
||||
|
||||
def test_ping_devuelve_lista_vacia():
|
||||
"""ping -> []."""
|
||||
block = "event: ping\ndata: {\"type\": \"ping\"}"
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_content_block_start_text_devuelve_vacio():
|
||||
"""content_block_start para un bloque de texto -> []."""
|
||||
block = (
|
||||
"event: content_block_start\n"
|
||||
'data: {"type":"content_block_start","index":0,'
|
||||
'"content_block":{"type":"text","text":""}}'
|
||||
)
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_content_block_start_tool_use():
|
||||
"""content_block_start tool_use -> tool_use_start con name e id."""
|
||||
block = (
|
||||
"event: content_block_start\n"
|
||||
'data: {"type":"content_block_start","index":1,'
|
||||
'"content_block":{"type":"tool_use","id":"toolu_01abc","name":"Bash"}}'
|
||||
)
|
||||
result = event_to_ndjson(block, 2, {})
|
||||
assert result == [
|
||||
{
|
||||
"type": "tool_use_start",
|
||||
"stream_id": 2,
|
||||
"tool_name": "Bash",
|
||||
"tool_id": "toolu_01abc",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_tool_json_delta():
|
||||
"""content_block_delta input_json_delta -> tool_json_delta."""
|
||||
# Construir el bloque SSE con json.dumps para que el partial_json quede
|
||||
# correctamente escapado dentro del JSON del campo data:
|
||||
import json as _json
|
||||
data_payload = {
|
||||
"type": "content_block_delta",
|
||||
"index": 1,
|
||||
"delta": {
|
||||
"type": "input_json_delta",
|
||||
"partial_json": '{"command":"ls',
|
||||
},
|
||||
}
|
||||
block = "event: content_block_delta\ndata: " + _json.dumps(data_payload)
|
||||
result = event_to_ndjson(block, 3, {})
|
||||
assert result == [
|
||||
{
|
||||
"type": "tool_json_delta",
|
||||
"stream_id": 3,
|
||||
"partial_json": '{"command":"ls',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_json_invalido_en_data_devuelve_vacio():
|
||||
"""Linea data: con JSON invalido -> [] (sin excepcion)."""
|
||||
block = "event: content_block_delta\ndata: {esto no es json"
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_bloque_sin_data_devuelve_vacio():
|
||||
"""Bloque sin linea data: -> []."""
|
||||
block = "event: content_block_stop\n"
|
||||
result = event_to_ndjson(block, 1, {})
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integración del parseo: secuencia completa produce PONG + message_stop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_integracion_secuencia_completa_produce_pong_y_stop():
|
||||
"""Los 8 bloques en orden producen text_delta 'P'+'ONG' y un message_stop end_turn."""
|
||||
events, leftover = split_sse_events(_RAW_SSE)
|
||||
assert leftover == b""
|
||||
|
||||
stop_holder: dict = {}
|
||||
all_ndjson: list[dict] = []
|
||||
for block in events:
|
||||
all_ndjson.extend(event_to_ndjson(block, 1, stop_holder))
|
||||
|
||||
text_deltas = [o for o in all_ndjson if o["type"] == "text_delta"]
|
||||
message_stops = [o for o in all_ndjson if o["type"] == "message_stop"]
|
||||
|
||||
concatenated = "".join(d["text"] for d in text_deltas)
|
||||
assert concatenated == "PONG"
|
||||
|
||||
assert len(message_stops) == 1
|
||||
assert message_stops[0]["stop_reason"] == "end_turn"
|
||||
assert message_stops[0]["stream_id"] == 1
|
||||
|
||||
|
||||
def test_integracion_stream_id_se_propaga():
|
||||
"""stream_id se propaga correctamente a todos los eventos emitidos."""
|
||||
events, _ = split_sse_events(_RAW_SSE)
|
||||
stop_holder: dict = {}
|
||||
for block in events:
|
||||
for obj in event_to_ndjson(block, 42, stop_holder):
|
||||
assert obj["stream_id"] == 42
|
||||
|
||||
|
||||
def test_integracion_determinismo():
|
||||
"""Parsear el mismo buffer dos veces produce exactamente el mismo resultado."""
|
||||
def parse_all(stream_id: int) -> list[dict]:
|
||||
evs, _ = split_sse_events(_RAW_SSE)
|
||||
holder: dict = {}
|
||||
result: list[dict] = []
|
||||
for b in evs:
|
||||
result.extend(event_to_ndjson(b, stream_id, holder))
|
||||
return result
|
||||
|
||||
assert parse_all(1) == parse_all(1)
|
||||
Reference in New Issue
Block a user