feat: implement ISSUE-WP-0005 connector mapping and scope alignment
Add MappingService (work_record_uuid ↔ external id), project/map CLI with outward push-status, optional TaskSpec work_record fields, boundary-sync policy, and packaging/capability framing cleanup. Mark WP-0005 finished.
This commit is contained in:
parent
6855fb2a32
commit
f88e9a28fe
16 changed files with 1300 additions and 179 deletions
|
|
@ -189,6 +189,42 @@ def test_ingest_persists_traceability_metadata(client, valid_payload, tmp_issue_
|
|||
backend.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ingest_with_work_record_uuid_creates_mapping(client, valid_payload, tmp_issue_store):
|
||||
wr = str(uuid.uuid4())
|
||||
valid_payload["work_record_uuid"] = wr
|
||||
valid_payload["work_record_id"] = "ISSUE-WP-0005-T06"
|
||||
valid_payload["work_record_kind"] = "task"
|
||||
response = client.post(
|
||||
"/issues/",
|
||||
json=valid_payload,
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
issue_id = response.json()["issue_id"]
|
||||
|
||||
from issue_core.core.mapping import MappingService
|
||||
|
||||
svc = MappingService()
|
||||
svc.connect(str(tmp_issue_store / "mappings.db"))
|
||||
try:
|
||||
mapping = svc.get_active_by_uuid(wr, backend="sqlite")
|
||||
assert mapping is not None
|
||||
assert mapping.external_id == issue_id
|
||||
assert mapping.work_record_id == "ISSUE-WP-0005-T06"
|
||||
finally:
|
||||
svc.disconnect()
|
||||
|
||||
# Idempotent second post returns same issue
|
||||
response2 = client.post(
|
||||
"/issues/",
|
||||
json=valid_payload,
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
)
|
||||
assert response2.status_code == 201
|
||||
assert response2.json()["issue_id"] == issue_id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_app_refuses_without_api_key_env(monkeypatch, tmp_issue_store, valid_payload):
|
||||
monkeypatch.delenv("ISSUE_CORE_API_KEY", raising=False)
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ class TestCLICommands:
|
|||
"""Test main CLI help displays correctly."""
|
||||
result = self.runner.invoke(cli, ['--help'])
|
||||
assert result.exit_code == 0
|
||||
assert 'Universal Issue Tracking System' in result.output
|
||||
assert 'issue list' in result.output
|
||||
assert 'issue show' in result.output
|
||||
assert 'External issue-tracker connector' in result.output
|
||||
assert 'project' in result.output
|
||||
assert 'map' in result.output
|
||||
|
||||
def test_backend_list_command(self):
|
||||
"""Test backend list command."""
|
||||
|
|
@ -121,35 +121,39 @@ class TestCLICommands:
|
|||
result = self.runner.invoke(cli, ['--version'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
@patch('issue_core.cli.utils.get_backend')
|
||||
@patch('issue_core.cli.commands.get_backend')
|
||||
def test_list_command_basic(self, mock_get_backend):
|
||||
"""Test basic list command functionality."""
|
||||
# This test will help us identify the existing bug
|
||||
from issue_core.core.models import Issue, IssueState
|
||||
from datetime import timezone
|
||||
|
||||
mock_backend = Mock()
|
||||
|
||||
# Create mock issues
|
||||
mock_issue1 = Mock()
|
||||
mock_issue1.number = 1
|
||||
mock_issue1.title = "First Issue"
|
||||
mock_issue1.state.value = "open"
|
||||
|
||||
mock_issue2 = Mock()
|
||||
mock_issue2.number = 2
|
||||
mock_issue2.title = "Second Issue"
|
||||
mock_issue2.state.value = "closed"
|
||||
|
||||
mock_backend.list_issues.return_value = [mock_issue1, mock_issue2]
|
||||
now = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
mock_backend.list_issues.return_value = [
|
||||
Issue(
|
||||
id="1",
|
||||
number=1,
|
||||
title="First Issue",
|
||||
description="",
|
||||
state=IssueState.OPEN,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
Issue(
|
||||
id="2",
|
||||
number=2,
|
||||
title="Second Issue",
|
||||
description="",
|
||||
state=IssueState.CLOSED,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
]
|
||||
mock_get_backend.return_value = mock_backend
|
||||
|
||||
result = self.runner.invoke(cli, ['list'])
|
||||
|
||||
# This might fail due to the existing bug, which is what we want to identify
|
||||
if result.exit_code != 0:
|
||||
print(f"List command failed with: {result.output}")
|
||||
print(f"Exception: {result.exception}")
|
||||
|
||||
# We expect this to work properly after fixes
|
||||
assert result.exit_code == 0 or "'Sentinel' object has no attribute 'lower'" in str(result.exception)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "First Issue" in result.output
|
||||
|
||||
|
||||
class TestBackendConfiguration:
|
||||
|
|
|
|||
129
tests/test_mapping.py
Normal file
129
tests/test_mapping.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""Unit tests for MappingService and status mapping."""
|
||||
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from issue_core.core.mapping import (
|
||||
MappingService,
|
||||
map_work_record_status_to_issue_state,
|
||||
)
|
||||
from issue_core.core.models import Issue, IssueState, Label
|
||||
from issue_core.backends.local import LocalSQLiteBackend
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_status_mapping_table():
|
||||
assert map_work_record_status_to_issue_state("todo") == "open"
|
||||
assert map_work_record_status_to_issue_state("progress") == "in_progress"
|
||||
assert map_work_record_status_to_issue_state("done") == "closed"
|
||||
assert map_work_record_status_to_issue_state("cancel") == "closed"
|
||||
assert map_work_record_status_to_issue_state("blocked") == "blocked"
|
||||
with pytest.raises(ValueError):
|
||||
map_work_record_status_to_issue_state("nope")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_upsert_idempotent(tmp_path):
|
||||
db = str(tmp_path / "mappings.db")
|
||||
svc = MappingService()
|
||||
svc.connect(db)
|
||||
wr = str(uuid.uuid4())
|
||||
m1 = svc.upsert(
|
||||
work_record_uuid=wr,
|
||||
work_record_id="ISSUE-WP-0005-T04",
|
||||
work_record_kind="task",
|
||||
backend="sqlite",
|
||||
external_id="1",
|
||||
direction="outward",
|
||||
)
|
||||
m2 = svc.upsert(
|
||||
work_record_uuid=wr,
|
||||
work_record_id="ISSUE-WP-0005-T04",
|
||||
work_record_kind="task",
|
||||
backend="sqlite",
|
||||
external_id="1",
|
||||
direction="outward",
|
||||
)
|
||||
assert m1.id == m2.id
|
||||
assert m2.external_id == "1"
|
||||
assert svc.get_active_by_canonical_id("ISSUE-WP-0005-T04").work_record_uuid == wr
|
||||
svc.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unique_external_conflict(tmp_path):
|
||||
db = str(tmp_path / "mappings.db")
|
||||
svc = MappingService()
|
||||
svc.connect(db)
|
||||
a = str(uuid.uuid4())
|
||||
b = str(uuid.uuid4())
|
||||
svc.upsert(work_record_uuid=a, backend="sqlite", external_id="9")
|
||||
with pytest.raises(ValueError, match="already mapped"):
|
||||
svc.upsert(work_record_uuid=b, backend="sqlite", external_id="9")
|
||||
svc.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_detach(tmp_path):
|
||||
db = str(tmp_path / "mappings.db")
|
||||
svc = MappingService()
|
||||
svc.connect(db)
|
||||
wr = str(uuid.uuid4())
|
||||
svc.upsert(work_record_uuid=wr, backend="gitea", external_id="42")
|
||||
detached = svc.detach(work_record_uuid=wr, backend="gitea")
|
||||
assert detached is not None
|
||||
assert detached.status == "detached"
|
||||
assert svc.get_active_by_uuid(wr) is None
|
||||
# Can re-map after detach
|
||||
again = svc.upsert(work_record_uuid=wr, backend="gitea", external_id="43")
|
||||
assert again.status == "active"
|
||||
assert again.external_id == "43"
|
||||
svc.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_project_flow_local_backend(tmp_path, monkeypatch):
|
||||
"""End-to-end: create issue on local backend + mapping row."""
|
||||
from datetime import datetime, timezone
|
||||
from issue_core.core.mapping import MappingService
|
||||
|
||||
issues_db = str(tmp_path / "issues.db")
|
||||
map_db = str(tmp_path / "mappings.db")
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({"db_path": issues_db})
|
||||
wr = str(uuid.uuid4())
|
||||
now = datetime.now(timezone.utc)
|
||||
issue = backend.create_issue(
|
||||
Issue(
|
||||
id="",
|
||||
number=0,
|
||||
title="Projected task",
|
||||
description="",
|
||||
state=IssueState.OPEN,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
labels=[Label(name="work_record:ISSUE-WP-0005-T04")],
|
||||
backend_type="local",
|
||||
)
|
||||
)
|
||||
svc = MappingService()
|
||||
svc.connect(map_db)
|
||||
mapping = svc.upsert(
|
||||
work_record_uuid=wr,
|
||||
work_record_id="ISSUE-WP-0005-T04",
|
||||
work_record_kind="task",
|
||||
backend="sqlite",
|
||||
external_id=issue.id or str(issue.number),
|
||||
direction="outward",
|
||||
)
|
||||
# push-status simulation
|
||||
target = map_work_record_status_to_issue_state("progress")
|
||||
issue.state = IssueState.from_string(target)
|
||||
updated = backend.update_issue(issue)
|
||||
assert updated.state == IssueState.IN_PROGRESS
|
||||
svc.mark_pushed(mapping.id)
|
||||
assert svc.get_active_by_uuid(wr).last_pushed_at is not None
|
||||
svc.disconnect()
|
||||
backend.disconnect()
|
||||
Loading…
Add table
Add a link
Reference in a new issue