Codeseys's picture
Wave 7+8+9: spikes 006/007/008 — close vision-validation gaps V2/V5/V8
57af35d
Raw
History Blame Contribute Delete
7.9 kB
"""Spike 007 ingestion tests — Claude Code JSONL → TraceState.
Uses fixtures/synthetic_session.jsonl which conforms to the Claude Code 2.1.x
schema. Real-session test (skipped if no local sessions) is included as a
sanity check; CI users can ignore it.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(HERE))
from claude_code_ingester import ( # noqa: E402
ClaudeCodeIngester,
IngestionStats,
SYSTEM_PROMPT,
)
FIXTURE = HERE / "fixtures" / "synthetic_session.jsonl"
# ---------------------------------------------------------------------
# Synthetic-fixture tests (always run, deterministic)
# ---------------------------------------------------------------------
def test_fixture_exists():
assert FIXTURE.exists(), f"missing test fixture: {FIXTURE}"
def test_ingest_emits_three_states():
"""Synthetic session has 3 assistant turns → 3 TraceState records."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
assert len(states) == 3, (
f"expected 3 states (3 assistant turns), got {len(states)}"
)
def test_state_id_uniqueness():
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
ids = [s["state_id"] for s in states]
assert len(ids) == len(set(ids)), f"non-unique state_ids: {ids}"
def test_messages_history_grows():
"""Each subsequent state's messages list should be longer than the previous."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
lengths = [len(s["messages"]) for s in states]
for i in range(1, len(lengths)):
assert lengths[i] > lengths[i - 1], (
f"history did not grow: {lengths}"
)
def test_first_state_has_system_prompt_and_user_message():
"""State 0 has [system, user] in messages (history before first asst turn)."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
assert states[0]["messages"][0]["role"] == "system"
assert states[0]["messages"][0]["content"] == SYSTEM_PROMPT
assert states[0]["messages"][1]["role"] == "user"
assert "1MB" in states[0]["messages"][1]["content"]
def test_thinking_stripped_from_teacher_history():
"""The thinking block in turn 1 should not appear in turn 2's messages history."""
ingester = ClaudeCodeIngester(strip_thinking=True)
states = list(ingester.ingest(FIXTURE))
# State 1's history includes the assistant's first turn (which had a thinking block)
history_2 = states[1]["messages"]
asst_msgs_2 = [m for m in history_2 if m["role"] == "assistant"]
assert len(asst_msgs_2) == 1, "state 1 should have 1 prior assistant turn"
assert "[THINKING]" not in asst_msgs_2[0]["content"], (
f"thinking leaked into teacher history: {asst_msgs_2[0]['content']!r}"
)
def test_thinking_kept_in_student_action():
"""State 0 first assistant turn HAD a thinking block — must appear in student_action."""
ingester = ClaudeCodeIngester(strip_thinking=True)
states = list(ingester.ingest(FIXTURE))
assert "[THINKING]" in states[0]["student_action"], (
f"thinking missing from student_action: {states[0]['student_action']!r}"
)
def test_tool_use_serialization():
"""Tool use blocks should be serialized as [TOOL_USE] name=... input=..."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
assert "[TOOL_USE]" in states[0]["student_action"]
assert "name=Bash" in states[0]["student_action"]
# Input should be JSON
assert "find" in states[0]["student_action"]
def test_tool_result_in_user_history():
"""The tool_result observation should be in state 1's history as a user msg."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(FIXTURE))
history_1 = states[1]["messages"]
user_msgs = [m for m in history_1 if m["role"] == "user"]
assert any("[TOOL_RESULT]" in m["content"] for m in user_msgs), (
f"tool_result missing from history: {user_msgs}"
)
def test_summary_records_skipped():
ingester = ClaudeCodeIngester()
list(ingester.ingest(FIXTURE))
assert ingester.last_stats.skipped_summary >= 1
def test_stats_populated():
ingester = ClaudeCodeIngester()
list(ingester.ingest(FIXTURE))
s = ingester.last_stats
assert s.n_assistant_turns == 3
assert s.n_tool_use_blocks == 2
assert s.n_text_blocks >= 2 # 2 turns have text blocks
assert s.n_states_emitted == 3
# ---------------------------------------------------------------------
# Subagent skip
# ---------------------------------------------------------------------
def test_subagent_filename_skipped(tmp_path):
"""Files starting with `agent-` should be entirely skipped."""
fake = tmp_path / "agent-12345.jsonl"
fake.write_text(FIXTURE.read_text())
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(fake))
assert states == [], "subagent file should yield nothing"
def test_sidechain_records_skipped(tmp_path):
"""isSidechain=true records should be skipped."""
fake = tmp_path / "with_sidechain.jsonl"
raw = FIXTURE.read_text().splitlines()
# Add a sidechain assistant record
sidechain = {
"type": "assistant",
"uuid": "side1",
"parentUuid": "a6",
"sessionId": "test-session",
"timestamp": "2026-05-26T10:00:20Z",
"cwd": "/tmp/test",
"version": "2.1.143",
"isSidechain": True,
"message": {
"role": "assistant",
"model": "claude-opus-4-7",
"content": [{"type": "text", "text": "subagent talking"}],
},
}
raw.append(json.dumps(sidechain))
fake.write_text("\n".join(raw) + "\n")
ingester = ClaudeCodeIngester(skip_sidechain=True)
list(ingester.ingest(fake))
assert ingester.last_stats.skipped_subagent >= 1
# ---------------------------------------------------------------------
# Error tolerance
# ---------------------------------------------------------------------
def test_truncated_line_tolerated(tmp_path):
"""A truncated/malformed JSON line should be skipped, not crash the ingester."""
fake = tmp_path / "broken.jsonl"
raw = FIXTURE.read_text().splitlines()
raw.insert(2, '{"type": "assistant", "message": {bad json')
fake.write_text("\n".join(raw) + "\n")
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(fake))
assert ingester.last_stats.skipped_truncated_lines == 1
assert len(states) == 3, "valid records should still parse"
# ---------------------------------------------------------------------
# Real session smoke (skipped if not present)
# ---------------------------------------------------------------------
REAL_SESSION = Path(
"/home/codeseys/.claude/projects/-mnt-e-CS-github-VIGOR--overstory-worktrees-builder-iteration-checkpoint/e4a34e2b-40c6-49ce-b253-912a43224aae.jsonl"
)
@pytest.mark.skipif(not REAL_SESSION.exists(), reason="real Claude Code session not on this machine")
def test_real_session_ingest_smoke():
"""Sanity-check the ingester on a real session — should yield ≥10 states with no exceptions."""
ingester = ClaudeCodeIngester()
states = list(ingester.ingest(REAL_SESSION))
assert len(states) >= 10, f"expected ≥10 states from real session, got {len(states)}"
# Spot-check: every state should have a non-empty student_action
for i, s in enumerate(states):
assert s["student_action"], f"empty student_action at state {i}"
assert s["messages"], f"empty messages at state {i}"
# No version warnings on a known-good session
assert not ingester.last_stats.version_warnings, (
f"unexpected version warnings: {ingester.last_stats.version_warnings}"
)