ace-test-runner-e2e 0.29.8 → 0.38.11
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/.ace-defaults/e2e-runner/config.yml +14 -2
- data/CHANGELOG.md +178 -0
- data/README.md +2 -2
- data/exe/ace-test-e2e-sh +9 -4
- data/handbook/guides/e2e-testing.g.md +43 -9
- data/handbook/guides/scenario-yml-reference.g.md +16 -8
- data/handbook/guides/tc-authoring.g.md +12 -5
- data/handbook/skills/as-e2e-fix/SKILL.md +2 -2
- data/handbook/skills/as-e2e-review/SKILL.md +2 -2
- data/handbook/templates/ace-taskflow-fixture.template.md +17 -17
- data/handbook/templates/agent-experience-report.template.md +3 -2
- data/handbook/templates/scenario.yml.template.yml +7 -2
- data/handbook/templates/tc-file.template.md +14 -4
- data/handbook/workflow-instructions/e2e/analyze-failures.wf.md +53 -6
- data/handbook/workflow-instructions/e2e/create.wf.md +118 -25
- data/handbook/workflow-instructions/e2e/execute.wf.md +11 -7
- data/handbook/workflow-instructions/e2e/fix.wf.md +65 -15
- data/handbook/workflow-instructions/e2e/plan-changes.wf.md +17 -1
- data/handbook/workflow-instructions/e2e/review.wf.md +36 -25
- data/handbook/workflow-instructions/e2e/rewrite.wf.md +15 -8
- data/handbook/workflow-instructions/e2e/run.wf.md +50 -26
- data/handbook/workflow-instructions/e2e/setup-sandbox.wf.md +4 -4
- data/lib/ace/test/end_to_end_runner/atoms/skill_prompt_builder.rb +7 -5
- data/lib/ace/test/end_to_end_runner/atoms/skill_result_parser.rb +73 -7
- data/lib/ace/test/end_to_end_runner/cli/commands/run_test.rb +21 -8
- data/lib/ace/test/end_to_end_runner/models/test_case.rb +8 -2
- data/lib/ace/test/end_to_end_runner/models/test_result.rb +9 -3
- data/lib/ace/test/end_to_end_runner/models/test_scenario.rb +4 -2
- data/lib/ace/test/end_to_end_runner/molecules/affected_detector.rb +7 -2
- data/lib/ace/test/end_to_end_runner/molecules/bwrap_sandbox_backend.rb +271 -0
- data/lib/ace/test/end_to_end_runner/molecules/config_loader.rb +28 -1
- data/lib/ace/test/end_to_end_runner/molecules/integration_runner.rb +122 -0
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_executor.rb +157 -16
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_prompt_bundler.rb +121 -8
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_report_generator.rb +91 -19
- data/lib/ace/test/end_to_end_runner/molecules/pipeline_sandbox_builder.rb +119 -18
- data/lib/ace/test/end_to_end_runner/molecules/report_writer.rb +13 -12
- data/lib/ace/test/end_to_end_runner/molecules/sandbox_runtime_builder.rb +282 -0
- data/lib/ace/test/end_to_end_runner/molecules/scenario_loader.rb +85 -5
- data/lib/ace/test/end_to_end_runner/molecules/setup_executor.rb +98 -16
- data/lib/ace/test/end_to_end_runner/molecules/suite_report_writer.rb +241 -97
- data/lib/ace/test/end_to_end_runner/molecules/test_discoverer.rb +38 -13
- data/lib/ace/test/end_to_end_runner/molecules/test_executor.rb +27 -5
- data/lib/ace/test/end_to_end_runner/organisms/suite_orchestrator.rb +73 -15
- data/lib/ace/test/end_to_end_runner/organisms/test_orchestrator.rb +120 -19
- data/lib/ace/test/end_to_end_runner/version.rb +1 -1
- data/lib/ace/test/end_to_end_runner.rb +2 -0
- metadata +19 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "yaml"
|
|
6
|
+
require "set"
|
|
6
7
|
require "ace/b36ts"
|
|
7
8
|
|
|
8
9
|
module Ace
|
|
@@ -57,6 +58,7 @@ module Ace
|
|
|
57
58
|
# @option options [Integer] :timeout Timeout per test in seconds
|
|
58
59
|
# @return [Hash] Summary of results
|
|
59
60
|
def run(options = {})
|
|
61
|
+
pre_run_worktree = git_status_snapshot
|
|
60
62
|
packages = @discoverer.list_packages(base_dir: @base_dir)
|
|
61
63
|
|
|
62
64
|
if packages.empty?
|
|
@@ -135,9 +137,9 @@ module Ace
|
|
|
135
137
|
|
|
136
138
|
# Execute tests
|
|
137
139
|
if options[:parallel]
|
|
138
|
-
run_parallel(package_tests, options)
|
|
140
|
+
run_parallel(package_tests, options, pre_run_worktree)
|
|
139
141
|
else
|
|
140
|
-
run_sequential(package_tests, options)
|
|
142
|
+
run_sequential(package_tests, options, pre_run_worktree)
|
|
141
143
|
end
|
|
142
144
|
end
|
|
143
145
|
|
|
@@ -210,7 +212,7 @@ module Ace
|
|
|
210
212
|
# @param package_tests [Hash] Package to tests mapping
|
|
211
213
|
# @param options [Hash] Execution options
|
|
212
214
|
# @return [Hash] Summary of results
|
|
213
|
-
def run_sequential(package_tests, options)
|
|
215
|
+
def run_sequential(package_tests, options, pre_run_worktree)
|
|
214
216
|
results = {total: 0, passed: 0, failed: 0, errors: 0, total_cases: 0, passed_cases: 0, packages: {}}
|
|
215
217
|
start_time = Time.now
|
|
216
218
|
|
|
@@ -265,7 +267,7 @@ module Ace
|
|
|
265
267
|
done = true
|
|
266
268
|
refresh_thread&.join
|
|
267
269
|
|
|
268
|
-
finalize_run(results, package_tests, start_time)
|
|
270
|
+
finalize_run(results, package_tests, start_time, pre_run_worktree)
|
|
269
271
|
end
|
|
270
272
|
|
|
271
273
|
# Run tests in parallel using subprocesses
|
|
@@ -273,7 +275,7 @@ module Ace
|
|
|
273
275
|
# @param package_tests [Hash] Package to tests mapping
|
|
274
276
|
# @param options [Hash] Execution options
|
|
275
277
|
# @return [Hash] Summary of results
|
|
276
|
-
def run_parallel(package_tests, options)
|
|
278
|
+
def run_parallel(package_tests, options, pre_run_worktree)
|
|
277
279
|
results = {total: 0, passed: 0, failed: 0, errors: 0, total_cases: 0, passed_cases: 0, packages: {}}
|
|
278
280
|
queue = build_test_queue(package_tests)
|
|
279
281
|
run_ids = generate_run_ids(queue.size)
|
|
@@ -297,7 +299,7 @@ module Ace
|
|
|
297
299
|
check_running_processes(running, results)
|
|
298
300
|
end
|
|
299
301
|
|
|
300
|
-
finalize_run(results, package_tests, start_time)
|
|
302
|
+
finalize_run(results, package_tests, start_time, pre_run_worktree)
|
|
301
303
|
end
|
|
302
304
|
|
|
303
305
|
# Build a flat queue of test items
|
|
@@ -497,6 +499,7 @@ module Ace
|
|
|
497
499
|
# @return [Hash] Parsed result with :passed_cases and :total_cases
|
|
498
500
|
def parse_subprocess_result(process)
|
|
499
501
|
result = parse_test_output(process[:output], process[:thread].value.exitstatus, extract_test_name(process[:test_file]))
|
|
502
|
+
result[:report_dir] = normalize_report_dir(result[:report_dir], result[:test_name])
|
|
500
503
|
result[:raw_output] = process[:output]
|
|
501
504
|
|
|
502
505
|
# For non-pass results, check agent-written metadata as authoritative source
|
|
@@ -510,6 +513,34 @@ module Ace
|
|
|
510
513
|
{status: "error", error: "Failed to parse result: #{e.message}"}
|
|
511
514
|
end
|
|
512
515
|
|
|
516
|
+
def normalize_report_dir(report_dir, test_name)
|
|
517
|
+
return report_dir if report_dir.nil? || report_dir.empty?
|
|
518
|
+
return report_dir if File.directory?(report_dir)
|
|
519
|
+
return report_dir unless File.file?(report_dir)
|
|
520
|
+
|
|
521
|
+
resolved = resolve_report_dir_from_suite_report(report_dir, canonical_test_id(test_name))
|
|
522
|
+
resolved || report_dir
|
|
523
|
+
rescue
|
|
524
|
+
report_dir
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def resolve_report_dir_from_suite_report(report_path, test_id)
|
|
528
|
+
return nil unless report_path.end_with?(".md")
|
|
529
|
+
return nil if test_id.nil? || test_id.empty?
|
|
530
|
+
|
|
531
|
+
content = File.read(report_path)
|
|
532
|
+
escaped = Regexp.escape(test_id)
|
|
533
|
+
table_match = content.match(/^\|\s*#{escaped}\s*\|\s*`([^`]+)`\s*\|$/m)
|
|
534
|
+
return nil unless table_match
|
|
535
|
+
|
|
536
|
+
File.expand_path(table_match[1], File.dirname(report_path))
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def canonical_test_id(test_name)
|
|
540
|
+
match = test_name.to_s.match(/\A(TS-[A-Z0-9]+-\d+[a-z]*)/i)
|
|
541
|
+
match ? match[1].upcase : test_name
|
|
542
|
+
end
|
|
543
|
+
|
|
513
544
|
# Override result from agent-written metadata.yml when subprocess exit code is misleading
|
|
514
545
|
#
|
|
515
546
|
# @param result [Hash] Parsed result with :report_dir
|
|
@@ -576,7 +607,9 @@ module Ace
|
|
|
576
607
|
error_msg ||= "Test execution returned ERROR status"
|
|
577
608
|
base.merge(status: "error", error: error_msg)
|
|
578
609
|
else
|
|
579
|
-
summary = output.
|
|
610
|
+
summary = output.lines.filter_map { |line| line[/^(Preflight failed: .+?)\s*$/, 1] }.last
|
|
611
|
+
summary ||= output.match(/(\d+)\/(\d+) passed/)&.captures&.join("/")
|
|
612
|
+
summary ||= "Test failed"
|
|
580
613
|
base.merge(status: "fail", summary: summary)
|
|
581
614
|
end
|
|
582
615
|
rescue => e
|
|
@@ -589,8 +622,9 @@ module Ace
|
|
|
589
622
|
# @param package_tests [Hash] Package to test files mapping
|
|
590
623
|
# @param start_time [Time] When the run started
|
|
591
624
|
# @return [Hash] Results with optional :report_path
|
|
592
|
-
def finalize_run(results, package_tests, start_time)
|
|
625
|
+
def finalize_run(results, package_tests, start_time, pre_run_worktree)
|
|
593
626
|
write_failure_stubs(results, package_tests)
|
|
627
|
+
results[:suite_diagnostics] = build_suite_diagnostics(pre_run_worktree)
|
|
594
628
|
|
|
595
629
|
@display.show_summary(results, Time.now - start_time)
|
|
596
630
|
warn_on_lingering_claude_processes
|
|
@@ -641,6 +675,7 @@ module Ace
|
|
|
641
675
|
"status" => result[:status]
|
|
642
676
|
}
|
|
643
677
|
File.write(File.join(stub_dir, "metadata.yml"), YAML.dump(stub_data))
|
|
678
|
+
result[:report_dir] = stub_dir
|
|
644
679
|
|
|
645
680
|
if result[:raw_output] && !result[:raw_output].empty?
|
|
646
681
|
File.write(File.join(stub_dir, "subprocess_output.log"), result[:raw_output])
|
|
@@ -709,7 +744,9 @@ module Ace
|
|
|
709
744
|
all_results, all_scenarios,
|
|
710
745
|
package: "suite",
|
|
711
746
|
timestamp: timestamp,
|
|
712
|
-
base_dir: @base_dir
|
|
747
|
+
base_dir: @base_dir,
|
|
748
|
+
report_kind: :suite,
|
|
749
|
+
diagnostics: results[:suite_diagnostics]
|
|
713
750
|
)
|
|
714
751
|
rescue => e
|
|
715
752
|
warn "Warning: Suite report generation failed (#{e.class}: #{e.message})"
|
|
@@ -726,19 +763,40 @@ module Ace
|
|
|
726
763
|
total = result_hash[:total_cases] || 0
|
|
727
764
|
failed = [total - passed, 0].max
|
|
728
765
|
|
|
729
|
-
test_cases = []
|
|
730
|
-
passed.times { |i| test_cases << {id: "TC-#{format("%03d", i + 1)}", description: "", status: "pass"} }
|
|
731
|
-
failed.times { |i| test_cases << {id: "TC-#{format("%03d", passed + i + 1)}", description: "", status: "fail"} }
|
|
732
|
-
|
|
733
766
|
Models::TestResult.new(
|
|
734
767
|
test_id: result_hash[:test_name] || "unknown",
|
|
735
768
|
status: result_hash[:status] || "error",
|
|
736
|
-
test_cases:
|
|
769
|
+
test_cases: [],
|
|
737
770
|
summary: result_hash[:summary] || result_hash[:error] || "",
|
|
738
|
-
report_dir: result_hash[:report_dir]
|
|
771
|
+
report_dir: result_hash[:report_dir],
|
|
772
|
+
metadata: {"tcs-passed" => passed, "tcs-total" => total, "tcs-failed" => failed}
|
|
739
773
|
)
|
|
740
774
|
end
|
|
741
775
|
|
|
776
|
+
def git_status_snapshot
|
|
777
|
+
stdout, _stderr, status = Open3.capture3("git", "status", "--short", chdir: @base_dir)
|
|
778
|
+
return nil unless status.success?
|
|
779
|
+
|
|
780
|
+
stdout.lines.map(&:rstrip)
|
|
781
|
+
rescue
|
|
782
|
+
nil
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def build_suite_diagnostics(pre_run_worktree)
|
|
786
|
+
post_run_worktree = git_status_snapshot
|
|
787
|
+
return {} unless pre_run_worktree && post_run_worktree
|
|
788
|
+
|
|
789
|
+
before = pre_run_worktree.to_set
|
|
790
|
+
new_entries = post_run_worktree.reject { |line| before.include?(line) }
|
|
791
|
+
new_tracked_entries = new_entries.reject { |line| line.start_with?("?? ") }
|
|
792
|
+
return {} if new_tracked_entries.empty?
|
|
793
|
+
|
|
794
|
+
{
|
|
795
|
+
dirty_worktree: true,
|
|
796
|
+
new_tracked_entries: new_tracked_entries
|
|
797
|
+
}
|
|
798
|
+
end
|
|
799
|
+
|
|
742
800
|
# Load a scenario from file into a Models::TestScenario, with fallback
|
|
743
801
|
#
|
|
744
802
|
# @param package [String] Package name
|
|
@@ -4,6 +4,8 @@ require "fileutils"
|
|
|
4
4
|
require "date"
|
|
5
5
|
require "yaml"
|
|
6
6
|
require "ace/b36ts"
|
|
7
|
+
require "ace/test_support/sandbox_package_copy"
|
|
8
|
+
require "ace/test/end_to_end_runner/molecules/integration_runner"
|
|
7
9
|
|
|
8
10
|
module Ace
|
|
9
11
|
module Test
|
|
@@ -28,19 +30,29 @@ module Ace
|
|
|
28
30
|
# @param timestamp_generator [#call] Callable that returns a timestamp string
|
|
29
31
|
# @param executor [#execute] Injectable test executor (for testing)
|
|
30
32
|
# @param progress [Boolean] Enable animated progress display
|
|
31
|
-
def initialize(provider: nil, timeout: nil, parallel: nil, base_dir: nil, timestamp_generator: nil,
|
|
33
|
+
def initialize(provider: nil, timeout: nil, parallel: nil, base_dir: nil, timestamp_generator: nil,
|
|
34
|
+
executor: nil, progress: false, discoverer: nil, integration_runner: nil,
|
|
35
|
+
scenario_loader: nil, report_writer: nil, suite_report_writer: nil,
|
|
36
|
+
setup_executor_factory: nil, runtime_builder: nil)
|
|
32
37
|
config = Molecules::ConfigLoader.load
|
|
33
|
-
@provider = provider || config.dig("execution", "
|
|
38
|
+
@provider = provider || config.dig("execution", "runner_provider") ||
|
|
39
|
+
config.dig("execution", "provider") || "claude:sonnet"
|
|
34
40
|
@timeout = timeout || config.dig("execution", "timeout") || 300
|
|
35
41
|
@parallel = parallel || config.dig("execution", "parallel") || 3
|
|
36
42
|
@base_dir = base_dir || Dir.pwd
|
|
37
43
|
@timestamp_generator = timestamp_generator || method(:default_timestamp)
|
|
38
44
|
@progress = progress
|
|
39
|
-
@discoverer = Molecules::TestDiscoverer.new
|
|
40
|
-
@
|
|
45
|
+
@discoverer = discoverer || Molecules::TestDiscoverer.new
|
|
46
|
+
@integration_runner = integration_runner || Molecules::IntegrationRunner.new(base_dir: @base_dir)
|
|
47
|
+
@loader = scenario_loader || Molecules::ScenarioLoader.new
|
|
41
48
|
@executor = executor || Molecules::TestExecutor.new(provider: @provider, timeout: @timeout, config: config)
|
|
42
|
-
@report_writer = Molecules::ReportWriter.new
|
|
43
|
-
@suite_report_writer = Molecules::SuiteReportWriter.new(config: config)
|
|
49
|
+
@report_writer = report_writer || Molecules::ReportWriter.new
|
|
50
|
+
@suite_report_writer = suite_report_writer || Molecules::SuiteReportWriter.new(config: config)
|
|
51
|
+
@setup_executor_factory = setup_executor_factory || ->(sandbox_backend: nil) { Molecules::SetupExecutor.new(sandbox_backend: sandbox_backend) }
|
|
52
|
+
@runtime_builder = runtime_builder || Molecules::SandboxRuntimeBuilder.new(
|
|
53
|
+
source_root: @base_dir,
|
|
54
|
+
ruby_version: config.dig("sandbox", "ruby_version") || Molecules::ConfigLoader.default_sandbox_ruby_version
|
|
55
|
+
)
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
# Run E2E tests for a package, optionally filtering by test ID
|
|
@@ -54,6 +66,11 @@ module Ace
|
|
|
54
66
|
# @return [Array<Models::TestResult>] List of test results
|
|
55
67
|
def run(package:, test_id: nil, test_cases: nil, verify: false, tags: nil,
|
|
56
68
|
cli_args: nil, run_id: nil, report_dir: nil, output: $stdout)
|
|
69
|
+
integration_files = @discoverer.find_integration_tests(
|
|
70
|
+
package: package,
|
|
71
|
+
base_dir: @base_dir
|
|
72
|
+
)
|
|
73
|
+
|
|
57
74
|
# Discover tests
|
|
58
75
|
files = @discoverer.find_tests(
|
|
59
76
|
package: package,
|
|
@@ -62,7 +79,7 @@ module Ace
|
|
|
62
79
|
base_dir: @base_dir
|
|
63
80
|
)
|
|
64
81
|
|
|
65
|
-
if files.empty?
|
|
82
|
+
if files.empty? && integration_files.empty?
|
|
66
83
|
output.puts "No E2E tests found in #{package}" +
|
|
67
84
|
(test_id ? " matching #{test_id}" : "")
|
|
68
85
|
return []
|
|
@@ -71,7 +88,7 @@ module Ace
|
|
|
71
88
|
# Generate timestamp for this run (use external run_id when provided)
|
|
72
89
|
timestamp = run_id || generate_timestamp
|
|
73
90
|
|
|
74
|
-
if files.size == 1
|
|
91
|
+
if files.size == 1 && integration_files.empty?
|
|
75
92
|
run_single_test(
|
|
76
93
|
files.first,
|
|
77
94
|
timestamp,
|
|
@@ -82,7 +99,16 @@ module Ace
|
|
|
82
99
|
report_dir: report_dir
|
|
83
100
|
)
|
|
84
101
|
else
|
|
85
|
-
run_package_tests(
|
|
102
|
+
run_package_tests(
|
|
103
|
+
files,
|
|
104
|
+
package,
|
|
105
|
+
timestamp,
|
|
106
|
+
cli_args,
|
|
107
|
+
output,
|
|
108
|
+
test_cases: test_cases,
|
|
109
|
+
verify: verify,
|
|
110
|
+
integration_files: integration_files
|
|
111
|
+
)
|
|
86
112
|
end
|
|
87
113
|
end
|
|
88
114
|
|
|
@@ -107,13 +133,43 @@ module Ace
|
|
|
107
133
|
return [nil, nil, nil] unless cli_provider? && scenario.setup_steps.any?
|
|
108
134
|
|
|
109
135
|
sandbox_dir = File.join(@base_dir, ".ace-local", "test-e2e", scenario.dir_name(timestamp))
|
|
110
|
-
|
|
136
|
+
package_copy = Ace::TestSupport::SandboxPackageCopy.new(source_root: @base_dir)
|
|
137
|
+
package_source = File.join(@base_dir, scenario.package.to_s)
|
|
138
|
+
package_copy_result = if File.directory?(package_source)
|
|
139
|
+
package_copy.prepare(
|
|
140
|
+
package_name: scenario.package,
|
|
141
|
+
sandbox_root: sandbox_dir
|
|
142
|
+
)
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
env: {
|
|
146
|
+
"PROJECT_ROOT_PATH" => File.expand_path(sandbox_dir),
|
|
147
|
+
"ACE_E2E_SOURCE_ROOT" => File.expand_path(@base_dir)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
Molecules::PipelineSandboxBuilder.new(config_root: @base_dir).sync_protocol_sources_into(sandbox_dir)
|
|
152
|
+
runtime_result = @runtime_builder.prepare(
|
|
153
|
+
sandbox_root: sandbox_dir,
|
|
154
|
+
env: package_copy_result[:env],
|
|
155
|
+
tool_names: scenario.requires.fetch("tools", [])
|
|
156
|
+
)
|
|
157
|
+
sandbox_backend = Molecules::BwrapSandboxBackend.new(
|
|
158
|
+
sandbox_root: sandbox_dir,
|
|
159
|
+
source_root: runtime_result.dig(:env, "ACE_E2E_SOURCE_ROOT")
|
|
160
|
+
)
|
|
161
|
+
setup_executor = if @setup_executor_factory.arity.zero?
|
|
162
|
+
@setup_executor_factory.call
|
|
163
|
+
else
|
|
164
|
+
@setup_executor_factory.call(sandbox_backend: sandbox_backend)
|
|
165
|
+
end
|
|
111
166
|
result = setup_executor.execute(
|
|
112
|
-
setup_steps: scenario
|
|
167
|
+
setup_steps: effective_setup_steps_for(scenario),
|
|
113
168
|
sandbox_dir: sandbox_dir,
|
|
114
169
|
fixture_source: scenario.fixture_path,
|
|
115
170
|
scenario_name: scenario.test_id,
|
|
116
|
-
run_id: timestamp
|
|
171
|
+
run_id: timestamp,
|
|
172
|
+
initial_env: runtime_result[:env]
|
|
117
173
|
)
|
|
118
174
|
|
|
119
175
|
unless result[:success]
|
|
@@ -130,6 +186,29 @@ module Ace
|
|
|
130
186
|
[File.expand_path(sandbox_dir), env, setup_executor]
|
|
131
187
|
end
|
|
132
188
|
|
|
189
|
+
def effective_setup_steps_for(scenario)
|
|
190
|
+
steps = Array(scenario.setup_steps)
|
|
191
|
+
return steps unless scenario.sandbox_profile == "ace-default"
|
|
192
|
+
|
|
193
|
+
has_config_init = setup_contains_command?(steps, "ace-config init")
|
|
194
|
+
has_handbook_sync = setup_contains_command?(steps, "ace-handbook sync")
|
|
195
|
+
bootstrap = []
|
|
196
|
+
bootstrap << {"run" => "ace-config init"} unless has_config_init
|
|
197
|
+
bootstrap << {"run" => "ace-handbook sync"} unless has_handbook_sync
|
|
198
|
+
return steps if bootstrap.empty?
|
|
199
|
+
|
|
200
|
+
insert_after = steps.index("git-init")
|
|
201
|
+
return bootstrap + steps unless insert_after
|
|
202
|
+
|
|
203
|
+
steps.dup.insert(insert_after + 1, *bootstrap)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def setup_contains_command?(steps, fragment)
|
|
207
|
+
steps.any? do |step|
|
|
208
|
+
step.is_a?(Hash) && step["run"].to_s.include?(fragment)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
133
212
|
# Run a single test
|
|
134
213
|
# @param test_cases [Array<String>, nil] Optional test case IDs to filter
|
|
135
214
|
# @param report_dir [String, nil] Explicit report directory path (overrides computed path)
|
|
@@ -192,7 +271,23 @@ module Ace
|
|
|
192
271
|
# Run all tests in a package
|
|
193
272
|
# @param test_cases [Array<String>, nil] Optional test case IDs to filter
|
|
194
273
|
# @return [Array<Models::TestResult>] Results for all tests
|
|
195
|
-
def run_package_tests(files, package, timestamp, cli_args, output, test_cases: nil, verify: false
|
|
274
|
+
def run_package_tests(files, package, timestamp, cli_args, output, test_cases: nil, verify: false,
|
|
275
|
+
integration_files: [])
|
|
276
|
+
integration_result = @integration_runner.run(
|
|
277
|
+
package: package,
|
|
278
|
+
files: integration_files,
|
|
279
|
+
timestamp: timestamp,
|
|
280
|
+
output: output
|
|
281
|
+
)
|
|
282
|
+
if integration_result && %w[fail error].include?(integration_result.status)
|
|
283
|
+
output.puts integration_result.summary
|
|
284
|
+
return [integration_result]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
if files.empty?
|
|
288
|
+
return integration_result ? [integration_result] : []
|
|
289
|
+
end
|
|
290
|
+
|
|
196
291
|
# Load scenarios upfront for titles and report generation
|
|
197
292
|
scenarios = files.map { |f| @loader.load(File.dirname(f)) }
|
|
198
293
|
|
|
@@ -291,15 +386,17 @@ module Ace
|
|
|
291
386
|
done = true
|
|
292
387
|
refresh_thread&.join
|
|
293
388
|
|
|
389
|
+
combined_results = integration_result ? [integration_result] + results : results
|
|
390
|
+
|
|
294
391
|
# Write suite report
|
|
295
392
|
report_path = @suite_report_writer.write(
|
|
296
|
-
|
|
297
|
-
package: package, timestamp: timestamp, base_dir: @base_dir
|
|
393
|
+
combined_results, scenarios,
|
|
394
|
+
package: package, timestamp: timestamp, base_dir: @base_dir, report_kind: :package
|
|
298
395
|
)
|
|
299
396
|
|
|
300
|
-
display.show_summary(
|
|
397
|
+
display.show_summary(combined_results, report_path)
|
|
301
398
|
|
|
302
|
-
|
|
399
|
+
combined_results
|
|
303
400
|
end
|
|
304
401
|
|
|
305
402
|
# Build the appropriate display manager for this run
|
|
@@ -332,12 +429,16 @@ module Ace
|
|
|
332
429
|
# Uses Ace::B36ts library to encode unique IDs with 50ms precision,
|
|
333
430
|
# ensuring distinct timestamps for parallel test runs.
|
|
334
431
|
#
|
|
432
|
+
# Offset uses 0.1 (100ms) instead of 0.05 to avoid collisions with
|
|
433
|
+
# the 50ms encoder's approximate bucket size.
|
|
434
|
+
#
|
|
335
435
|
# @param count [Integer] Number of unique timestamps needed
|
|
336
436
|
# @return [Array<String>] Array of unique timestamp strings
|
|
337
437
|
def generate_timestamps(count)
|
|
438
|
+
base_time = Time.now.utc
|
|
439
|
+
|
|
338
440
|
count.times.map do |i|
|
|
339
|
-
|
|
340
|
-
Ace::B36ts.encode(time, format: :"50ms")
|
|
441
|
+
Ace::B36ts.encode(base_time + (i * 0.1), format: :"50ms")
|
|
341
442
|
end
|
|
342
443
|
end
|
|
343
444
|
|
|
@@ -20,6 +20,8 @@ require_relative "end_to_end_runner/atoms/display_helpers"
|
|
|
20
20
|
# Molecules
|
|
21
21
|
require_relative "end_to_end_runner/molecules/fixture_copier"
|
|
22
22
|
require_relative "end_to_end_runner/molecules/scenario_loader"
|
|
23
|
+
require_relative "end_to_end_runner/molecules/bwrap_sandbox_backend"
|
|
24
|
+
require_relative "end_to_end_runner/molecules/sandbox_runtime_builder"
|
|
23
25
|
require_relative "end_to_end_runner/molecules/setup_executor"
|
|
24
26
|
require_relative "end_to_end_runner/molecules/config_loader"
|
|
25
27
|
require_relative "end_to_end_runner/molecules/test_discoverer"
|
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.38.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.9'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ace-support-test-helpers
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.14'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.14'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: ace-llm
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -172,15 +186,18 @@ files:
|
|
|
172
186
|
- lib/ace/test/end_to_end_runner/models/test_result.rb
|
|
173
187
|
- lib/ace/test/end_to_end_runner/models/test_scenario.rb
|
|
174
188
|
- lib/ace/test/end_to_end_runner/molecules/affected_detector.rb
|
|
189
|
+
- lib/ace/test/end_to_end_runner/molecules/bwrap_sandbox_backend.rb
|
|
175
190
|
- lib/ace/test/end_to_end_runner/molecules/config_loader.rb
|
|
176
191
|
- lib/ace/test/end_to_end_runner/molecules/failure_finder.rb
|
|
177
192
|
- lib/ace/test/end_to_end_runner/molecules/fixture_copier.rb
|
|
193
|
+
- lib/ace/test/end_to_end_runner/molecules/integration_runner.rb
|
|
178
194
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_executor.rb
|
|
179
195
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_prompt_bundler.rb
|
|
180
196
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_report_generator.rb
|
|
181
197
|
- lib/ace/test/end_to_end_runner/molecules/pipeline_sandbox_builder.rb
|
|
182
198
|
- lib/ace/test/end_to_end_runner/molecules/progress_display_manager.rb
|
|
183
199
|
- lib/ace/test/end_to_end_runner/molecules/report_writer.rb
|
|
200
|
+
- lib/ace/test/end_to_end_runner/molecules/sandbox_runtime_builder.rb
|
|
184
201
|
- lib/ace/test/end_to_end_runner/molecules/scenario_loader.rb
|
|
185
202
|
- lib/ace/test/end_to_end_runner/molecules/setup_executor.rb
|
|
186
203
|
- lib/ace/test/end_to_end_runner/molecules/simple_display_manager.rb
|