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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/ask/ruby/harness/audit_log.rb +1 -0
- data/lib/ask/ruby/harness/tools/run_tests.rb +50 -21
- data/lib/ask/ruby/harness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb7b07e97747d9737683a53429df81442e810b0ff9825724a02e64c49bb49c0b
|
|
4
|
+
data.tar.gz: 27b471f9c55911b41636bd3e81d0ea68fc4d168322fffdfaea387211fc58c13d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
69
|
-
lockfile =
|
|
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 &&
|
|
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:
|
|
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 =
|
|
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(
|
|
284
|
+
def rel(path, base = app_root)
|
|
285
|
+
path.relative_path_from(base).to_s
|
|
257
286
|
end
|
|
258
287
|
end
|
|
259
288
|
end
|