Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4509be623 | |||
| 00bc1f4348 | |||
| 4d5ac70980 |
@@ -84,9 +84,6 @@ jobs:
|
||||
# stale committed lockfile). Fix locally with `uv lock`.
|
||||
run: uv sync --locked --extra dev
|
||||
|
||||
- name: Run formatting, lint, and typing checks
|
||||
run: uv run pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
@@ -104,6 +101,9 @@ jobs:
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Run formatting, lint, and typing checks
|
||||
run: uv run pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
- name: Type-check ap-web
|
||||
working-directory: ap-web
|
||||
run: npm run type-check
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Tests for account entity dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.account import Account, AccountToken
|
||||
|
||||
# ── Account ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_account_construction() -> None:
|
||||
acct = Account(
|
||||
id="alice@example.com",
|
||||
is_admin=True,
|
||||
created_at=1700000000,
|
||||
last_login_at=1700001000,
|
||||
has_password=True,
|
||||
)
|
||||
assert acct.id == "alice@example.com"
|
||||
assert acct.is_admin is True
|
||||
assert acct.created_at == 1700000000
|
||||
assert acct.last_login_at == 1700001000
|
||||
assert acct.has_password is True
|
||||
|
||||
|
||||
def test_account_nullable_timestamps() -> None:
|
||||
"""Legacy/header-auth rows have None timestamps."""
|
||||
acct = Account(
|
||||
id="local",
|
||||
is_admin=False,
|
||||
created_at=None,
|
||||
last_login_at=None,
|
||||
has_password=False,
|
||||
)
|
||||
assert acct.created_at is None
|
||||
assert acct.last_login_at is None
|
||||
|
||||
|
||||
def test_account_is_frozen() -> None:
|
||||
"""Account is a frozen dataclass — mutations raise."""
|
||||
acct = Account(
|
||||
id="bob",
|
||||
is_admin=False,
|
||||
created_at=1,
|
||||
last_login_at=None,
|
||||
has_password=False,
|
||||
)
|
||||
try:
|
||||
acct.id = "eve" # type: ignore[misc]
|
||||
raise AssertionError("Expected FrozenInstanceError")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def test_account_equality() -> None:
|
||||
"""Frozen dataclasses support value-based equality."""
|
||||
a = Account(id="x", is_admin=False, created_at=1, last_login_at=None, has_password=False)
|
||||
b = Account(id="x", is_admin=False, created_at=1, last_login_at=None, has_password=False)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_account_inequality() -> None:
|
||||
a = Account(id="x", is_admin=False, created_at=1, last_login_at=None, has_password=False)
|
||||
b = Account(id="y", is_admin=False, created_at=1, last_login_at=None, has_password=False)
|
||||
assert a != b
|
||||
|
||||
|
||||
# ── AccountToken ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_account_token_invite() -> None:
|
||||
token = AccountToken(
|
||||
id="tok_abc123",
|
||||
kind="invite",
|
||||
user_id=None,
|
||||
created_by="admin@example.com",
|
||||
created_at=1700000000,
|
||||
expires_at=1700086400,
|
||||
invited_is_admin=False,
|
||||
)
|
||||
assert token.kind == "invite"
|
||||
assert token.user_id is None
|
||||
assert token.created_by == "admin@example.com"
|
||||
assert token.invited_is_admin is False
|
||||
|
||||
|
||||
def test_account_token_magic() -> None:
|
||||
token = AccountToken(
|
||||
id="tok_magic_xyz",
|
||||
kind="magic",
|
||||
user_id="alice@example.com",
|
||||
created_by=None,
|
||||
created_at=1700000000,
|
||||
expires_at=1700003600,
|
||||
invited_is_admin=False,
|
||||
)
|
||||
assert token.kind == "magic"
|
||||
assert token.user_id == "alice@example.com"
|
||||
assert token.created_by is None
|
||||
|
||||
|
||||
def test_account_token_is_frozen() -> None:
|
||||
token = AccountToken(
|
||||
id="tok_1",
|
||||
kind="invite",
|
||||
user_id=None,
|
||||
created_by="admin",
|
||||
created_at=1,
|
||||
expires_at=2,
|
||||
invited_is_admin=True,
|
||||
)
|
||||
try:
|
||||
token.id = "tok_2" # type: ignore[misc]
|
||||
raise AssertionError("Expected FrozenInstanceError")
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Tests for agent entity dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.agent import Agent
|
||||
|
||||
|
||||
def test_agent_minimal() -> None:
|
||||
agent = Agent(
|
||||
id="ag_abc123",
|
||||
created_at=1700000000,
|
||||
name="research-agent",
|
||||
bundle_location="ag_abc123/a1b2c3d4",
|
||||
)
|
||||
assert agent.id == "ag_abc123"
|
||||
assert agent.name == "research-agent"
|
||||
assert agent.version == 1
|
||||
assert agent.description is None
|
||||
assert agent.updated_at is None
|
||||
assert agent.session_id is None
|
||||
|
||||
|
||||
def test_agent_full() -> None:
|
||||
agent = Agent(
|
||||
id="ag_xyz",
|
||||
created_at=1700000000,
|
||||
name="coder",
|
||||
bundle_location="ag_xyz/deadbeef",
|
||||
version=3,
|
||||
description="A coding agent",
|
||||
updated_at=1700001000,
|
||||
session_id="conv_session1",
|
||||
)
|
||||
assert agent.version == 3
|
||||
assert agent.description == "A coding agent"
|
||||
assert agent.updated_at == 1700001000
|
||||
assert agent.session_id == "conv_session1"
|
||||
|
||||
|
||||
def test_agent_is_mutable() -> None:
|
||||
"""Agent is a regular (non-frozen) dataclass — version bumps are allowed."""
|
||||
agent = Agent(
|
||||
id="ag_1",
|
||||
created_at=1,
|
||||
name="a",
|
||||
bundle_location="ag_1/hash",
|
||||
)
|
||||
agent.version = 2
|
||||
assert agent.version == 2
|
||||
|
||||
|
||||
def test_agent_defaults_independent() -> None:
|
||||
"""Each Agent gets independent default values."""
|
||||
a = Agent(id="ag_a", created_at=1, name="a", bundle_location="a/h")
|
||||
b = Agent(id="ag_b", created_at=1, name="b", bundle_location="b/h")
|
||||
a.description = "modified"
|
||||
assert b.description is None
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests for comment entity dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.comment import Comment, CommentsFingerprint
|
||||
|
||||
# ── Comment ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_comment_construction() -> None:
|
||||
comment = Comment(
|
||||
id="a1b2c3d4-0000-0000-0000-000000000000",
|
||||
conversation_id="conv_abc123",
|
||||
path="src/App.tsx",
|
||||
start_index=100,
|
||||
end_index=200,
|
||||
body="This needs a null check.",
|
||||
status="draft",
|
||||
created_at=1700000000,
|
||||
updated_at=1700000000_000000,
|
||||
)
|
||||
assert comment.id == "a1b2c3d4-0000-0000-0000-000000000000"
|
||||
assert comment.conversation_id == "conv_abc123"
|
||||
assert comment.path == "src/App.tsx"
|
||||
assert comment.start_index == 100
|
||||
assert comment.end_index == 200
|
||||
assert comment.body == "This needs a null check."
|
||||
assert comment.status == "draft"
|
||||
assert comment.anchor_content is None
|
||||
assert comment.created_by is None
|
||||
|
||||
|
||||
def test_comment_with_optional_fields() -> None:
|
||||
comment = Comment(
|
||||
id="c1",
|
||||
conversation_id="conv_1",
|
||||
path="main.py",
|
||||
start_index=0,
|
||||
end_index=10,
|
||||
body="Fix this",
|
||||
status="addressed",
|
||||
created_at=1700000000,
|
||||
updated_at=1700000001_000000,
|
||||
anchor_content="old code here",
|
||||
created_by="alice@example.com",
|
||||
)
|
||||
assert comment.anchor_content == "old code here"
|
||||
assert comment.created_by == "alice@example.com"
|
||||
assert comment.status == "addressed"
|
||||
|
||||
|
||||
def test_comment_is_mutable() -> None:
|
||||
comment = Comment(
|
||||
id="c1",
|
||||
conversation_id="conv_1",
|
||||
path="x.py",
|
||||
start_index=0,
|
||||
end_index=5,
|
||||
body="draft body",
|
||||
status="draft",
|
||||
created_at=1,
|
||||
updated_at=1,
|
||||
)
|
||||
comment.status = "addressed"
|
||||
comment.body = "updated body"
|
||||
assert comment.status == "addressed"
|
||||
assert comment.body == "updated body"
|
||||
|
||||
|
||||
# ── CommentsFingerprint ───────────────────────────────
|
||||
|
||||
|
||||
def test_comments_fingerprint() -> None:
|
||||
fp = CommentsFingerprint(count=5, last_updated_at=1700000001_000000)
|
||||
assert fp.count == 5
|
||||
assert fp.last_updated_at == 1700000001_000000
|
||||
|
||||
|
||||
def test_comments_fingerprint_empty() -> None:
|
||||
"""A conversation with zero comments still has a valid fingerprint."""
|
||||
fp = CommentsFingerprint(count=0, last_updated_at=0)
|
||||
assert fp.count == 0
|
||||
@@ -0,0 +1,379 @@
|
||||
"""Extended tests for conversation entity types not covered by existing tests.
|
||||
|
||||
Covers: ErrorData, CompactionData, NativeToolData, ResourceEventData,
|
||||
TerminalCommandData, NON_CONTENT_ITEM_TYPES, ITEM_TYPE_TO_DATA_CLS,
|
||||
_validate_type_matches_data, and Conversation field defaults.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from omnigent.entities.conversation import (
|
||||
ITEM_TYPE_TO_DATA_CLS,
|
||||
NON_CONTENT_ITEM_TYPES,
|
||||
CompactionData,
|
||||
Conversation,
|
||||
ConversationItem,
|
||||
ErrorData,
|
||||
FunctionCallData,
|
||||
MessageData,
|
||||
NativeToolData,
|
||||
NewConversationItem,
|
||||
ResourceEventData,
|
||||
TerminalCommandData,
|
||||
_validate_type_matches_data,
|
||||
parse_item_data,
|
||||
)
|
||||
|
||||
# ── ErrorData ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_error_data_valid() -> None:
|
||||
err = ErrorData(
|
||||
source="execution",
|
||||
code="native_terminal_start_failed",
|
||||
message="Native Codex requires the 'codex' CLI on PATH.",
|
||||
)
|
||||
assert err.source == "execution"
|
||||
assert err.code == "native_terminal_start_failed"
|
||||
|
||||
|
||||
def test_error_data_strips_whitespace() -> None:
|
||||
err = ErrorData(source="llm", code=" rate_limit ", message=" Too many requests ")
|
||||
assert err.code == "rate_limit"
|
||||
assert err.message == "Too many requests"
|
||||
|
||||
|
||||
def test_error_data_rejects_empty_code() -> None:
|
||||
with pytest.raises(ValidationError, match="non-empty"):
|
||||
ErrorData(source="execution", code="", message="Something broke")
|
||||
|
||||
|
||||
def test_error_data_rejects_empty_message() -> None:
|
||||
with pytest.raises(ValidationError, match="non-empty"):
|
||||
ErrorData(source="execution", code="some_code", message=" ")
|
||||
|
||||
|
||||
def test_error_data_rejects_whitespace_only_code() -> None:
|
||||
with pytest.raises(ValidationError, match="non-empty"):
|
||||
ErrorData(source="tool", code=" \t ", message="msg")
|
||||
|
||||
|
||||
def test_error_data_rejects_invalid_source() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
ErrorData(source="unknown", code="c", message="m") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_error_data_all_valid_sources() -> None:
|
||||
for source in ("llm", "execution", "tool"):
|
||||
err = ErrorData(source=source, code="c", message="m") # type: ignore[arg-type]
|
||||
assert err.source == source
|
||||
|
||||
|
||||
# ── CompactionData ────────────────────────────────────
|
||||
|
||||
|
||||
def test_compaction_data_valid() -> None:
|
||||
cd = CompactionData(
|
||||
summary="User asked to analyze data. Agent loaded CSV.",
|
||||
last_item_id="msg_abc123",
|
||||
model="openai/gpt-4o",
|
||||
token_count=342,
|
||||
)
|
||||
assert cd.summary.startswith("User asked")
|
||||
assert cd.last_item_id == "msg_abc123"
|
||||
assert cd.model == "openai/gpt-4o"
|
||||
assert cd.token_count == 342
|
||||
|
||||
|
||||
def test_compaction_data_missing_field() -> None:
|
||||
with pytest.raises(ValidationError, match="last_item_id"):
|
||||
CompactionData(summary="s", model="m", token_count=1) # type: ignore[call-arg]
|
||||
|
||||
|
||||
# ── NativeToolData ────────────────────────────────────
|
||||
|
||||
|
||||
def test_native_tool_data_valid() -> None:
|
||||
ntd = NativeToolData(
|
||||
item={
|
||||
"type": "web_search_call",
|
||||
"id": "ws_abc",
|
||||
"status": "completed",
|
||||
}
|
||||
)
|
||||
assert ntd.item["type"] == "web_search_call"
|
||||
assert ntd.item["id"] == "ws_abc"
|
||||
|
||||
|
||||
def test_native_tool_data_empty_item() -> None:
|
||||
ntd = NativeToolData(item={})
|
||||
assert ntd.item == {}
|
||||
|
||||
|
||||
# ── ResourceEventData ─────────────────────────────────
|
||||
|
||||
|
||||
def test_resource_event_created() -> None:
|
||||
red = ResourceEventData(
|
||||
event_type="session.resource.created",
|
||||
resource_id="terminal_bash_s1",
|
||||
resource_type="terminal",
|
||||
resource={"id": "terminal_bash_s1", "name": "bash"},
|
||||
)
|
||||
assert red.event_type == "session.resource.created"
|
||||
assert red.resource is not None
|
||||
assert red.resource["id"] == "terminal_bash_s1"
|
||||
|
||||
|
||||
def test_resource_event_deleted() -> None:
|
||||
red = ResourceEventData(
|
||||
event_type="session.resource.deleted",
|
||||
resource_id="file_abc123",
|
||||
resource_type="file",
|
||||
)
|
||||
assert red.resource is None
|
||||
|
||||
|
||||
# ── TerminalCommandData ───────────────────────────────
|
||||
|
||||
|
||||
def test_terminal_command_input() -> None:
|
||||
tcd = TerminalCommandData(kind="input", input="pwd")
|
||||
assert tcd.kind == "input"
|
||||
assert tcd.input == "pwd"
|
||||
assert tcd.stdout is None
|
||||
assert tcd.stderr is None
|
||||
|
||||
|
||||
def test_terminal_command_output() -> None:
|
||||
tcd = TerminalCommandData(
|
||||
kind="output",
|
||||
stdout="/home/user\n",
|
||||
stderr="",
|
||||
)
|
||||
assert tcd.kind == "output"
|
||||
assert tcd.input is None
|
||||
assert tcd.stdout == "/home/user\n"
|
||||
|
||||
|
||||
def test_terminal_command_invalid_kind() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
TerminalCommandData(kind="unknown") # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ── NON_CONTENT_ITEM_TYPES ───────────────────────────
|
||||
|
||||
|
||||
def test_non_content_item_types_complete() -> None:
|
||||
"""All expected non-content types are present."""
|
||||
expected = {"compaction", "error", "resource_event", "slash_command", "terminal_command"}
|
||||
assert expected == NON_CONTENT_ITEM_TYPES
|
||||
|
||||
|
||||
def test_non_content_item_types_is_frozenset() -> None:
|
||||
assert isinstance(NON_CONTENT_ITEM_TYPES, frozenset)
|
||||
|
||||
|
||||
# ── ITEM_TYPE_TO_DATA_CLS ────────────────────────────
|
||||
|
||||
|
||||
def test_item_type_map_covers_all_types() -> None:
|
||||
expected_types = {
|
||||
"message",
|
||||
"function_call",
|
||||
"function_call_output",
|
||||
"error",
|
||||
"reasoning",
|
||||
"compaction",
|
||||
"native_tool",
|
||||
"resource_event",
|
||||
"slash_command",
|
||||
"terminal_command",
|
||||
}
|
||||
assert set(ITEM_TYPE_TO_DATA_CLS.keys()) == expected_types
|
||||
|
||||
|
||||
# ── _validate_type_matches_data ───────────────────────
|
||||
|
||||
|
||||
def test_validate_type_matches_data_ok() -> None:
|
||||
msg = MessageData(role="user", content=[])
|
||||
_validate_type_matches_data("message", msg) # should not raise
|
||||
|
||||
|
||||
def test_validate_type_matches_data_mismatch() -> None:
|
||||
msg = MessageData(role="user", content=[])
|
||||
with pytest.raises(ValueError, match="requires FunctionCallData, got MessageData"):
|
||||
_validate_type_matches_data("function_call", msg)
|
||||
|
||||
|
||||
def test_validate_type_matches_data_unknown_type() -> None:
|
||||
msg = MessageData(role="user", content=[])
|
||||
with pytest.raises(ValueError, match="unknown item type"):
|
||||
_validate_type_matches_data("nonexistent", msg)
|
||||
|
||||
|
||||
# ── parse_item_data extended ──────────────────────────
|
||||
|
||||
|
||||
def test_parse_error_data() -> None:
|
||||
data = parse_item_data("error", {"source": "execution", "code": "c", "message": "m"})
|
||||
assert isinstance(data, ErrorData)
|
||||
|
||||
|
||||
def test_parse_compaction_data() -> None:
|
||||
data = parse_item_data(
|
||||
"compaction",
|
||||
{"summary": "s", "last_item_id": "id1", "model": "m", "token_count": 10},
|
||||
)
|
||||
assert isinstance(data, CompactionData)
|
||||
|
||||
|
||||
def test_parse_native_tool_data() -> None:
|
||||
data = parse_item_data("native_tool", {"item": {"type": "web_search_call"}})
|
||||
assert isinstance(data, NativeToolData)
|
||||
|
||||
|
||||
def test_parse_resource_event_data() -> None:
|
||||
data = parse_item_data(
|
||||
"resource_event",
|
||||
{"event_type": "session.resource.created", "resource_id": "r1", "resource_type": "file"},
|
||||
)
|
||||
assert isinstance(data, ResourceEventData)
|
||||
|
||||
|
||||
def test_parse_terminal_command_data() -> None:
|
||||
data = parse_item_data("terminal_command", {"kind": "input", "input": "ls"})
|
||||
assert isinstance(data, TerminalCommandData)
|
||||
|
||||
|
||||
# ── Conversation field defaults ───────────────────────
|
||||
|
||||
|
||||
def test_conversation_all_defaults() -> None:
|
||||
conv = Conversation(
|
||||
id="conv_1",
|
||||
created_at=1,
|
||||
updated_at=1,
|
||||
root_conversation_id="conv_1",
|
||||
)
|
||||
assert conv.kind == "default"
|
||||
assert conv.parent_conversation_id is None
|
||||
assert conv.agent_id is None
|
||||
assert conv.runner_id is None
|
||||
assert conv.host_id is None
|
||||
assert conv.labels == {}
|
||||
assert conv.session_state == {}
|
||||
assert conv.session_usage == {}
|
||||
assert conv.reasoning_effort is None
|
||||
assert conv.model_override is None
|
||||
assert conv.cost_control_mode_override is None
|
||||
assert conv.harness_override is None
|
||||
assert conv.sub_agent_name is None
|
||||
assert conv.external_session_id is None
|
||||
assert conv.terminal_launch_args is None
|
||||
assert conv.workspace is None
|
||||
assert conv.git_branch is None
|
||||
assert conv.archived is False
|
||||
|
||||
|
||||
def test_conversation_sub_agent() -> None:
|
||||
conv = Conversation(
|
||||
id="conv_child",
|
||||
created_at=1,
|
||||
updated_at=1,
|
||||
root_conversation_id="conv_root",
|
||||
kind="sub_agent",
|
||||
parent_conversation_id="conv_parent",
|
||||
sub_agent_name="summarizer",
|
||||
)
|
||||
assert conv.kind == "sub_agent"
|
||||
assert conv.parent_conversation_id == "conv_parent"
|
||||
assert conv.sub_agent_name == "summarizer"
|
||||
|
||||
|
||||
def test_conversation_session_state_independent() -> None:
|
||||
"""Each Conversation gets its own session_state dict."""
|
||||
a = Conversation(id="a", created_at=1, updated_at=1, root_conversation_id="a")
|
||||
b = Conversation(id="b", created_at=1, updated_at=1, root_conversation_id="b")
|
||||
a.session_state["counter"] = 5
|
||||
assert b.session_state == {}
|
||||
|
||||
|
||||
def test_conversation_session_usage_independent() -> None:
|
||||
"""Each Conversation gets its own session_usage dict."""
|
||||
a = Conversation(id="a", created_at=1, updated_at=1, root_conversation_id="a")
|
||||
b = Conversation(id="b", created_at=1, updated_at=1, root_conversation_id="b")
|
||||
a.session_usage["total_tokens"] = 1000
|
||||
assert b.session_usage == {}
|
||||
|
||||
|
||||
# ── ConversationItem.to_api_dict extended ─────────────
|
||||
|
||||
|
||||
def test_to_api_dict_function_call() -> None:
|
||||
item = ConversationItem(
|
||||
id="item_fc",
|
||||
type="function_call",
|
||||
status="completed",
|
||||
response_id="resp_1",
|
||||
created_at=1,
|
||||
data=FunctionCallData(
|
||||
agent="my-agent", name="search", arguments='{"q": "test"}', call_id="call_1"
|
||||
),
|
||||
)
|
||||
api = item.to_api_dict()
|
||||
assert api["id"] == "item_fc"
|
||||
assert api["type"] == "function_call"
|
||||
assert api["model"] == "my-agent" # alias
|
||||
assert api["name"] == "search"
|
||||
assert api["call_id"] == "call_1"
|
||||
assert "created_by" not in api
|
||||
|
||||
|
||||
def test_to_api_dict_error() -> None:
|
||||
item = ConversationItem(
|
||||
id="item_err",
|
||||
type="error",
|
||||
status="completed",
|
||||
response_id="resp_1",
|
||||
created_at=1,
|
||||
data=ErrorData(source="execution", code="terminal_fail", message="No CLI"),
|
||||
)
|
||||
api = item.to_api_dict()
|
||||
assert api["source"] == "execution"
|
||||
assert api["code"] == "terminal_fail"
|
||||
assert api["message"] == "No CLI"
|
||||
|
||||
|
||||
# ── NewConversationItem with new types ────────────────
|
||||
|
||||
|
||||
def test_new_item_error() -> None:
|
||||
item = NewConversationItem(
|
||||
type="error",
|
||||
response_id="resp_1",
|
||||
data=ErrorData(source="tool", code="timeout", message="Tool timed out"),
|
||||
)
|
||||
assert item.type == "error"
|
||||
|
||||
|
||||
def test_new_item_compaction() -> None:
|
||||
item = NewConversationItem(
|
||||
type="compaction",
|
||||
response_id="resp_1",
|
||||
data=CompactionData(summary="s", last_item_id="id1", model="m", token_count=10),
|
||||
)
|
||||
assert item.type == "compaction"
|
||||
|
||||
|
||||
def test_new_item_terminal_command() -> None:
|
||||
item = NewConversationItem(
|
||||
type="terminal_command",
|
||||
response_id="resp_1",
|
||||
data=TerminalCommandData(kind="input", input="ls"),
|
||||
)
|
||||
assert item.type == "terminal_command"
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Tests for file entity dataclass."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.file import StoredFile
|
||||
|
||||
|
||||
def test_stored_file_minimal() -> None:
|
||||
f = StoredFile(
|
||||
id="file_abc123",
|
||||
created_at=1700000000,
|
||||
filename="report.pdf",
|
||||
bytes=1024,
|
||||
)
|
||||
assert f.id == "file_abc123"
|
||||
assert f.filename == "report.pdf"
|
||||
assert f.bytes == 1024
|
||||
assert f.content_type is None
|
||||
assert f.session_id is None
|
||||
|
||||
|
||||
def test_stored_file_full() -> None:
|
||||
f = StoredFile(
|
||||
id="file_xyz",
|
||||
created_at=1700000000,
|
||||
filename="image.png",
|
||||
bytes=204800,
|
||||
content_type="image/png",
|
||||
session_id="conv_abc123",
|
||||
)
|
||||
assert f.content_type == "image/png"
|
||||
assert f.session_id == "conv_abc123"
|
||||
|
||||
|
||||
def test_stored_file_is_mutable() -> None:
|
||||
f = StoredFile(id="f1", created_at=1, filename="a.txt", bytes=10)
|
||||
f.filename = "b.txt"
|
||||
assert f.filename == "b.txt"
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Tests for pagination entity and paginate_in_memory helper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.pagination import PagedList, paginate_in_memory
|
||||
|
||||
# ── PagedList ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_paged_list_defaults() -> None:
|
||||
page = PagedList()
|
||||
assert page.data == []
|
||||
assert page.first_id is None
|
||||
assert page.last_id is None
|
||||
assert page.has_more is False
|
||||
|
||||
|
||||
def test_paged_list_independent_defaults() -> None:
|
||||
"""Each PagedList gets its own data list (no mutable-default footgun)."""
|
||||
a = PagedList()
|
||||
b = PagedList()
|
||||
a.data.append("x")
|
||||
assert b.data == []
|
||||
|
||||
|
||||
# ── paginate_in_memory ────────────────────────────────
|
||||
|
||||
|
||||
_ITEMS = [
|
||||
{"id": "1", "name": "first"},
|
||||
{"id": "2", "name": "second"},
|
||||
{"id": "3", "name": "third"},
|
||||
{"id": "4", "name": "fourth"},
|
||||
{"id": "5", "name": "fifth"},
|
||||
]
|
||||
|
||||
|
||||
def _id_fn(item: dict) -> str:
|
||||
return item["id"]
|
||||
|
||||
|
||||
def test_paginate_asc_no_cursor() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=3, order="asc")
|
||||
assert len(result.data) == 3
|
||||
assert result.first_id == "1"
|
||||
assert result.last_id == "3"
|
||||
assert result.has_more is True
|
||||
|
||||
|
||||
def test_paginate_asc_all_fit() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, order="asc")
|
||||
assert len(result.data) == 5
|
||||
assert result.has_more is False
|
||||
assert result.first_id == "1"
|
||||
assert result.last_id == "5"
|
||||
|
||||
|
||||
def test_paginate_desc_no_cursor() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=3, order="desc")
|
||||
assert len(result.data) == 3
|
||||
assert result.first_id == "5"
|
||||
assert result.last_id == "3"
|
||||
assert result.has_more is True
|
||||
|
||||
|
||||
def test_paginate_after_cursor_asc() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, after="2", order="asc")
|
||||
assert [_id_fn(i) for i in result.data] == ["3", "4"]
|
||||
assert result.has_more is True
|
||||
|
||||
|
||||
def test_paginate_after_cursor_exhausts() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="3", order="asc")
|
||||
assert [_id_fn(i) for i in result.data] == ["4", "5"]
|
||||
assert result.has_more is False
|
||||
|
||||
|
||||
def test_paginate_before_cursor_asc() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, before="4", order="asc")
|
||||
assert [_id_fn(i) for i in result.data] == ["1", "2", "3"]
|
||||
assert result.has_more is False
|
||||
|
||||
|
||||
def test_paginate_after_and_before() -> None:
|
||||
"""Both cursors narrow the window."""
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="1", before="5", order="asc")
|
||||
assert [_id_fn(i) for i in result.data] == ["2", "3", "4"]
|
||||
|
||||
|
||||
def test_paginate_empty_list() -> None:
|
||||
result = paginate_in_memory([], _id_fn, limit=10, order="asc")
|
||||
assert result.data == []
|
||||
assert result.first_id is None
|
||||
assert result.last_id is None
|
||||
assert result.has_more is False
|
||||
|
||||
|
||||
def test_paginate_cursor_not_found() -> None:
|
||||
"""Unknown cursor id is silently ignored (no items skipped)."""
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="999", order="asc")
|
||||
assert len(result.data) == 5
|
||||
|
||||
|
||||
def test_paginate_limit_one() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=1, order="asc")
|
||||
assert len(result.data) == 1
|
||||
assert result.first_id == "1"
|
||||
assert result.last_id == "1"
|
||||
assert result.has_more is True
|
||||
|
||||
|
||||
def test_paginate_desc_after_cursor() -> None:
|
||||
"""After cursor in desc order — items appear reversed, cursor still works."""
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, after="4", order="desc")
|
||||
# desc reverses to [5,4,3,2,1]; after "4" gives [3,2,1]; limit 2 = [3,2]
|
||||
assert [_id_fn(i) for i in result.data] == ["3", "2"]
|
||||
assert result.has_more is True
|
||||
|
||||
|
||||
def test_paginate_desc_before_cursor() -> None:
|
||||
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, before="3", order="desc")
|
||||
# desc reverses to [5,4,3,2,1]; before "3" gives [5,4]
|
||||
assert [_id_fn(i) for i in result.data] == ["5", "4"]
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Tests for permission entity dataclasses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.permission import ResolvedAccess, SessionPermission
|
||||
|
||||
# ── SessionPermission ─────────────────────────────────
|
||||
|
||||
|
||||
def test_session_permission_construction() -> None:
|
||||
perm = SessionPermission(
|
||||
user_id="alice@example.com",
|
||||
conversation_id="conv_abc123",
|
||||
level=2,
|
||||
)
|
||||
assert perm.user_id == "alice@example.com"
|
||||
assert perm.conversation_id == "conv_abc123"
|
||||
assert perm.level == 2
|
||||
|
||||
|
||||
def test_session_permission_public() -> None:
|
||||
"""Public access uses the __public__ sentinel."""
|
||||
perm = SessionPermission(
|
||||
user_id="__public__",
|
||||
conversation_id="conv_1",
|
||||
level=1,
|
||||
)
|
||||
assert perm.user_id == "__public__"
|
||||
assert perm.level == 1
|
||||
|
||||
|
||||
def test_session_permission_is_mutable() -> None:
|
||||
perm = SessionPermission(user_id="u", conversation_id="c", level=1)
|
||||
perm.level = 3
|
||||
assert perm.level == 3
|
||||
|
||||
|
||||
# ── ResolvedAccess ────────────────────────────────────
|
||||
|
||||
|
||||
def test_resolved_access_admin() -> None:
|
||||
access = ResolvedAccess(
|
||||
is_admin=True,
|
||||
user_grant_level=None,
|
||||
public_grant_level=None,
|
||||
)
|
||||
assert access.is_admin is True
|
||||
assert access.user_grant_level is None
|
||||
assert access.public_grant_level is None
|
||||
|
||||
|
||||
def test_resolved_access_user_grant() -> None:
|
||||
access = ResolvedAccess(
|
||||
is_admin=False,
|
||||
user_grant_level=2,
|
||||
public_grant_level=None,
|
||||
)
|
||||
assert access.user_grant_level == 2
|
||||
|
||||
|
||||
def test_resolved_access_public_grant() -> None:
|
||||
access = ResolvedAccess(
|
||||
is_admin=False,
|
||||
user_grant_level=None,
|
||||
public_grant_level=1,
|
||||
)
|
||||
assert access.public_grant_level == 1
|
||||
|
||||
|
||||
def test_resolved_access_both_grants() -> None:
|
||||
"""User may have both a direct grant and public access."""
|
||||
access = ResolvedAccess(
|
||||
is_admin=False,
|
||||
user_grant_level=3,
|
||||
public_grant_level=1,
|
||||
)
|
||||
assert access.user_grant_level == 3
|
||||
assert access.public_grant_level == 1
|
||||
|
||||
|
||||
def test_resolved_access_is_frozen() -> None:
|
||||
access = ResolvedAccess(is_admin=False, user_grant_level=None, public_grant_level=None)
|
||||
try:
|
||||
access.is_admin = True # type: ignore[misc]
|
||||
raise AssertionError("Expected FrozenInstanceError")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def test_resolved_access_equality() -> None:
|
||||
a = ResolvedAccess(is_admin=True, user_grant_level=3, public_grant_level=1)
|
||||
b = ResolvedAccess(is_admin=True, user_grant_level=3, public_grant_level=1)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_resolved_access_inequality() -> None:
|
||||
a = ResolvedAccess(is_admin=True, user_grant_level=3, public_grant_level=1)
|
||||
b = ResolvedAccess(is_admin=False, user_grant_level=3, public_grant_level=1)
|
||||
assert a != b
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for policy entity dataclass."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from omnigent.entities.policy import Policy
|
||||
|
||||
|
||||
def test_policy_minimal() -> None:
|
||||
pol = Policy(
|
||||
id="pol_abc123",
|
||||
name="block_push",
|
||||
session_id="conv_1",
|
||||
created_at=1700000000,
|
||||
type="python",
|
||||
handler="omnigent.policies.builtins.safety.block_push",
|
||||
)
|
||||
assert pol.id == "pol_abc123"
|
||||
assert pol.name == "block_push"
|
||||
assert pol.session_id == "conv_1"
|
||||
assert pol.type == "python"
|
||||
assert pol.handler == "omnigent.policies.builtins.safety.block_push"
|
||||
assert pol.factory_params is None
|
||||
assert pol.enabled is True
|
||||
assert pol.updated_at is None
|
||||
assert pol.created_by is None
|
||||
|
||||
|
||||
def test_policy_full() -> None:
|
||||
pol = Policy(
|
||||
id="pol_xyz",
|
||||
name="cost_budget",
|
||||
session_id=None,
|
||||
created_at=1700000000,
|
||||
type="python",
|
||||
handler="omnigent.policies.builtins.cost.cost_budget",
|
||||
factory_params={"limit": 10.0, "currency": "USD"},
|
||||
enabled=False,
|
||||
updated_at=1700001000,
|
||||
created_by="admin@example.com",
|
||||
)
|
||||
assert pol.session_id is None # server-wide default
|
||||
assert pol.factory_params == {"limit": 10.0, "currency": "USD"}
|
||||
assert pol.enabled is False
|
||||
assert pol.updated_at == 1700001000
|
||||
assert pol.created_by == "admin@example.com"
|
||||
|
||||
|
||||
def test_policy_url_type() -> None:
|
||||
pol = Policy(
|
||||
id="pol_url1",
|
||||
name="external_check",
|
||||
session_id="conv_1",
|
||||
created_at=1700000000,
|
||||
type="url",
|
||||
handler="https://hooks.example.com/policy",
|
||||
)
|
||||
assert pol.type == "url"
|
||||
assert pol.handler.startswith("https://")
|
||||
|
||||
|
||||
def test_policy_is_mutable() -> None:
|
||||
pol = Policy(
|
||||
id="pol_1",
|
||||
name="p",
|
||||
session_id=None,
|
||||
created_at=1,
|
||||
type="python",
|
||||
handler="mod.func",
|
||||
)
|
||||
pol.enabled = False
|
||||
pol.updated_at = 2
|
||||
assert pol.enabled is False
|
||||
assert pol.updated_at == 2
|
||||
Reference in New Issue
Block a user