feat: better injection, pty snapshots
This commit is contained in:
+197
-48
@@ -1,9 +1,8 @@
|
||||
"""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*).
|
||||
tested against the format claude 2.1.147 itself writes (verified empirically
|
||||
against real session transcripts).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -67,42 +66,32 @@ def test_hash_history_text_blocks_collide_with_string_form() -> None:
|
||||
# --- build_seed_jsonl -----------------------------------------------------
|
||||
|
||||
|
||||
def _records(seed: str) -> list[dict]:
|
||||
return [json.loads(line) for line in seed.strip().splitlines()]
|
||||
|
||||
|
||||
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:
|
||||
def test_build_seed_jsonl_starts_with_permission_mode() -> None:
|
||||
"""Non-empty seed must lead with a `permission-mode` record — claude
|
||||
2.1.147 writes one at session start and expects it on resume."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "My name is Beaver."},
|
||||
{"role": "assistant", "content": "Got it."},
|
||||
],
|
||||
[{"role": "user", "content": "hi"}],
|
||||
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"
|
||||
recs = _records(seed)
|
||||
assert recs[0]["type"] == "permission-mode"
|
||||
assert recs[0]["sessionId"] == "sid-1"
|
||||
assert recs[0]["permissionMode"] == "bypassPermissions"
|
||||
|
||||
|
||||
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."""
|
||||
def test_build_seed_jsonl_snapshot_precedes_each_user_prompt() -> None:
|
||||
"""Before every new user prompt, claude writes a
|
||||
`file-history-snapshot` whose `messageId` matches the user record's
|
||||
`uuid`. Resume parsing depends on this pairing."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "u1"},
|
||||
@@ -113,38 +102,198 @@ def test_build_seed_jsonl_chains_parent_uuids_across_turns() -> None:
|
||||
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"]
|
||||
recs = _records(seed)
|
||||
snapshots = [r for r in recs if r.get("type") == "file-history-snapshot"]
|
||||
users = [r for r in recs if r.get("type") == "user"]
|
||||
assert len(snapshots) == 2
|
||||
assert len(users) == 2
|
||||
for snap, usr in zip(snapshots, users, strict=True):
|
||||
assert snap["messageId"] == usr["uuid"]
|
||||
assert snap["snapshot"]["messageId"] == usr["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."""
|
||||
def test_build_seed_jsonl_user_record_has_permission_mode_no_isMeta() -> None:
|
||||
"""User records carry `permissionMode` but no `isMeta` — that's what
|
||||
claude 2.1.147 writes. Adding `isMeta` to user records is one of the
|
||||
things that made the old seed look 'wrong' on strict resume."""
|
||||
seed = build_seed_jsonl(
|
||||
[{"role": "user", "content": "hi"}],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
user_rec = next(r for r in _records(seed) if r.get("type") == "user")
|
||||
assert user_rec["permissionMode"] == "bypassPermissions"
|
||||
assert "isMeta" not in user_rec
|
||||
assert user_rec["version"] == "2.1.147"
|
||||
assert user_rec["gitBranch"] == "HEAD"
|
||||
assert "promptId" in user_rec
|
||||
|
||||
|
||||
def test_build_seed_jsonl_assistant_string_content_wraps_as_text_block() -> None:
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "42"},
|
||||
],
|
||||
}
|
||||
{"role": "user", "content": "u"},
|
||||
{"role": "assistant", "content": "Got it."},
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
rec = json.loads(seed.strip())
|
||||
assert rec["message"]["content"] == [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "42"},
|
||||
asst_recs = [r for r in _records(seed) if r.get("type") == "assistant"]
|
||||
assert len(asst_recs) == 1
|
||||
msg = asst_recs[0]["message"]
|
||||
assert msg["content"] == [{"type": "text", "text": "Got it."}]
|
||||
assert msg["stop_reason"] == "end_turn"
|
||||
assert msg["diagnostics"] is None
|
||||
assert "server_tool_use" in msg["usage"]
|
||||
assert "cache_creation" in msg["usage"]
|
||||
|
||||
|
||||
def test_build_seed_jsonl_splits_assistant_blocks_into_separate_records() -> None:
|
||||
"""A multi-block assistant message becomes one record per block, all
|
||||
sharing the same `msg_id` and `requestId`. parentUuid chains them."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "u"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "ponder", "signature": "sig"},
|
||||
{"type": "tool_use", "id": "tu1", "name": "bash", "input": {}},
|
||||
],
|
||||
},
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
asst_recs = [r for r in _records(seed) if r.get("type") == "assistant"]
|
||||
assert len(asst_recs) == 2
|
||||
assert asst_recs[0]["message"]["id"] == asst_recs[1]["message"]["id"]
|
||||
assert asst_recs[0]["requestId"] == asst_recs[1]["requestId"]
|
||||
assert asst_recs[1]["parentUuid"] == asst_recs[0]["uuid"]
|
||||
assert asst_recs[0]["message"]["content"][0]["type"] == "thinking"
|
||||
assert asst_recs[1]["message"]["content"][0]["type"] == "tool_use"
|
||||
assert asst_recs[0]["message"]["stop_reason"] == "tool_use"
|
||||
|
||||
|
||||
def test_build_seed_jsonl_tool_result_parents_to_matching_tool_use() -> None:
|
||||
"""A user message with a `tool_result` block becomes a user record
|
||||
whose parentUuid points to the assistant record that emitted the
|
||||
matching `tool_use`. `sourceToolAssistantUUID` mirrors that link."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "u"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "tu1", "name": "bash", "input": {}}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "tu1", "content": "ok"}
|
||||
],
|
||||
},
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
recs = _records(seed)
|
||||
asst = next(r for r in recs if r.get("type") == "assistant")
|
||||
# second user record is the tool_result one (first was the prompt)
|
||||
user_recs = [r for r in recs if r.get("type") == "user"]
|
||||
tool_result_rec = user_recs[1]
|
||||
assert tool_result_rec["parentUuid"] == asst["uuid"]
|
||||
assert tool_result_rec["sourceToolAssistantUUID"] == asst["uuid"]
|
||||
assert tool_result_rec["toolUseResult"] == "ok"
|
||||
assert tool_result_rec["message"]["content"] == [
|
||||
{"type": "tool_result", "tool_use_id": "tu1", "content": "ok"}
|
||||
]
|
||||
|
||||
|
||||
def test_build_seed_jsonl_splits_multi_tool_result_user_message() -> None:
|
||||
"""When a user message carries multiple tool_result blocks (one per
|
||||
parallel tool_use), claude writes one record per result. Each parents
|
||||
on the corresponding assistant record."""
|
||||
seed = build_seed_jsonl(
|
||||
[
|
||||
{"role": "user", "content": "u"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "A", "name": "x", "input": {}}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "B", "name": "y", "input": {}}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "A", "content": "a"},
|
||||
{"type": "tool_result", "tool_use_id": "B", "content": "b"},
|
||||
],
|
||||
},
|
||||
],
|
||||
session_id="s",
|
||||
cwd="/tmp",
|
||||
)
|
||||
recs = _records(seed)
|
||||
asst_recs = [r for r in recs if r.get("type") == "assistant"]
|
||||
tool_result_recs = [
|
||||
r
|
||||
for r in recs
|
||||
if r.get("type") == "user"
|
||||
and isinstance(r["message"]["content"], list)
|
||||
and r["message"]["content"][0].get("type") == "tool_result"
|
||||
]
|
||||
assert len(tool_result_recs) == 2
|
||||
a_uuid = next(
|
||||
r["uuid"]
|
||||
for r in asst_recs
|
||||
if r["message"]["content"][0].get("id") == "A"
|
||||
)
|
||||
b_uuid = next(
|
||||
r["uuid"]
|
||||
for r in asst_recs
|
||||
if r["message"]["content"][0].get("id") == "B"
|
||||
)
|
||||
assert tool_result_recs[0]["sourceToolAssistantUUID"] == a_uuid
|
||||
assert tool_result_recs[1]["sourceToolAssistantUUID"] == b_uuid
|
||||
|
||||
|
||||
def test_build_seed_jsonl_chains_parent_uuids_linearly() -> None:
|
||||
"""Every content-carrying record (user, assistant, tool_result) chains
|
||||
via parentUuid back through the record graph. The first user has
|
||||
parentUuid=None; subsequent records have non-null parents."""
|
||||
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",
|
||||
)
|
||||
chain = [
|
||||
r
|
||||
for r in _records(seed)
|
||||
if r.get("type") in ("user", "assistant")
|
||||
]
|
||||
assert chain[0]["parentUuid"] is None
|
||||
for prev, nxt in zip(chain, chain[1:], strict=False):
|
||||
assert nxt["parentUuid"] == prev["uuid"]
|
||||
|
||||
|
||||
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_seed_jsonl(
|
||||
[{"role": "system", "content": "x"}], session_id="s", cwd="/tmp"
|
||||
)
|
||||
|
||||
|
||||
# --- build_concat_prompt --------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user