feat: vibed out some slop over here also
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""Unit tests for `history_injection` helpers.
|
||||
|
||||
Pure functions, no claude / no filesystem. The seed-JSONL shape is regression
|
||||
tested against the same minimal contract that `probe_jsonl_injection.py`
|
||||
proved out empirically (see FINDINGS § *Native JSONL injection works on
|
||||
--resume*).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from claude_code_api.injection import (
|
||||
build_concat_prompt,
|
||||
build_seed_jsonl,
|
||||
hash_history,
|
||||
)
|
||||
|
||||
# --- hash_history ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_hash_history_empty_is_stable() -> None:
|
||||
assert hash_history([]) == hash_history([])
|
||||
|
||||
|
||||
def test_hash_history_distinguishes_content() -> None:
|
||||
a = [{"role": "user", "content": "hi"}]
|
||||
b = [{"role": "user", "content": "bye"}]
|
||||
assert hash_history(a) != hash_history(b)
|
||||
|
||||
|
||||
def test_hash_history_ignores_block_key_order() -> None:
|
||||
"""Two clients that serialize the same block in different key orders
|
||||
must collide. Canonical-JSON serialization handles this."""
|
||||
a = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "t1", "name": "echo", "input": {"x": 1}}],
|
||||
}
|
||||
]
|
||||
b = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"input": {"x": 1}, "name": "echo", "id": "t1", "type": "tool_use"}],
|
||||
}
|
||||
]
|
||||
assert hash_history(a) == hash_history(b)
|
||||
|
||||
|
||||
def test_hash_history_rejects_unknown_role() -> None:
|
||||
with pytest.raises(ValueError, match="role"):
|
||||
hash_history([{"role": "system", "content": "x"}])
|
||||
|
||||
|
||||
def test_hash_history_text_blocks_collide_with_string_form() -> None:
|
||||
"""A bare string `content` and the equivalent single text block hash to
|
||||
DIFFERENT values. They represent the same semantic content but appear
|
||||
on the wire differently — the gateway must pick one form per role and
|
||||
stay consistent. We don't try to paper over that here."""
|
||||
a = [{"role": "user", "content": "hello"}]
|
||||
b = [{"role": "user", "content": [{"type": "text", "text": "hello"}]}]
|
||||
assert hash_history(a) != hash_history(b)
|
||||
|
||||
|
||||
# --- build_seed_jsonl -----------------------------------------------------
|
||||
|
||||
|
||||
def test_build_seed_jsonl_empty_is_empty_string() -> None:
|
||||
assert build_seed_jsonl([], session_id="s", cwd="/tmp") == ""
|
||||
|
||||
|
||||
def test_build_seed_jsonl_two_records_for_one_turn() -> None:
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "My name is Beaver."},
|
||||
{"role": "assistant", "content": "Got it."},
|
||||
],
|
||||
session_id="sid-1",
|
||||
cwd="/work",
|
||||
)
|
||||
lines = [json.loads(line) for line in seed.strip().splitlines()]
|
||||
assert len(lines) == 2
|
||||
user_rec, asst_rec = lines
|
||||
|
||||
assert user_rec["type"] == "user"
|
||||
assert user_rec["sessionId"] == "sid-1"
|
||||
assert user_rec["cwd"] == "/work"
|
||||
assert user_rec["parentUuid"] is None
|
||||
assert user_rec["message"] == {"role": "user", "content": "My name is Beaver."}
|
||||
assert user_rec["isMeta"] is False
|
||||
assert "uuid" in user_rec and "timestamp" in user_rec
|
||||
|
||||
assert asst_rec["type"] == "assistant"
|
||||
assert asst_rec["parentUuid"] == user_rec["uuid"]
|
||||
assert asst_rec["message"]["role"] == "assistant"
|
||||
assert asst_rec["message"]["content"] == [{"type": "text", "text": "Got it."}]
|
||||
assert asst_rec["message"]["stop_reason"] == "end_turn"
|
||||
assert asst_rec["sessionId"] == "sid-1"
|
||||
|
||||
|
||||
def test_build_seed_jsonl_chains_parent_uuids_across_turns() -> None:
|
||||
"""The parentUuid graph must form a linear chain across turns — that's
|
||||
how claude reconstructs conversation order on resume."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "u1"},
|
||||
{"role": "assistant", "content": "a1"},
|
||||
{"role": "user", "content": "u2"},
|
||||
{"role": "assistant", "content": "a2"},
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
recs = [json.loads(line) for line in seed.strip().splitlines()]
|
||||
assert len(recs) == 4
|
||||
assert recs[0]["parentUuid"] is None
|
||||
assert recs[1]["parentUuid"] == recs[0]["uuid"]
|
||||
assert recs[2]["parentUuid"] == recs[1]["uuid"]
|
||||
assert recs[3]["parentUuid"] == recs[2]["uuid"]
|
||||
|
||||
|
||||
def test_build_seed_jsonl_passes_list_content_through_for_user() -> None:
|
||||
"""A user record with a tool_result block (the only list-form user
|
||||
content claude itself writes) must round-trip verbatim."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "42"},
|
||||
],
|
||||
}
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
rec = json.loads(seed.strip())
|
||||
assert rec["message"]["content"] == [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "42"},
|
||||
]
|
||||
|
||||
|
||||
def test_build_seed_jsonl_rejects_unknown_role() -> None:
|
||||
with pytest.raises(ValueError, match="role"):
|
||||
build_seed_jsonl([{"role": "system", "content": "x"}], session_id="s", cwd="/tmp")
|
||||
|
||||
|
||||
# --- build_concat_prompt --------------------------------------------------
|
||||
|
||||
|
||||
def test_build_concat_prompt_empty_history_returns_just_last_user() -> None:
|
||||
assert build_concat_prompt([], "hello") == "hello"
|
||||
|
||||
|
||||
def test_build_concat_prompt_renders_alternating_history() -> None:
|
||||
out = build_concat_prompt(
|
||||
[
|
||||
{"role": "user", "content": "u1"},
|
||||
{"role": "assistant", "content": "a1"},
|
||||
{"role": "user", "content": "u2"},
|
||||
{"role": "assistant", "content": "a2"},
|
||||
],
|
||||
"u3",
|
||||
)
|
||||
assert "Previous conversation context:" in out
|
||||
assert "[User]: u1" in out
|
||||
assert "[Assistant]: a1" in out
|
||||
assert "[User]: u2" in out
|
||||
assert "[Assistant]: a2" in out
|
||||
assert "Continue from here. New user message: u3" in out
|
||||
# The new prompt must come after the history, not interleaved.
|
||||
assert out.index("[Assistant]: a2") < out.index("Continue from here")
|
||||
|
||||
|
||||
def test_build_concat_prompt_flattens_text_blocks_and_skips_tools() -> None:
|
||||
"""Content-as-list with text blocks gets flattened; tool blocks are
|
||||
skipped (they don't round-trip through stdin in any useful form)."""
|
||||
out = build_concat_prompt(
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{"type": "tool_use", "id": "t1", "name": "x", "input": {}},
|
||||
{"type": "text", "text": "world"},
|
||||
],
|
||||
},
|
||||
],
|
||||
"ping",
|
||||
)
|
||||
assert "[Assistant]: hello world" in out
|
||||
Reference in New Issue
Block a user