ask-ruby-harness 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd5a2b1d1916502e5eb5cc92220a4d06149817a05f2bf8ed00e8327630272881
4
- data.tar.gz: 847841b9d7fd758f8fb2c3da5d19e8b51414f3e3f2ee94c8e398e3231abe3b6a
3
+ metadata.gz: cb7b07e97747d9737683a53429df81442e810b0ff9825724a02e64c49bb49c0b
4
+ data.tar.gz: 27b471f9c55911b41636bd3e81d0ea68fc4d168322fffdfaea387211fc58c13d
5
5
  SHA512:
6
- metadata.gz: 604554da7aa7e84bd2d779cd68123645757d185000d62575694e51201ef1086e5419e48d24c70679546098a4352bf9c686184ff22725fec22d0995aafa6e02f2
7
- data.tar.gz: 9f4bdf2110e1a2dd490cf59c0d0af545726bec839dc64f59e7cdfe8e6438170750ebe6e2dcc1530cedf51586001b8d7a686897b2c46368eefd60c2a3a25a010f
6
+ metadata.gz: 943ae9fcc4698ff7a6c79feba5d93f30b31975417c6355a38394e146308545ecef270ad70a0675addd2929361c8043fbe8bc6f24b6e439f416fa689e4b5b0861
7
+ data.tar.gz: ae876247273fa9e6936d0f817ac42f6b57c250abdc4fd7671403a7c289f9da0b03d20b7fb48b6685d7f168db854017a5790f2130430e20f6240b22ae79d477ff
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.2.0] — 2026-08-10
2
+
3
+ ### Added
4
+
5
+ - **Monorepo support in `run_tests`** — when the `file:` param points into a
6
+ subproject (a directory with its own Gemfile/Rakefile), the suite runs
7
+ there, so rake/rails resolve the subproject's tasks. Artifacts
8
+ (`tmp/test/.ask/`) live in the subproject too. (The MCP server for this
9
+ gem lives in the separate `ask-ruby-harness-mcp` gem.)
10
+
1
11
  ## [0.1.0] — 2026-08-10
2
12
 
3
13
  ### Added
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "active_support/notifications"
4
5
  require "active_support/core_ext/string/filters"
5
6
 
6
7
  module Ask
@@ -24,34 +24,38 @@ module Ask
24
24
 
25
25
  def execute(file: nil, name: nil, failed_only: false, timeout: DEFAULT_TIMEOUT)
26
26
  files = split_files(file)
27
- runner = detect_runner
27
+ # Monorepo support: a file inside a subproject (a dir with its
28
+ # own Gemfile/Rakefile) runs that project's suite.
29
+ run_root = resolve_run_root(files)
30
+ files = files.map { |f| rel_to_run_root(f, run_root) }
31
+ runner = detect_runner(run_root)
28
32
 
29
- failed_tests = failed_only ? load_failed_tests : nil
33
+ failed_tests = failed_only ? load_failed_tests(run_root) : nil
30
34
  if failed_only && failed_tests.empty?
31
35
  return Ask::Result.failure("No failed tests from the previous run to rerun.")
32
36
  end
33
37
 
34
- artifact_dir = app_root.join(*ARTIFACT_DIR).tap(&:mkpath)
38
+ artifact_dir = run_root.join(*ARTIFACT_DIR).tap(&:mkpath)
35
39
  log_path = artifact_dir.join("last-test.log")
36
40
  json_path = artifact_dir.join("last-test.json")
37
41
  status_path = artifact_dir.join("last-failures.json")
38
42
 
39
43
  command, env = build_command(runner, files, name, failed_tests, json_path)
40
- outcome = run(command, env, log_path, timeout)
44
+ outcome = run(command, env, log_path, timeout, run_root)
41
45
 
42
46
  results = parse_results(runner, json_path)
43
47
  unless results
44
48
  # A killed run can't produce results — report it structurally
45
49
  # instead of failing, so the agent still gets the artifact path.
46
- return Ask::Result.ok(data: timed_out_report(runner, command, env, outcome, status_path, log_path)) if outcome[:timed_out]
50
+ return Ask::Result.ok(data: timed_out_report(runner, command, env, outcome, status_path, log_path, run_root)) if outcome[:timed_out]
47
51
 
48
52
  return Ask::Result.failure(
49
53
  "Test run finished without machine-readable results (#{runner}); " \
50
- "full output at #{rel(log_path)}"
54
+ "full output at #{rel(log_path, run_root)}"
51
55
  )
52
56
  end
53
57
 
54
- report = build_report(runner, command, env, outcome, results, status_path, log_path)
58
+ report = build_report(runner, command, env, outcome, results, status_path, log_path, run_root)
55
59
  Ask::Result.ok(data: report)
56
60
  end
57
61
 
@@ -64,14 +68,39 @@ module Ask
64
68
 
65
69
  # Rails app → bin/rails test; rspec in the bundle with a spec/ dir →
66
70
  # rspec; anything else → minitest via rake test.
67
- def detect_runner
68
- return :rails if app_root.join("bin", "rails").exist?
69
- lockfile = app_root.join("Gemfile.lock")
71
+ def detect_runner(run_root)
72
+ return :rails if run_root.join("bin", "rails").exist?
73
+ lockfile = run_root.join("Gemfile.lock")
70
74
  rspec = lockfile.exist? && lockfile.read.include?("rspec")
