Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] a4509be623 fix: run Node.js setup and npm ci before pre-commit checks in lint workflow 2026-06-15 15:28:17 +00:00
Tomu Hirata 00bc1f4348 lint
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-06-16 00:19:38 +09:00
Tomu Hirata 4d5ac70980 test(entities): add unit tests for all untested entity DTOs
Cover Account, AccountToken, Agent, Comment, CommentsFingerprint,
StoredFile, PagedList, paginate_in_memory, SessionPermission,
ResolvedAccess, Policy, and extend conversation.py coverage with
ErrorData, CompactionData, NativeToolData, ResourceEventData,
TerminalCommandData, NON_CONTENT_ITEM_TYPES, and
_validate_type_matches_data.

Co-authored-by: Isaac
2026-06-16 00:12:38 +09:00
9 changed files with 969 additions and 3 deletions
+3 -3
View File
@@ -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
+115
View File
@@ -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
+57
View File
@@ -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
+82
View File
@@ -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"
+38
View File
@@ -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"
+123
View File
@@ -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"]
+99
View File
@@ -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
+73
View File
@@ -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