@container-tool-exec
Feature: Container-aware tool execution and I/O forwarding
  As a CleverAgents developer
  I want tools to execute inside containers when the execution environment is CONTAINER
  So that file-based tools operate on the container filesystem transparently

  Background:
    Given I have the container tool execution module imported

  # -- PathMapper ---------------------------------------------------------

  Scenario: PathMapper maps host path to container path
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map host path "/tmp/sandbox/src/main.py" to container
    Then the container path should be "/workspace/src/main.py"

  Scenario: PathMapper maps container path to host path
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map container path "/workspace/src/main.py" to host
    Then the host path should be "/tmp/sandbox/src/main.py"

  Scenario: PathMapper returns unmapped path when outside root
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map host path "/usr/lib/libc.so" to container
    Then the container path should be "/usr/lib/libc.so"

  Scenario: PathMapper maps root path exactly
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map host path "/tmp/sandbox" to container
    Then the container path should be "/workspace"

  Scenario: PathMapper is_host_path returns true for host paths
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    Then "/tmp/sandbox/file.txt" should be a host path
    And "/usr/lib/libc.so" should not be a host path

  Scenario: PathMapper is_container_path returns true for container paths
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    Then "/workspace/file.txt" should be a container path
    And "/tmp/other/file.txt" should not be a container path

  # -- ContainerConfig ----------------------------------------------------

  Scenario: ContainerConfig has sensible defaults
    When I create a default ContainerConfig
    Then the workspace_folder should be "/workspace"
    And the container timeout_seconds should be 120

  # -- ContainerMetadata --------------------------------------------------

  Scenario: ContainerMetadata is frozen and holds execution details
    When I create a ContainerMetadata with container_id "abc123" and image "node:20"
    Then the container_metadata container_id should be "abc123"
    And the container_metadata image should be "node:20"
    And the container_metadata timed_out should be false

  # -- ContainerToolExecutor basic ----------------------------------------

  Scenario: ContainerToolExecutor can be instantiated with config
    Given I have a ContainerConfig with workspace "/workspace" and container_id "test-ctr"
    When I create a ContainerToolExecutor with that config
    Then the executor config container_id should be "test-ctr"
    And the executor should have a path mapper

  Scenario: ContainerToolExecutor maps input paths before execution
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool inputs with path "/tmp/sandbox/src/file.py"
    Then the mapped input path should be "/workspace/src/file.py"

  Scenario: ContainerToolExecutor maps output paths after execution
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool output with path "/workspace/build/output.txt"
    Then the mapped output path should be "/tmp/sandbox/build/output.txt"

  Scenario: ContainerToolExecutor handles nested input path mapping
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool inputs with nested paths under "/tmp/sandbox"
    Then all nested paths should be mapped to container paths

  # -- Timeout handling ---------------------------------------------------

  Scenario: ContainerToolExecutor reports timeout as structured error
    Given I have a ContainerToolExecutor with a mock that times out
    When I execute tool "file_read" with inputs in the container
    Then the container tool result should indicate failure
    And the container tool result error should mention "timed out"
    And the tool result metadata should contain container info with timed_out true

  # -- Error reporting ----------------------------------------------------

  Scenario: ContainerToolExecutor reports non-zero exit as structured error
    Given I have a ContainerToolExecutor with a mock that returns exit code 1
    When I execute tool "file_read" with inputs in the container
    Then the container tool result should indicate failure
    And the container tool result error should mention "exit code 1"

  Scenario: ContainerToolExecutor reports OSError as structured error
    Given I have a ContainerToolExecutor with subprocess raising OSError
    When I execute tool "compile" with inputs in the container
    Then the container tool result should indicate failure

  # -- Successful execution -----------------------------------------------

  Scenario: ContainerToolExecutor returns parsed JSON output on success
    Given I have a ContainerToolExecutor with a mock that returns JSON output
    When I execute tool "lint" with inputs in the container
    Then the container tool result should indicate success
    And the tool result output should contain the parsed JSON
    And the tool result metadata should contain container info

  Scenario: ContainerToolExecutor returns raw output when not JSON
    Given I have a ContainerToolExecutor with a mock that returns plain text
    When I execute tool "echo" with inputs in the container
    Then the container tool result should indicate success
    And the tool result output should contain raw_output

  # -- Command building ---------------------------------------------------

  Scenario: _build_exec_command places --container-id flag adjacent to its value
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I build an exec command for tool "test_tool" with timeout 30
    Then the command should have "--container-id" adjacent to its value

  # -- ToolRunner container routing ---------------------------------------

  @tdd_issue @tdd_issue_4243 @tdd_expected_fail
  Scenario: ToolRunner delegates to ContainerToolExecutor when env is CONTAINER
    Given I have a ToolRunner with a ContainerToolExecutor mock
    When I execute a tool with container execution environment
    Then the ContainerToolExecutor should have been called
    And the resolver should have been called with the correct arguments

  @tdd_issue @tdd_issue_4243 @tdd_expected_fail
  Scenario: ToolRunner returns error when container executor is not configured
    Given I have a ToolRunner without a ContainerToolExecutor
    When I execute a tool with container execution environment
    Then the container tool result error should mention "ContainerToolExecutor must be configured"

  # -- ToolInvocation audit trail -----------------------------------------

  Scenario: ToolInvocation has container_metadata field
    When I create a ToolInvocation with container_metadata
    Then the ToolInvocation container_metadata should contain "container_id"
    And the ToolInvocation container_metadata should contain "image"

  Scenario: ToolInvocation container_metadata defaults to None
    When I create a ToolInvocation without container_metadata
    Then the ToolInvocation container_metadata should be None

  # -- ContainerExecutionError --------------------------------------------

  Scenario: ContainerExecutionError carries exit_code and stderr
    When I raise a ContainerExecutionError with exit_code 2 and stderr "oops"
    Then the error exit_code should be 2
    And the error stderr should be "oops"

  Scenario: ContainerTimeoutError carries timeout_seconds
    When I raise a ContainerTimeoutError with timeout 30
    Then the timeout error message should mention "30"
    And the timeout error timed_out should be true

  # -- Signal exit code preservation ----------------------------------------

  Scenario: ContainerToolExecutor preserves negative exit codes from signal kills
    Given I have a ContainerToolExecutor with a mock that returns exit code -9
    When I execute tool "compile" with inputs in the container
    Then the container tool result should indicate failure
    And the tool result metadata should have exit_code -9

  # -- Path mapping with spaces --------------------------------------------

  Scenario: _looks_like_path accepts paths with spaces
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool inputs with path "/tmp/sandbox/My Documents/file.py"
    Then the mapped input path should be "/workspace/My Documents/file.py"

  # -- Output truncation ---------------------------------------------------

  Scenario: ContainerToolExecutor truncates output exceeding max bytes
    Given I have a ContainerToolExecutor with a mock that returns oversized output
    When I execute tool "big_output" with inputs in the container
    Then the container tool result stdout should be truncated

  # -- Path traversal protection -------------------------------------------

  Scenario: sync_results_to_host rejects path traversal attempts
    Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir
    When I attempt to sync a path that traverses outside the sandbox
    Then a ContainerExecutionError should be raised for path traversal

  # -- Sync results -------------------------------------------------------

  Scenario: sync_results_to_host maps container path to host
    Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir
    When I sync container path "/workspace/output.txt" to host
    Then the synced host path should be under the host sandbox root

  # -- execute_tool input validation (P2-19) --------------------------------

  Scenario: execute_tool rejects empty tool_name
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I execute tool with empty tool_name
    Then a ValueError should be raised with message "non-empty"

  Scenario: execute_tool rejects non-dict inputs
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I execute tool with non-dict inputs
    Then a container TypeError should be raised with message "must be a dict"

  Scenario: execute_tool rejects negative timeout_seconds
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I execute tool with negative timeout
    Then a ValueError should be raised with message "must be > 0"

  # -- extract_container_metadata helper (P1-9) -----------------------------

  Scenario: extract_container_metadata returns container dict from ToolResult
    When I create a ToolResult with container metadata in metadata dict
    Then extract_container_metadata should return the container dict

  # =========================================================================
  # Safe initialization — PathMapper validation guards
  # =========================================================================

  Scenario: PathMapper rejects null bytes in host_root
    When I try to create a PathMapper with null bytes in host_root
    Then a PathMapper ValueError should be raised mentioning "null"

  Scenario: PathMapper rejects null bytes in container_root
    When I try to create a PathMapper with null bytes in container_root
    Then a PathMapper ValueError should be raised mentioning "null"

  Scenario: PathMapper rejects empty host_root
    When I try to create a PathMapper with empty host_root
    Then a PathMapper ValueError should be raised mentioning "empty"

  Scenario: PathMapper rejects filesystem-root host_root
    When I try to create a PathMapper with host_root "/"
    Then a PathMapper ValueError should be raised mentioning "/"

  Scenario: PathMapper rejects filesystem-root container_root
    When I try to create a PathMapper with container_root "/"
    Then a PathMapper ValueError should be raised mentioning "/"

  Scenario: PathMapper rejects overlapping roots
    When I try to create a PathMapper with overlapping roots
    Then a PathMapper ValueError should be raised mentioning "overlap"

  Scenario: PathMapper is_host_path returns false for null-byte paths
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    Then a path with null bytes should not be a host path

  Scenario: PathMapper is_container_path returns false for null-byte paths
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    Then a path with null bytes should not be a container path

  # =========================================================================
  # Safe initialization — ContainerConfig workspace_folder guards
  # =========================================================================

  Scenario: ContainerConfig rejects relative workspace_folder
    When I try to create a ContainerConfig with workspace_folder "relative/path"
    Then a ContainerConfig ValueError should be raised mentioning "absolute"

  Scenario: ContainerConfig rejects workspace_folder that normalises to root
    When I try to create a ContainerConfig with workspace_folder "/."
    Then a ContainerConfig ValueError should be raised mentioning "root"

  Scenario: ContainerConfig rejects workspace_folder with dotdot traversal
    When I try to create a ContainerConfig with workspace_folder "/workspace/../etc"
    Then a ContainerConfig ValueError should be raised mentioning ".."

  # =========================================================================
  # Safe initialization — ContainerToolExecutor binary resolution
  # =========================================================================

  Scenario: ContainerToolExecutor without devcontainer binary uses workspace-folder target args
    Given I have a ContainerToolExecutor with no container_id configured
    When I request the target CLI arguments
    Then the target args should use "--workspace-folder"

  Scenario: ContainerToolExecutor raises when devcontainer binary is missing at execution
    Given I have a ContainerToolExecutor with no devcontainer binary
    When I try to require the devcontainer binary
    Then a ContainerExecutionError should be raised about missing binary

  # =========================================================================
  # Cleanup — sync_results_to_host error paths
  # =========================================================================

  Scenario: sync_results_to_host rejects null bytes in container_path
    Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir
    When I try to sync a path containing null bytes
    Then a sync ValueError should be raised mentioning "null"

  Scenario: sync_results_to_host rejects relative container_path
    Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir
    When I try to sync a relative container path
    Then a sync ValueError should be raised mentioning "absolute"

  Scenario: sync_results_to_host raises on timeout during file sync
    Given I have a ContainerToolExecutor with a mock that times out on sync
    When I try to sync container path to host with timeout
    Then a ContainerTimeoutError should be raised from sync

  Scenario: sync_results_to_host raises on non-zero exit during file sync
    Given I have a ContainerToolExecutor with a mock that returns non-zero exit on sync
    When I try to sync container path to host with failure
    Then a ContainerExecutionError should be raised from sync failure

  Scenario: sync_results_to_host raises on OSError during file write
    Given I have a ContainerToolExecutor with a mock that triggers OSError on write
    When I try to sync container path to host triggering write error
    Then a ContainerExecutionError should be raised from write failure

  # =========================================================================
  # Safe execution — _looks_like_path heuristic boundary checks
  # =========================================================================

  Scenario: _looks_like_path rejects strings with newlines
    When I check whether a string with newlines looks like a path
    Then it should not look like a path

  Scenario: _looks_like_path rejects strings with null bytes
    When I check whether a string with null bytes looks like a path
    Then it should not look like a path

  Scenario: _looks_like_path rejects protocol-relative URIs
    When I check whether a protocol-relative URI looks like a path
    Then it should not look like a path

  Scenario: _looks_like_path rejects strings with query parameters
    When I check whether a path with query string looks like a path
    Then it should not look like a path

  Scenario: _looks_like_path rejects strings with fragment identifiers
    When I check whether a path with fragment identifier looks like a path
    Then it should not look like a path

  Scenario: _looks_like_path rejects strings with scheme separators
    When I check whether a URL-like string looks like a path
    Then it should not look like a path

  # =========================================================================
  # Safe execution — recursion depth guards on path mapping
  # =========================================================================

  Scenario: _map_input_paths stops at max recursion depth
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map deeply nested input paths exceeding the recursion limit
    Then the input mapping should return without error

  Scenario: _map_output_paths stops at max recursion depth
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map deeply nested output paths exceeding the recursion limit
    Then the output mapping should return without error

  # =========================================================================
  # Safe execution — _parse_output edge cases
  # =========================================================================

  Scenario: _parse_output returns result key for non-dict JSON
    When I parse stdout containing a JSON array
    Then the parsed output should wrap it under "result" key

  Scenario: _parse_output returns empty dict for blank stdout
    When I parse empty stdout
    Then the parsed output should be an empty dict

  # =========================================================================
  # Safe execution — _map_output_paths skips raw_output key
  # =========================================================================

  Scenario: _map_output_paths preserves raw_output without path mapping
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map output containing a raw_output key with container path
    Then the raw_output value should remain unmapped

  # =========================================================================
  # Safe execution — container tool non-JSON-serialisable inputs
  # =========================================================================

  Scenario: execute_tool returns error for non-JSON-serialisable inputs
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I execute a tool with non-serialisable inputs
    Then the container tool result should indicate failure
    And the container tool result error should mention "JSON-serialisable"

  # =========================================================================
  # ToolRunner full lifecycle — discover, activate, deactivate
  # =========================================================================

  Scenario: ToolRunner discover returns tools from registry
    Given I have a ToolRunner with a populated registry
    When I call discover on the runner
    Then the discovered tools list should not be empty

  Scenario: ToolRunner activate marks a tool as ready
    Given I have a ToolRunner with a populated registry
    When I activate a tool on the runner
    Then the activated tool spec should be returned

  Scenario: ToolRunner activate raises ToolError for unknown tool
    Given I have a ToolRunner with a populated registry
    When I try to activate an unknown tool
    Then a ToolError should be raised for activation

  Scenario: ToolRunner deactivate cleans up active tool
    Given I have a ToolRunner with a populated registry
    When I activate and then deactivate a tool
    Then deactivate should return true
    And a second deactivate should return false

  # =========================================================================
  # ToolRunner host execution path
  # =========================================================================

  Scenario: ToolRunner executes tool on host and returns success
    Given I have a ToolRunner with a host-routed tool
    When I execute the host tool with valid inputs
    Then the host tool result should indicate success
    And the host tool result output should contain handler data

  Scenario: ToolRunner normalises non-dict handler output
    Given I have a ToolRunner with a tool returning non-dict output
    When I execute the non-dict-output tool
    Then the result output should wrap the value under "result"

  Scenario: ToolRunner catches handler exception and returns error
    Given I have a ToolRunner with a tool that raises an exception
    When I execute the raising tool
    Then the tool result should indicate failure with exception info

  Scenario: ToolRunner returns error for non-JSON-serialisable host output
    Given I have a ToolRunner with a tool returning non-serialisable output
    When I execute the non-serialisable-output tool
    Then the tool result should indicate failure mentioning "not JSON-serialisable"

  Scenario: ToolRunner returns error for non-JSON-serialisable host inputs
    Given I have a ToolRunner with a host-routed tool
    When I execute the host tool with non-serialisable inputs
    Then the tool result should indicate failure mentioning "not JSON-serialisable"

  Scenario: ToolRunner raises ToolError for unregistered tool in execute
    Given I have a ToolRunner with an empty registry
    When I try to execute an unregistered tool
    Then a ToolError should be raised for execution

  # =========================================================================
  # ToolRunner container routing — additional edge cases
  # =========================================================================

  @tdd_issue @tdd_issue_4243 @tdd_expected_fail
  Scenario: ToolRunner returns error when container env resolver raises ContainerUnavailableError
    Given I have a ToolRunner with an unavailable-container resolver
    When I execute a tool through the unavailable-container runner
    Then the tool result should indicate container unavailable

  @tdd_issue @tdd_issue_4243 @tdd_expected_fail
  Scenario: ToolRunner validates missing required input fields for container tools
    Given I have a ToolRunner with a container executor and required-field tool
    When I execute the container tool with missing required fields
    Then the tool result error should mention "Missing required input fields"

  Scenario: ToolRunner returns error for non-JSON-serialisable container inputs
    Given I have a ToolRunner with a container executor for JSON validation
    When I execute a container tool with non-serialisable inputs
    Then the container tool result error should mention "not JSON-serialisable"

  @tdd_issue @tdd_issue_4243 @tdd_expected_fail
  Scenario: ToolRunner catches container executor exception
    Given I have a ToolRunner with a failing container executor
    When I execute a tool through the failing container runner
    Then the tool result error should mention "Container execution error"

  # =========================================================================
  # Safe execution — list-type value mapping branches
  # =========================================================================

  Scenario: _map_input_paths handles list values containing host paths
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool inputs containing a list of host paths
    Then the list values should be mapped to container paths

  Scenario: _map_output_paths handles list values containing container paths
    Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace"
    When I map tool output containing a list of container paths
    Then the list values should be mapped to host paths

  # =========================================================================
  # Safe execution — PathMapper null byte rejection in mapping methods
  # =========================================================================

  Scenario: PathMapper host_to_container rejects null bytes
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I try to map a host path with null bytes to container
    Then a mapping ValueError should be raised about null bytes

  Scenario: PathMapper container_to_host rejects null bytes
    Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace"
    When I try to map a container path with null bytes to host
    Then a mapping ValueError should be raised about null bytes

  # =========================================================================
  # ToolResult validation — success/error consistency
  # =========================================================================

  Scenario: ToolResult rejects success=True with error message
    When I try to create a ToolResult with success true and an error
    Then a ToolResult validation error should be raised

  Scenario: ToolResult rejects success=False without error message
    When I try to create a ToolResult with success false and no error
    Then a ToolResult validation error should be raised

  # =========================================================================
  # ToolError structured attributes
  # =========================================================================

  Scenario: ToolError carries structured context attributes
    When I create a ToolError with known attributes
    Then the ToolError should expose tool_name, error_type, and details
