Files
issue-core/tests/test_core_models.py
tegwick b605d970e3 feat: rename to issue-core and add task ingestion endpoint
Renames the package, distribution, CLI alias, Makefile targets, and
working directory from issue-facade to issue-core, signalling its
role as the authoritative task lifecycle manager for the Coulomb org
(peer to activity-core, rules-core, project-core).

Adds POST /issues/ ingestion endpoint for activity-core's IssueSink,
under a new optional [api] extra. The endpoint is served by `issue
serve`, authenticates via the ISSUE_CORE_API_KEY env var (Bearer or
X-API-Key header), and routes the TaskSpec payload to the configured
default backend with full traceability metadata embedded in
sync_metadata.

- T01: Python package issue_tracker -> issue_core, dir rename
- T02: registered in state hub under custodian domain
- T03: INTENT.md (what it is, what it isn't, how it fits)
- T04: SCOPE.md (in/out-of-scope, integration boundaries)
- T05: POST /issues/ via FastAPI + Uvicorn, 9 unit tests
- T06: docs/nats-task-ingestion.md design stub

Closes ISSC-WP-0001.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 05:16:27 +02:00

603 lines
21 KiB
Python

"""
Test suite for Core Domain Models.
These tests ensure the domain models (Issue, Label, User, etc.) work correctly,
including state management, validation, and business logic.
"""
import pytest
from datetime import datetime, timezone
from issue_core.core.models import (
Issue, Label, User, Milestone, Comment,
IssueState, Priority, IssueType, LabelCategories
)
@pytest.mark.unit
class TestIssueState:
"""Test IssueState enumeration."""
def test_from_string_standard_states(self):
"""Test converting standard strings to IssueState."""
assert IssueState.from_string("open") == IssueState.OPEN
assert IssueState.from_string("closed") == IssueState.CLOSED
assert IssueState.from_string("in_progress") == IssueState.IN_PROGRESS
assert IssueState.from_string("blocked") == IssueState.BLOCKED
def test_from_string_variants(self):
"""Test converting variant strings to IssueState."""
assert IssueState.from_string("in-progress") == IssueState.IN_PROGRESS
assert IssueState.from_string("progress") == IssueState.IN_PROGRESS
assert IssueState.from_string("OPEN") == IssueState.OPEN
def test_from_string_unknown_defaults_to_open(self):
"""Test unknown state strings default to OPEN."""
assert IssueState.from_string("unknown") == IssueState.OPEN
assert IssueState.from_string("") == IssueState.OPEN
def test_to_backend_string_gitea(self):
"""Test converting to Gitea backend string."""
assert IssueState.OPEN.to_backend_string("gitea") == "open"
assert IssueState.CLOSED.to_backend_string("gitea") == "closed"
assert IssueState.IN_PROGRESS.to_backend_string("gitea") == "open"
assert IssueState.BLOCKED.to_backend_string("gitea") == "open"
def test_to_backend_string_github(self):
"""Test converting to GitHub backend string."""
assert IssueState.OPEN.to_backend_string("github") == "open"
assert IssueState.CLOSED.to_backend_string("github") == "closed"
@pytest.mark.unit
class TestPriority:
"""Test Priority enumeration."""
def test_from_label_with_priority_prefix(self):
"""Test extracting priority from label."""
assert Priority.from_label("priority:low") == Priority.LOW
assert Priority.from_label("priority:medium") == Priority.MEDIUM
assert Priority.from_label("priority:high") == Priority.HIGH
assert Priority.from_label("priority:critical") == Priority.CRITICAL
def test_from_label_without_prefix(self):
"""Test non-priority labels return None."""
assert Priority.from_label("bug") is None
assert Priority.from_label("enhancement") is None
def test_from_label_invalid_priority(self):
"""Test invalid priority returns None."""
assert Priority.from_label("priority:invalid") is None
@pytest.mark.unit
class TestIssueType:
"""Test IssueType enumeration."""
def test_from_label_standard_types(self):
"""Test extracting issue type from label."""
assert IssueType.from_label("bug") == IssueType.BUG
assert IssueType.from_label("feature") == IssueType.FEATURE
assert IssueType.from_label("enhancement") == IssueType.ENHANCEMENT
assert IssueType.from_label("task") == IssueType.TASK
assert IssueType.from_label("documentation") == IssueType.DOCUMENTATION
assert IssueType.from_label("question") == IssueType.QUESTION
def test_from_label_case_insensitive(self):
"""Test case insensitive type matching."""
assert IssueType.from_label("BUG") == IssueType.BUG
assert IssueType.from_label("Bug") == IssueType.BUG
def test_from_label_invalid_type(self):
"""Test invalid type returns None."""
assert IssueType.from_label("invalid") is None
@pytest.mark.unit
class TestLabel:
"""Test Label model."""
def test_label_creation(self):
"""Test creating a label."""
label = Label(name="bug", color="red", description="Bug reports")
assert label.name == "bug"
assert label.color == "red"
assert label.description == "Bug reports"
def test_label_category_priority(self):
"""Test priority label categorization."""
label = Label(name="priority:high")
assert label.category == "priority"
def test_label_category_status(self):
"""Test status label categorization."""
label = Label(name="status:in-progress")
assert label.category == "status"
def test_label_category_type_with_prefix(self):
"""Test type label with prefix categorization."""
label = Label(name="type:bug")
assert label.category == "type"
def test_label_category_type_without_prefix(self):
"""Test type label without prefix categorization."""
label = Label(name="bug")
assert label.category == "type"
label2 = Label(name="feature")
assert label2.category == "type"
def test_label_category_other(self):
"""Test other label categorization."""
label = Label(name="good-first-issue")
assert label.category == "other"
def test_label_priority_property(self):
"""Test extracting priority from label."""
label = Label(name="priority:high")
assert label.priority == Priority.HIGH
label2 = Label(name="bug")
assert label2.priority is None
def test_label_issue_type_property(self):
"""Test extracting issue type from label."""
label = Label(name="bug")
assert label.issue_type == IssueType.BUG
label2 = Label(name="priority:high")
assert label2.issue_type is None
@pytest.mark.unit
class TestUser:
"""Test User model."""
def test_user_creation(self):
"""Test creating a user."""
user = User(
id="user123",
username="alice",
display_name="Alice Smith",
email="alice@example.com"
)
assert user.id == "user123"
assert user.username == "alice"
assert user.display_name == "Alice Smith"
assert user.email == "alice@example.com"
@pytest.mark.unit
class TestMilestone:
"""Test Milestone model."""
def test_milestone_creation(self):
"""Test creating a milestone."""
now = datetime.now(timezone.utc)
milestone = Milestone(
id="m1",
title="v1.0",
description="First release",
state="open",
due_date=now,
created_at=now,
updated_at=now
)
assert milestone.id == "m1"
assert milestone.title == "v1.0"
assert milestone.state == "open"
@pytest.mark.unit
class TestComment:
"""Test Comment model."""
def test_comment_creation(self):
"""Test creating a comment."""
author = User(id="user1", username="alice")
now = datetime.now(timezone.utc)
comment = Comment(
id="c1",
body="Great issue!",
author=author,
created_at=now
)
assert comment.id == "c1"
assert comment.body == "Great issue!"
assert comment.author.username == "alice"
@pytest.mark.unit
class TestIssueBasics:
"""Test basic Issue model functionality."""
def test_issue_creation(self):
"""Test creating an issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1",
number=1,
title="Test Issue",
description="Test description",
state=IssueState.OPEN,
created_at=now,
updated_at=now
)
assert issue.id == "issue1"
assert issue.number == 1
assert issue.title == "Test Issue"
assert issue.state == IssueState.OPEN
def test_issue_with_labels(self):
"""Test creating issue with labels."""
now = datetime.now(timezone.utc)
labels = [
Label(name="bug", color="red"),
Label(name="priority:high", color="orange")
]
issue = Issue(
id="issue1", number=1, title="Bug", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels
)
assert len(issue.labels) == 2
def test_issue_with_assignees(self):
"""Test creating issue with assignees."""
now = datetime.now(timezone.utc)
assignees = [User(id="u1", username="alice")]
issue = Issue(
id="issue1", number=1, title="Task", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
assignees=assignees
)
assert len(issue.assignees) == 1
assert issue.primary_assignee.username == "alice"
def test_primary_assignee_when_none(self):
"""Test primary_assignee returns None when no assignees."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Task", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now
)
assert issue.primary_assignee is None
@pytest.mark.unit
class TestIssueLabelCategorization:
"""Test Issue label categorization."""
def test_label_categories_property(self):
"""Test label_categories property organizes labels."""
now = datetime.now(timezone.utc)
labels = [
Label(name="bug"),
Label(name="priority:high"),
Label(name="status:in-review"),
Label(name="good-first-issue")
]
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels
)
categories = issue.label_categories
assert len(categories.type_labels) == 1
assert len(categories.priority_labels) == 1
assert len(categories.status_labels) == 1
assert len(categories.other_labels) == 1
def test_issue_priority_property(self):
"""Test issue.priority extracts priority from labels."""
now = datetime.now(timezone.utc)
labels = [Label(name="priority:high"), Label(name="bug")]
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels
)
assert issue.priority == Priority.HIGH
def test_issue_priority_none_when_no_priority_label(self):
"""Test issue.priority is None without priority label."""
now = datetime.now(timezone.utc)
labels = [Label(name="bug")]
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels
)
assert issue.priority is None
def test_issue_type_property(self):
"""Test issue.issue_type extracts type from labels."""
now = datetime.now(timezone.utc)
labels = [Label(name="bug"), Label(name="priority:high")]
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels
)
assert issue.issue_type == IssueType.BUG
def test_cache_invalidation(self):
"""Test label cache is invalidated when labels change."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[Label(name="bug")]
)
# Access to populate cache
assert issue.issue_type == IssueType.BUG
# Manually modify labels
issue.labels = [Label(name="feature")]
issue.invalidate_cache()
# Should reflect new labels
assert issue.issue_type == IssueType.FEATURE
@pytest.mark.unit
class TestIssueStateTransitions:
"""Test Issue state transition methods."""
def test_close_issue(self):
"""Test closing an open issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now
)
issue.close()
assert issue.state == IssueState.CLOSED
assert issue.closed_at is not None
def test_close_issue_with_custom_timestamp(self):
"""Test closing issue with custom timestamp."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now
)
custom_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
issue.close(closed_at=custom_time)
assert issue.closed_at == custom_time
def test_close_already_closed_issue_raises_error(self):
"""Test closing already closed issue raises error."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.CLOSED, created_at=now, updated_at=now,
closed_at=now
)
with pytest.raises(ValueError, match="already closed"):
issue.close()
def test_reopen_issue(self):
"""Test reopening a closed issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.CLOSED, created_at=now, updated_at=now,
closed_at=now
)
issue.reopen()
assert issue.state == IssueState.OPEN
assert issue.closed_at is None
def test_reopen_open_issue_raises_error(self):
"""Test reopening non-closed issue raises error."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now
)
with pytest.raises(ValueError, match="not closed"):
issue.reopen()
@pytest.mark.unit
class TestIssueLabelMethods:
"""Test Issue label manipulation methods."""
def test_add_label(self):
"""Test adding a label to issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[]
)
bug_label = Label(name="bug", color="red")
issue.add_label(bug_label)
assert len(issue.labels) == 1
assert issue.labels[0].name == "bug"
def test_add_duplicate_label_ignored(self):
"""Test adding duplicate label is ignored."""
now = datetime.now(timezone.utc)
bug_label = Label(name="bug", color="red")
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[bug_label]
)
issue.add_label(bug_label)
assert len(issue.labels) == 1
def test_remove_label(self):
"""Test removing a label from issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[Label(name="bug"), Label(name="feature")]
)
result = issue.remove_label("bug")
assert result is True
assert len(issue.labels) == 1
assert issue.labels[0].name == "feature"
def test_remove_nonexistent_label_returns_false(self):
"""Test removing non-existent label returns False."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[Label(name="bug")]
)
result = issue.remove_label("nonexistent")
assert result is False
assert len(issue.labels) == 1
def test_has_label(self):
"""Test checking if issue has a label."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=[Label(name="bug"), Label(name="priority:high")]
)
assert issue.has_label("bug")
assert issue.has_label("priority:high")
assert not issue.has_label("feature")
@pytest.mark.unit
class TestIssueAssigneeMethods:
"""Test Issue assignee manipulation methods."""
def test_add_assignee(self):
"""Test adding an assignee to issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
assignees=[]
)
user = User(id="u1", username="alice")
issue.add_assignee(user)
assert len(issue.assignees) == 1
assert issue.assignees[0].username == "alice"
def test_add_duplicate_assignee_ignored(self):
"""Test adding duplicate assignee is ignored."""
now = datetime.now(timezone.utc)
user = User(id="u1", username="alice")
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
assignees=[user]
)
issue.add_assignee(user)
assert len(issue.assignees) == 1
def test_remove_assignee(self):
"""Test removing an assignee from issue."""
now = datetime.now(timezone.utc)
user1 = User(id="u1", username="alice")
user2 = User(id="u2", username="bob")
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
assignees=[user1, user2]
)
result = issue.remove_assignee("u1")
assert result is True
assert len(issue.assignees) == 1
assert issue.assignees[0].username == "bob"
def test_remove_nonexistent_assignee_returns_false(self):
"""Test removing non-existent assignee returns False."""
now = datetime.now(timezone.utc)
user = User(id="u1", username="alice")
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
assignees=[user]
)
result = issue.remove_assignee("nonexistent")
assert result is False
assert len(issue.assignees) == 1
@pytest.mark.unit
class TestIssueCommentMethods:
"""Test Issue comment methods."""
def test_add_comment(self):
"""Test adding a comment to issue."""
now = datetime.now(timezone.utc)
issue = Issue(
id="issue1", number=1, title="Test", description="Desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
comments=[]
)
author = User(id="u1", username="alice")
comment = Comment(id="c1", body="Great!", author=author, created_at=now)
issue.add_comment(comment)
assert len(issue.comments) == 1
assert issue.comments[0].body == "Great!"
@pytest.mark.unit
class TestIssueSerialization:
"""Test Issue serialization."""
def test_to_dict(self):
"""Test converting issue to dictionary."""
now = datetime.now(timezone.utc)
labels = [Label(name="bug", color="red")]
assignees = [User(id="u1", username="alice", display_name="Alice")]
milestone = Milestone(id="m1", title="v1.0")
issue = Issue(
id="issue1", number=1, title="Test Issue", description="Test desc",
state=IssueState.OPEN, created_at=now, updated_at=now,
labels=labels, assignees=assignees, milestone=milestone,
backend_id="gitea-123", backend_type="gitea"
)
issue_dict = issue.to_dict()
assert issue_dict['id'] == "issue1"
assert issue_dict['number'] == 1
assert issue_dict['title'] == "Test Issue"
assert issue_dict['state'] == "open"
assert issue_dict['backend_id'] == "gitea-123"
assert issue_dict['backend_type'] == "gitea"
assert len(issue_dict['labels']) == 1
assert len(issue_dict['assignees']) == 1
assert issue_dict['milestone']['id'] == "m1"