Skip to content

v0.16.9: Unified job polling and test coverage improvements#374

Open
rogermt wants to merge 4 commits intomainfrom
v0.16.9
Open

v0.16.9: Unified job polling and test coverage improvements#374
rogermt wants to merge 4 commits intomainfrom
v0.16.9

Conversation

@rogermt
Copy link
Copy Markdown
Owner

@rogermt rogermt commented Mar 25, 2026

Summary

  • Fix duplicate polling race condition in App.tsx (one poller instead of two)
  • Add App.polling.test.tsx with 4 tests for polling behavior
  • Fix test_get_job_results_invalid_json to use UUID in output_path
  • Add coverage upload to CI workflow for web-ui

The polling fix prevents stale state overwrites and race conditions by consolidating two separate polling effects into one authoritative poller.

TEST-CHANGE Justification

The following test changes are included in this PR:

  • App.polling.test.tsx - NEW: 4 tests for unified job polling behavior (single poller, no stale overwrites, terminal state handling)
  • test_get_job_results_invalid_json - FIX: Updated output_path to include UUID so storage service can extract it

These test changes are required to validate the polling refactor and ensure no regressions.

Files Changed

  • web-ui/src/App.tsx - Unified job polling (lines 223-259)
  • web-ui/src/App.polling.test.tsx - New test file
  • server/tests/api/routes/test_jobs_unified.py - Fixed test
  • .github/workflows/ci.yml - Added coverage upload

Summary by CodeRabbit

  • Bug Fixes
    • Stabilised job polling in the web UI to prevent overlapping/duplicate polls and stale updates; API now returns stored job summaries and verifies output file presence for completed jobs.
  • Tests
    • Added polling tests and updated worker/API tests to reflect precomputed summary behaviour.
  • Chores
    • Enabled test coverage generation and publishing in CI (coverage artifact).

Roger MT added 2 commits March 25, 2026 00:28
v0.16.8: Decouple server from dictating summary structure.

- Worker now uses plugin-provided summary when available
- Falls back to derive_video_summary() for legacy plugins
- API route reads pre-computed summary from job.summary
- API no longer parses results JSON, just checks file existence
- Tests updated to set job.summary for completed jobs

This allows plugins to define their own summary metrics
without server changes.
- Fix duplicate polling race condition in App.tsx (one poller instead of two)
- Add App.polling.test.tsx with 4 tests for polling behavior
- Fix test_get_job_results_invalid_json to use UUID in output_path
- Add coverage upload to CI workflow for web-ui

The polling fix prevents stale state overwrites and race conditions
by consolidating two separate polling effects into one authoritative poller.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97eb7d36-22e8-4cc6-a65c-b9dc1716e248

📥 Commits

Reviewing files that changed from the base of the PR and between b779fa7 and 7e0b602.

📒 Files selected for processing (1)
  • server/app/api_routes/routes/jobs.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/app/api_routes/routes/jobs.py

Walkthrough

Job summaries are now computed by the worker and stored on the job record; the API returns the stored summary and only verifies result file existence. Frontend polling is consolidated into a single serialized poller per selected job. CI now generates and uploads web-ui test coverage artifacts.

Changes

Cohort / File(s) Summary
CI workflow
​.github/workflows/ci.yml
Enable web-ui test coverage via npm test -- --run --coverage and upload web-ui/coverage as web-ui-coverage artifact.
Server API job endpoint
server/app/api_routes/routes/jobs.py
Stop deriving summary from results JSON; check output file existence and return result_url plus job.summary (JSON-decoded when present). Removed results-file JSON parsing that previously could return 500.
Worker job finalisation
server/app/workers/worker.py
Prefer output_data["summary"] when present; fallback to derive_video_summary() only for "video"/"video_multi". Persist job.summary as JSON string or None. Log uses provided job_id arg.
API route tests
server/tests/api/routes/test_jobs_unified.py
Tests updated to expect precomputed job.summary and that API only verifies result-file existence; fixtures now include summary=json.dumps(...).
Worker tests
server/tests/app/workers/test_worker.py
Mock plugin output with precomputed summary; assertions verify stored summary matches plugin-provided values (including frame_count).
Frontend polling logic
web-ui/src/App.tsx
Replace multiple intervals with a serialized recursive timeout poller keyed to selectedJob?.job_id; prevent overlapping requests and stale updates; disable separate uploadResult poller.
Frontend polling tests
web-ui/src/App.polling.test.tsx
New Vitest suite asserting single polling interval for pending jobs, no polling for completed jobs, no extra uploadResult poller, and polling stops after terminal status.

Sequence Diagram(s)

sequenceDiagram
  participant App as Web UI (App)
  participant Poller as Poller (in App)
  participant API as API Server
  participant Storage as Object Storage
  participant WorkerDB as Worker DB

  Note over Poller,App: Poller tied to selectedJob.job_id (serialized timeout)
  Poller->>API: GET /jobs/{job_id}
  API->>WorkerDB: read job record (status, summary, output_path)
  alt job completed
    API->>Storage: HEAD / check output_path exists
    Storage-->>API: 200 / 404
    API-->>Poller: {status: completed, result_url, summary (from DB)}
  else job pending/running
    API-->>Poller: {status: pending|running, summary (maybe null)}
  end
  Poller-->>App: update selectedJob (only if job_id matches)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested labels

