From 5487fe8b869a2df6067ba22fe443b84839de0beb Mon Sep 17 00:00:00 2001 From: chen Date: Tue, 26 May 2026 05:47:14 +0800 Subject: [PATCH 1/5] test: add real-session JSONL fixtures and dispatch-order regression --- scripts/gen_real_session_fixtures.py | 223 +++++++++++++++++ .../real_session_all_tool_types.jsonl | 18 ++ .../real_session_malformed_lines.jsonl | 6 + tests/fixtures/real_session_minimal.jsonl | 3 + .../fixtures/real_session_nested_tools.jsonl | 5 + .../real_session_unknown_fields.jsonl | 3 + tests/test_real_session_fixtures.py | 234 ++++++++++++++++++ 7 files changed, 492 insertions(+) create mode 100644 scripts/gen_real_session_fixtures.py create mode 100644 tests/fixtures/real_session_all_tool_types.jsonl create mode 100644 tests/fixtures/real_session_malformed_lines.jsonl create mode 100644 tests/fixtures/real_session_minimal.jsonl create mode 100644 tests/fixtures/real_session_nested_tools.jsonl create mode 100644 tests/fixtures/real_session_unknown_fields.jsonl create mode 100644 tests/test_real_session_fixtures.py diff --git a/scripts/gen_real_session_fixtures.py b/scripts/gen_real_session_fixtures.py new file mode 100644 index 0000000..78fe4e3 --- /dev/null +++ b/scripts/gen_real_session_fixtures.py @@ -0,0 +1,223 @@ +"""One-off generator for Tuesday real_session_*.jsonl fixtures. Run from repo root: + python scripts/gen_real_session_fixtures.py +""" +from __future__ import annotations + +import json +import os + +FIXTURES = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures") +CWD = "/sanitized/project/path" +TS = "2026-05-26T10:{:02d}:00Z" + + +def _line(obj: dict) -> str: + return json.dumps(obj, ensure_ascii=False) + "\n" + + +def _user( + minute: int, + text: str = "", + tool_result: dict | None = None, + *, + sidechain: bool = False, + extra: dict | None = None, +) -> dict: + entry: dict = { + "type": "user", + "timestamp": TS.format(minute), + "cwd": CWD, + "message": {"content": [{"type": "text", "text": text}] if text else []}, + } + if tool_result is not None: + entry["toolUseResult"] = tool_result + if sidechain: + entry["isSidechain"] = True + if extra: + entry.update(extra) + return entry + + +def _assistant(minute: int, content: list, model: str = "claude-sanitized") -> dict: + return { + "type": "assistant", + "timestamp": TS.format(minute), + "message": { + "model": model, + "content": content, + "usage": {"input_tokens": 10, "output_tokens": 5}, + }, + } + + +def write_minimal() -> None: + lines = [ + _user(0, "Sanitized minimal real-shaped session opener"), + _assistant(1, [{"type": "text", "text": "Acknowledged."}]), + _user(2, tool_result={"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}), + ] + path = os.path.join(FIXTURES, "real_session_minimal.jsonl") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.writelines(_line(x) for x in lines) + + +def write_all_tool_types() -> None: + tool_results = [ + {"stdout": "ok\n", "stderr": "", "exitCode": 0}, + {"filePath": f"{CWD}/a.py", "structuredPatch": "@@ sanitized"}, + {"filePath": f"{CWD}/b.txt", "content": "sanitized body"}, + {"filenames": ["sanitized.py"], "numFiles": 1, "truncated": False}, + {"mode": "content", "numFiles": 1, "numLines": 2, "content": "sanitized match"}, + { + "file": { + "filePath": f"{CWD}/readme.md", + "numLines": 3, + "content": "sanitized read", + } + }, + {"query": "sanitized query", "results": [{"url": "https://example.com/a"}]}, + {"url": "https://example.com/doc", "code": 200, "durationMs": 40}, + {"task_id": "task-sanitized-msg", "task_type": "sub"}, + { + "retrieval_status": "found", + "task": {"task_id": "task-sanitized-ret", "description": "REDACTED"}, + }, + { + "agentId": "agent-sanitized-done", + "totalDurationMs": 1000, + "status": "completed", + }, + { + "agentId": "agent-sanitized-async", + "isAsync": True, + "status": "running", + "description": "REDACTED background task", + }, + {"newTodos": [{"id": "1", "content": "sanitized todo"}]}, + {"questions": [{"id": "q1"}], "answers": {"q1": "sanitized answer"}}, + {"plan": [], "filePath": f"{CWD}/plan.md"}, + # Dispatch-order overlap: message key present — locks task_message winning over completed + { + "agentId": "agent-sanitized-overlap", + "totalDurationMs": 500, + "status": "completed", + "message": "status update sanitized", + }, + ] + lines = [ + _user(0, "Exercise all fifteen tool-result dispatch predicates"), + _assistant( + 1, + [{"type": "tool_use", "id": "tu-1", "name": "Bash", "input": {"command": "echo ok"}}], + ), + ] + for i, tr in enumerate(tool_results): + lines.append(_user(2 + i, tool_result=tr)) + path = os.path.join(FIXTURES, "real_session_all_tool_types.jsonl") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.writelines(_line(x) for x in lines) + + +def write_nested_tools() -> None: + lines = [ + _user(0, "Parent turn with nested subagent tool activity"), + _assistant( + 1, + [ + { + "type": "tool_use", + "id": "parent-tool-1", + "name": "Task", + "input": {"description": "sanitized subagent task"}, + } + ], + ), + _user( + 2, + tool_result={"stdout": "sidechain output\n", "stderr": "", "exitCode": 0}, + sidechain=True, + ), + { + "type": "progress", + "timestamp": TS.format(3), + "toolUseID": "child-tool-1", + "parentToolUseID": "parent-tool-1", + "isSidechain": True, + "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}, + }, + _assistant( + 4, + [ + { + "type": "tool_use", + "id": "nested-read-1", + "name": "Read", + "input": {"file_path": f"{CWD}/nested.txt"}, + } + ], + ), + ] + path = os.path.join(FIXTURES, "real_session_nested_tools.jsonl") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.writelines(_line(x) for x in lines) + + +def write_unknown_fields() -> None: + lines = [ + _user( + 0, + "Forward-compat entry with unknown top-level keys", + extra={"_futureSchemaVersion": 2, "experimentalFlag": True}, + ), + _assistant(1, [{"type": "text", "text": "Handled unknown fields."}]), + _user( + 2, + tool_result={ + "stdout": "still bash\n", + "stderr": "", + "exitCode": 0, + "_unknownToolMeta": {"vendor": "sanitized"}, + }, + ), + ] + path = os.path.join(FIXTURES, "real_session_unknown_fields.jsonl") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.writelines(_line(x) for x in lines) + + +def write_malformed_lines() -> None: + valid = [ + _user(0, "Valid line before malformed section"), + _assistant(1, [{"type": "text", "text": "Recovered after bad lines."}]), + ] + path = os.path.join(FIXTURES, "real_session_malformed_lines.jsonl") + with open(path, "w", encoding="utf-8", newline="\n") as f: + for obj in valid: + f.write(_line(obj)) + f.write("\n") + f.write("{not valid json\n") + f.write('{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "message": {"content": ') + f.write("\n") + f.write( + _line( + _user( + 3, + "Valid line after malformed section", + tool_result={"stderr": "warn only", "exitCode": 1}, + ) + ) + ) + + +def main() -> None: + os.makedirs(FIXTURES, exist_ok=True) + write_minimal() + write_all_tool_types() + write_nested_tools() + write_unknown_fields() + write_malformed_lines() + print("Wrote 5 fixtures to", FIXTURES) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/real_session_all_tool_types.jsonl b/tests/fixtures/real_session_all_tool_types.jsonl new file mode 100644 index 0000000..fb42eeb --- /dev/null +++ b/tests/fixtures/real_session_all_tool_types.jsonl @@ -0,0 +1,18 @@ +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Exercise all fifteen tool-result dispatch predicates"}]}} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "tu-1", "name": "Bash", "input": {"command": "echo ok"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "ok\n", "stderr": "", "exitCode": 0}} +{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/a.py", "structuredPatch": "@@ sanitized"}} +{"type": "user", "timestamp": "2026-05-26T10:04:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/b.txt", "content": "sanitized body"}} +{"type": "user", "timestamp": "2026-05-26T10:05:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filenames": ["sanitized.py"], "numFiles": 1, "truncated": false}} +{"type": "user", "timestamp": "2026-05-26T10:06:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"mode": "content", "numFiles": 1, "numLines": 2, "content": "sanitized match"}} +{"type": "user", "timestamp": "2026-05-26T10:07:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"file": {"filePath": "/sanitized/project/path/readme.md", "numLines": 3, "content": "sanitized read"}}} +{"type": "user", "timestamp": "2026-05-26T10:08:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"query": "sanitized query", "results": [{"url": "https://example.com/a"}]}} +{"type": "user", "timestamp": "2026-05-26T10:09:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"url": "https://example.com/doc", "code": 200, "durationMs": 40}} +{"type": "user", "timestamp": "2026-05-26T10:10:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"task_id": "task-sanitized-msg", "task_type": "sub"}} +{"type": "user", "timestamp": "2026-05-26T10:11:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"retrieval_status": "found", "task": {"task_id": "task-sanitized-ret", "description": "REDACTED"}}} +{"type": "user", "timestamp": "2026-05-26T10:12:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-done", "totalDurationMs": 1000, "status": "completed"}} +{"type": "user", "timestamp": "2026-05-26T10:13:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-async", "isAsync": true, "status": "running", "description": "REDACTED background task"}} +{"type": "user", "timestamp": "2026-05-26T10:14:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"newTodos": [{"id": "1", "content": "sanitized todo"}]}} +{"type": "user", "timestamp": "2026-05-26T10:15:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"questions": [{"id": "q1"}], "answers": {"q1": "sanitized answer"}}} +{"type": "user", "timestamp": "2026-05-26T10:16:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"plan": [], "filePath": "/sanitized/project/path/plan.md"}} +{"type": "user", "timestamp": "2026-05-26T10:17:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-overlap", "totalDurationMs": 500, "status": "completed", "message": "status update sanitized"}} diff --git a/tests/fixtures/real_session_malformed_lines.jsonl b/tests/fixtures/real_session_malformed_lines.jsonl new file mode 100644 index 0000000..a52c5dd --- /dev/null +++ b/tests/fixtures/real_session_malformed_lines.jsonl @@ -0,0 +1,6 @@ +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line before malformed section"}]}} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Recovered after bad lines."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} + +{not valid json +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "message": {"content": +{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line after malformed section"}]}, "toolUseResult": {"stderr": "warn only", "exitCode": 1}} diff --git a/tests/fixtures/real_session_minimal.jsonl b/tests/fixtures/real_session_minimal.jsonl new file mode 100644 index 0000000..49a8d3a --- /dev/null +++ b/tests/fixtures/real_session_minimal.jsonl @@ -0,0 +1,3 @@ +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Sanitized minimal real-shaped session opener"}]}} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Acknowledged."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}} diff --git a/tests/fixtures/real_session_nested_tools.jsonl b/tests/fixtures/real_session_nested_tools.jsonl new file mode 100644 index 0000000..84342c6 --- /dev/null +++ b/tests/fixtures/real_session_nested_tools.jsonl @@ -0,0 +1,5 @@ +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Parent turn with nested subagent tool activity"}]}} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "parent-tool-1", "name": "Task", "input": {"description": "sanitized subagent task"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sidechain output\n", "stderr": "", "exitCode": 0}, "isSidechain": true} +{"type": "progress", "timestamp": "2026-05-26T10:03:00Z", "toolUseID": "child-tool-1", "parentToolUseID": "parent-tool-1", "isSidechain": true, "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}} +{"type": "assistant", "timestamp": "2026-05-26T10:04:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "nested-read-1", "name": "Read", "input": {"file_path": "/sanitized/project/path/nested.txt"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} diff --git a/tests/fixtures/real_session_unknown_fields.jsonl b/tests/fixtures/real_session_unknown_fields.jsonl new file mode 100644 index 0000000..f244803 --- /dev/null +++ b/tests/fixtures/real_session_unknown_fields.jsonl @@ -0,0 +1,3 @@ +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Forward-compat entry with unknown top-level keys"}]}, "_futureSchemaVersion": 2, "experimentalFlag": true} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Handled unknown fields."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "still bash\n", "stderr": "", "exitCode": 0, "_unknownToolMeta": {"vendor": "sanitized"}}} diff --git a/tests/test_real_session_fixtures.py b/tests/test_real_session_fixtures.py new file mode 100644 index 0000000..2c1dfc1 --- /dev/null +++ b/tests/test_real_session_fixtures.py @@ -0,0 +1,234 @@ +"""Tuesday real-session fixtures: production-shaped JSONL + dispatch-order regression.""" + +from __future__ import annotations + +import json +import os + +import pytest + +from utils.jsonl_parser import ( + _TOOL_RESULT_DISPATCH, + _parse_tool_result, + parse_session, +) + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures") + + +def _fixture_path(name: str) -> str: + return os.path.join(FIXTURES_DIR, name) + + +def _assert_session_shape(session: dict) -> None: + assert session["session_id"] + assert session["title"] + assert isinstance(session["messages"], list) + assert isinstance(session["metadata"], dict) + assert session["metadata"]["session_id"] == session["session_id"] + + +# Golden message counts recorded when fixtures were authored (gen_real_session_fixtures.py). +_FIXTURE_MESSAGE_COUNTS = { + "real_session_minimal.jsonl": 3, + "real_session_all_tool_types.jsonl": 18, + "real_session_nested_tools.jsonl": 5, + "real_session_unknown_fields.jsonl": 3, + "real_session_malformed_lines.jsonl": 3, +} + + +@pytest.mark.parametrize( + "fixture_name,expected_count", + list(_FIXTURE_MESSAGE_COUNTS.items()), + ids=[n.replace(".jsonl", "") for n in _FIXTURE_MESSAGE_COUNTS], +) +def test_real_fixture_parses_with_expected_message_count( + fixture_name: str, expected_count: int +) -> None: + session = parse_session(_fixture_path(fixture_name)) + _assert_session_shape(session) + assert len(session["messages"]) == expected_count + + +def test_real_session_minimal_has_bash_tool_result() -> None: + session = parse_session(_fixture_path("real_session_minimal.jsonl")) + parsed_types = [ + m["tool_result_parsed"]["result_type"] + for m in session["messages"] + if m.get("tool_result_parsed") + ] + assert "bash" in parsed_types + assert len(session["messages"]) >= 2 + + +def test_real_session_all_tool_types_covers_dispatch_predicates() -> None: + hit: set[int] = set() + path = _fixture_path("real_session_all_tool_types.jsonl") + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + entry = json.loads(line) + tr = entry.get("toolUseResult") + if not isinstance(tr, dict): + continue + for i, (pred, _) in enumerate(_TOOL_RESULT_DISPATCH): + if pred(tr): + hit.add(i) + break + assert len(hit) == len(_TOOL_RESULT_DISPATCH) + + +def test_real_session_nested_tools_has_sidechain_and_tool_use() -> None: + session = parse_session(_fixture_path("real_session_nested_tools.jsonl")) + assert session["metadata"]["sidechain_messages"] >= 1 + assert session["metadata"]["total_tool_calls"] >= 1 + tool_use_msgs = [m for m in session["messages"] if m.get("tool_uses")] + assert len(tool_use_msgs) >= 1 + + +def test_real_session_unknown_fields_tolerated() -> None: + session = parse_session(_fixture_path("real_session_unknown_fields.jsonl")) + _assert_session_shape(session) + assert len(session["messages"]) == _FIXTURE_MESSAGE_COUNTS[ + "real_session_unknown_fields.jsonl" + ] + + +def test_real_session_malformed_lines_skips_bad_lines() -> None: + """Matches parse_session contract: skip invalid JSON / blank lines, keep valid rows.""" + session = parse_session(_fixture_path("real_session_malformed_lines.jsonl")) + texts = [m.get("text") or "" for m in session["messages"] if m["role"] == "user"] + assert any("before malformed" in t for t in texts) + assert any("after malformed" in t for t in texts) + assert len(session["messages"]) == _FIXTURE_MESSAGE_COUNTS[ + "real_session_malformed_lines.jsonl" + ] + + +def test_task_retrieval_not_misclassified_as_task_message() -> None: + tr = { + "retrieval_status": "found", + "task": {"task_id": "task-123", "description": "sanitized"}, + } + result = _parse_tool_result(tr) + assert result is not None + assert result["result_type"] == "task" + assert result.get("retrieval_status") == "found" + assert "retrieval_status" in tr + + +def test_task_completed_with_message_key_matches_task_message_first() -> None: + """Legacy dispatch: broad task_message runs before task_completed when ``message`` present.""" + tr = { + "agentId": "agent-sanitized", + "totalDurationMs": 1000, + "status": "completed", + "message": "status update", + } + result = _parse_tool_result(tr) + assert result is not None + assert result["result_type"] == "task" + assert result.get("task_id") is None + assert result.get("agent_id") is None + + +def test_overlap_blob_from_all_tool_types_fixture_locks_task_message_order() -> None: + tr = { + "agentId": "agent-sanitized-overlap", + "totalDurationMs": 500, + "status": "completed", + "message": "status update sanitized", + } + result = _parse_tool_result(tr) + assert result is not None + assert result["result_type"] == "task" + assert result.get("agent_id") is None + + +@pytest.mark.parametrize( + "tool_result,expected_type,expected_key", + [ + ({"stdout": "x", "stderr": "", "exitCode": 0}, "bash", "stdout"), + ({"filePath": "/sanitized/a.py", "structuredPatch": "@@"}, "file_edit", "file_path"), + ({"filePath": "/sanitized/b.txt", "content": "hi"}, "file_write", "file_path"), + ( + {"filenames": ["x.py"], "numFiles": 1, "truncated": False}, + "glob", + "filenames", + ), + ( + {"mode": "content", "numFiles": 1, "numLines": 1, "content": "m"}, + "grep", + "mode", + ), + ( + { + "file": { + "filePath": "/sanitized/r.md", + "numLines": 1, + "content": "c", + } + }, + "file_read", + "file_path", + ), + ( + {"query": "q", "results": []}, + "web_search", + "query", + ), + ({"url": "https://example.com", "code": 200}, "web_fetch", "url"), + ({"task_id": "t1", "task_type": "sub"}, "task", "task_id"), + ( + {"retrieval_status": "ok", "task": {"task_id": "tid"}}, + "task", + "retrieval_status", + ), + ( + {"agentId": "ag", "totalDurationMs": 1, "status": "done"}, + "task", + "agent_id", + ), + ( + {"agentId": "ag2", "isAsync": True, "status": "running"}, + "task", + "agent_id", + ), + ({"newTodos": [{"id": "1", "content": "c"}]}, "todo_write", "todo_count"), + ( + {"questions": [{"id": "q"}], "answers": {"q": "a"}}, + "user_input", + "questions", + ), + ({"plan": [], "filePath": "/sanitized/plan.md"}, "plan", "file_path"), + ], + ids=[ + "bash", + "file_edit", + "file_write", + "glob", + "grep", + "file_read", + "web_search", + "web_fetch", + "task_message", + "task_retrieval", + "task_completed", + "task_async", + "todo_write", + "user_input", + "plan", + ], +) +def test_dispatch_predicate_coverage( + tool_result: dict, + expected_type: str, + expected_key: str, +) -> None: + result = _parse_tool_result(tool_result) + assert result is not None + assert result["result_type"] == expected_type + assert expected_key in result From c877dae058f87bcc4c16e924a955dc147bc10c75 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 27 May 2026 04:03:26 +0800 Subject: [PATCH 2/5] test: tighten real-session fixture tests per CodeRabbit review --- scripts/gen_real_session_fixtures.py | 103 +++++++++++++----- .../real_session_all_tool_types.jsonl | 36 +++--- .../real_session_malformed_lines.jsonl | 6 +- tests/fixtures/real_session_minimal.jsonl | 6 +- .../fixtures/real_session_nested_tools.jsonl | 10 +- .../real_session_unknown_fields.jsonl | 6 +- tests/test_real_session_fixtures.py | 27 ++++- 7 files changed, 129 insertions(+), 65 deletions(-) diff --git a/scripts/gen_real_session_fixtures.py b/scripts/gen_real_session_fixtures.py index 78fe4e3..0bdbe51 100644 --- a/scripts/gen_real_session_fixtures.py +++ b/scripts/gen_real_session_fixtures.py @@ -1,5 +1,8 @@ """One-off generator for Tuesday real_session_*.jsonl fixtures. Run from repo root: python scripts/gen_real_session_fixtures.py + +Each entry includes top-level ``sessionId`` (real Claude Code JSONL shape). The parser +currently ignores it and uses the filename for ``session_id``. """ from __future__ import annotations @@ -10,16 +13,31 @@ CWD = "/sanitized/project/path" TS = "2026-05-26T10:{:02d}:00Z" +# Sanitized sessionId values (one per fixture file). +SESSION_IDS = { + "minimal": "session-sanitized-minimal", + "all_tool_types": "session-sanitized-all-tool-types", + "nested_tools": "session-sanitized-nested-tools", + "unknown_fields": "session-sanitized-unknown-fields", + "malformed_lines": "session-sanitized-malformed-lines", +} + def _line(obj: dict) -> str: return json.dumps(obj, ensure_ascii=False) + "\n" +def _stamp(entry: dict, session_id: str) -> dict: + entry["sessionId"] = session_id + return entry + + def _user( minute: int, text: str = "", tool_result: dict | None = None, *, + session_id: str, sidechain: bool = False, extra: dict | None = None, ) -> dict: @@ -35,26 +53,40 @@ def _user( entry["isSidechain"] = True if extra: entry.update(extra) - return entry + return _stamp(entry, session_id) -def _assistant(minute: int, content: list, model: str = "claude-sanitized") -> dict: - return { - "type": "assistant", - "timestamp": TS.format(minute), - "message": { - "model": model, - "content": content, - "usage": {"input_tokens": 10, "output_tokens": 5}, +def _assistant( + minute: int, + content: list, + *, + session_id: str, + model: str = "claude-sanitized", +) -> dict: + return _stamp( + { + "type": "assistant", + "timestamp": TS.format(minute), + "message": { + "model": model, + "content": content, + "usage": {"input_tokens": 10, "output_tokens": 5}, + }, }, - } + session_id, + ) def write_minimal() -> None: + sid = SESSION_IDS["minimal"] lines = [ - _user(0, "Sanitized minimal real-shaped session opener"), - _assistant(1, [{"type": "text", "text": "Acknowledged."}]), - _user(2, tool_result={"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}), + _user(0, "Sanitized minimal real-shaped session opener", session_id=sid), + _assistant(1, [{"type": "text", "text": "Acknowledged."}], session_id=sid), + _user( + 2, + tool_result={"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}, + session_id=sid, + ), ] path = os.path.join(FIXTURES, "real_session_minimal.jsonl") with open(path, "w", encoding="utf-8", newline="\n") as f: @@ -62,6 +94,10 @@ def write_minimal() -> None: def write_all_tool_types() -> None: + # 16 tool-result user entries: 15 to cover each dispatch predicate once, plus 1 + # overlap blob (agentId + totalDurationMs + message) for task_message-beats- + # task_completed dispatch-order regression. Total lines: 2 opener + 16 = 18 messages. + sid = SESSION_IDS["all_tool_types"] tool_results = [ {"stdout": "ok\n", "stderr": "", "exitCode": 0}, {"filePath": f"{CWD}/a.py", "structuredPatch": "@@ sanitized"}, @@ -105,22 +141,24 @@ def write_all_tool_types() -> None: }, ] lines = [ - _user(0, "Exercise all fifteen tool-result dispatch predicates"), + _user(0, "Exercise all fifteen tool-result dispatch predicates", session_id=sid), _assistant( 1, [{"type": "tool_use", "id": "tu-1", "name": "Bash", "input": {"command": "echo ok"}}], + session_id=sid, ), ] for i, tr in enumerate(tool_results): - lines.append(_user(2 + i, tool_result=tr)) + lines.append(_user(2 + i, tool_result=tr, session_id=sid)) path = os.path.join(FIXTURES, "real_session_all_tool_types.jsonl") with open(path, "w", encoding="utf-8", newline="\n") as f: f.writelines(_line(x) for x in lines) def write_nested_tools() -> None: + sid = SESSION_IDS["nested_tools"] lines = [ - _user(0, "Parent turn with nested subagent tool activity"), + _user(0, "Parent turn with nested subagent tool activity", session_id=sid), _assistant( 1, [ @@ -131,20 +169,25 @@ def write_nested_tools() -> None: "input": {"description": "sanitized subagent task"}, } ], + session_id=sid, ), _user( 2, tool_result={"stdout": "sidechain output\n", "stderr": "", "exitCode": 0}, + session_id=sid, sidechain=True, ), - { - "type": "progress", - "timestamp": TS.format(3), - "toolUseID": "child-tool-1", - "parentToolUseID": "parent-tool-1", - "isSidechain": True, - "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}, - }, + _stamp( + { + "type": "progress", + "timestamp": TS.format(3), + "toolUseID": "child-tool-1", + "parentToolUseID": "parent-tool-1", + "isSidechain": True, + "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}, + }, + sid, + ), _assistant( 4, [ @@ -155,6 +198,7 @@ def write_nested_tools() -> None: "input": {"file_path": f"{CWD}/nested.txt"}, } ], + session_id=sid, ), ] path = os.path.join(FIXTURES, "real_session_nested_tools.jsonl") @@ -163,13 +207,15 @@ def write_nested_tools() -> None: def write_unknown_fields() -> None: + sid = SESSION_IDS["unknown_fields"] lines = [ _user( 0, "Forward-compat entry with unknown top-level keys", + session_id=sid, extra={"_futureSchemaVersion": 2, "experimentalFlag": True}, ), - _assistant(1, [{"type": "text", "text": "Handled unknown fields."}]), + _assistant(1, [{"type": "text", "text": "Handled unknown fields."}], session_id=sid), _user( 2, tool_result={ @@ -178,6 +224,7 @@ def write_unknown_fields() -> None: "exitCode": 0, "_unknownToolMeta": {"vendor": "sanitized"}, }, + session_id=sid, ), ] path = os.path.join(FIXTURES, "real_session_unknown_fields.jsonl") @@ -186,9 +233,10 @@ def write_unknown_fields() -> None: def write_malformed_lines() -> None: + sid = SESSION_IDS["malformed_lines"] valid = [ - _user(0, "Valid line before malformed section"), - _assistant(1, [{"type": "text", "text": "Recovered after bad lines."}]), + _user(0, "Valid line before malformed section", session_id=sid), + _assistant(1, [{"type": "text", "text": "Recovered after bad lines."}], session_id=sid), ] path = os.path.join(FIXTURES, "real_session_malformed_lines.jsonl") with open(path, "w", encoding="utf-8", newline="\n") as f: @@ -204,6 +252,7 @@ def write_malformed_lines() -> None: 3, "Valid line after malformed section", tool_result={"stderr": "warn only", "exitCode": 1}, + session_id=sid, ) ) ) diff --git a/tests/fixtures/real_session_all_tool_types.jsonl b/tests/fixtures/real_session_all_tool_types.jsonl index fb42eeb..a5b03d0 100644 --- a/tests/fixtures/real_session_all_tool_types.jsonl +++ b/tests/fixtures/real_session_all_tool_types.jsonl @@ -1,18 +1,18 @@ -{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Exercise all fifteen tool-result dispatch predicates"}]}} -{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "tu-1", "name": "Bash", "input": {"command": "echo ok"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} -{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "ok\n", "stderr": "", "exitCode": 0}} -{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/a.py", "structuredPatch": "@@ sanitized"}} -{"type": "user", "timestamp": "2026-05-26T10:04:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/b.txt", "content": "sanitized body"}} -{"type": "user", "timestamp": "2026-05-26T10:05:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filenames": ["sanitized.py"], "numFiles": 1, "truncated": false}} -{"type": "user", "timestamp": "2026-05-26T10:06:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"mode": "content", "numFiles": 1, "numLines": 2, "content": "sanitized match"}} -{"type": "user", "timestamp": "2026-05-26T10:07:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"file": {"filePath": "/sanitized/project/path/readme.md", "numLines": 3, "content": "sanitized read"}}} -{"type": "user", "timestamp": "2026-05-26T10:08:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"query": "sanitized query", "results": [{"url": "https://example.com/a"}]}} -{"type": "user", "timestamp": "2026-05-26T10:09:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"url": "https://example.com/doc", "code": 200, "durationMs": 40}} -{"type": "user", "timestamp": "2026-05-26T10:10:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"task_id": "task-sanitized-msg", "task_type": "sub"}} -{"type": "user", "timestamp": "2026-05-26T10:11:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"retrieval_status": "found", "task": {"task_id": "task-sanitized-ret", "description": "REDACTED"}}} -{"type": "user", "timestamp": "2026-05-26T10:12:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-done", "totalDurationMs": 1000, "status": "completed"}} -{"type": "user", "timestamp": "2026-05-26T10:13:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-async", "isAsync": true, "status": "running", "description": "REDACTED background task"}} -{"type": "user", "timestamp": "2026-05-26T10:14:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"newTodos": [{"id": "1", "content": "sanitized todo"}]}} -{"type": "user", "timestamp": "2026-05-26T10:15:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"questions": [{"id": "q1"}], "answers": {"q1": "sanitized answer"}}} -{"type": "user", "timestamp": "2026-05-26T10:16:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"plan": [], "filePath": "/sanitized/project/path/plan.md"}} -{"type": "user", "timestamp": "2026-05-26T10:17:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-overlap", "totalDurationMs": 500, "status": "completed", "message": "status update sanitized"}} +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Exercise all fifteen tool-result dispatch predicates"}]}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "tu-1", "name": "Bash", "input": {"command": "echo ok"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "ok\n", "stderr": "", "exitCode": 0}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/a.py", "structuredPatch": "@@ sanitized"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:04:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filePath": "/sanitized/project/path/b.txt", "content": "sanitized body"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:05:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"filenames": ["sanitized.py"], "numFiles": 1, "truncated": false}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:06:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"mode": "content", "numFiles": 1, "numLines": 2, "content": "sanitized match"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:07:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"file": {"filePath": "/sanitized/project/path/readme.md", "numLines": 3, "content": "sanitized read"}}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:08:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"query": "sanitized query", "results": [{"url": "https://example.com/a"}]}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:09:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"url": "https://example.com/doc", "code": 200, "durationMs": 40}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:10:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"task_id": "task-sanitized-msg", "task_type": "sub"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:11:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"retrieval_status": "found", "task": {"task_id": "task-sanitized-ret", "description": "REDACTED"}}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:12:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-done", "totalDurationMs": 1000, "status": "completed"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:13:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-async", "isAsync": true, "status": "running", "description": "REDACTED background task"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:14:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"newTodos": [{"id": "1", "content": "sanitized todo"}]}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:15:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"questions": [{"id": "q1"}], "answers": {"q1": "sanitized answer"}}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:16:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"plan": [], "filePath": "/sanitized/project/path/plan.md"}, "sessionId": "session-sanitized-all-tool-types"} +{"type": "user", "timestamp": "2026-05-26T10:17:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"agentId": "agent-sanitized-overlap", "totalDurationMs": 500, "status": "completed", "message": "status update sanitized"}, "sessionId": "session-sanitized-all-tool-types"} diff --git a/tests/fixtures/real_session_malformed_lines.jsonl b/tests/fixtures/real_session_malformed_lines.jsonl index a52c5dd..c3eda7a 100644 --- a/tests/fixtures/real_session_malformed_lines.jsonl +++ b/tests/fixtures/real_session_malformed_lines.jsonl @@ -1,6 +1,6 @@ -{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line before malformed section"}]}} -{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Recovered after bad lines."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line before malformed section"}]}, "sessionId": "session-sanitized-malformed-lines"} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Recovered after bad lines."}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-malformed-lines"} {not valid json {"type": "user", "timestamp": "2026-05-26T10:02:00Z", "message": {"content": -{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line after malformed section"}]}, "toolUseResult": {"stderr": "warn only", "exitCode": 1}} +{"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line after malformed section"}]}, "toolUseResult": {"stderr": "warn only", "exitCode": 1}, "sessionId": "session-sanitized-malformed-lines"} diff --git a/tests/fixtures/real_session_minimal.jsonl b/tests/fixtures/real_session_minimal.jsonl index 49a8d3a..ad62a13 100644 --- a/tests/fixtures/real_session_minimal.jsonl +++ b/tests/fixtures/real_session_minimal.jsonl @@ -1,3 +1,3 @@ -{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Sanitized minimal real-shaped session opener"}]}} -{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Acknowledged."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} -{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}} +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Sanitized minimal real-shaped session opener"}]}, "sessionId": "session-sanitized-minimal"} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Acknowledged."}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-minimal"} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sanitized output\n", "stderr": "", "exitCode": 0}, "sessionId": "session-sanitized-minimal"} diff --git a/tests/fixtures/real_session_nested_tools.jsonl b/tests/fixtures/real_session_nested_tools.jsonl index 84342c6..3c9d9b7 100644 --- a/tests/fixtures/real_session_nested_tools.jsonl +++ b/tests/fixtures/real_session_nested_tools.jsonl @@ -1,5 +1,5 @@ -{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Parent turn with nested subagent tool activity"}]}} -{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "parent-tool-1", "name": "Task", "input": {"description": "sanitized subagent task"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} -{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sidechain output\n", "stderr": "", "exitCode": 0}, "isSidechain": true} -{"type": "progress", "timestamp": "2026-05-26T10:03:00Z", "toolUseID": "child-tool-1", "parentToolUseID": "parent-tool-1", "isSidechain": true, "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}} -{"type": "assistant", "timestamp": "2026-05-26T10:04:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "nested-read-1", "name": "Read", "input": {"file_path": "/sanitized/project/path/nested.txt"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}} +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Parent turn with nested subagent tool activity"}]}, "sessionId": "session-sanitized-nested-tools"} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "parent-tool-1", "name": "Task", "input": {"description": "sanitized subagent task"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-nested-tools"} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "sidechain output\n", "stderr": "", "exitCode": 0}, "isSidechain": true, "sessionId": "session-sanitized-nested-tools"} +{"type": "progress", "timestamp": "2026-05-26T10:03:00Z", "toolUseID": "child-tool-1", "parentToolUseID": "parent-tool-1", "isSidechain": true, "data": {"type": "bash_progress", "output": "sanitized streaming chunk"}, "sessionId": "session-sanitized-nested-tools"} +{"type": "assistant", "timestamp": "2026-05-26T10:04:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "tool_use", "id": "nested-read-1", "name": "Read", "input": {"file_path": "/sanitized/project/path/nested.txt"}}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-nested-tools"} diff --git a/tests/fixtures/real_session_unknown_fields.jsonl b/tests/fixtures/real_session_unknown_fields.jsonl index f244803..01ff5dd 100644 --- a/tests/fixtures/real_session_unknown_fields.jsonl +++ b/tests/fixtures/real_session_unknown_fields.jsonl @@ -1,3 +1,3 @@ -{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Forward-compat entry with unknown top-level keys"}]}, "_futureSchemaVersion": 2, "experimentalFlag": true} -{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Handled unknown fields."}], "usage": {"input_tokens": 10, "output_tokens": 5}}} -{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "still bash\n", "stderr": "", "exitCode": 0, "_unknownToolMeta": {"vendor": "sanitized"}}} +{"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Forward-compat entry with unknown top-level keys"}]}, "_futureSchemaVersion": 2, "experimentalFlag": true, "sessionId": "session-sanitized-unknown-fields"} +{"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Handled unknown fields."}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-unknown-fields"} +{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "cwd": "/sanitized/project/path", "message": {"content": []}, "toolUseResult": {"stdout": "still bash\n", "stderr": "", "exitCode": 0, "_unknownToolMeta": {"vendor": "sanitized"}}, "sessionId": "session-sanitized-unknown-fields"} diff --git a/tests/test_real_session_fixtures.py b/tests/test_real_session_fixtures.py index 2c1dfc1..b0d059c 100644 --- a/tests/test_real_session_fixtures.py +++ b/tests/test_real_session_fixtures.py @@ -1,4 +1,9 @@ -"""Tuesday real-session fixtures: production-shaped JSONL + dispatch-order regression.""" +"""Tuesday real-session fixtures: production-shaped JSONL + dispatch-order regression. + +Fixtures include top-level ``sessionId`` on each entry (as in real Claude Code JSONL). +``parse_session()`` still derives ``session_id`` from the filename; ``sessionId`` is +retained for schema fidelity and to catch accidental parser coupling to that field. +""" from __future__ import annotations @@ -21,8 +26,10 @@ def _fixture_path(name: str) -> str: def _assert_session_shape(session: dict) -> None: - assert session["session_id"] - assert session["title"] + assert isinstance(session["session_id"], str) and session["session_id"] + assert session["title"] not in ("", "Untitled Session"), ( + "Expected a real title from the fixture's first user message" + ) assert isinstance(session["messages"], list) assert isinstance(session["metadata"], dict) assert session["metadata"]["session_id"] == session["session_id"] @@ -59,7 +66,6 @@ def test_real_session_minimal_has_bash_tool_result() -> None: if m.get("tool_result_parsed") ] assert "bash" in parsed_types - assert len(session["messages"]) >= 2 def test_real_session_all_tool_types_covers_dispatch_predicates() -> None: @@ -74,11 +80,14 @@ def test_real_session_all_tool_types_covers_dispatch_predicates() -> None: tr = entry.get("toolUseResult") if not isinstance(tr, dict): continue + matched = False for i, (pred, _) in enumerate(_TOOL_RESULT_DISPATCH): if pred(tr): hit.add(i) + matched = True break - assert len(hit) == len(_TOOL_RESULT_DISPATCH) + assert matched, f"toolUseResult matched no predicate: {list(tr.keys())}" + assert hit == set(range(len(_TOOL_RESULT_DISPATCH))) def test_real_session_nested_tools_has_sidechain_and_tool_use() -> None: @@ -121,7 +130,13 @@ def test_task_retrieval_not_misclassified_as_task_message() -> None: def test_task_completed_with_message_key_matches_task_message_first() -> None: - """Legacy dispatch: broad task_message runs before task_completed when ``message`` present.""" + """Legacy dispatch: broad task_message runs before task_completed when ``message`` present. + + ``_tool_result_pred_task_message`` matches any dict with a ``message`` or ``task_id`` + key. Future tool shapes that add ``message`` for status text (e.g. web-fetch) would + be misclassified as task until dispatch order is refined — this test locks that + known false-positive surface. + """ tr = { "agentId": "agent-sanitized", "totalDurationMs": 1000, From f9f0d7fa851739b4eabb13cda392042662a815be Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 27 May 2026 04:11:52 +0800 Subject: [PATCH 3/5] test: assert session title is a non-empty str in fixture shape check --- tests/test_real_session_fixtures.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_real_session_fixtures.py b/tests/test_real_session_fixtures.py index b0d059c..b238b8d 100644 --- a/tests/test_real_session_fixtures.py +++ b/tests/test_real_session_fixtures.py @@ -27,9 +27,10 @@ def _fixture_path(name: str) -> str: def _assert_session_shape(session: dict) -> None: assert isinstance(session["session_id"], str) and session["session_id"] - assert session["title"] not in ("", "Untitled Session"), ( - "Expected a real title from the fixture's first user message" - ) + assert isinstance(session["title"], str) and session["title"] not in ( + "", + "Untitled Session", + ), "Expected a real title from the fixture's first user message" assert isinstance(session["messages"], list) assert isinstance(session["metadata"], dict) assert session["metadata"]["session_id"] == session["session_id"] From 9af802a4bfe6d36e5cfcd0327f9f30b9f2d0d7d0 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 27 May 2026 04:52:22 +0800 Subject: [PATCH 4/5] test: load overlap blob from fixture and tighten minimal bash asserts Read the task_message overlap toolUseResult from real_session_all_tool_types instead of duplicating an inline dict. Pin minimal fixture bash stdout, exit code, and message index. Document JSONDecodeError skip path in malformed_lines. --- scripts/gen_real_session_fixtures.py | 4 ++ .../real_session_malformed_lines.jsonl | 1 + tests/test_real_session_fixtures.py | 40 +++++++++++++------ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/scripts/gen_real_session_fixtures.py b/scripts/gen_real_session_fixtures.py index 0bdbe51..7dcd7bc 100644 --- a/scripts/gen_real_session_fixtures.py +++ b/scripts/gen_real_session_fixtures.py @@ -243,6 +243,10 @@ def write_malformed_lines() -> None: for obj in valid: f.write(_line(obj)) f.write("\n") + f.write( + "# parse_session skips lines where json.loads raises JSONDecodeError " + "(blank, garbage, truncated JSON below).\n" + ) f.write("{not valid json\n") f.write('{"type": "user", "timestamp": "2026-05-26T10:02:00Z", "message": {"content": ') f.write("\n") diff --git a/tests/fixtures/real_session_malformed_lines.jsonl b/tests/fixtures/real_session_malformed_lines.jsonl index c3eda7a..93eb929 100644 --- a/tests/fixtures/real_session_malformed_lines.jsonl +++ b/tests/fixtures/real_session_malformed_lines.jsonl @@ -1,6 +1,7 @@ {"type": "user", "timestamp": "2026-05-26T10:00:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line before malformed section"}]}, "sessionId": "session-sanitized-malformed-lines"} {"type": "assistant", "timestamp": "2026-05-26T10:01:00Z", "message": {"model": "claude-sanitized", "content": [{"type": "text", "text": "Recovered after bad lines."}], "usage": {"input_tokens": 10, "output_tokens": 5}}, "sessionId": "session-sanitized-malformed-lines"} +# parse_session skips lines where json.loads raises JSONDecodeError (blank, garbage, truncated JSON below). {not valid json {"type": "user", "timestamp": "2026-05-26T10:02:00Z", "message": {"content": {"type": "user", "timestamp": "2026-05-26T10:03:00Z", "cwd": "/sanitized/project/path", "message": {"content": [{"type": "text", "text": "Valid line after malformed section"}]}, "toolUseResult": {"stderr": "warn only", "exitCode": 1}, "sessionId": "session-sanitized-malformed-lines"} diff --git a/tests/test_real_session_fixtures.py b/tests/test_real_session_fixtures.py index b238b8d..7c67ffd 100644 --- a/tests/test_real_session_fixtures.py +++ b/tests/test_real_session_fixtures.py @@ -25,6 +25,27 @@ def _fixture_path(name: str) -> str: return os.path.join(FIXTURES_DIR, name) +def _overlap_tool_result_from_all_tool_types_fixture() -> dict: + """Last toolUseResult in real_session_all_tool_types (task_message overlap blob).""" + path = _fixture_path("real_session_all_tool_types.jsonl") + overlap: dict | None = None + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + entry = json.loads(line) + tr = entry.get("toolUseResult") + if isinstance(tr, dict) and tr.get("agentId") == "agent-sanitized-overlap": + overlap = tr + if overlap is None: + pytest.fail( + "overlap toolUseResult (agent-sanitized-overlap) missing from " + "real_session_all_tool_types.jsonl" + ) + return overlap + + def _assert_session_shape(session: dict) -> None: assert isinstance(session["session_id"], str) and session["session_id"] assert isinstance(session["title"], str) and session["title"] not in ( @@ -61,12 +82,12 @@ def test_real_fixture_parses_with_expected_message_count( def test_real_session_minimal_has_bash_tool_result() -> None: session = parse_session(_fixture_path("real_session_minimal.jsonl")) - parsed_types = [ - m["tool_result_parsed"]["result_type"] - for m in session["messages"] - if m.get("tool_result_parsed") - ] - assert "bash" in parsed_types + assert len(session["messages"]) == _FIXTURE_MESSAGE_COUNTS["real_session_minimal.jsonl"] + parsed = session["messages"][2]["tool_result_parsed"] + assert parsed is not None + assert parsed["result_type"] == "bash" + assert parsed["stdout"] == "sanitized output\n" + assert parsed["exit_code"] == 0 def test_real_session_all_tool_types_covers_dispatch_predicates() -> None: @@ -152,12 +173,7 @@ def test_task_completed_with_message_key_matches_task_message_first() -> None: def test_overlap_blob_from_all_tool_types_fixture_locks_task_message_order() -> None: - tr = { - "agentId": "agent-sanitized-overlap", - "totalDurationMs": 500, - "status": "completed", - "message": "status update sanitized", - } + tr = _overlap_tool_result_from_all_tool_types_fixture() result = _parse_tool_result(tr) assert result is not None assert result["result_type"] == "task" From b4ac2e5b6620ef7e5c662149069e1c46dc67e657 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 27 May 2026 05:03:50 +0800 Subject: [PATCH 5/5] test: narrow overlap fixture helper return type for mypy --- tests/test_real_session_fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_real_session_fixtures.py b/tests/test_real_session_fixtures.py index 7c67ffd..f6da553 100644 --- a/tests/test_real_session_fixtures.py +++ b/tests/test_real_session_fixtures.py @@ -43,6 +43,7 @@ def _overlap_tool_result_from_all_tool_types_fixture() -> dict: "overlap toolUseResult (agent-sanitized-overlap) missing from " "real_session_all_tool_types.jsonl" ) + assert overlap is not None # narrow for mypy (pytest.fail is not NoReturn) return overlap