Class: Rubino::Tools::TestTool

Inherits:
Base
  • Object
show all
Defined in:
lib/rubino/tools/test_tool.rb

Overview

Runs the workspace project’s test suite and returns a STRUCTURED result instead of the raw toolchain firehose the ‘shell` tool emits.

Why this exists (issue #101): to run tests the model used to drive ‘shell` and reason its way through the whole Ruby toolchain — bundler version mismatches, missing gems, which command to use. On real tasks that burned several tool calls and twice sent the agent chasing toolchain errors (bundler `GemNotFound`, an `undefined method ’untaint’‘ crash from an old pinned bundler) instead of the user’s actual request; one earlier run even drifted toward ‘gem uninstall bundler` / `rm -rf …`. This tool:

- auto-detects the framework (rspec / minitest / rake) and the right
  invocation, preferring `bundle exec` when a Gemfile is present and the
  bundle is usable, falling back to the bare runner when it is not (so a
  stale lockfile degrades gracefully rather than making the model fight
  bundler),
- returns pass/fail counts, the failing examples (name + file:line +
  short message) parsed from the runner output, and a short raw tail —
  not the full backtrace,
- distinguishes "the suite could not even start" (toolchain error) from
  "the suite ran and N failed", via the structured `error_code`.

Execution mirrors ShellTool’s foreground path: own process group, SIGTERM on timeout/cancel, cwd = workspace root (same resolution as ruby/shell).

Constant Summary collapse

DEFAULT_TIMEOUT =
300
MAX_TIMEOUT =
600
TICK =
0.05
RAW_TAIL_LINES =

Lines of raw runner output to keep for context. Enough to show the tail of a failure dump without dragging the full backtrace into context.

40

Instance Attribute Summary

Attributes inherited from Base

#cancel_token, #read_tracker, #stream_chunk

Instance Method Summary collapse

Methods inherited from Base

#cancellation_requested?, #config_key, #emit_chunk, #risky?, #to_tool_definition, workspace_root, workspace_roots

Instance Method Details

#call(arguments) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/rubino/tools/test_tool.rb', line 83

def call(arguments)
  args      = arguments.is_a?(Hash) ? arguments : {}
  path      = args["path"]      || args[:path]
  override  = args["framework"] || args[:framework]
  timeout   = (args["timeout"]  || args[:timeout] || DEFAULT_TIMEOUT).to_i
  timeout   = [[timeout, 1].max, MAX_TIMEOUT].min

  root = resolve_workspace
  return { output: "Error: cannot access workspace directory", error_code: :workspace_error } unless root

  framework = (override && !override.to_s.empty? ? override.to_s : detect_framework(root))
  unless framework
    return { output: "Error: no test setup detected in #{root} — looked for " \
                     "spec/ (.rspec), test/, and a Rakefile. Pass `framework` " \
                     "to override, or use the shell tool for a custom command.",
             error_code: :no_test_setup }
  end

  command = build_command(root, framework, path)
  run     = execute(command, root, timeout)

  build_result(framework, command, run)
end

#descriptionObject



41
42
43
44
45
46
47
48
49
50
# File 'lib/rubino/tools/test_tool.rb', line 41

def description
  "Run the workspace project's test suite and return a structured result " \
    "(framework, command, exit status, example/failure counts, and the " \
    "failing examples with file:line and message). Auto-detects RSpec, " \
    "Minitest, or a Rakefile default task; prefers `bundle exec` when a " \
    "Gemfile is present and falls back to the bare runner if the bundle is " \
    "broken. Optional `path` runs a single file or pattern; optional " \
    "`framework` (rspec/minitest/rake) overrides detection. Use this " \
    "instead of driving `shell` by hand to run tests."
end

#input_schemaObject



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rubino/tools/test_tool.rb', line 52

def input_schema
  {
    type: "object",
    properties: {
      path: {
        type: "string",
        description: "Optional file or pattern to run a subset (e.g. " \
                     "'spec/models/user_spec.rb' or 'spec/models/'). " \
                     "Runs the whole suite when omitted."
      },
      framework: {
        type: "string",
        enum: %w[rspec minitest rake],
        description: "Override framework detection. Omit to auto-detect."
      },
      timeout: {
        type: "integer",
        description: "Timeout in seconds (default #{DEFAULT_TIMEOUT}, max #{MAX_TIMEOUT})."
      }
    },
    required: []
  }
end

#nameObject



37
38
39
# File 'lib/rubino/tools/test_tool.rb', line 37

def name
  "run_tests"
end

#risk_levelObject

Runs project code (the test suite), so gated like ‘ruby`: not destructive, but it does execute arbitrary code. :medium → asks in manual mode, auto-allowed in auto mode.



79
80
81
# File 'lib/rubino/tools/test_tool.rb', line 79

def risk_level
  :medium
end