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
24 changes: 20 additions & 4 deletions amplifier_module_provider_github_copilot/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,27 @@ def add(self, event: DomainEvent) -> None:
self.is_complete = True

def get_result(self) -> AccumulatedResponse:
"""Get accumulated response."""
"""Get accumulated response.

Applies the same finish_reason normalization as to_chat_response() so that
callers of either method see canonical values per streaming-contract:FinishReason:MUST:5.
"""
# streaming-contract:FinishReason:MUST:5 — normalize to lowercase canonical values
if self.tool_calls:
# Tool calls always override SDK finish_reason (deny/capture flow)
normalized_finish_reason: str | None = "tool_calls"
elif self.finish_reason:
normalized_finish_reason = self.finish_reason.lower()
else:
normalized_finish_reason = self.finish_reason

return AccumulatedResponse(
text_content=self.text_content,
thinking_content=self.thinking_content,
reasoning_opaque=self.reasoning_opaque,
tool_calls=self.tool_calls,
usage=self.usage,
finish_reason=self.finish_reason,
finish_reason=normalized_finish_reason,
error=self.error,
is_complete=self.is_complete,
)
Expand Down Expand Up @@ -381,8 +394,11 @@ def to_chat_response(self) -> "StreamingChatResponse":
# Use "stop" per amplifier-core proto (not "end_turn" which is an SDK input key)
normalized_finish_reason = "stop"
else:
# No tool calls but SDK provided finish_reason: preserve it
normalized_finish_reason = self.finish_reason
# No tool calls but SDK provided finish_reason: normalize to lowercase
# streaming-contract:FinishReason:MUST:5 — finish_reason MUST be lowercase
# Defense-in-depth: translate_event() applies finish_reason_map first, but
# the accumulator normalizes here in case raw SDK values pass through directly.
normalized_finish_reason = self.finish_reason.lower()

# Contract: content_blocks is None when empty (not empty list)
# streaming-contract:StreamingResponse:MUST:4
Expand Down
57 changes: 0 additions & 57 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,22 +245,6 @@ def sdk_module() -> Any:
return pytest.importorskip("copilot", reason="github-copilot-sdk not installed")


@pytest.fixture
def restore_github_tokens() -> Generator[None, None, None]:
"""Restore saved GitHub tokens for live tests.

Use this fixture in @pytest.mark.live tests that need real tokens.
Tokens were saved and cleared at module load for security (P1-6).
"""
for var, val in _saved_tokens.items():
os.environ[var] = val
yield
# Restore cleared state after test
for var in _saved_tokens:
if var in os.environ:
del os.environ[var]


@pytest.fixture(autouse=True)
def _auto_restore_tokens_for_live_tests( # pyright: ignore[reportUnusedFunction]
request: pytest.FixtureRequest,
Expand All @@ -284,44 +268,3 @@ def _auto_restore_tokens_for_live_tests( # pyright: ignore[reportUnusedFunction
yield


@pytest.fixture
def mock_sdk_event_dict() -> dict[str, Any]:
"""Sample SDK event as dict for testing helpers.

Contract: sdk-boundary:EventShape:MUST:1
Reference: SDK SessionEvent structure from github-copilot-sdk

SDK v0.1.33+ uses nested data structure:
- event.data.delta_content for streaming deltas
- event.data.content for complete messages
"""
return {
"type": "assistant.message_delta",
"data": {
"delta_content": "hello",
"message_id": "msg_001",
},
}


@pytest.fixture
def mock_sdk_event_object() -> Any:
"""Sample SDK event as object for testing helpers.

Contract: sdk-boundary:EventShape:MUST:2, sdk-boundary:EventShape:MUST:3
Reference: SDK SessionEvent structure from github-copilot-sdk

Matches real SDK SessionEvent structure from generated SessionEvents.
"""

class MockData:
delta_content = "hello"
content = None
message_id = "msg_001"
reasoning_id = None

class MockEvent:
type = "assistant.message_delta"
data = MockData()

return MockEvent()
2 changes: 0 additions & 2 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
SessionEventType,
error_event,
idle_event,
message_complete_event,
text_delta_event,
usage_event,
)
Expand All @@ -33,7 +32,6 @@
"SessionEventType",
"idle_event",
"text_delta_event",
"message_complete_event",
"error_event",
"usage_event",
# SDK response fixtures
Expand Down
8 changes: 0 additions & 8 deletions tests/fixtures/sdk_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,6 @@ def text_delta_event(text: str) -> SessionEvent:
)


def message_complete_event(finish_reason: str = "stop") -> SessionEvent:
"""Create message complete event."""
return SessionEvent(
type=SessionEventType.ASSISTANT_MESSAGE,
data=SessionEventData(finish_reason=finish_reason),
)


def error_event(message: str) -> SessionEvent:
"""Create error event."""
return SessionEvent(
Expand Down
34 changes: 2 additions & 32 deletions tests/fixtures/sdk_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,5 @@ class MockUsage:
total_tokens: int = 0


# Pre-built fixtures for common test scenarios


def create_simple_response(content: str) -> MockSDKResponse:
"""Create a simple SDK response with text content."""
return MockSDKResponse(data=MockData(content=content))


def create_empty_response() -> MockSDKResponse:
"""Create an SDK response with empty content."""
return MockSDKResponse(data=MockData(content=""))


def create_none_response() -> MockSDKResponse:
"""Create an SDK response with None data."""
return MockSDKResponse(data=None)


def create_dict_response(content: str) -> MockSDKResponse:
"""Create an SDK response with dict data (backward compat)."""
return MockSDKResponse(data={"content": content})


# Fixture registry for parametrized tests
SDK_RESPONSE_FIXTURES = {
"simple": create_simple_response("Hello, world!"),
"empty": create_empty_response(),
"none": create_none_response(),
"dict": create_dict_response("Dict content"),
"multiline": create_simple_response("Line 1\nLine 2\nLine 3"),
"unicode": create_simple_response("Unicode: 你好世界 🌍"),
}
# Pre-built fixtures for common test scenarios and SDK_RESPONSE_FIXTURES removed —
# zero consumers found (orphaned from deleted test files).
132 changes: 0 additions & 132 deletions tests/sdk_helpers.py

This file was deleted.

Loading