ace-test-runner-e2e 0.40.6 → 0.42.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 +21 -0
- data/handbook/guides/scenario-yml-reference.g.md +6 -5
- data/handbook/templates/scenario.yml.template.yml +2 -2
- data/lib/ace/test/end_to_end_runner/cli/commands/run_suite.rb +38 -6
- 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 +37 -4
- 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 +300 -21
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8c03365266949f68dd3364885060b9c27b4087d3ff84b0b42ac5c231ab092ed
|
|
4
|
+
data.tar.gz: ea5b5009323c90d85ec27e417ae012ea09c2af8838b248bf8057caf2fd9174cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3a65486f96beb2288ca5648d16305da412e1d90f69afc6586818db90f029e8d3dbb3037ca3a780619cf0b067d18d2c96b739dcdf724c7a4dd6ad0a090b00dce
|
|
7
|
+
data.tar.gz: 34c8755617d888e23307c21c53b21fd0de777195c6c6ac77d7eaf79cc3cc0efe8ba9fd5b8e1741e5ddb29ccaea7d9c181eb10c80b541d026d24d2713edadda9d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.42.0] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- The `tmux-session` setup step is replaced by `runtime-session`: `runtime: tmux` (default) keeps the detached-session behavior and exports `ACE_TMUX_SESSION` + `ACE_RUNTIME=tmux`; `runtime: herdr` inherits the caller's live herdr environment and exports `ACE_RUNTIME=herdr`, failing explicitly when no herdr context is live (no fabricated stand-in).
|
|
14
|
+
|
|
15
|
+
## [0.41.0] - 2026-09-29
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Pipeline verdict integrity: a passing report requires a completed verifier response covering every selected test case; an incomplete verdict set becomes an explicit ERROR (`incomplete_verdict_set`) instead of an inferred PASS.
|
|
19
|
+
- Pipeline failures record `failure_phase`, `failure_category`, and structured execution evidence in the ERROR report; sessions whose provider process began and then ended without a confirmed completion are marked `uncertain_execution: true` in metadata.yml.
|
|
20
|
+
- The full-suite automatic retry (retry-failures-once) excludes uncertain scenarios — their side effects are never replayed. They stay in the final tally as errors and are listed by `FailureFinder#find_uncertain_scenarios` for explicit reconciliation. Explicit single-run `--only-failures` keeps them runnable (deliberate human reconciliation).
|
|
21
|
+
- `read_agent_result` (single and package runs) and the suite metadata reconcile keep a fresh pipeline ERROR authoritative: retained metadata.yml can no longer upgrade an errored execution to PASS.
|
|
22
|
+
- Verifier failures report `infrastructure-error` (runner/repair failures keep `runner-error`).
|
|
23
|
+
- TS-MONO-001-style scenarios can declare a `release-manifest` setup action: the runner
|
|
24
|
+
carries only the explicitly allowed `ACE_RELEASE_MANIFEST` host input (absolute path,
|
|
25
|
+
defaulting to the source root) into deterministic setup, which validates the strict
|
|
26
|
+
schema-version-1 manifest and copies its exact bytes into the sandbox before any
|
|
27
|
+
install goal can run. Invalid or missing input fails setup, and a failed deterministic
|
|
28
|
+
setup now aborts the scenario with an ERROR result instead of degrading to an
|
|
29
|
+
LLM-driven sandbox.
|
|
30
|
+
|
|
10
31
|
## [0.40.6] - 2026-09-29
|
|
11
32
|
|
|
12
33
|
### Fixed
|
|
@@ -112,17 +112,18 @@ Available directives:
|
|
|
112
112
|
- `copy-fixtures` — Copy fixtures/ directory into sandbox
|
|
113
113
|
- `write-file:` — Write inline content to a file (`path:` + `content:`)
|
|
114
114
|
- `agent-env:` — Environment variables passed to the runner/verifier agent subprocess (not setup commands)
|
|
115
|
-
- `
|
|
116
|
-
- String form: `
|
|
117
|
-
- Hash form: `
|
|
118
|
-
-
|
|
115
|
+
- `runtime-session` — Establish a live terminal runtime context for the scenario
|
|
116
|
+
- String form: `runtime-session` (tmux session with scenario-based naming)
|
|
117
|
+
- Hash form: `runtime-session: { name-source: run-id }` (tmux session named by the E2E run ID)
|
|
118
|
+
- Hash form: `runtime-session: { runtime: herdr }` (inherits the caller's live HERDR_SESSION/HERDR_PANE; fails explicitly when absent)
|
|
119
|
+
- tmux sessions export ACE_TMUX_SESSION; both runtimes export ACE_RUNTIME. tmux sessions are removed by runner teardown after test execution
|
|
119
120
|
|
|
120
121
|
Example:
|
|
121
122
|
|
|
122
123
|
```yaml
|
|
123
124
|
setup:
|
|
124
125
|
- git-init
|
|
125
|
-
-
|
|
126
|
+
- runtime-session:
|
|
126
127
|
name-source: run-id
|
|
127
128
|
- run: "cp ${ACE_E2E_SOURCE_ROOT:-$PROJECT_ROOT_PATH}/mise.toml mise.toml && mise trust mise.toml"
|
|
128
129
|
- copy-fixtures
|
|
@@ -51,14 +51,14 @@ requires:
|
|
|
51
51
|
ruby: ">= 3.0"
|
|
52
52
|
|
|
53
53
|
# Setup directives (executed before test cases)
|
|
54
|
-
# Available: git-init, run:, copy-fixtures, write-file:, agent-env:,
|
|
54
|
+
# Available: git-init, run:, copy-fixtures, write-file:, agent-env:, runtime-session
|
|
55
55
|
# Rules:
|
|
56
56
|
# - setup is fail-fast (do not use `|| true`)
|
|
57
57
|
# - fixtures/setup own sandbox preparation; runner TCs do not re-implement setup
|
|
58
58
|
setup:
|
|
59
59
|
- git-init
|
|
60
60
|
# Optional detached tmux session for test isolation (uses unique run ID)
|
|
61
|
-
# -
|
|
61
|
+
# - runtime-session:
|
|
62
62
|
# name-source: run-id
|
|
63
63
|
- run: "cp ${ACE_E2E_SOURCE_ROOT:-$PROJECT_ROOT_PATH}/mise.toml mise.toml && mise trust mise.toml"
|
|
64
64
|
# Uncomment as needed:
|
|
@@ -116,7 +116,11 @@ module Ace
|
|
|
116
116
|
timeout: options[:timeout],
|
|
117
117
|
tags: tags,
|
|
118
118
|
exclude_tags: exclude_tags,
|
|
119
|
-
verify: options[:verify]
|
|
119
|
+
verify: options[:verify],
|
|
120
|
+
# An explicit --only-failures run is deliberate human
|
|
121
|
+
# reconciliation: uncertain scenarios stay selectable there.
|
|
122
|
+
# The automatic retry pass below always excludes them.
|
|
123
|
+
include_uncertain_failures: only_failures
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
results = run_suite_with_retry(
|
|
@@ -190,20 +194,35 @@ module Ace
|
|
|
190
194
|
)
|
|
191
195
|
return annotated unless retry_failures_once && suite_failed?(initial_results)
|
|
192
196
|
|
|
197
|
+
# Scenarios whose provider session ended uncertainly are excluded
|
|
198
|
+
# from replay by the failure finder; account for them explicitly
|
|
199
|
+
# so they stay visible failures instead of vanishing.
|
|
200
|
+
uncertain = orchestrator.uncertain_scenarios(packages: run_options[:packages])
|
|
201
|
+
unless uncertain.empty?
|
|
202
|
+
output.puts "#{uncertain.length} scenario(s) ended in uncertain execution and will not be retried (side effects are not replayed): #{uncertain.join(", ")}"
|
|
203
|
+
output.puts "Reconcile those sessions manually before rerunning them."
|
|
204
|
+
end
|
|
205
|
+
|
|
193
206
|
output.puts "Retrying failed scenarios once..."
|
|
194
|
-
retry_results = orchestrator.run(
|
|
195
|
-
|
|
207
|
+
retry_results = orchestrator.run(
|
|
208
|
+
run_options.merge(only_failures: true, include_uncertain_failures: false)
|
|
209
|
+
)
|
|
210
|
+
if retry_results[:total].zero? && uncertain.empty?
|
|
196
211
|
raise Ace::Support::Cli::Error.new(
|
|
197
212
|
"Retry pass found no failed test scenarios from attempt 1; aborting instead of silently passing"
|
|
198
213
|
)
|
|
199
214
|
end
|
|
200
215
|
|
|
216
|
+
retry_results = with_uncertain_accounting(retry_results, uncertain)
|
|
217
|
+
|
|
201
218
|
flaky_scenarios = recovered_flaky_scenarios(initial_results, retry_results)
|
|
202
|
-
remaining_failures = failure_scenarios(retry_results)
|
|
219
|
+
remaining_failures = (failure_scenarios(retry_results) + uncertain).uniq.sort
|
|
203
220
|
final_report_path = write_retry_summary_report(initial_results, retry_results)
|
|
204
221
|
output.puts "Final Report: #{final_report_path}" if final_report_path
|
|
205
222
|
|
|
206
|
-
if remaining_failures.empty?
|
|
223
|
+
if (remaining_failures - uncertain).empty? && !uncertain.empty?
|
|
224
|
+
output.puts "All retried scenario(s) recovered; #{uncertain.length} uncertain scenario(s) still require reconciliation"
|
|
225
|
+
elsif remaining_failures.empty?
|
|
207
226
|
output.puts "#{flaky_scenarios.length} scenario(s) recovered on retry and were marked flaky"
|
|
208
227
|
else
|
|
209
228
|
output.puts "#{remaining_failures.length} scenario(s) still failing after retry"
|
|
@@ -217,10 +236,23 @@ module Ace
|
|
|
217
236
|
remaining_failures: remaining_failures,
|
|
218
237
|
initial_report_path: initial_results[:report_path],
|
|
219
238
|
retry_report_path: retry_results[:report_path],
|
|
220
|
-
report_path: final_report_path || retry_results[:report_path]
|
|
239
|
+
report_path: final_report_path || retry_results[:report_path],
|
|
240
|
+
uncertain_scenarios: uncertain
|
|
221
241
|
)
|
|
222
242
|
end
|
|
223
243
|
|
|
244
|
+
# Keep uncertain scenarios in the final tally. The retry pass does
|
|
245
|
+
# not run them (by design), so without this they would disappear
|
|
246
|
+
# from counts and a suite with uncertain sessions could exit 0.
|
|
247
|
+
def with_uncertain_accounting(retry_results, uncertain_ids)
|
|
248
|
+
return retry_results if uncertain_ids.empty?
|
|
249
|
+
|
|
250
|
+
tally = retry_results.dup
|
|
251
|
+
tally[:total] = tally[:total].to_i + uncertain_ids.size
|
|
252
|
+
tally[:errors] = tally[:errors].to_i + uncertain_ids.size
|
|
253
|
+
tally
|
|
254
|
+
end
|
|
255
|
+
|
|
224
256
|
def write_retry_summary_report(initial_results, retry_results)
|
|
225
257
|
build_retry_report_writer.write_retry_summary(
|
|
226
258
|
initial_results: initial_results,
|
|
@@ -6,15 +6,23 @@ module Ace
|
|
|
6
6
|
module Test
|
|
7
7
|
module EndToEndRunner
|
|
8
8
|
module Molecules
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
# Scans cache for failed test cases from previous E2E test runs
|
|
10
|
+
#
|
|
11
|
+
# Reads metadata.yml files from .ace-local/test-e2e/*-reports/ directories
|
|
12
|
+
# and extracts failed_test_cases arrays. Used by --only-failures CLI flag
|
|
13
|
+
# to re-run only tests that failed previously.
|
|
14
|
+
#
|
|
15
|
+
# Scenarios whose most recent run ended in uncertain execution (the provider
|
|
16
|
+
# session began and then ended without a confirmed completion) are excluded
|
|
17
|
+
# from find_failures_by_scenario — the automatic retry surface — because
|
|
18
|
+
# re-running them would replay side effects. They are listed by
|
|
19
|
+
# find_uncertain_scenarios instead so callers can surface an actionable
|
|
20
|
+
# diagnostic. Explicit single-run --only-failures paths keep them included:
|
|
21
|
+
# invoking it is a deliberate human reconciliation decision.
|
|
22
|
+
#
|
|
23
|
+
# Note: This is a Molecule (not an Atom) because it performs filesystem
|
|
24
|
+
# I/O via Dir.glob and YAML file reading.
|
|
25
|
+
class FailureFinder
|
|
18
26
|
CACHE_DIR = ".ace-local/test-e2e"
|
|
19
27
|
METADATA_FILE = "metadata.yml"
|
|
20
28
|
REPORTS_SUFFIX = "-reports"
|
|
@@ -81,11 +89,17 @@ module Ace
|
|
|
81
89
|
# Like find_failures_by_package but preserves per-scenario granularity.
|
|
82
90
|
# Callers can use this to re-run full failed scenarios.
|
|
83
91
|
#
|
|
92
|
+
# By default, scenarios whose most recent run is marked
|
|
93
|
+
# uncertain_execution are excluded — replaying them would repeat
|
|
94
|
+
# unverified side effects. The explicit reconciliation path passes
|
|
95
|
+
# include_uncertain: true to select them deliberately.
|
|
96
|
+
#
|
|
84
97
|
# @param packages [Array<String>] Package names to scan
|
|
85
98
|
# @param base_dir [String] Base directory to search from (default: current dir)
|
|
99
|
+
# @param include_uncertain [Boolean] keep uncertain scenarios selectable
|
|
86
100
|
# @return [Hash{String => Hash{String => Array<String>}}]
|
|
87
101
|
# Package name => { test-id => failed TC IDs }
|
|
88
|
-
def find_failures_by_scenario(packages:, base_dir: Dir.pwd)
|
|
102
|
+
def find_failures_by_scenario(packages:, base_dir: Dir.pwd, include_uncertain: false)
|
|
89
103
|
metadata_files = discover_metadata_files(base_dir)
|
|
90
104
|
return {} if metadata_files.empty?
|
|
91
105
|
|
|
@@ -96,6 +110,8 @@ module Ace
|
|
|
96
110
|
|
|
97
111
|
scenario_failures = {}
|
|
98
112
|
most_recent.each do |entry|
|
|
113
|
+
next if !include_uncertain && uncertain_execution?(entry[:data])
|
|
114
|
+
|
|
99
115
|
test_id = entry[:data]["test-id"]
|
|
100
116
|
failed_ids = extract_failed_test_cases(entry[:data])
|
|
101
117
|
scenario_failures[test_id] = failed_ids unless failed_ids.empty?
|
|
@@ -106,6 +122,32 @@ module Ace
|
|
|
106
122
|
result
|
|
107
123
|
end
|
|
108
124
|
|
|
125
|
+
# Find scenarios whose most recent run ended in uncertain execution,
|
|
126
|
+
# grouped by package. These need manual reconciliation before rerun;
|
|
127
|
+
# they are excluded from find_failures_by_scenario.
|
|
128
|
+
#
|
|
129
|
+
# @param packages [Array<String>] Package names to scan
|
|
130
|
+
# @param base_dir [String] Base directory to search from (default: current dir)
|
|
131
|
+
# @return [Hash{String => Array<String>}] Package name => uncertain test-ids
|
|
132
|
+
def find_uncertain_scenarios(packages:, base_dir: Dir.pwd)
|
|
133
|
+
metadata_files = discover_metadata_files(base_dir)
|
|
134
|
+
return {} if metadata_files.empty?
|
|
135
|
+
|
|
136
|
+
result = {}
|
|
137
|
+
packages.each do |package|
|
|
138
|
+
package_metadata = filter_by_package(metadata_files, package)
|
|
139
|
+
most_recent = most_recent_per_test(package_metadata)
|
|
140
|
+
|
|
141
|
+
uncertain = most_recent
|
|
142
|
+
.select { |entry| uncertain_execution?(entry[:data]) }
|
|
143
|
+
.map { |entry| entry[:data]["test-id"] }
|
|
144
|
+
.sort
|
|
145
|
+
|
|
146
|
+
result[package] = uncertain unless uncertain.empty?
|
|
147
|
+
end
|
|
148
|
+
result
|
|
149
|
+
end
|
|
150
|
+
|
|
109
151
|
private
|
|
110
152
|
|
|
111
153
|
# Discover all metadata.yml files in the cache directory
|
|
@@ -169,6 +211,13 @@ module Ace
|
|
|
169
211
|
entries.flat_map { |entry| extract_failed_test_cases(entry[:data]) }.uniq
|
|
170
212
|
end
|
|
171
213
|
|
|
214
|
+
# True when the latest run of this scenario ended in uncertain
|
|
215
|
+
# execution (the provider session began and ended without a
|
|
216
|
+
# confirmed completion; side effects may have happened).
|
|
217
|
+
def uncertain_execution?(data)
|
|
218
|
+
data["uncertain_execution"] == true
|
|
219
|
+
end
|
|
220
|
+
|
|
172
221
|
# Extract failed test case IDs from a single metadata hash
|
|
173
222
|
#
|
|
174
223
|
# Returns specific TC IDs when available, or ["*"] as a wildcard
|
|
@@ -42,7 +42,12 @@ module Ace
|
|
|
42
42
|
# @return [Models::TestResult]
|
|
43
43
|
def execute(scenario:, cli_args:, sandbox_path:, report_dir:, env_vars: nil, test_cases: nil)
|
|
44
44
|
started_at = Time.now
|
|
45
|
+
phase = :setup
|
|
45
46
|
FileUtils.mkdir_p(report_dir)
|
|
47
|
+
# A retry in the same report directory must start with neither
|
|
48
|
+
# terminal marker present: only this attempt's outcome may remain.
|
|
49
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-complete.json"))
|
|
50
|
+
FileUtils.rm_f(File.join(report_dir, ".host-pipeline-failed.json"))
|
|
46
51
|
write_command_record(report_dir, "runner", provider: @provider, cli_args: cli_args)
|
|
47
52
|
write_tc_manifests(report_dir, scenario, test_cases: test_cases)
|
|
48
53
|
|
|
@@ -72,6 +77,7 @@ module Ace
|
|
|
72
77
|
test_cases: test_cases,
|
|
73
78
|
artifact_contract: declared_artifact_contract(scenario, test_cases: test_cases)
|
|
74
79
|
)
|
|
80
|
+
phase = :runner
|
|
75
81
|
runner_response = run_llm(
|
|
76
82
|
prompt_path: runner[:prompt_path],
|
|
77
83
|
system_path: runner[:system_path],
|
|
@@ -101,6 +107,7 @@ module Ace
|
|
|
101
107
|
artifact_contract: artifact_contract,
|
|
102
108
|
repair_mode: true
|
|
103
109
|
)
|
|
110
|
+
phase = :repair
|
|
104
111
|
repair_response = run_llm(
|
|
105
112
|
prompt_path: repair_runner[:prompt_path],
|
|
106
113
|
system_path: repair_runner[:system_path],
|
|
@@ -126,6 +133,7 @@ module Ace
|
|
|
126
133
|
artifact_contract: artifact_contract
|
|
127
134
|
)
|
|
128
135
|
write_command_record(report_dir, "verifier", provider: @verifier_provider, cli_args: cli_args)
|
|
136
|
+
phase = :verifier
|
|
129
137
|
verifier_response = run_llm(
|
|
130
138
|
prompt_path: verifier[:prompt_path],
|
|
131
139
|
system_path: verifier[:system_path],
|
|
@@ -137,13 +145,14 @@ module Ace
|
|
|
137
145
|
fallback: query_fallback_for(@verifier_provider)
|
|
138
146
|
)
|
|
139
147
|
|
|
140
|
-
@report_generator.generate(
|
|
148
|
+
report = @report_generator.generate(
|
|
141
149
|
scenario: scenario,
|
|
142
150
|
verifier_output: verifier_response[:text],
|
|
143
151
|
report_dir: report_dir,
|
|
144
152
|
provider: @verifier_provider,
|
|
145
153
|
started_at: started_at,
|
|
146
154
|
completed_at: Time.now,
|
|
155
|
+
expected_test_case_ids: selected_test_case_ids(scenario, test_cases),
|
|
147
156
|
metadata: base_metadata(
|
|
148
157
|
report_dir,
|
|
149
158
|
runner_observations: runner_observations,
|
|
@@ -151,8 +160,26 @@ module Ace
|
|
|
151
160
|
initial_artifact_contract: initial_artifact_contract
|
|
152
161
|
)
|
|
153
162
|
)
|
|
163
|
+
# Host-side completion record, written after the report: the
|
|
164
|
+
# runner-writable metadata.yml alone cannot prove the pipeline
|
|
165
|
+
# finished. A rescue below never writes this file.
|
|
166
|
+
File.write(File.join(report_dir, ".host-pipeline-complete.json"), JSON.generate({
|
|
167
|
+
"scenario" => scenario.test_id,
|
|
168
|
+
"runner_provider" => @provider,
|
|
169
|
+
"verifier_provider" => @verifier_provider,
|
|
170
|
+
"completed_at" => Time.now.utc.iso8601
|
|
171
|
+
}))
|
|
172
|
+
report
|
|
154
173
|
rescue => e
|
|
155
174
|
begin
|
|
175
|
+
# A failed pipeline leaves an explicit failure record instead of
|
|
176
|
+
# a completion record; finalization treats absence as failure.
|
|
177
|
+
File.write(File.join(report_dir, ".host-pipeline-failed.json"), JSON.generate({
|
|
178
|
+
"scenario" => scenario.test_id,
|
|
179
|
+
"failure_phase" => phase.to_s,
|
|
180
|
+
"error" => "#{e.class}: #{e.message}",
|
|
181
|
+
"completed_at" => Time.now.utc.iso8601
|
|
182
|
+
}))
|
|
156
183
|
@report_generator.write_failure_report(
|
|
157
184
|
scenario: scenario,
|
|
158
185
|
report_dir: report_dir,
|
|
@@ -160,8 +187,8 @@ module Ace
|
|
|
160
187
|
started_at: started_at || Time.now,
|
|
161
188
|
completed_at: Time.now,
|
|
162
189
|
error_message: "#{e.class}: #{e.message}",
|
|
163
|
-
failure_category: "runner-error",
|
|
164
|
-
metadata: base_metadata(report_dir)
|
|
190
|
+
failure_category: phase == :verifier ? "infrastructure-error" : "runner-error",
|
|
191
|
+
metadata: base_metadata(report_dir).merge(failure_evidence_metadata(e, phase))
|
|
165
192
|
)
|
|
166
193
|
rescue => write_error
|
|
167
194
|
Models::TestResult.new(
|
|
@@ -288,6 +315,25 @@ module Ace
|
|
|
288
315
|
Array(scenario.test_cases).select { |tc| wanted.include?(tc.tc_id.to_s.upcase) }
|
|
289
316
|
end
|
|
290
317
|
|
|
318
|
+
# TC ids the verifier must cover for the verdict set to be complete.
|
|
319
|
+
def selected_test_case_ids(scenario, test_cases)
|
|
320
|
+
select_test_cases(scenario, test_cases).map(&:tc_id)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Structured failure metadata for the error report. When the failure
|
|
324
|
+
# carries execution evidence (the provider session began and ended
|
|
325
|
+
# without a confirmed completion), the scenario is marked uncertain:
|
|
326
|
+
# automatic retries must not replay its side effects.
|
|
327
|
+
def failure_evidence_metadata(error, phase)
|
|
328
|
+
metadata = {"failure_phase" => phase.to_s}
|
|
329
|
+
evidence = Ace::LLM::Atoms::ErrorClassifier.execution_evidence_for(error)
|
|
330
|
+
return metadata unless evidence
|
|
331
|
+
|
|
332
|
+
metadata["uncertain_execution"] = true if evidence.uncertain_execution?
|
|
333
|
+
metadata["execution_evidence"] = evidence.to_h
|
|
334
|
+
metadata
|
|
335
|
+
end
|
|
336
|
+
|
|
291
337
|
def base_metadata(report_dir, runner_observations: nil, artifact_contract: nil, initial_artifact_contract: nil)
|
|
292
338
|
metadata = {
|
|
293
339
|
"runner_provider" => @provider,
|
|
@@ -163,9 +163,32 @@ module Ace
|
|
|
163
163
|
match ? match[1].upcase : nil
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
# Bundler machinery (installed gem trees, compact-index caches,
|
|
167
|
+
# config, shared-gem dirs) is install plumbing, not verifier
|
|
168
|
+
# evidence: `--full-index` alone pulls a ~97MB compact index, which
|
|
169
|
+
# previously got inlined into the verifier prompt whole.
|
|
170
|
+
BUNDLER_MACHINE_SEGMENTS = %w[
|
|
171
|
+
.bundle .gem bundler-cache bundler-home bundler-config
|
|
172
|
+
compact_index specifications cache bin
|
|
173
|
+
].freeze
|
|
174
|
+
MAX_ARTIFACT_FILE_BYTES = 256 * 1024
|
|
175
|
+
MAX_ARTIFACT_CONTENT_BYTES = 2 * 1024 * 1024
|
|
176
|
+
|
|
177
|
+
def artifact_evidence?(relative_path)
|
|
178
|
+
return false if relative_path.start_with?(".bundle/", ".gem/")
|
|
179
|
+
|
|
180
|
+
BUNDLER_MACHINE_SEGMENTS.none? do |segment|
|
|
181
|
+
relative_path.include?("/#{segment}/") || relative_path.start_with?("#{segment}/")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
166
185
|
def build_artifact_section(sandbox_path)
|
|
167
186
|
sandbox_path = File.expand_path(sandbox_path)
|
|
168
|
-
files = Dir.glob(File.join(sandbox_path, "results", "**", "*"))
|
|
187
|
+
files = Dir.glob(File.join(sandbox_path, "results", "**", "*"))
|
|
188
|
+
.select { |f| File.file?(f) }
|
|
189
|
+
.map { |f| File.expand_path(f) }
|
|
190
|
+
.select { |f| artifact_evidence?(relative_path(f, sandbox_path)) }
|
|
191
|
+
.sort
|
|
169
192
|
tree_entries = files.map { |f| relative_path(f, sandbox_path) }
|
|
170
193
|
|
|
171
194
|
parts = []
|
|
@@ -186,12 +209,21 @@ module Ace
|
|
|
186
209
|
parts << "## File contents"
|
|
187
210
|
parts << ""
|
|
188
211
|
|
|
212
|
+
content_budget = MAX_ARTIFACT_CONTENT_BYTES
|
|
189
213
|
files.each do |file|
|
|
214
|
+
size = File.size(file)
|
|
215
|
+
body = if size <= MAX_ARTIFACT_FILE_BYTES
|
|
216
|
+
safe_read(file)
|
|
217
|
+
else
|
|
218
|
+
"#{safe_read(file, MAX_ARTIFACT_FILE_BYTES)}\n... [truncated, #{size} bytes total]"
|
|
219
|
+
end
|
|
220
|
+
content_budget -= body.bytesize
|
|
190
221
|
parts << "### `#{relative_path(file, sandbox_path)}`"
|
|
191
222
|
parts << "```"
|
|
192
|
-
parts <<
|
|
223
|
+
parts << body
|
|
193
224
|
parts << "```"
|
|
194
225
|
parts << ""
|
|
226
|
+
break if content_budget <= 0
|
|
195
227
|
end
|
|
196
228
|
|
|
197
229
|
parts.join("\n").rstrip
|
|
@@ -324,8 +356,9 @@ module Ace
|
|
|
324
356
|
File.expand_path(path).sub("#{File.expand_path(root)}/", "")
|
|
325
357
|
end
|
|
326
358
|
|
|
327
|
-
def safe_read(path)
|
|
328
|
-
File.binread(path
|
|
359
|
+
def safe_read(path, limit = nil)
|
|
360
|
+
raw = limit ? File.binread(path, limit) : File.binread(path)
|
|
361
|
+
raw.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
|
329
362
|
end
|
|
330
363
|
end
|
|
331
364
|
end
|
|
@@ -31,9 +31,12 @@ module Ace
|
|
|
31
31
|
# @param provider [String]
|
|
32
32
|
# @param started_at [Time]
|
|
33
33
|
# @param completed_at [Time]
|
|
34
|
+
# @param expected_test_case_ids [Array<String>, nil] TC ids the verifier must cover
|
|
34
35
|
# @return [Models::TestResult]
|
|
35
|
-
def generate(scenario:, verifier_output:, report_dir:, provider:, started_at:, completed_at:,
|
|
36
|
+
def generate(scenario:, verifier_output:, report_dir:, provider:, started_at:, completed_at:,
|
|
37
|
+
expected_test_case_ids: nil, metadata: {})
|
|
36
38
|
parsed = parse_verifier_output(verifier_output, scenario)
|
|
39
|
+
parsed = enforce_verdict_completeness(parsed, expected_test_case_ids)
|
|
37
40
|
merged_metadata = metadata.merge(parsed[:metadata] || {})
|
|
38
41
|
|
|
39
42
|
result = Models::TestResult.new(
|
|
@@ -94,9 +97,30 @@ module Ace
|
|
|
94
97
|
result.with_report_dir(report_dir)
|
|
95
98
|
end
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# A passing verdict requires one valid verdict per selected test case.
|
|
103
|
+
# A verifier response covering only some of them stays an explicit
|
|
104
|
+
# ERROR — bundle output must never synthesize the missing verdicts.
|
|
105
|
+
def enforce_verdict_completeness(parsed, expected_test_case_ids)
|
|
106
|
+
expected = Array(expected_test_case_ids).map { |id| id.to_s.upcase }
|
|
107
|
+
return parsed if expected.empty? || parsed[:status] == "error"
|
|
108
|
+
|
|
109
|
+
verdict_ids = parsed[:test_cases].map { |tc| tc[:id].to_s.upcase }
|
|
110
|
+
missing = expected - verdict_ids
|
|
111
|
+
return parsed if missing.empty?
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
status: "error",
|
|
115
|
+
test_cases: parsed[:test_cases],
|
|
116
|
+
summary: "Incomplete verifier verdict set (#{verdict_ids.size}/#{expected.size} test cases)",
|
|
117
|
+
error: "Verifier response is missing verdicts for: #{missing.join(", ")}",
|
|
118
|
+
observations: parsed[:observations],
|
|
119
|
+
metadata: (parsed[:metadata] || {}).merge("incomplete_verdict_set" => true)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
def parse_verifier_output(text, scenario)
|
|
100
124
|
goals = parse_goal_sections(text, scenario)
|
|
101
125
|
return build_result_from_goals(goals, text) unless goals.empty?
|
|
102
126
|
|