Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions snakesee/tui/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3092,6 +3092,19 @@ def _poll_state(self) -> tuple[WorkflowProgress, TimeEstimate | None]:
log_reader=reader,
)

# Infer total_jobs from the Job stats table when the progress line
# hasn't appeared yet (it only appears after the first completion).
# This lets the pending panel show correct counts immediately.
if progress.total_jobs == 0 and self._estimator is not None:
if not self._estimator.expected_job_counts:
self._init_current_rules_from_log()
if self._estimator.expected_job_counts:
from dataclasses import replace

inferred_total = sum(self._estimator.expected_job_counts.values())
if inferred_total > 0:
progress = replace(progress, total_jobs=inferred_total)
Comment on lines +3099 to +3106
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard total_jobs inference against stale expected_job_counts.

At Line [3099], inference uses any existing non-empty self._estimator.expected_job_counts without log scoping. That can apply stale/latest-run counts at Line [3104] and overwrite progress.total_jobs for the wrong run (especially in historical view).

💡 Suggested fix
diff --git a/snakesee/tui/monitor.py b/snakesee/tui/monitor.py
@@
-        if progress.total_jobs == 0 and self._estimator is not None:
+        if (
+            progress.total_jobs == 0
+            and self._estimator is not None
+            and self._current_log_index == 0  # infer only for latest/live run
+        ):
             if not self._estimator.expected_job_counts:
                 self._init_current_rules_from_log()
             if self._estimator.expected_job_counts:
                 from dataclasses import replace
@@
                 if inferred_total > 0:
                     progress = replace(progress, total_jobs=inferred_total)
diff --git a/snakesee/tui/monitor.py b/snakesee/tui/monitor.py
@@
-        job_counts = parse_job_stats_counts_from_log(log_path)
-        if job_counts:
-            self._estimator.expected_job_counts = job_counts
+        job_counts = parse_job_stats_counts_from_log(log_path)
+        # Always replace to avoid carrying stale counts across runs/log switches
+        self._estimator.expected_job_counts = job_counts or None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@snakesee/tui/monitor.py` around lines 3099 - 3106, The current code
unconditionally uses self._estimator.expected_job_counts to infer
progress.total_jobs and can apply stale counts from a previous log; update the
guard so you only use expected_job_counts when they were populated for the same
log/context as the current progress. In practice modify the logic around
_init_current_rules_from_log and the inference block to (a) ensure
_init_current_rules_from_log is called and succeeds for the current progress/log
before using expected_job_counts, or (b) add a lightweight context check (e.g.
compare a current_log_id/timestamp field on self._estimator or progress) and
reject/clear expected_job_counts if they don’t match; only then compute
inferred_total and replace(progress, total_jobs=inferred_total). Ensure you
reference and update self._estimator.expected_job_counts,
_init_current_rules_from_log, and progress.total_jobs in the change.


# Sync log reader's completed jobs to registry when events aren't available
# This ensures the registry is the single source of truth regardless of
# whether the snakesee logger plugin is being used
Expand Down
85 changes: 85 additions & 0 deletions tests/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2347,3 +2347,88 @@ def test_incomplete_legend_splits_interrupted_from_pending(
# Should show both incomplete and remaining counts
assert "5 incomplete" in output
assert "45 remaining" in output


class TestPendingJobsBeforeCompletion:
"""Tests that pending panel works before any job completes."""

def test_total_jobs_inferred_from_expected_counts(
self, tui_with_mocks: "WorkflowMonitorTUI"
) -> None:
"""total_jobs should be inferred from expected_job_counts when progress line
hasn't appeared yet (total_jobs == 0)."""
from unittest.mock import MagicMock

tui = tui_with_mocks
estimator = MagicMock()
estimator.expected_job_counts = {"align": 5, "sort": 3}
estimator.estimate_remaining.return_value = None
tui._estimator = estimator

# Simulate progress with total_jobs=0 (no progress line yet)
progress = make_workflow_progress(total_jobs=0, completed_jobs=0)

with patch.object(tui, "_read_new_events", return_value=[]):
with patch("snakesee.tui.monitor.parse_workflow_state", return_value=progress):
result_progress, _ = tui._poll_state()

assert result_progress.total_jobs == 8 # 5 + 3
assert result_progress.pending_jobs == 8 # all pending, none running/completed

def test_pending_count_accounts_for_running_jobs(
self, tui_with_mocks: "WorkflowMonitorTUI"
) -> None:
"""pending_jobs should decrease as running_jobs increases, even before first
completion."""
from unittest.mock import MagicMock

tui = tui_with_mocks
estimator = MagicMock()
estimator.expected_job_counts = {"align": 5, "sort": 3}
estimator.estimate_remaining.return_value = None
tui._estimator = estimator

running = [make_job_info(rule="align", job_id="1"), make_job_info(rule="align", job_id="2")]
progress = make_workflow_progress(total_jobs=0, completed_jobs=0, running_jobs=running)

with patch.object(tui, "_read_new_events", return_value=[]):
with patch("snakesee.tui.monitor.parse_workflow_state", return_value=progress):
result_progress, _ = tui._poll_state()

assert result_progress.total_jobs == 8
assert result_progress.pending_jobs == 6 # 8 - 2 running

def test_no_inference_without_estimator(self, tui_with_mocks: "WorkflowMonitorTUI") -> None:
"""total_jobs stays 0 when there's no estimator to infer from."""
tui = tui_with_mocks
tui._estimator = None

progress = make_workflow_progress(total_jobs=0, completed_jobs=0)

with patch.object(tui, "_read_new_events", return_value=[]):
with patch("snakesee.tui.monitor.parse_workflow_state", return_value=progress):
result_progress, _ = tui._poll_state()

assert result_progress.total_jobs == 0

def test_no_override_when_progress_line_exists(
self, tui_with_mocks: "WorkflowMonitorTUI"
) -> None:
"""total_jobs from the progress line takes precedence over inferred value."""
from unittest.mock import MagicMock

tui = tui_with_mocks
estimator = MagicMock()
estimator.expected_job_counts = {"align": 5, "sort": 3}
estimator.estimate_remaining.return_value = None
tui._estimator = estimator

# total_jobs=10 from progress line (already parsed a completion)
progress = make_workflow_progress(total_jobs=10, completed_jobs=1)

with patch.object(tui, "_read_new_events", return_value=[]):
with patch("snakesee.tui.monitor.parse_workflow_state", return_value=progress):
result_progress, _ = tui._poll_state()

# Should keep the progress line value, not override with 8
assert result_progress.total_jobs == 10