diff --git a/atvm/docs/automation/run-learnings.md b/atvm/docs/automation/run-learnings.md index 05e0806..35842a0 100644 --- a/atvm/docs/automation/run-learnings.md +++ b/atvm/docs/automation/run-learnings.md @@ -572,3 +572,14 @@ This file stores run-specific examples only when a run produced a new learning r - For categorized grouped recovery, stop each `Cloud Run Finished` block at that grouped run's `🏁 Recorded Run:` line when it is present. - Do not let categorized summary parsing continue into the next grouped batch's runner output. - Keep grouped host-row parsing scoped to the actual summary table rows for that grouped run only. + +## Run Learning: 2026-04-22 (Wrapped duration rows in parent `Cloud Run Finished` tables must not drop hosts) +- Observed failure mode: + - A non-categorized `cmc-migrateops` run completed four hosts, and the launch log's parent `Cloud Run Finished` table showed all four host rows. + - The saved watcher state still only kept two hosts. + - The Currents summary wrapped the trailing `s` in long duration values such as `16m 13.9s` onto its own continuation row. + - The watcher normalization appended that standalone `s` to the far end of the host row, which broke the host-row regex for those wrapped rows. +- Action for future runs: + - When parsing parent `Cloud Run Finished` tables, treat standalone wrapped `s` rows as duration-cell continuations and remove those rows instead of appending `s` to the end of the host line. + - Rely on the existing duration parser to accept wrapped values without the trailing `s`. + - Replay the exact launch log through the current watcher code after this fix before trusting a corrected host count. diff --git a/atvm/watcher-service/atvm_run_watcher.py b/atvm/watcher-service/atvm_run_watcher.py index 8da115a..f80c440 100644 --- a/atvm/watcher-service/atvm_run_watcher.py +++ b/atvm/watcher-service/atvm_run_watcher.py @@ -673,7 +673,10 @@ def summarize_host_detail_with_mochawesome(detail: str, testcase: str, message: def extract_host_results_from_run_finished_segment(segment_text: str, inventory: Dict[str, str]) -> Dict[str, HostResult]: host_results: Dict[str, HostResult] = {} - normalized = re.sub(r"│\n\s*│\s*s\s*│", "s │", segment_text) + # Currents can wrap the trailing "s" in long duration cells onto its own table row. + # The duration parser already accepts values without the trailing "s", so drop the + # standalone continuation row instead of appending it to the end of the host row. + normalized = re.sub(r"\n\s*│\s*s\s*│\s*", "\n", segment_text) for host_match in re.finditer( r"(?m)^\s*│\s*([✔✖])\s+(atvm[^\s]+)\.ts\s+([0-9:hms. ]+?)\s+(\d+)\s+(\d+)\s+([-\d]+)\s+([-\d]+)\s+([-\d]+)\s*│\s*$", normalized,