bug, refactor

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main changes: unified job polling refactoring and test coverage improvements, matching the PR's primary objectives.
Description check ✅ Passed The description includes all required sections: a clear summary of changes, detailed TEST-CHANGE justification explaining new and modified tests, and a checklist. All required information is present and complete.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v0.16.9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
web-ui/src/App.polling.test.tsx (1)

117-263: The suite still misses the out-of-order response race.

Every mockGetJob here resolves immediately and in order, so none of these tests reproduces the case where an older poll resolves after a newer one or after the user selects a different job. That means the stale-overwrite regression this PR is meant to prevent can still slip through. Add one deferred-promise test that resolves the first poll last and asserts the late response is ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/App.polling.test.tsx` around lines 117 - 263, Add a new test in
App.polling.test.tsx that simulates an out-of-order response for mockGetJob: use
deferred promises so the first getJob call (started by selecting
"select-pending-job") returns a promise you resolve later, then trigger a second
getJob (e.g., by selecting a different job or causing an immediate poll) that
resolves immediately to the newer state; after resolving the newer promise
assert the UI/state reflects the newer result and then resolve the original
(older) deferred promise and assert its late result was ignored (no overwrite).
Target the existing test helpers and identifiers (mockGetJob,
select-pending-job, select-completed-job, vi.advanceTimersByTime, and the
polling behavior tested elsewhere) to locate where to add this test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/app/api_routes/routes/jobs.py`:
- Around line 243-244: Guard the JSON parsing of job.summary the same way as
list_jobs(): wrap the json.loads(job.summary) call in a try/except catching
json.JSONDecodeError (and general Exception if you prefer) and set summary =
None on failure (optionally log the error). Specifically update the assignment
that currently reads summary = json.loads(job.summary) if job.summary else None
to perform safe parsing using json.loads inside a try/except block so a
malformed job.summary does not raise a 500 in the route handling function.

In `@server/app/workers/worker.py`:
- Around line 679-683: The code currently calls
derive_video_summary(output_data) whenever a plugin omits "summary", causing
failures for non-video jobs; change the logic around summary_dict so that after
summary_dict = output_data.get("summary") you only call
derive_video_summary(output_data) when job.job_type indicates a video job (e.g.,
"video" or "video_multi"); for image/image_multi and other non-video types leave
summary_dict as None (or the plugin-provided value). Apply the same guard to the
similar block that appears later (the other occurrence noted around the second
range).

In `@web-ui/src/App.tsx`:
- Around line 230-241: The poller can apply stale responses because async getJob
calls overlap; fix by serializing or tokenizing requests: introduce a per-poll
token/counter (e.g., latestPollTickRef or pollToken) incremented before calling
apiClient.getJob(jobId) and verify the token still matches (and that the
currently selected job id still equals jobId) before calling
setSelectedJob(job); alternatively, avoid overlapping calls by replacing
setInterval with a recursive async poll loop that awaits apiClient.getJob and
only schedules the next poll after completion (ensuring jobId, selectedJob, and
apiClient.getJob are re-checked before updating). Ensure you reference and
update jobId, apiClient.getJob, setSelectedJob, and selectedJob accordingly.

---

Nitpick comments:
In `@web-ui/src/App.polling.test.tsx`:
- Around line 117-263: Add a new test in App.polling.test.tsx that simulates an
out-of-order response for mockGetJob: use deferred promises so the first getJob
call (started by selecting "select-pending-job") returns a promise you resolve
later, then trigger a second getJob (e.g., by selecting a different job or
causing an immediate poll) that resolves immediately to the newer state; after
resolving the newer promise assert the UI/state reflects the newer result and
then resolve the original (older) deferred promise and assert its late result
was ignored (no overwrite). Target the existing test helpers and identifiers
(mockGetJob, select-pending-job, select-completed-job, vi.advanceTimersByTime,
and the polling behavior tested elsewhere) to locate where to add this test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: abbe665e-6339-4d13-aa67-f8faaf0af79c

📥 Commits

Reviewing files that changed from the base of the PR and between 248f212 and 7a10285.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • server/app/api_routes/routes/jobs.py
  • server/app/workers/worker.py
  • server/tests/api/routes/test_jobs_unified.py
  • server/tests/app/workers/test_worker.py
  • web-ui/src/App.polling.test.tsx
  • web-ui/src/App.tsx

Comment thread server/app/api_routes/routes/jobs.py Outdated
Comment thread server/app/workers/worker.py
Comment thread web-ui/src/App.tsx Outdated
Roger MT added 2 commits March 26, 2026 00:15
- worker.py: Only call derive_video_summary for video/video_multi jobs
- App.tsx: Use serialized async polling with cancelled flag to prevent race conditions

Fixes two issues:
1. Non-video jobs were failing because derive_video_summary expects video-shaped payloads
2. Polling could still replay stale job state due to overlapping async getJob calls
Match the pattern used in list_jobs() - wrap json.loads in try/except
to handle malformed summary JSON gracefully instead of raising 500.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant