ace-test-runner-e2e 0.40.5 → 0.41.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 +28 -0
- data/lib/ace/test/end_to_end_runner/cli/commands/run_suite.rb +38 -6
- data/lib/ace/test/end_to_end_runner/molecules/bwrap_sandbox_backend.rb +11 -5
- data/lib/ace/test/end_to_end_runner/molecules/failure_finder.rb +59 -10
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_executor.rb +49 -3
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_prompt_bundler.rb +2 -1
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_report_generator.rb +27 -3
- data/lib/ace/test/end_to_end_runner/molecules/release_manifest.rb +212 -0
- data/lib/ace/test/end_to_end_runner/molecules/report_writer.rb +5 -0
- data/lib/ace/test/end_to_end_runner/molecules/setup_executor.rb +252 -3
- data/lib/ace/test/end_to_end_runner/organisms/suite_orchestrator.rb +25 -1
- data/lib/ace/test/end_to_end_runner/organisms/test_orchestrator.rb +287 -14
- data/lib/ace/test/end_to_end_runner/version.rb +1 -1
- data/lib/ace/test/end_to_end_runner.rb +1 -0
- metadata +5 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "date"
|
|
5
|
+
require "json"
|
|
5
6
|
require "yaml"
|
|
6
7
|
require "ace/b36ts"
|
|
7
8
|
require "ace/test_support/sandbox_package_copy"
|
|
@@ -23,6 +24,11 @@ module Ace
|
|
|
23
24
|
#
|
|
24
25
|
# For API providers: orchestrator writes reports as before.
|
|
25
26
|
class TestOrchestrator
|
|
27
|
+
# Raised when deterministic sandbox setup fails. The scenario must
|
|
28
|
+
# not be handed to the LLM afterwards: no goal (and therefore no
|
|
29
|
+
# install command) may run from a failed setup.
|
|
30
|
+
class SandboxSetupFailed < StandardError; end
|
|
31
|
+
|
|
26
32
|
# @param provider [String] LLM provider:model string
|
|
27
33
|
# @param timeout [Integer] Request timeout per test in seconds
|
|
28
34
|
# @param parallel [Integer] Number of tests to run in parallel
|
|
@@ -30,10 +36,12 @@ module Ace
|
|
|
30
36
|
# @param timestamp_generator [#call] Callable that returns a timestamp string
|
|
31
37
|
# @param executor [#execute] Injectable test executor (for testing)
|
|
32
38
|
# @param progress [Boolean] Enable animated progress display
|
|
39
|
+
# @param release_manifest_input [String, nil] Explicit release manifest path; when nil,
|
|
40
|
+
# the single allow-listed host input ACE_RELEASE_MANIFEST is honored.
|
|
33
41
|
def initialize(provider: nil, timeout: nil, parallel: nil, base_dir: nil, timestamp_generator: nil,
|
|
34
42
|
executor: nil, progress: false, discoverer: nil, integration_runner: nil,
|
|
35
43
|
scenario_loader: nil, report_writer: nil, suite_report_writer: nil,
|
|
36
|
-
setup_executor_factory: nil, runtime_builder: nil)
|
|
44
|
+
setup_executor_factory: nil, runtime_builder: nil, release_manifest_input: nil)
|
|
37
45
|
config = Molecules::ConfigLoader.load
|
|
38
46
|
@provider = provider || config.dig("execution", "runner_provider") ||
|
|
39
47
|
config.dig("execution", "provider") || "claude:sonnet"
|
|
@@ -42,6 +50,9 @@ module Ace
|
|
|
42
50
|
@base_dir = base_dir || Dir.pwd
|
|
43
51
|
@timestamp_generator = timestamp_generator || method(:default_timestamp)
|
|
44
52
|
@progress = progress
|
|
53
|
+
@release_manifest_input = sanitize_release_manifest_input(
|
|
54
|
+
release_manifest_input || ENV["ACE_RELEASE_MANIFEST"]
|
|
55
|
+
)
|
|
45
56
|
@discoverer = discoverer || Molecules::TestDiscoverer.new
|
|
46
57
|
@integration_runner = integration_runner || Molecules::IntegrationRunner.new(base_dir: @base_dir)
|
|
47
58
|
@loader = scenario_loader || Molecules::ScenarioLoader.new
|
|
@@ -128,11 +139,15 @@ module Ace
|
|
|
128
139
|
# @param scenario [Models::TestScenario] The test scenario
|
|
129
140
|
# @param timestamp [String] Timestamp for sandbox directory naming
|
|
130
141
|
# @param output [IO] Output stream for progress messages
|
|
142
|
+
# @param sandbox_dir_override [String, nil] Explicit sandbox directory (used by
|
|
143
|
+
# report-dir retries so setup and execution share one sandbox path)
|
|
144
|
+
# @param state_file [String, nil] Host-side retry-state path (report directory)
|
|
131
145
|
# @return [Array(String, Hash, SetupExecutor)] [sandbox_path, env_vars, setup_executor] or [nil, nil, nil]
|
|
132
|
-
def setup_sandbox_if_ts(scenario, timestamp, output)
|
|
146
|
+
def setup_sandbox_if_ts(scenario, timestamp, output, sandbox_dir_override: nil, state_file: nil)
|
|
133
147
|
return [nil, nil, nil] unless cli_provider? && scenario.setup_steps.any?
|
|
134
148
|
|
|
135
|
-
sandbox_dir =
|
|
149
|
+
sandbox_dir = sandbox_dir_override ||
|
|
150
|
+
File.join(@base_dir, ".ace-local", "test-e2e", scenario.dir_name(timestamp))
|
|
136
151
|
setup_steps = effective_setup_steps_for(scenario)
|
|
137
152
|
package_copy = Ace::TestSupport::SandboxPackageCopy.new(source_root: @base_dir)
|
|
138
153
|
package_source = File.join(@base_dir, scenario.package.to_s)
|
|
@@ -176,13 +191,18 @@ module Ace
|
|
|
176
191
|
scenario,
|
|
177
192
|
protocol_packages,
|
|
178
193
|
setup_steps: setup_steps
|
|
179
|
-
)
|
|
194
|
+
),
|
|
195
|
+
release_manifest_path: @release_manifest_input,
|
|
196
|
+
state_file: state_file
|
|
180
197
|
)
|
|
181
198
|
|
|
182
199
|
unless result[:success]
|
|
183
|
-
output.puts "
|
|
200
|
+
output.puts "Sandbox setup failed: #{result[:error]}"
|
|
201
|
+
# Teardown before raising: the caller's rescue fires before its
|
|
202
|
+
# local variable is assigned, so its ensure would skip cleanup
|
|
203
|
+
# of resources this executor already created (tmux sessions).
|
|
184
204
|
setup_executor.teardown
|
|
185
|
-
|
|
205
|
+
raise SandboxSetupFailed, result[:error].to_s
|
|
186
206
|
end
|
|
187
207
|
|
|
188
208
|
env = result[:env]
|
|
@@ -216,6 +236,89 @@ module Ace
|
|
|
216
236
|
end
|
|
217
237
|
end
|
|
218
238
|
|
|
239
|
+
def sanitize_release_manifest_input(raw)
|
|
240
|
+
value = raw.to_s.strip
|
|
241
|
+
value.empty? ? nil : value
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Reuse of a completed sandbox restores the environment recorded by
|
|
245
|
+
# the original setup, so later pipeline phases see the same env
|
|
246
|
+
# contract (PROJECT_ROOT_PATH, source root, manifest inputs).
|
|
247
|
+
|
|
248
|
+
# TS-MONO-001-style scenarios depend on deterministic setup gates
|
|
249
|
+
# (release-manifest validation) that only the CLI-provider pipeline
|
|
250
|
+
# runs. An API-provider run must not execute them at all.
|
|
251
|
+
def assert_deterministic_gate_supported!(scenario)
|
|
252
|
+
has_manifest_gate = scenario.setup_steps.any? do |step|
|
|
253
|
+
step.is_a?(Hash) && step.key?("release-manifest")
|
|
254
|
+
end
|
|
255
|
+
return unless has_manifest_gate && !cli_provider?
|
|
256
|
+
|
|
257
|
+
raise SandboxSetupFailed,
|
|
258
|
+
"#{scenario.test_id} requires a CLI provider: its release-manifest gate runs in deterministic setup"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# rm_rf targets derived from user input (--report-dir) must stay
|
|
262
|
+
# inside the runner-owned e2e cache root.
|
|
263
|
+
def owned_sandbox_path?(sandbox_path)
|
|
264
|
+
cache_root = File.expand_path(File.join(@base_dir, ".ace-local", "test-e2e"))
|
|
265
|
+
FileUtils.mkdir_p(cache_root)
|
|
266
|
+
expanded = File.expand_path(sandbox_path)
|
|
267
|
+
# A symlinked sandbox (or ancestor) could redirect rm_rf outside
|
|
268
|
+
# the cache; resolve what exists before the containment check.
|
|
269
|
+
resolved = begin
|
|
270
|
+
File.realpath(expanded)
|
|
271
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
272
|
+
nearest = expanded
|
|
273
|
+
nearest = File.dirname(nearest) until nearest == "/" || File.exist?(nearest)
|
|
274
|
+
File.join(File.realpath(nearest), expanded.delete_prefix(nearest))
|
|
275
|
+
end
|
|
276
|
+
real_cache_root = File.realpath(cache_root)
|
|
277
|
+
resolved.start_with?(real_cache_root + File::SEPARATOR)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Deletion targets derived from scenario data must stay immediate
|
|
281
|
+
# children of the sandbox: a package value like "../.." must never
|
|
282
|
+
# reach FileUtils.rm_rf.
|
|
283
|
+
def sandbox_child(sandbox_path, name)
|
|
284
|
+
return nil unless name.to_s.match?(/\A[A-Za-z0-9._-]+\z/)
|
|
285
|
+
return nil if name.to_s.start_with?(".", "-")
|
|
286
|
+
|
|
287
|
+
target = File.join(File.expand_path(sandbox_path), name.to_s)
|
|
288
|
+
return nil unless File.dirname(target) == File.expand_path(sandbox_path)
|
|
289
|
+
|
|
290
|
+
target
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# SandboxPackageCopy.prepare leaves an existing package copy in
|
|
294
|
+
# place; a retry against an existing sandbox must never execute a
|
|
295
|
+
# stale copy of the runner assets.
|
|
296
|
+
def refresh_package_copy(sandbox_path, scenario)
|
|
297
|
+
package_source = File.join(@base_dir, scenario.package.to_s)
|
|
298
|
+
return unless File.directory?(package_source)
|
|
299
|
+
|
|
300
|
+
package_target = sandbox_child(sandbox_path, scenario.package)
|
|
301
|
+
return unless package_target
|
|
302
|
+
|
|
303
|
+
FileUtils.rm_rf(package_target)
|
|
304
|
+
Ace::TestSupport::SandboxPackageCopy.new(source_root: @base_dir).prepare(
|
|
305
|
+
package_name: scenario.package,
|
|
306
|
+
sandbox_root: sandbox_path
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def setup_failed_result(scenario, error_message)
|
|
311
|
+
Models::TestResult.new(
|
|
312
|
+
test_id: scenario.test_id,
|
|
313
|
+
status: "error",
|
|
314
|
+
test_cases: [],
|
|
315
|
+
summary: "Sandbox setup failed",
|
|
316
|
+
error: error_message,
|
|
317
|
+
started_at: Time.now,
|
|
318
|
+
completed_at: Time.now
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
|
|
219
322
|
def sandbox_support_git_excludes(scenario, protocol_packages, setup_steps:)
|
|
220
323
|
package_paths = ([scenario.package] + Array(protocol_packages))
|
|
221
324
|
.map(&:to_s)
|
|
@@ -256,6 +359,13 @@ module Ace
|
|
|
256
359
|
scenario = @loader.load(File.dirname(file))
|
|
257
360
|
display = build_display_manager([scenario], output)
|
|
258
361
|
setup_executor = nil
|
|
362
|
+
begin
|
|
363
|
+
assert_deterministic_gate_supported!(scenario)
|
|
364
|
+
rescue SandboxSetupFailed => e
|
|
365
|
+
result = setup_failed_result(scenario, e.message)
|
|
366
|
+
display.show_single_result(result)
|
|
367
|
+
return [result]
|
|
368
|
+
end
|
|
259
369
|
|
|
260
370
|
output.puts "Running E2E test: #{scenario.test_id} (#{scenario.package})"
|
|
261
371
|
if test_cases
|
|
@@ -265,11 +375,138 @@ module Ace
|
|
|
265
375
|
|
|
266
376
|
run_id = cli_provider? ? timestamp : nil
|
|
267
377
|
# When report_dir is provided, derive sandbox path from it (strip -reports suffix)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
378
|
+
begin
|
|
379
|
+
if report_dir
|
|
380
|
+
sandbox_path = report_dir.sub(/-reports\z/, "")
|
|
381
|
+
# Every filesystem mutation below (refresh_package_copy's
|
|
382
|
+
# rm_rf, sandbox recreation) requires a runner-owned sandbox;
|
|
383
|
+
# reject anything else before touching the filesystem.
|
|
384
|
+
unless owned_sandbox_path?(sandbox_path)
|
|
385
|
+
result = setup_failed_result(
|
|
386
|
+
scenario,
|
|
387
|
+
"report directory must live under the runner-owned .ace-local/test-e2e cache: #{report_dir}"
|
|
388
|
+
)
|
|
389
|
+
display.show_single_result(result)
|
|
390
|
+
return [result]
|
|
391
|
+
end
|
|
392
|
+
state_file = File.join(report_dir, Molecules::SetupExecutor::SETUP_STATE_FILE)
|
|
393
|
+
# Effective steps (including profile bootstrap) are what
|
|
394
|
+
# setup executed, so they are what retry state must match.
|
|
395
|
+
effective_steps = effective_setup_steps_for(scenario)
|
|
396
|
+
# Transient resources (tmux sessions) die with the first
|
|
397
|
+
# run's teardown; such scenarios always re-set-up on retry.
|
|
398
|
+
declares_transient = effective_steps.any? do |step|
|
|
399
|
+
step == "tmux-session" || (step.is_a?(Hash) && step.key?("tmux-session"))
|
|
400
|
+
end
|
|
401
|
+
state = declares_transient ? nil : Molecules::SetupExecutor.setup_state_for(
|
|
402
|
+
state_file, sandbox_path, setup_steps: effective_steps
|
|
403
|
+
)
|
|
404
|
+
if state
|
|
405
|
+
# A completed sandbox whose recorded environment and
|
|
406
|
+
# essential outputs are intact may be reused. Prior goal
|
|
407
|
+
# evidence never survives a reuse: results are cleared and
|
|
408
|
+
# the validated manifest copy restored by revalidation.
|
|
409
|
+
# The prior attempt's terminal markers and completion
|
|
410
|
+
# record are invalidated FIRST: if any later retry step
|
|
411
|
+
# fails, the run is an ERROR, never the previous pass.
|
|
412
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-complete.json"))
|
|
413
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-failed.json"))
|
|
414
|
+
FileUtils.rm_f(File.join(report_dir, "metadata.yml"))
|
|
415
|
+
# Transient-resource scenarios and full retries re-setup in
|
|
416
|
+
# a FRESH sandbox: rerunning idempotence-sensitive steps
|
|
417
|
+
# (initial commits) or trusting prior package/Gemfile state
|
|
418
|
+
# in a used sandbox is not safe.
|
|
419
|
+
fresh_sandbox_for_retry = declares_transient ||
|
|
420
|
+
(test_cases.nil? || test_cases.empty?)
|
|
421
|
+
env_vars = fresh_sandbox_for_retry ? nil : Molecules::SetupExecutor.build_reuse_env(state["env"])
|
|
422
|
+
reusable = env_vars && File.file?(File.join(sandbox_path, "Gemfile"))
|
|
423
|
+
if reusable
|
|
424
|
+
# The sandbox Gemfile is derived from the source root's:
|
|
425
|
+
# a changed root Gemfile (or a tampered copy) forces a
|
|
426
|
+
# fresh setup.
|
|
427
|
+
gemfile_state = state["gemfile"]
|
|
428
|
+
if gemfile_state.is_a?(Hash) && gemfile_state["source_digest"]
|
|
429
|
+
source_root = gemfile_state["source_root"].to_s
|
|
430
|
+
root_gemfile = source_root.empty? ? nil : File.join(source_root, "Gemfile")
|
|
431
|
+
generated_gemfile = File.join(sandbox_path, "Gemfile")
|
|
432
|
+
reusable = root_gemfile && File.file?(root_gemfile) &&
|
|
433
|
+
Digest::SHA256.file(root_gemfile).hexdigest == gemfile_state["source_digest"] &&
|
|
434
|
+
Digest::SHA256.file(generated_gemfile).hexdigest == gemfile_state["generated_digest"]
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
if reusable
|
|
438
|
+
refresh_package_copy(sandbox_path, scenario)
|
|
439
|
+
# A filtered retry clears only its selected goals' result
|
|
440
|
+
# directories; prerequisite goals' evidence is retained
|
|
441
|
+
# for classification.
|
|
442
|
+
Array(test_cases).each do |tc_id|
|
|
443
|
+
FileUtils.rm_rf(File.join(sandbox_path, "results", "tc", format("%02d", tc_id.delete_prefix("TC-").to_i)))
|
|
444
|
+
end
|
|
445
|
+
reusable = Molecules::SetupExecutor.revalidate_release_manifest(
|
|
446
|
+
state_file: state_file, sandbox_dir: sandbox_path, explicit: @release_manifest_input,
|
|
447
|
+
setup_steps: effective_steps
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
unless reusable
|
|
452
|
+
# Every non-reused run starts from a pristine sandbox: no
|
|
453
|
+
# prior attempt state (evidence, git repo, tmux, runtime)
|
|
454
|
+
# survives into a fresh deterministic setup. The deletion
|
|
455
|
+
# target must be a runner-owned directory under the e2e
|
|
456
|
+
# cache root; an unrestricted --report-dir value must
|
|
457
|
+
# never reach rm_rf.
|
|
458
|
+
if owned_sandbox_path?(sandbox_path)
|
|
459
|
+
FileUtils.rm_rf(sandbox_path)
|
|
460
|
+
else
|
|
461
|
+
return [setup_failed_result(
|
|
462
|
+
scenario,
|
|
463
|
+
"report directory must live under the runner-owned .ace-local/test-e2e cache: #{report_dir}"
|
|
464
|
+
)]
|
|
465
|
+
end
|
|
466
|
+
sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(
|
|
467
|
+
scenario, timestamp, output,
|
|
468
|
+
sandbox_dir_override: sandbox_path, state_file: state_file
|
|
469
|
+
)
|
|
470
|
+
end
|
|
471
|
+
else
|
|
472
|
+
# No usable retry state (failed setup, changed manifest, or
|
|
473
|
+
# escaped manifest target) forces a fresh deterministic
|
|
474
|
+
# setup against the retry's own sandbox path. When setup is
|
|
475
|
+
# inapplicable (returns nils), the derived path stays.
|
|
476
|
+
# The prior attempt's records are invalidated first, and the
|
|
477
|
+
# sandbox itself is recreated: rerunning deterministic setup
|
|
478
|
+
# in a used sandbox (existing git repo, prior goal changes)
|
|
479
|
+
# is not safe.
|
|
480
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-complete.json"))
|
|
481
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-failed.json"))
|
|
482
|
+
FileUtils.rm_f(File.join(report_dir, "metadata.yml"))
|
|
483
|
+
FileUtils.rm_rf(sandbox_path) if Dir.exist?(sandbox_path)
|
|
484
|
+
refresh_package_copy(sandbox_path, scenario)
|
|
485
|
+
new_path, new_env, setup_executor = setup_sandbox_if_ts(
|
|
486
|
+
scenario, timestamp, output,
|
|
487
|
+
sandbox_dir_override: sandbox_path, state_file: state_file
|
|
488
|
+
)
|
|
489
|
+
if new_path
|
|
490
|
+
sandbox_path, env_vars = new_path, new_env
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
else
|
|
494
|
+
# The first run records retry state under its report directory;
|
|
495
|
+
# the sandbox itself lives next to it without the -reports suffix.
|
|
496
|
+
sandbox_path = report_dir_for(scenario, timestamp).sub(/-reports\z/, "")
|
|
497
|
+
state_file = File.join(report_dir_for(scenario, timestamp), Molecules::SetupExecutor::SETUP_STATE_FILE)
|
|
498
|
+
sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(
|
|
499
|
+
scenario, timestamp, output,
|
|
500
|
+
sandbox_dir_override: sandbox_path, state_file: state_file
|
|
501
|
+
)
|
|
502
|
+
end
|
|
503
|
+
rescue SandboxSetupFailed => e
|
|
504
|
+
# A failed deterministic setup must never degrade into an
|
|
505
|
+
# LLM-driven run: no goal executes, so no install can happen.
|
|
506
|
+
result = setup_failed_result(scenario, e.message)
|
|
507
|
+
display.show_single_result(result)
|
|
508
|
+
output.puts "Error: #{e.message}"
|
|
509
|
+
return [result]
|
|
273
510
|
end
|
|
274
511
|
result = execute_scenario(
|
|
275
512
|
scenario,
|
|
@@ -329,6 +566,19 @@ module Ace
|
|
|
329
566
|
|
|
330
567
|
# Load scenarios upfront for titles and report generation
|
|
331
568
|
scenarios = files.map { |f| @loader.load(File.dirname(f)) }
|
|
569
|
+
# API providers cannot run deterministic setup gates: those
|
|
570
|
+
# scenarios get an explicit error while ungated ones still run.
|
|
571
|
+
gate_blocked = {}
|
|
572
|
+
unless cli_provider?
|
|
573
|
+
scenarios.each_with_index do |scenario, index|
|
|
574
|
+
next unless scenario.setup_steps.any? { |step| step.is_a?(Hash) && step.key?("release-manifest") }
|
|
575
|
+
|
|
576
|
+
gate_blocked[index] = setup_failed_result(
|
|
577
|
+
scenario,
|
|
578
|
+
"#{scenario.test_id} requires a CLI provider: its release-manifest gate runs in deterministic setup"
|
|
579
|
+
)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
332
582
|
|
|
333
583
|
display = build_display_manager(scenarios, output)
|
|
334
584
|
display.initialize_display
|
|
@@ -337,11 +587,15 @@ module Ace
|
|
|
337
587
|
run_ids = cli_provider? ? generate_timestamps(scenarios.size) : Array.new(scenarios.size)
|
|
338
588
|
|
|
339
589
|
queue = Queue.new
|
|
340
|
-
scenarios.each_with_index
|
|
590
|
+
scenarios.each_with_index do |scenario, index|
|
|
591
|
+
queue << [index, scenario, run_ids[index]] unless gate_blocked.key?(index)
|
|
592
|
+
end
|
|
341
593
|
|
|
342
594
|
results = Array.new(files.size)
|
|
595
|
+
gate_blocked.each { |index, result| results[index] = result }
|
|
343
596
|
mutex = Mutex.new
|
|
344
597
|
completed = 0
|
|
598
|
+
gate_blocked.size.times { completed += 1 }
|
|
345
599
|
|
|
346
600
|
thread_count = [@parallel, files.size].min
|
|
347
601
|
done = false
|
|
@@ -383,7 +637,11 @@ module Ace
|
|
|
383
637
|
)
|
|
384
638
|
else
|
|
385
639
|
begin
|
|
386
|
-
|
|
640
|
+
run_report_dir = report_dir_for(scenario, run_id || timestamp)
|
|
641
|
+
sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(
|
|
642
|
+
scenario, run_id || timestamp, output,
|
|
643
|
+
state_file: File.join(run_report_dir, Molecules::SetupExecutor::SETUP_STATE_FILE)
|
|
644
|
+
)
|
|
387
645
|
result = execute_scenario(
|
|
388
646
|
scenario,
|
|
389
647
|
cli_args: cli_args,
|
|
@@ -393,6 +651,11 @@ module Ace
|
|
|
393
651
|
env_vars: env_vars,
|
|
394
652
|
verify: verify
|
|
395
653
|
)
|
|
654
|
+
rescue SandboxSetupFailed => e
|
|
655
|
+
# A failed deterministic setup must never degrade into
|
|
656
|
+
# an LLM-driven run: no goal executes, so no install
|
|
657
|
+
# can happen.
|
|
658
|
+
result = setup_failed_result(scenario, e.message)
|
|
396
659
|
ensure
|
|
397
660
|
setup_executor&.teardown
|
|
398
661
|
end
|
|
@@ -487,11 +750,17 @@ module Ace
|
|
|
487
750
|
# pass/fail status. The orchestrator's parsed response text may not
|
|
488
751
|
# match, so we trust metadata.yml when present.
|
|
489
752
|
#
|
|
753
|
+
# A fresh pipeline ERROR stays authoritative: retained metadata can
|
|
754
|
+
# clarify a completed run but must never upgrade an incomplete or
|
|
755
|
+
# errored execution to PASS.
|
|
756
|
+
#
|
|
490
757
|
# @param scenario [Models::TestScenario] The test scenario
|
|
491
758
|
# @param agent_dir [String] Path to agent report directory
|
|
492
759
|
# @param fallback_result [Models::TestResult] Result to use if metadata unreadable
|
|
493
760
|
# @return [Models::TestResult] Result with authoritative status
|
|
494
761
|
def read_agent_result(scenario, agent_dir, fallback_result)
|
|
762
|
+
return fallback_result.with_report_dir(agent_dir) if fallback_result.status == "error"
|
|
763
|
+
|
|
495
764
|
metadata_path = File.join(agent_dir, "metadata.yml")
|
|
496
765
|
return fallback_result.with_report_dir(agent_dir) unless File.exist?(metadata_path)
|
|
497
766
|
|
|
@@ -534,12 +803,16 @@ module Ace
|
|
|
534
803
|
def missing_agent_report_result(scenario, expected_dir, fallback_result)
|
|
535
804
|
return fallback_result.with_report_dir(expected_dir) if fallback_result.status == "skip"
|
|
536
805
|
|
|
806
|
+
# Preserve any recorded setup/execution failure: a missing report
|
|
807
|
+
# directory must not mask why the run went wrong.
|
|
808
|
+
inherited = fallback_result.error.to_s.strip
|
|
537
809
|
Models::TestResult.new(
|
|
538
810
|
test_id: scenario.test_id,
|
|
539
811
|
status: "error",
|
|
540
812
|
test_cases: fallback_result.test_cases,
|
|
541
813
|
summary: "Missing CLI report directory",
|
|
542
|
-
error: "Expected report directory was not created: #{expected_dir}"
|
|
814
|
+
error: inherited.empty? ? "Expected report directory was not created: #{expected_dir}" :
|
|
815
|
+
"#{inherited} (expected report directory was not created: #{expected_dir})",
|
|
543
816
|
started_at: fallback_result.started_at,
|
|
544
817
|
completed_at: fallback_result.completed_at,
|
|
545
818
|
report_dir: expected_dir
|
|
@@ -20,6 +20,7 @@ require_relative "end_to_end_runner/atoms/display_helpers"
|
|
|
20
20
|
|
|
21
21
|
# Molecules
|
|
22
22
|
require_relative "end_to_end_runner/molecules/fixture_copier"
|
|
23
|
+
require_relative "end_to_end_runner/molecules/release_manifest"
|
|
23
24
|
require_relative "end_to_end_runner/molecules/scenario_loader"
|
|
24
25
|
require_relative "end_to_end_runner/molecules/bwrap_sandbox_backend"
|
|
25
26
|
require_relative "end_to_end_runner/molecules/sandbox_runtime_builder"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-test-runner-e2e
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.41.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.
|
|
74
|
+
version: '0.42'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.
|
|
81
|
+
version: '0.42'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: ace-b36ts
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -198,6 +198,7 @@ files:
|
|
|
198
198
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_report_generator.rb
|
|
199
199
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_sandbox_builder.rb
|
|
200
200
|
- lib/ace/test/end_to_end_runner/molecules/progress_display_manager.rb
|
|
201
|
+
- lib/ace/test/end_to_end_runner/molecules/release_manifest.rb
|
|
201
202
|
- lib/ace/test/end_to_end_runner/molecules/report_writer.rb
|
|
202
203
|
- lib/ace/test/end_to_end_runner/molecules/sandbox_runtime_builder.rb
|
|
203
204
|
- lib/ace/test/end_to_end_runner/molecules/scenario_loader.rb
|