Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions formatters.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
"""Форматирование вывода: таблицы, JSON, Markdown."""

from __future__ import annotations

import json
import os
from typing import Any

from config import IGNORE_DIRS
from i18n import _


def print_json(data) -> None:
def print_json(data: Any) -> None:
"""Выводит данные в JSON-формате."""
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))


def print_table_simple(headers, rows, sep=" ") -> None:
def print_table_simple(headers: list[str], rows: list[list], sep: str = " ") -> None:
"""Выводит таблицу с авто-шириной колонок."""
if not rows:
print(_("empty"))
Expand All @@ -37,15 +40,15 @@ def print_table_simple(headers, rows, sep=" ") -> None:
print(_("fmt.rows_count", n=len(rows)))


def summarize_val(v, max_len=120) -> str:
def summarize_val(v: Any, max_len: int = 120) -> str:
"""Обрезает длинное значение."""
s = str(v)
if len(s) > max_len:
s = s[:max_len] + "..."
return s


def format_part_to_md(part, full=False):
def format_part_to_md(part: dict, full: bool = False) -> str:
"""Преобразует часть сообщения в строку Markdown.

Args:
Expand Down Expand Up @@ -149,7 +152,7 @@ def format_part_to_md(part, full=False):
return ""


def collect_snapshot(project_dir):
def collect_snapshot(project_dir: str) -> dict[str, Any]:
"""Собирает статистику по проекту для log.md.

Returns:
Expand Down Expand Up @@ -182,7 +185,14 @@ def collect_snapshot(project_dir):
return snapshot


def update_log(output_dir, title, note, dialog_filename, snapshot, now_str) -> None:
def update_log(
output_dir: str,
title: str,
note: str | None,
dialog_filename: str | None,
snapshot: dict[str, Any],
now_str: str,
) -> None:
"""Создаёт или дополняет log.md в директории проекта."""
log_path = os.path.join(output_dir, "log.md")
heading = f"{title}: {note}" if note else title
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "opencode-db"
version = "0.6.1"
version = "0.6.2"
description = "OpenCode database CLI manager — browse, export, analyze, clean up sessions"
readme = "README_PYPI.md"
requires-python = ">=3.12"
Expand Down
72 changes: 72 additions & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,75 @@ def test_skips_empty_part_data(self, db) -> None:
)
messages = get_messages(db, "ses_001")
assert len(messages) == 2


class TestEdgeCases:
def test_session_null_title_and_model(self, db) -> None:
db.execute(
"INSERT INTO session (id, time_created, title, model, cost) VALUES (?, ?, ?, ?, ?)",
("ses_null_meta", 50000, None, None, 0.0),
)
db.commit()
info = get_session_info(db, "ses_null_meta")
assert info["title"] is None
assert info["model"] is None

def test_session_zero_cost(self, db) -> None:
db.execute(
"INSERT INTO session (id, time_created, cost) VALUES (?, ?, ?)",
("ses_zero_cost", 55000, 0.0),
)
db.commit()
info = get_session_info(db, "ses_zero_cost")
assert info["cost"] == 0.0

def test_session_null_parent(self, db) -> None:
db.execute(
"INSERT INTO session (id, time_created, parent_id) VALUES (?, ?, ?)",
("ses_orphan", 60000, None),
)
db.commit()
children = get_children_sessions(db, "ses_orphan")
assert children == []

def test_part_without_type_has_empty_parts(self, db) -> None:
db.execute(
"INSERT INTO session (id, time_created) VALUES (?, ?)",
("ses_skip", 1000),
)
db.execute(
"INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)",
("msg_skip", "ses_skip", 1000, json.dumps({"role": "user"})),
)
db.execute(
"INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)",
("part_skip", "msg_skip", "ses_skip", 1000, json.dumps({"text": "no type"})),
)
db.commit()
messages = get_messages(db, "ses_skip")
assert len(messages) == 1
assert len(messages[0]["parts"]) == 0

def test_message_null_role(self, db) -> None:
db.execute(
"INSERT INTO session (id, time_created) VALUES (?, ?)",
("ses_edge_b", 2000),
)
db.execute(
"INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)",
("msg_edge_b", "ses_edge_b", 2000, json.dumps({"role": None})),
)
db.execute(
"INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)",
(
"part_edge_b",
"msg_edge_b",
"ses_edge_b",
2000,
json.dumps({"type": "text", "text": "hi"}),
),
)
db.commit()
messages = get_messages(db, "ses_edge_b")
assert len(messages) == 1
assert messages[0]["role"] is None or messages[0]["role"] == "?"
45 changes: 45 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,48 @@ def test_full_mode_shows_full_output(self):
full=True,
)
assert "line" in result

def test_unknown_type_returns_empty(self):
result = format_part_to_md({"type": "unknown_type"})
assert result == ""

def test_empty_part_returns_empty(self):
result = format_part_to_md({"type": "text", "text": ""})
assert result == ""

def test_tool_without_input_key(self):
result = format_part_to_md(
{
"type": "tool",
"tool": "bash",
}
)
assert "bash" in result

def test_tool_with_null_output(self):
result = format_part_to_md(
{
"type": "tool",
"tool": "bash",
"input": {"command": "ls"},
"output": None,
"status": "completed",
}
)
assert "bash" in result

def test_tool_with_unknown_status(self):
result = format_part_to_md(
{
"type": "tool",
"tool": "bash",
"input": {"command": "ls"},
"output": "",
"status": "unknown",
}
)
assert "🔧" in result

def test_reasoning_empty_text(self):
result = format_part_to_md({"type": "reasoning", "text": ""})
assert result == ""