Feature: Session workflow coverage boost
  Coverage boost for session_workflow.py helper functions (M1, n4).

  Background:
    Given the session workflow coverage environment is set up

  # _extract_content
  Scenario: _extract_content extracts text from content attribute
    Given coverage boost a mock response with content "hello world"
    When coverage boost _extract_content is called
    Then coverage boost the extracted result should be "hello world"

  Scenario: _extract_content falls back to text attribute
    Given coverage boost a mock response with text "from text attr" and no content
    When coverage boost _extract_content is called
    Then coverage boost the extracted result should be "from text attr"

  Scenario: _extract_content handles list content
    Given coverage boost a mock response with list content containing text dicts and plain strings
    When coverage boost _extract_content is called
    Then coverage boost the result should contain the concatenated texts

  Scenario: _extract_content falls back to str for unknown types
    Given coverage boost a mock response with no content or text attribute
    When coverage boost _extract_content is called
    Then coverage boost the result should be a string

  # _extract_token_usage
  Scenario: _extract_token_usage reads from response_metadata.usage
    Given coverage boost a mock response with response_metadata usage input_tokens=100 output_tokens=50
    When coverage boost _extract_token_usage is called
    Then coverage boost input tokens should be 100 and output tokens should be 50

  Scenario: _extract_token_usage reads from response_metadata.token_usage
    Given coverage boost a mock response with response_metadata token_usage input_tokens=200 output_tokens=100
    When coverage boost _extract_token_usage is called
    Then coverage boost input tokens should be 200 and output tokens should be 100

  Scenario: _extract_token_usage reads prompt_tokens and completion_tokens
    Given coverage boost a mock response with response_metadata usage prompt_tokens=300 completion_tokens=150
    When coverage boost _extract_token_usage is called
    Then coverage boost input tokens should be 300 and output tokens should be 150

  Scenario: _extract_token_usage reads from usage_metadata
    Given coverage boost a mock response with usage_metadata input_tokens=400 output_tokens=200
    When coverage boost _extract_token_usage is called
    Then coverage boost input tokens should be 400 and output tokens should be 200

  Scenario: _extract_token_usage returns zeros for no metadata
    Given coverage boost a mock response with no usage metadata
    When coverage boost _extract_token_usage is called
    Then coverage boost input tokens should be 0 and output tokens should be 0

  # _estimate_cost
  Scenario: _estimate_cost computes cost from token counts
    Given coverage boost input tokens 1000 and output tokens 500
    When coverage boost _estimate_cost is called
    Then coverage boost the estimated cost should be positive

  # _history_to_langchain_messages
  Scenario: _history_to_langchain_messages converts all roles
    Given coverage boost session messages with roles SYSTEM, USER, ASSISTANT, and TOOL
    When coverage boost _history_to_langchain_messages is called
    Then coverage boost the result should contain SystemMessage, HumanMessage, AIMessage, and ToolMessage

  Scenario: _history_to_langchain_messages treats unknown role as human
    Given coverage boost a session message with an unknown role
    When coverage boost _history_to_langchain_messages is called
    Then coverage boost the result should contain a HumanMessage

  Scenario: _history_to_langchain_messages handles empty list
    Given coverage boost an empty list of session messages
    When coverage boost _history_to_langchain_messages is called
    Then coverage boost the result should be an empty list

  # LangChainSessionCaller.invoke() tool_results branch
  Scenario: LangChainSessionCaller.invoke appends tool results
    Given coverage boost a LangChainSessionCaller with a stub LLM and empty history
    When coverage boost invoke is called with tool_results containing one success and one failure
    Then coverage boost the accumulated messages should include tool result messages

  # LangChainSessionCaller.invoke() with tool_calls in response
  Scenario: LangChainSessionCaller.invoke extracts tool calls from response
    Given coverage boost a LangChainSessionCaller with a stub LLM that returns tool calls
    When coverage boost invoke is called for the first time
    Then coverage boost the LLMResponse should contain the extracted tool calls

  # _build_lc_messages_from_history
  Scenario: _build_lc_messages_from_history adds system prompt when absent
    Given coverage boost a SessionWorkflow with a stub service and no registry
    And coverage boost session history without a system message
    When coverage boost _build_lc_messages_from_history is called with a prompt
    Then coverage boost the first message should be a SystemMessage with the session system prompt

  # _MinimalStubLLM
  Scenario: _MinimalStubLLM.invoke returns stub response
    Given coverage boost a _MinimalStubLLM instance
    When coverage boost invoke on the stub is called
    Then coverage boost the stub response content should be "(no LLM configured)"
    And coverage boost the stub response should have empty tool_calls

  Scenario: _MinimalStubLLM.stream yields stub chunk
    Given coverage boost a _MinimalStubLLM instance
    When coverage boost stream on the stub is called
    Then coverage boost it should yield a chunk with content "(no LLM configured)"
