feat: msgspec schemas for jsonl decoding (#37)
This commit is contained in:
@@ -11,11 +11,26 @@ from takopi.runners.opencode import (
|
||||
ENGINE,
|
||||
translate_opencode_event,
|
||||
)
|
||||
from takopi.schemas import opencode as opencode_schema
|
||||
|
||||
|
||||
def _load_fixture(name: str) -> list[dict]:
|
||||
def _load_fixture(name: str) -> list[opencode_schema.OpenCodeEvent]:
|
||||
path = Path(__file__).parent / "fixtures" / name
|
||||
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
||||
events: list[opencode_schema.OpenCodeEvent] = []
|
||||
for line in path.read_bytes().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
events.append(opencode_schema.decode_event(line))
|
||||
except Exception as exc:
|
||||
raise AssertionError(
|
||||
f"{name} contained unparseable line: {line!r}"
|
||||
) from exc
|
||||
return events
|
||||
|
||||
|
||||
def _decode_event(payload: dict) -> opencode_schema.OpenCodeEvent:
|
||||
return opencode_schema.decode_event(json.dumps(payload).encode("utf-8"))
|
||||
|
||||
|
||||
def test_opencode_resume_format_and_extract() -> None:
|
||||
@@ -59,9 +74,6 @@ def test_translate_success_fixture() -> None:
|
||||
assert completed.resume == started.resume
|
||||
assert completed.answer == "```\nhello\n```"
|
||||
|
||||
assert completed.usage is not None
|
||||
assert "tokens" in completed.usage
|
||||
|
||||
|
||||
def test_translate_missing_reason_success() -> None:
|
||||
state = OpenCodeStreamState()
|
||||
@@ -74,7 +86,6 @@ def test_translate_missing_reason_success() -> None:
|
||||
fallback = runner.stream_end_events(
|
||||
resume=None,
|
||||
found_session=started.resume,
|
||||
stderr_tail="",
|
||||
state=state,
|
||||
)
|
||||
|
||||
@@ -82,14 +93,13 @@ def test_translate_missing_reason_success() -> None:
|
||||
assert completed.ok is True
|
||||
assert completed.resume == started.resume
|
||||
assert completed.answer == "All done."
|
||||
assert completed.usage is not None
|
||||
|
||||
|
||||
def test_translate_accumulates_text() -> None:
|
||||
state = OpenCodeStreamState()
|
||||
|
||||
events = translate_opencode_event(
|
||||
{"type": "step_start", "sessionID": "ses_test123", "part": {}},
|
||||
_decode_event({"type": "step_start", "sessionID": "ses_test123", "part": {}}),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -97,20 +107,24 @@ def test_translate_accumulates_text() -> None:
|
||||
assert isinstance(events[0], StartedEvent)
|
||||
|
||||
translate_opencode_event(
|
||||
{
|
||||
"type": "text",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"type": "text", "text": "Hello "},
|
||||
},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "text",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"type": "text", "text": "Hello "},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
translate_opencode_event(
|
||||
{
|
||||
"type": "text",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"type": "text", "text": "World"},
|
||||
},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "text",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"type": "text", "text": "World"},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -118,11 +132,13 @@ def test_translate_accumulates_text() -> None:
|
||||
assert state.last_text == "Hello World"
|
||||
|
||||
events = translate_opencode_event(
|
||||
{
|
||||
"type": "step_finish",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"reason": "stop", "tokens": {"input": 100, "output": 10}},
|
||||
},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "step_finish",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"reason": "stop", "tokens": {"input": 100, "output": 10}},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -140,22 +156,24 @@ def test_translate_tool_use_completed() -> None:
|
||||
state.emitted_started = True
|
||||
|
||||
events = translate_opencode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"command": "ls -la"},
|
||||
"output": "file1.txt\nfile2.txt",
|
||||
"title": "List files",
|
||||
"metadata": {"exit": 0},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"command": "ls -la"},
|
||||
"output": "file1.txt\nfile2.txt",
|
||||
"title": "List files",
|
||||
"metadata": {"exit": 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -175,22 +193,24 @@ def test_translate_tool_use_with_error() -> None:
|
||||
state.emitted_started = True
|
||||
|
||||
events = translate_opencode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"command": "exit 1"},
|
||||
"output": "error",
|
||||
"title": "Run failing command",
|
||||
"metadata": {"exit": 1},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "bash",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"command": "exit 1"},
|
||||
"output": "error",
|
||||
"title": "Run failing command",
|
||||
"metadata": {"exit": 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -209,21 +229,23 @@ def test_translate_tool_use_read_title_wraps_path() -> None:
|
||||
path = Path.cwd() / "src" / "takopi" / "runners" / "opencode.py"
|
||||
|
||||
events = translate_opencode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "read",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"filePath": str(path)},
|
||||
"output": "file contents",
|
||||
"title": "src/takopi/runners/opencode.py",
|
||||
_decode_event(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"id": "prt_123",
|
||||
"callID": "call_abc",
|
||||
"tool": "read",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {"filePath": str(path)},
|
||||
"output": "file contents",
|
||||
"title": "src/takopi/runners/opencode.py",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
@@ -255,11 +277,16 @@ def test_step_finish_tool_calls_does_not_complete() -> None:
|
||||
state.emitted_started = True
|
||||
|
||||
events = translate_opencode_event(
|
||||
{
|
||||
"type": "step_finish",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {"reason": "tool-calls", "tokens": {"input": 100, "output": 10}},
|
||||
},
|
||||
_decode_event(
|
||||
{
|
||||
"type": "step_finish",
|
||||
"sessionID": "ses_test123",
|
||||
"part": {
|
||||
"reason": "tool-calls",
|
||||
"tokens": {"input": 100, "output": 10},
|
||||
},
|
||||
}
|
||||
),
|
||||
title="opencode",
|
||||
state=state,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user