ace-test-runner-e2e 0.40.6 → 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.
@@ -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 = File.join(@base_dir, ".ace-local", "test-e2e", scenario.dir_name(timestamp))
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 "Warning: sandbox setup failed: #{result[:error]}"
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
- return [nil, nil, nil]
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
- if report_dir
269
- sandbox_path = report_dir.sub(/-reports\z/, "")
270
- sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(scenario, timestamp, output) unless Dir.exist?(sandbox_path)
271
- else
272
- sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(scenario, timestamp, output)
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 { |scenario, index| queue << [index, scenario, run_ids[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
- sandbox_path, env_vars, setup_executor = setup_sandbox_if_ts(scenario, run_id || timestamp, output)
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
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Test
5
5
  module EndToEndRunner
6
- VERSION = "0.40.6"
6
+ VERSION = "0.41.0"
7
7
  end
8
8
  end
9
9
  end
@@ -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.40.6
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-29 00:00:00.000000000 Z
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.39'
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.39'
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