Skip to content

UN-3016 [FIX] Record why an execution failed; stop dispatching skipped files - #2256

Open
hari-kuriakose wants to merge 7 commits into
mainfrom
worktree-un3016-fix
Open

UN-3016 [FIX] Record why an execution failed; stop dispatching skipped files#2256
hari-kuriakose wants to merge 7 commits into
mainfrom
worktree-un3016-fix

Conversation

@hari-kuriakose

Copy link
Copy Markdown
Contributor

UN-3016 — an execution failed with no explanation

A customer (Moody's) execution ended in ERROR with nothing telling the user why. Root-caused from code and then corroborated against the actual prod row:

Field Value
status ERROR
error_message (blank)
total_files 1
file ...Application.xlsm (application/vnd.ms-excel.sheet.macroenabled.12)
file_hash temp-hash-9d260345dd444238a58eb42ab562bcd7

The prod logs for this run are long gone (30-day retention vs a 283-day-old incident), so the DB row is the evidence.

What was happening

The temp-hash- prefix is written in exactly one place — the backend MIME-skip branch — which pins the mechanism:

  1. The backend correctly rejected the .xlsm and took the skip branch, which deliberately never writes the file's bytes. But it returned the file with is_executed=True and a fake hash.
  2. Nothing downstream filters on is_executed, so the file was counted in total_files and dispatched to a worker anyway.
  3. The worker failed on a file that was never staged. Both halves of the stored error are bare paths — what FileNotFoundError(path) stringifies to.
  4. failed_files == total_files (1 == 1) escalated the execution to ERROR.
  5. _determine_execution_status_unified returned only a status and counts — no reason string — so every call site passed error_message=None. WorkflowExecution.update_execution() guards on if error:, so error_message was never written.

Verified present at 0da9da5a8, the commit that was HEAD the day the ticket was filed — pre-existing, not a regression.

Changes

  1. Executions now record why they failed. _determine_execution_status_unified returns a reason built from the per-file errors already aggregated in aggregated_results; all call sites pass it through, including notifications. Capped to the CharField(256) that otherwise truncates silently.
  2. A skipped file is no longer dispatched. Unsupported files are excluded from the staged set rather than returned with a fake hash. If every file is skipped, the request fails with a 400 naming the types instead of dispatching an empty execution that would finish as a vacuous success.
  3. Workers' AllowedFileTypes was missing XLSM while the backend had it. The two MIME lists are now identical, so a file the API accepts cannot be rejected again inside the worker.

PGMQ

All three apply to the PGMQ stack. It is not a separate callback path — it shares _process_batch_callback_core and process_batch_callback_api with is_pg as a kwarg flag, so both patched call sites cover it. Staging is backend-side and transport-agnostic, and the PG stack has no MIME list of its own. Its own error paths ([pg-poison-drop], [pg-barrier-abort], [reaper-recovery]) already wrote descriptive reasons and never had this defect.

Tests

workers/tests/test_un3016_execution_error.py — 9 passing: the real incident error shape, the never-blank guarantee, the 256-char column fit, and a regression guard that no caller reintroduces error_message=None.

Reviewer notes

  • The full worker suite was not run locally — it needs the repo's unstract.core package, absent from this checkout. The 3→4 tuple change on _determine_execution_status_unified is the thing to watch; the two PG tests touching it mock with assert_not_called, so they should be unaffected, but CI is the real check.
  • Ruff was not run (not installed here). Files compile.
  • Known, deliberately not fixed here: the per-file error text is still a bare path pair built by interpolating two FileNotFoundError paths. This PR makes the execution record it rather than a blank — an improvement, not a cure. _handle_null_execution_result also still publishes nothing to the UI; proven not to be this incident's mechanism, so left alone. Both warrant a follow-up.
  • XLSM was absent from the backend enum at incident time and has since been added, so this customer's file would be accepted today. The other two defects remained for any other unsupported type, which is why this is not just an enum entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_0197gRFp4QFxu2VzxhTEozGn

hari-kuriakose and others added 5 commits August 29, 2026 23:09
…d files

A customer (Moody's) execution ended in ERROR with no explanation. Verified
against the prod row for 30b84e4a-8675-4e93-a235-8d5dbac89be8:
status=ERROR, error_message='' (blank), total_files=1.

Three distinct defects, fixed here:

1. Execution errors were recorded blank.
   _determine_execution_status_unified() decided ERROR purely from failure
   counts and returned no reason, so all three call sites passed
   error_message=None. WorkflowExecution.update_execution() guards on
   `if error:`, so error_message was never written. It now returns a reason
   summarised from the per-file errors already present in aggregated_results,
   capped to the CharField(256) that otherwise truncates silently.

2. A file skipped for an unsupported MIME type was still handed to a worker.
   The skip deliberately never wrote the file's bytes, but returned it with
   is_executed=True and a "temp-hash-<uuid>" sentinel. Nothing downstream
   filters on is_executed, so a worker ran, failed on the missing file, and
   produced the opaque "Execution: <path>; Destination: <path>" error seen in
   the incident row (that string is built by interpolating two FileNotFoundError
   paths, not by mangling a MIME message). Skipped files are now excluded from
   the staged set; if every file is skipped the request fails with a 400 naming
   the unsupported types instead of dispatching an empty execution.

3. The workers' AllowedFileTypes lacked XLSM while the backend had it.
   The two MIME lists are now identical, so a file the API accepts cannot be
   rejected again inside the worker.

Tests: workers/tests/test_un3016_execution_error.py, 9 passing — covers the
real incident error shape, the never-blank guarantee, the 256-char column fit,
and a regression guard that no caller reintroduces error_message=None.

Note: XLSM was absent from the backend enum at the time of the incident and has
since been added, so the customer's .xlsm would be accepted today; the defects
above remain for any other unsupported type.
Remediation of review findings on PR #2256.

F1 (High): the new UnsupportedMimeTypeError raised by
add_input_file_to_api_storage escaped the try/except in
WorkflowViewSet.execute that owns delete_api_storage_dir, so a partial
stage could leave written files behind. The staging call now has its own
handler mirroring the one at the end of the method. The sibling caller
(api_v2/deployment_helper.py:279) was already guarded.

F2 (High): test_no_caller_passes_a_hardcoded_none_error asserted
"error_message=None" not in the whole 1900-line tasks.py. It matched
nothing at the sites it meant to guard and would trip on any unrelated
keyword default. Now an AST check scoped to _process_batch_callback_core
and process_batch_callback_api. Mutation-checked: reintroducing
error_message=None at process_batch_callback_api fails the test with
that call site's line number.

F4 (Medium): _EXECUTION_ERROR_MAX_LENGTH now names its authority,
EXECUTION_ERROR_LENGTH in backend workflow_v2/models/execution.py,
following the convention in workflow_v2/undispatched_sweep.py.

Adversarial verification also found two comments I had written asserting
mechanisms the code does not have (the storage dir is a computed path,
not a mkdir; the AST check does not catch positional/indirected None).
Both false claims deleted rather than reworded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQhqNqCwFcXZZ7cQU6HxUE
…rce shape

Behaviour-preserving cleanup, run once after the review verdict settled.

- views.py: drop the second cleanup handler added in the previous commit and
  stage inside the existing try instead. That handler's guard is exactly the
  staging condition, so one handler now covers both paths rather than two
  copies of the same contract.

- test_un3016_execution_error.py: import callback.tasks directly instead of
  extracting the helper with ast/exec. The docstring's claim that importing
  pulls in an unusable celery runtime is false — conftest loads .env.test
  before collection, and test_pg_callback_duplicate_guard.py already imports
  the module at module level. Verified by running the import under pytest.
  test_status_function_returns_a_reason is now behavioural: it calls
  _determine_execution_status_unified and asserts the reason is non-blank.
  The old version asserted only that every return was a 4-tuple, which would
  have passed with an always-None fourth element — i.e. it could not detect
  the very defect it was named for. Mutation-checked: forcing
  error_message = None now fails the test.

- _summarize_file_errors: errors is keyed by file name, so entries are
  distinct by construction and the `entry not in seen` dedup could never
  fire. Removed, along with the duplicate early return it guarded.

- source.py: skipped_files was a dict never used as a mapping; now a list of
  pre-formatted entries.

Tests: 1298 passed, 132 skipped. test_pg_reaper.py deselected — it needs a
live Postgres on 127.0.0.1:5432 and hangs identically on unmodified HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQhqNqCwFcXZZ7cQU6HxUE
_determine_execution_status_unified marks ERROR from two places, and a blank
error_message from either one is the UN-3016 defect. Only the
failed_files == total_files branch was covered; the timeout branch
(files expected, no batch result came back) had no test at all — mutating its
error_message to None left the suite green.

Adds test_timeout_failure_also_returns_a_reason, which mocks the api_client
so get_workflow_execution reports total_files=3 with an empty
file_batch_results, driving has_timeout_failure. Mutation-checked: forcing
that branch's error_message to None now fails the test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQhqNqCwFcXZZ7cQU6HxUE
@hari-kuriakose

Copy link
Copy Markdown
Contributor Author

Note: partial file skips are silent to the API caller — deliberately not fixed here

Flagged during review and waived, not overlooked. Recording the reasoning so it is stated rather than rediscovered later.

The behaviour. The guard added in this PR is at backend/workflow_manager/endpoint_v2/source.py:1294:

if skipped_files and not file_hashes:
    raise UnsupportedMimeTypeError(
        "No files could be processed. Unsupported file type(s): " + ", ".join(skipped_files)
    )

The and not file_hashes means this fires only when every uploaded file is rejected. On a partial skip — say 10 files uploaded, 2 unsupported — the execution proceeds over the surviving 8 and returns 200. Nothing in the response names the 2 that were dropped; the reason exists only in the server log written just above (workflow_log.log_error). A caller has to count results to notice anything is missing.

Why it is not fixed in this PR.

  1. Surfacing skipped files in the response is a response-contract change. That is out of scope for this fix and wants its own review, since anything parsing the execution response could be affected.
  2. It is pre-existing, and this PR strictly improves it. Previously the unsupported file was returned with is_executed=True and a temp hash; nothing downstream filtered on is_executed, so the worker ran it anyway, failed on the missing file, and the whole execution died with an opaque Execution: <path>; Destination: <path> error. So the prior behaviour was one bad file kills the entire run, confusingly. This PR makes it one bad file is skipped and the rest succeed.

The asymmetry worth knowing about. Total failure is now loud and explicit; partial failure is quiet. That is a deliberate trade, not an oversight — but it does mean a user can get a successful run over fewer files than they uploaded without the response saying so.

Suggested follow-up: report skipped files to the caller, e.g. as a skipped_files field on the execution response, so a partial skip is as visible as a total one.

Reviewed with unstract:lite-remediation; finding F3, waived.

hari-kuriakose and others added 2 commits August 31, 2026 23:12
PR #2256's workers half had 8 tests; its backend half had none. Six tests
close that gap, all DB-free (the ORM boundary and file storage are patched,
so no Postgres is needed).

source.py — add_input_file_to_api_storage:
- partial skip returns only the supported files, and does NOT raise. A
  rejected file alongside a runnable one is simply absent from the mapping;
  the request proceeds with what can be run (UN-4055 tracks whether that
  should remain the behaviour, so this pins it rather than asserting a raise).
- a total skip raises UnsupportedMimeTypeError naming every skipped file and
  its MIME type, instead of dispatching an empty execution that would report
  a vacuous success.
- an empty request returns {} without raising: the guard is on something
  having been skipped, not merely on the mapping being empty.
- an accepted file's FileHash carries the sha256 of the staged bytes. This is
  characterisation only — it held before the fix and no mutation of the fix
  makes it fail; it documents what a returned entry looks like, which is what
  makes the rejected file's absence meaningful.

views.py — WorkflowViewSet.execute:
- a staging failure reaches delete_api_storage_dir, so a partial stage does
  not leave written files behind.
- a request that staged nothing does not attempt that cleanup.

Files are real SimpleUploadedFile objects, not mocks: a MagicMock's
content_type is never in AllowedFileTypes, so a mocked "supported" file would
silently take the skip branch and the test would pass for the wrong reason.

Mutation-checked, each mutation reverted and the restore verified on disk:
- re-adding the rejected file to file_hashes with a temp hash -> partial-skip
  and total-skip tests fail
- dropping "and not file_hashes" from the raise condition -> partial-skip
  test fails (it now raises on a request that has runnable files)
- dropping "skipped_files and" from the raise condition -> empty-request test
  fails
- deleting the raise block -> total-skip test fails
- stripping the filenames from the error message -> total-skip test fails
- moving staging back outside the try -> staging-failure test fails
- dropping the has_uploads guard on cleanup -> no-cleanup test fails

Five of the six tests are pinned by a mutation; the sha256 test is the
characterisation case noted above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQhqNqCwFcXZZ7cQU6HxUE
@hari-kuriakose
hari-kuriakose marked this pull request as ready for review August 31, 2026 19:48
@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents unsupported API uploads from being dispatched without staged bytes and records bounded, nonblank reasons when worker callbacks finalize executions as failed.

  • Unsupported files are excluded from worker fan-out, while all-unsupported requests return a descriptive client error.
  • Staging now occurs inside the execution cleanup boundary.
  • Callback status determination propagates file-error or timeout summaries to execution updates and notifications.
  • Worker MIME support is aligned with the backend by adding XLSM.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code defect remaining after review.

The staging, dispatch filtering, callback tuple propagation, execution update, and notification paths remain aligned, and the apparent lifecycle concerns are either handled by existing callers, explicitly intentional, or pre-existing.

Important Files Changed

Filename Overview
backend/workflow_manager/endpoint_v2/source.py Filters unsupported uploads out of the staged mapping and raises a descriptive error when no processable files remain.
backend/workflow_manager/workflow_v2/views.py Moves upload staging into the existing cleanup boundary so staging failures remove execution-scoped API storage.
workers/callback/tasks.py Adds bounded execution-error summaries and propagates them through both callback finalization and notification paths.
workers/shared/enums/file_types.py Adds the XLSM MIME type to align worker validation with backend acceptance.

Sequence Diagram

sequenceDiagram
    participant C as API client
    participant B as Backend staging
    participant W as File worker
    participant K as Callback
    participant E as Execution record
    C->>B: Upload files
    B->>B: Filter unsupported MIME types
    alt No supported files
        B-->>C: 400 with unsupported types
    else Supported files remain
        B->>W: Dispatch only staged files
        W-->>K: Per-file results
        K->>K: Determine final status and reason
        K->>E: Persist counts, status, and error message
        K-->>C: Notify with failure reason when applicable
    end
Loading

Reviews (1): Last reviewed commit: "Merge branch 'main' into worktree-un3016..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 20.9
e2e-coowners e2e 1 0 0 0 1.6
e2e-etl e2e 1 0 0 0 8.3
e2e-login e2e 2 0 0 0 1.3
e2e-prompt-studio e2e 1 0 0 0 4.4
e2e-smoke e2e 2 0 0 0 1.4
e2e-workflow e2e 1 0 0 0 16.6
integration-backend integration 310 0 0 26 49.5
integration-connectors integration 1 0 0 7 8.8
integration-workers integration 157 0 0 1 54.2
unit-backend unit 1164 0 0 1 33.3
unit-connectors unit 63 0 0 0 8.8
unit-core unit 33 0 0 0 1.0
unit-platform-service unit 15 0 0 0 2.0
unit-rig unit 117 0 0 0 4.6
unit-runner unit 5 0 0 0 3.7
unit-sdk1 unit 563 0 0 0 24.2
unit-workers unit 1407 0 0 1 120.8
TOTAL 3846 0 0 36 365.4

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@chandrasekharan-zipstack chandrasekharan-zipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Standardized review (FOLLOWUP) — High-severity findings only

Verdict: BLOCK — Critical 0 · High 5 · Medium 13 · Low 5 · Lenses 16/16.

This review posts only the 5 High findings. The 13 Medium and 5 Low are held back to keep the thread actionable; happy to post them on request.

Why BLOCK. The PR's three claimed fixes are (1) record a reason, (2) stop dispatching skipped files, (3) align the MIME lists. #2 works. #1 produces a content-free string in production and actively destroys better error messages that already existed. #3 is dead code. Two of the three are certified by tests that pass against shapes production never produces.


Prior round reconciled

The lite-remediation loop that produced F1–F4 never posted its findings to this thread, so the ledger below is reconstructed from commit 9e1adc4ca8's body and your waiver comment. Previous-review boundary: 9e1adc4ca8; surface added after it: af64650ca9, 1ceb723a5a, e15e3e2aad, 2c95b6a7d8.

# Sev Title Status
F1 High UnsupportedMimeTypeError escaped the try owning delete_api_storage_dir RESOLVED
F2 High Whole-file substring assertion in the caller guard test PARTIALLY RESOLVED
F3 Med Partial skips invisible to the API caller WAIVED (yours, in writing — UN-4055; not re-raised)
F4 Med _EXECUTION_ERROR_MAX_LENGTH did not name its authority PARTIALLY RESOLVED — new defect introduced
  • F1 → RESOLVED. views.py:268-276 stages inside the try; has_uploads at :261 is literally the same boolean as the cleanup guard at :297. Pinned by test_un3016_execute_staging_cleanup.py:57. But the move is unpinned on the success side — see H5.
  • F2 → PARTIALLY RESOLVED. Now an AST check scoped to the two functions (test_un3016_execution_error.py:179-191). It rejects a literal error_message=None keyword but not the mutation that actually happens — omitting the keyword, which both callees default to None. See H4.
  • F4 → PARTIALLY RESOLVED, new defect. The comment now names EXECUTION_ERROR_LENGTH (tasks.py:257-260) but cites a convention undispatched_sweep.py does not implement, and omits the real second hardcoded copy at internal_views.py:899/:904. Detail is in the held-back Mediums.
  • The two false comments deleted in 9e1adc4ca8 — confirmed gone, not resurrected.

Cleared, contra the earlier gate

  • OpenAPI needs no update. The 422 outcome is already documented with ExecuteResponse carrying execution_status/error (backend/api_v2/openapi_schema.py:206-208), and 400 at :143.
  • The MIME lists are genuinely identical — programmatic member-for-member diff at PR head: 22 members each, zero divergence. Your claim is accurate (though see H3 — the new entry is unreachable).
  • The 3→4 tuple breaks no consumer_determine_execution_status_unified is module-local; repo-wide grep plus unstract-cloud and unstract-cloud-platform find no external unpack.
  • No rate-limit slot leak on the newly-hot error path. Checked specifically because this PR makes that branch routine; zrem is idempotent and the correct release already fires via update_execution.
  • Rolling-deploy safeerror_message is pre-existing and optional on both the worker client and the serializer.

Bots: SonarCloud passed its gate but reports 0.0% coverage on new code, corroborating H4/H5. Greptile rated this "5/5 — safe to merge, no concrete changed-code defect remaining"; H1 and H3 contradict that directly.


Open questions

  1. H1 is the crux — was the errors map ever observed populated? Do you have a real execution where aggregated_results["errors"] was non-empty? Static reading and FileExecutionResult.__post_init__ say it cannot be. If so, what does this PR change about the Moody's row beyond blank → "All 1 file(s) failed."?
  2. H3 — do you have a real .xlsm that libmagic reports as application/vnd.ms-excel.sheet.macroenabled.12? The deployed worker's magic DB has no macroenabled entry at all. If not, what does the enum line do?
  3. H2 — is overwriting an existing error_message on an already-ERROR row intended? If yes it should be explicit in code; if not it is a regression this PR introduces.
  4. Should the public API-deployment endpoint return 400 or 422 for an all-unsupported upload? The description says 400; the code delivers 422 (deployment_helper.py:286's bare except Exception swallows the APIException, and api_deployment_views.py:168 re-surfaces it). Both are documented, so this is a product call — but the two callers should not disagree, and the description is currently wrong. Detail in the held-back Mediums.

Process note

The working tree at /home/chandru/zipstuff/unstract was switched off this PR's branch mid-review by a concurrent process, so early reads hit pre-PR code. That was detected, an isolated worktree was built at PR head, and every citation below was re-verified. All line numbers are PR-head numbers.

Comment thread workers/callback/tasks.py
Comment on lines +265 to +281
def _summarize_file_errors(aggregated_results: dict[str, Any], total_files: int) -> str:
"""Build an execution-level reason from the per-file errors.

The execution row previously recorded ERROR with a blank error_message,
leaving users with a failed run and no explanation (UN-3016). The per-file
errors are already aggregated as {file_name: error}; surface them here.
"""
errors: dict[str, Any] = aggregated_results.get("errors") or {}
# `errors` is keyed by file name, so every entry is distinct already; the
# cap is what keeps a large batch from bloating the column.
entries = [
f"{file_name}: {str(error).strip()}"
for file_name, error in errors.items()
if error and str(error).strip()
]
if not entries:
return f"All {total_files} file(s) failed."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [Lens 1, 3, 10, 13] — The error summary is a tautology in production: aggregated_results["errors"] is always empty

_summarize_file_errors reads only aggregated_results["errors"]. That map admits an entry only when file_result.get("status") == "error" (lowercase). No producer ever emits that string:

  • API pathworkers/file_processing/tasks.py:1844 wraps every result in FileExecutionResult, whose __post_init__ (unstract/core/src/unstract/core/worker_models.py:301-305) forces status to ApiDeploymentResultStatus.SUCCESS/FAILED = "Success" / "Failed" (worker_models.py:108-112).
  • ETL/TASK pathworkers/file_processing/tasks.py:1073-1081 constructs BatchExecutionResult with no file_results= argument at all.

So errors == {} always, and :281 always returns the fallback. The Moody's incident row would now read "All 1 file(s) failed." — a restatement of status=ERROR + failed_files=1. The user still cannot see why. The blank was replaced by a near-blank.

A repo-wide grep finds no file_results producer emitting "error". The PR's own fixture hand-writes {"status": "error", ...} at workers/tests/test_un3016_execution_error.py:85-93, so test_status_function_returns_a_reason (:97) is green against a shape production cannot produce.

Fix: key on the presence of an error, not on a status string — time_utils.py:180if isinstance(file_result, dict) and file_result.get("error"):, keying the name off file_name or file. Separately file_processing/tasks.py:1073 must pass file_results= or the ETL path keeps summarising nothing. Then rebuild the fixture from a real FileExecutionResult(...).to_dict().

Confidence: High — verified independently against PR-head source.

Comment thread workers/callback/tasks.py
aggregated_results=aggregated_results,
organization_id=context.organization_id,
error_message=None,
error_message=status_error,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [Lens 3, 6, 10] — The tautology now overwrites specific error messages that previously survived

Both backend writers — backend/workflow_manager/workflow_v2/models/execution.py:467 and :496 — apply if error: self.error_message = error[:EXECUTION_ERROR_LENGTH] unconditionally, and backend/workflow_manager/internal_views.py:570-573 documents that "ERROR is deliberately NOT protected."

Before this PR the callback passed error_message=None, so if error: was false and any earlier specific message survived. Now a late callback overwrites it.

Concrete sequence: the orchestrator writes a real cause, or update_execution_err records the staging cause; the batch callback then fires and replaces it with "All 2 file(s) failed.". Combined with H1 this is a net regression against this PR's own goal — a row that had a real diagnostic ends up with a tautology.

Same applies at :1799.

Fix: don't write error_message when the row already carries one (if error and not self.error_message:), or have the callback pass its summary only when the fetched execution's error_message is blank — _determine_execution_status_unified already fetches the execution at :344-359, so the current value is one field away. If overwrite is intended, say so in code.

Confidence: High on mechanism; Medium on frequency.

Comment on lines +40 to +42
# Kept in step with the backend's AllowedFileTypes (UN-3016): the two lists
# must agree or a file the API accepts is rejected again inside the worker.
XLSM = "application/vnd.ms-excel.sheet.macroenabled.12"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [Lens 1, 3, 13, 16] — This XLSM enum addition is dead code — defect #3 is not fixed

Two independent reasons the new member can never fire:

1. Wrong path. The only AllowedFileTypes.is_allowed call in workers/ is shared/workflow/execution/service.py:1226, inside _copy_filesystem_file (:1160). The API path is _copy_api_file (:1104-1157) and performs no MIME validation at all. So the comment's claim — "a file the API accepts is rejected again inside the worker" — describes a rejection that cannot occur.

2. Wrong MIME source. On the filesystem path the type comes from magic.from_buffer(chunk, mime=True) (service.py:1223) — content sniffing, not the client header the backend trusts (source.py:1233). libmagic's database has no macroenabled entry. Verified inside the running worker container: all three of /usr/share/misc/magic.mgc, /usr/lib/file/magic.mgc, /usr/share/file/magic.mgc report macroenabled=0 while spreadsheetml=1. A macro-enabled workbook sniffs as application/vnd.openxmlformats-officedocument.spreadsheetml.sheet — already allowed before this PR.

Also: zero tests reference AllowedFileTypes behaviourally anywhere in workers/tests, backend/**/tests, or tests/.

Fix: drop the change, or justify it with a real .xlsm fixture driven through magic.from_buffer. Correct the comment and the PR description's defect #3. If the lists genuinely must stay in lockstep, add a value-set equality test (it passes today — 22 members each, zero divergence).

Confidence: High — verified in the deployed worker image, not a synthetic fixture.

Comment thread workers/callback/tasks.py
pipeline_name=context.pipeline_name,
pipeline_type=context.pipeline_type,
error_message=None,
error_message=status_error,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [Lens 13 — Testing] — The entire caller-side half of the fix is unpinned: deleting all four error_message=status_error sites keeps CI green

The user-visible half of UN-3016 is "the execution row and the webhook payload carry a reason." Nothing tests that threading. Both callees default the parameter (tasks.py:417, :635), so a refactor dropping the kwarg silently restores the exact defect — ERROR status, blank error_message, webhook with no reason — with CI green.

Mutation executed. Deleting lines 1538, 1593, 1799 and 1860 → the new file gives 10 passed; six callback-adjacent suites give 92 passed.

The AST guard at test_un3016_execution_error.py:153-191 only rejects a literal error_message=None keyword; an omitted keyword contributes no entry to call.keywords. Its docstring enumerates the evasions it misses — positional-None, indirection, **kwargs splat — but not "delete the keyword," which is the one that actually happens.

Fix: one behavioural test per entry point driving _process_batch_callback_core / process_batch_callback_api with an all-failed batch, asserting api_client.update_workflow_execution_status.call_args.kwargs["error_message"] and the notification call both carry the summary. That kills all four mutations and makes the AST guard redundant.

Confidence: High (mutation executed).

Comment on lines +268 to 276
if has_uploads:
hashes_of_files = SourceConnector.add_input_file_to_api_storage(
pipeline_id=pipeline_guid,
workflow_id=workflow_id,
execution_id=execution_id,
file_objs=file_objs,
use_file_history=False,
)
workflow = self.get_workflow_by_id(workflow_id=workflow_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] [Lens 13 — Testing] — The staging move is unpinned against dropping the staged hashes

Both tests in test_un3016_execute_staging_cleanup.py (:57, :77) raise before execute_workflow is reached, so nothing asserts the staged hashes still flow through.

Losing this assignment dispatches an execution with hash_values_of_files={} — the run reports success having processed zero uploaded files. A silent correctness failure on the primary upload flow.

Mutation executed. Changing :269 to drop only the assignment → 2 passed. No other test exercises WorkflowViewSet.execute().

Fix: a third test asserting execute_workflow.call_args.kwargs["hash_values_of_files"] is the sentinel returned by staging, and that use_file_history is False.

Confidence: High (mutation executed).

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.

2 participants