fix(status): prevent false Completed status when mandatory pipeline steps have not run
Add a terminal-step guard (send_to_all_destinations) to all status calculation paths so that files are only marked Completed once the entire processing pipeline has been recorded. - get_file_overall_status: require TERMINAL_STEP to be present - get_files_processing_status: same guard for bulk status - get_step_summary: count missing terminal step as queued so total_main_steps > main_completed when pipeline is incomplete - apply_status_filter: SQL sub-query requires terminal step for completed filter - process_document: call initialize_file_steps after creating a new file record so all mandatory steps are pre-created as pending Define TERMINAL_STEP constant in step_manager.py and reference it in file_status.py and file_queries.py to avoid magic strings. Tests updated: add send_to_all_destinations to completed-file fixtures; add test verifying initialize_file_steps is called for new files. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Query, Session
|
||||
|
||||
from app.models import FileProcessingStep, FileRecord
|
||||
from app.utils.step_manager import TERMINAL_STEP
|
||||
|
||||
|
||||
def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Query:
|
||||
@@ -98,6 +99,9 @@ def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Que
|
||||
query = query.filter(FileRecord.id.in_(db.query(subq.c.file_id)))
|
||||
elif status == "completed":
|
||||
# Files where all real steps are either success or skipped (no failures or in_progress)
|
||||
# and the terminal send_to_all_destinations step has been recorded.
|
||||
# Excluding the terminal-step requirement allows files that only completed the
|
||||
# first few pipeline stages to be falsely labelled as "completed".
|
||||
# Exclude duplicates from completed
|
||||
query = query.filter(FileRecord.is_duplicate.is_(False))
|
||||
|
||||
@@ -113,9 +117,19 @@ def apply_status_filter(query: Query, db: Session, status: Optional[str]) -> Que
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# Select files with real steps that don't have issues
|
||||
query = query.filter(FileRecord.id.in_(db.query(files_with_real_steps.c.file_id))).filter(
|
||||
~FileRecord.id.in_(db.query(files_with_issues.c.file_id))
|
||||
# Get files that have the terminal processing step recorded
|
||||
files_with_terminal_step = (
|
||||
db.query(FileProcessingStep.file_id)
|
||||
.filter(FileProcessingStep.step_name == TERMINAL_STEP)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# Select files with real steps, no issues, and terminal step present
|
||||
query = (
|
||||
query.filter(FileRecord.id.in_(db.query(files_with_real_steps.c.file_id)))
|
||||
.filter(~FileRecord.id.in_(db.query(files_with_issues.c.file_id)))
|
||||
.filter(FileRecord.id.in_(db.query(files_with_terminal_step.c.file_id)))
|
||||
)
|
||||
elif status == "duplicate":
|
||||
# Files marked as duplicates
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Dict, List
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import FileProcessingStep, FileRecord, ProcessingLog
|
||||
from app.utils.step_manager import get_file_overall_status
|
||||
from app.utils.step_manager import TERMINAL_STEP, get_file_overall_status
|
||||
|
||||
|
||||
def get_file_processing_status(db: Session, file_id: int) -> Dict:
|
||||
@@ -150,7 +150,14 @@ def get_files_processing_status(db: Session, file_ids: List[int]) -> Dict[int, D
|
||||
elif in_progress_steps > 0:
|
||||
status = "processing"
|
||||
elif completed_steps + skipped_steps == total_steps:
|
||||
status = "completed"
|
||||
# Only mark as completed if the terminal processing step has been
|
||||
# recorded. Without this guard, files where later pipeline steps
|
||||
# have not yet started would be falsely marked as "completed".
|
||||
existing_step_names = {s.step_name for s in file_steps}
|
||||
if TERMINAL_STEP in existing_step_names:
|
||||
status = "completed"
|
||||
else:
|
||||
status = "pending"
|
||||
else:
|
||||
status = "pending"
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ OPTIONAL_PROCESSING_STEPS = {
|
||||
"check_for_duplicates": settings.enable_deduplication, # Only if deduplication is enabled
|
||||
}
|
||||
|
||||
# The terminal step is the last mandatory step in the processing pipeline.
|
||||
# A file is only considered "completed" once this step has been recorded.
|
||||
TERMINAL_STEP = "send_to_all_destinations"
|
||||
|
||||
# Combine steps based on configuration
|
||||
MAIN_PROCESSING_STEPS = []
|
||||
if settings.enable_deduplication:
|
||||
@@ -252,7 +256,14 @@ def get_file_overall_status(db: Session, file_id: int) -> Dict:
|
||||
elif in_progress_steps > 0:
|
||||
status = "processing"
|
||||
elif completed_steps + skipped_steps == total_steps:
|
||||
status = "completed"
|
||||
# Only mark as completed if the terminal processing step has been recorded.
|
||||
# Without this guard, files where later pipeline steps have not yet started
|
||||
# would be falsely marked as "completed" (e.g. only the first 3 steps ran).
|
||||
existing_step_names = {s.step_name for s in steps}
|
||||
if TERMINAL_STEP in existing_step_names:
|
||||
status = "completed"
|
||||
else:
|
||||
status = "pending"
|
||||
else:
|
||||
status = "pending"
|
||||
|
||||
@@ -340,6 +351,17 @@ def get_step_summary(db: Session, file_id: int) -> Dict:
|
||||
main_counts[status] += 1
|
||||
main_steps_count += 1
|
||||
|
||||
# Ensure the terminal step is always counted in total_main_steps.
|
||||
# If the terminal step has not been recorded yet, the pipeline is not
|
||||
# complete; counting it as "queued" prevents the status banner from
|
||||
# showing "Completed" before the full pipeline has run.
|
||||
# Note: if TERMINAL_STEP already appears in `steps`, the loop above has
|
||||
# already incremented `main_steps_count` for it, so we only add here when
|
||||
# the step is absent from the DB entirely.
|
||||
if not any(step.step_name == TERMINAL_STEP for step in steps if step.step_name in REAL_MAIN_STEPS):
|
||||
main_steps_count += 1
|
||||
main_counts["queued"] += 1
|
||||
|
||||
return {
|
||||
"main": main_counts,
|
||||
"uploads": upload_counts,
|
||||
|
||||
Reference in New Issue
Block a user