71
- return :rspec if rspec && app_root.join("spec").directory?
75
+ return :rspec if rspec && run_root.join("spec").directory?
72
76
  :minitest
73
77
  end
74
78
 
79
+ # --- monorepo support -------------------------------------------
80
+
81
+ # For a file inside a subproject of a monorepo (a dir with its own
82
+ # Gemfile/Rakefile), run the suite there so rake/rails resolve the
83
+ # subproject's tasks. Only when ALL files share the same subproject;
84
+ # otherwise (or at the app root itself) fall back to app_root.
85
+ def resolve_run_root(files)
86
+ roots = files.map { |f| project_root_for(f) }.uniq
87
+ roots.size == 1 && !roots.first.nil? ? roots.first : app_root
88
+ end
89
+
90
+ def project_root_for(file)
91
+ dir = File.expand_path(File.dirname(file), app_root)
92
+ root = app_root.to_s
93
+ while dir != root && dir.start_with?(root + File::SEPARATOR)
94
+ return Pathname.new(dir) if %w[Gemfile Rakefile].any? { |n| File.exist?(File.join(dir, n)) }
95
+ dir = File.dirname(dir)
96
+ end
97
+ nil
98
+ end
99
+
100
+ def rel_to_run_root(file, run_root)
101
+ Pathname.new(File.expand_path(file, app_root)).relative_path_from(run_root).to_s
102
+ end
103
+
75
104
  def build_command(runner, files, name, failed_tests, json_path)
76
105
  case runner
77
106
  when :rspec
@@ -129,7 +158,7 @@ module Ask
129
158
  "/#{escaped.join('|')}/"
130
159
  end
131
160
 
132
- def run(command, env, log_path, timeout)
161
+ def run(command, env, log_path, timeout, run_root)
133
162
  # The harness server may run with a deliberately small pool
134
163
  # (e.g. RAILS_MAX_THREADS=1 in its MCP config). Test runs are a
135
164
  # separate concern — let them use the app's normal pool sizes and
@@ -139,7 +168,7 @@ module Ask
139
168
  # would otherwise hijack it).
140
169
  child_env = env.merge("RAILS_MAX_THREADS" => nil)
141
170
  ENV.each_key { |k| child_env[k] = nil if k.start_with?("BUNDLE") }
142
- pid = Process.spawn(child_env, *command, chdir: app_root.to_s,
171
+ pid = Process.spawn(child_env, *command, chdir: run_root.to_s,
143
172
  out: [log_path.to_s, "w"], err: [:child, :out])
144
173
  status = nil
145
174
  timed_out = false
@@ -159,7 +188,7 @@ module Ask
159
188
  { exit_status: status&.exitstatus, timed_out: timed_out }
160
189
  end
161
190
 
162
- def build_report(runner, command, env, outcome, results, status_path, log_path)
191
+ def build_report(runner, command, env, outcome, results, status_path, log_path, run_root)
163
192
  summary = results[:summary]
164
193
  failed_tests = results[:failed_tests]
165
194
  persist_failed_tests(runner, failed_tests, status_path)
@@ -171,12 +200,12 @@ module Ask
171
200
  timed_out: outcome[:timed_out],
172
201
  summary: summary,
173
202
  failed_tests: failed_tests,
174
- artifact: rel(log_path),
203
+ artifact: rel(log_path, run_root),
175
204
  next: summary[:failures] + summary[:errors] > 0 ? "run_tests(failed_only: true)" : nil
176
205
  }
177
206
  end
178
207
 
179
- def timed_out_report(runner, command, env, outcome, status_path, log_path)
208
+ def timed_out_report(runner, command, env, outcome, status_path, log_path, run_root)
180
209
  persist_failed_tests(runner, [], status_path)
181
210
  {
182
211
  framework: runner.to_s,
@@ -185,7 +214,7 @@ module Ask
185
214
  timed_out: true,
186
215
  summary: nil,
187
216
  failed_tests: nil,
188
- artifact: rel(log_path),
217
+ artifact: rel(log_path, run_root),
189
218
  next: nil
190
219
  }
191
220
  end
@@ -240,8 +269,8 @@ module Ask
240
269
  status_path.write(JSON.pretty_generate(framework: runner.to_s, failed_tests: failed_tests))
241
270
  end
242
271
 
243
- def load_failed_tests
244
- path = app_root.join(*ARTIFACT_DIR, "last-failures.json")
272
+ def load_failed_tests(run_root)
273
+ path = run_root.join(*ARTIFACT_DIR, "last-failures.json")
245
274
  return [] unless path.exist?
246
275
  JSON.parse(path.read).fetch("failed_tests", []).map { |t| symbolize_keys(t) }
247
276
  rescue JSON::ParserError
@@ -252,8 +281,8 @@ module Ask
252
281
  hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
253
282
  end
254
283
 
255
- def rel(path)
256
- path.relative_path_from(app_root).to_s
284
+ def rel(path, base = app_root)
285
+ path.relative_path_from(base).to_s
257
286
  end
258
287
  end
259
288
  end
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module Ruby
5
5
  module Harness
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-ruby-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto