Skip to content

XS⚠️ ◾ fix: don't let a post-EXECUTING_TASK error un-complete the workflow (#306)#961

Open
tomek-i wants to merge 1 commit into
mainfrom
306-metadata-stage-finalization-fix
Open

XS⚠️ ◾ fix: don't let a post-EXECUTING_TASK error un-complete the workflow (#306)#961
tomek-i wants to merge 1 commit into
mainfrom
306-metadata-stage-finalization-fix

Conversation

@tomek-i

@tomek-i tomek-i commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

After creating a YakShaver issue from a video, the app could show "failed after 3 attempts - cannot connect to API" and never show the Final Result panel — even though the issue was created and the YouTube metadata had genuinely been updated. A maintainer noted the "Updating Metadata" step never got its ✅ checkmark despite the metadata actually being applied.

Closes #306

Root cause

In src/backend/ipc/process-video-handlers.ts, processVideoSource tracks a local currentStage variable that is advanced at the start of every stage (CONVERTING_AUDIO, TRANSCRIBING, … EXECUTING_TASK) so the outer catch can attribute an escaping error to the right stage. It was never advanced to UPDATING_METADATA.

So once EXECUTING_TASK completed (the issue was created), currentStage stayed pinned at EXECUTING_TASK for the rest of the run — through the best-effort portal submission and the YouTube metadata update. If any exception escaped to the outer catch during that window (e.g. a googleapis/gaxios network call that gives up after its default 3 retries — exactly "failed after 3 attempts" — during the metadata update, or an equivalent transient failure in the non-fatal portal submission), the outer catch did:

if (currentStage) {
  const stepState = workflowManager.getStepState(currentStage);
  if (stepState.status !== "failed") {
    workflowManager.failStage(currentStage, errorMessage);
  }
}

Since currentStage was still "executing_task" and that stage's status was "completed" (not "failed"), this silently flipped a genuinely-completed EXECUTING_TASK back to "failed" — even though the backlog item had already been created. The renderer's isWorkflowReadyForFinalOutput (src/ui/src/utils/index.ts) requires executing_task.status === "completed" before it will ever show the Final Result panel, so this single mis-attribution both:

  • explains why the metadata step's checkmark was missing (the run errored out before/around that stage even though the YouTube API call itself may have already gone through), and
  • explains why the Final Result panel never appeared (the completed EXECUTING_TASK stage had been silently un-completed).

This is the same class of "silent success masked as failure" bug this codebase has fixed before for the upload/metadata stages (#672, #861, #808) — this time for the outer catch's stage attribution itself.

Fix

  1. Advance currentStage to UPDATING_METADATA right before that stage's block runs, mirroring every other stage. Any stray exception in that region is now correctly attributed to UPDATING_METADATA (which the UI already tolerates as a terminal "failed" state) instead of the unrelated, already-completed EXECUTING_TASK.
  2. Removed an unguarded await from the portal-submission if condition (IdentityServerAuthService...isAuthenticated() was evaluated ahead of that block's own try). It's now the first line inside the try, closing a structural gap where a future change to that method (or condition) could throw with currentStage still stuck at EXECUTING_TASK.
  3. Hardened the outer catch with a new pure helper, shouldFailStageOnUnexpectedError (src/backend/services/workflow/youtube-stage-decisions.ts): only re-fail a stage that was genuinely "in_progress" or "not_started" when the error hit. A stage that already reached "completed" or "skipped" is never retroactively re-failed by an unrelated, later error. This closes the class of bug (not just today's known call sites) so a future stage that forgets to advance currentStage can't reproduce this again.

Bug repro evidence

I reproduced the panel-gating logic directly with a throwaway React Testing Library test against FinalResultPanel, confirming the pure UI state machine correctly shows the panel once updating_metadata reaches any terminal status (including "failed"). That ruled out the UI layer and pointed the investigation squarely at the backend's stage bookkeeping.

I then traced the exact code path: metadataBuilder.build() and youtube.updateVideoMetadata() calls are correctly wrapped by their own local try/catch and do fail the metadata stage cleanly on their own — but currentStage never reaching UPDATING_METADATA meant the outer catch was the actual defect: it could re-fail EXECUTING_TASK behind the scenes whenever anything else in that later window threw, independent of which specific dependency (portal submission, YouTube API, or an LLM call) was the trigger. Confirmed via googleapis-common's apirequest.js (options.retry = true by default) + gaxios's default retry: 3 / noResponseRetries: 2 config that a YouTube API call genuinely does give up after 3 attempts on a network blip, matching the reported error text.

Since a full end-to-end repro requires simulating an Electron main-process video-processing run with mocked Google/LLM/portal APIs, I instead added regression coverage at the same seam the codebase already uses for this kind of stage-routing logic (youtube-stage-decisions.ts, home of applyUploadStageOutcome/resolveMetadataStage from #672/#798): shouldFailStageOnUnexpectedError is unit-tested against every WorkflowStatus value, proving a stage that already completed/skipped is never re-failed while one still in-flight correctly still is.

Testing performed

  • npm run build — clean
  • npx vitest run --reporter=verbose --exclude 'src/ui/**' — 690 passed (67 files), including new shouldFailStageOnUnexpectedError coverage
  • npm --prefix src/ui test — 228 passed (32 files)
  • npm run lint — clean

Files changed

  • src/backend/ipc/process-video-handlers.ts — advance currentStage, move the auth check inside its try, use the new helper in the outer catch
  • src/backend/services/workflow/youtube-stage-decisions.ts — new shouldFailStageOnUnexpectedError helper
  • src/backend/services/workflow/youtube-stage-decisions.test.ts — regression tests for the new helper

🤖 Generated with Claude Code

…306)

Root cause: currentStage was never advanced to UPDATING_METADATA, so any
exception escaping after EXECUTING_TASK completed (e.g. a network retry
exhaustion during the best-effort portal submission or the metadata
update) got attributed to the outer catch's currentStage, which was still
pinned at EXECUTING_TASK. The outer catch then unconditionally re-failed
that stage even though it had already completed — silently un-completing
a run whose issue creation genuinely succeeded, and permanently blocking
the Final Result panel since isWorkflowReadyForFinalOutput requires
executing_task to stay "completed".

- Advance currentStage to UPDATING_METADATA before that stage's block, matching
  every earlier stage's pattern.
- Move the portal-submission's isAuthenticated() await inside its own try so no
  await in that gating condition can bypass local error handling.
- Harden the outer catch with shouldFailStageOnUnexpectedError: only re-fail a
  stage that was genuinely in-flight ("in_progress"/"not_started") when the
  error hit, never one that already reached "completed"/"skipped".

Closes #306
Copilot AI review requested due to automatic review settings July 15, 2026 06:51
@tomek-i tomek-i added the armada Eligible for the ARMADA fleet to pick up label Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Metrics

Thanks for keeping your pull request small.
⚠️ Consider adding additional tests.

Lines
Product Code 47
Test Code 36
Subtotal 83
Ignored Code -
Total 83

Metrics computed by PR Metrics. Add it to your Azure DevOps and GitHub PRs!

@github-actions github-actions Bot changed the title fix: don't let a post-EXECUTING_TASK error un-complete the workflow (#306) XS⚠️ ◾ fix: don't let a post-EXECUTING_TASK error un-complete the workflow (#306) Jul 15, 2026

Copilot AI 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.

Pull request overview

Fixes a workflow-state attribution bug where an exception occurring after EXECUTING_TASK could incorrectly flip that already-completed stage back to failed, which in turn blocked the UI’s Final Result panel (issue #306).

Changes:

  • Advance currentStage to UPDATING_METADATA before running that block so unexpected errors are attributed to the correct stage.
  • Move the portal-auth isAuthenticated() await inside the portal submission try to prevent unguarded async evaluation from escaping.
  • Add shouldFailStageOnUnexpectedError() and use it in the outer catch so only genuinely in-flight stages (not_started/in_progress) are failed.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/backend/services/workflow/youtube-stage-decisions.ts Adds shouldFailStageOnUnexpectedError() to prevent retroactively failing completed/skipped stages in outer catch scenarios.
src/backend/services/workflow/youtube-stage-decisions.test.ts Adds regression tests for the new helper’s status-handling behavior.
src/backend/ipc/process-video-handlers.ts Fixes stage attribution for metadata updates, hardens portal submission guard, and applies the new outer-catch decision helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +199 to +209
it("covers every WorkflowStatus value so a future status can't silently change behaviour unnoticed", () => {
const allStatuses: WorkflowStatus[] = [
"not_started",
"in_progress",
"completed",
"failed",
"skipped",
];
const results = allStatuses.map((status) => shouldFailStageOnUnexpectedError(status));
expect(results).toEqual([true, true, false, false, false]);
});
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Pre-release build is available for this PR:
https://github.com/SSWConsulting/SSW.YakShaver.Desktop/releases/tag/0.6.0-beta.961.1784098290

@github-actions

Copy link
Copy Markdown
Contributor

Hi there!

This PR has been here a while.

Did you know you should avoid merge debt?

Please review and merge or close.

Thanks!

@github-actions github-actions Bot added the Stale label Jul 19, 2026
@tomek-i tomek-i added the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 20, 2026
@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: ready-PR pipeline started — review → address → re-validate → gated merge.

@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

muster review — PR #961

Two independent lenses reviewed this diff in parallel:

  • Lens A — code-review (conventions + correctness): read process-video-handlers.ts, youtube-stage-decisions.ts/test, workflow-state-manager.ts, shared/types/workflow.ts. Traced the currentStage advancement, the shouldFailStageOnUnexpectedError helper's coverage of the WorkflowStatus union, the portal-submission nesting refactor, and cleanupTempFiles's reachability from the outer catch. No issues found.
  • Lens B — codex-rescue (independent root-cause second opinion): pushed on whether currentStage advancement (change ✨ Native Linux Support - Enable YakShaver Desktop App to run natively on Linux #1) is actually necessary given shouldFailStageOnUnexpectedError (change Copied the code from YakShaver #3), or redundant. Traced a concrete counter-scenario: without change ✨ Native Linux Support - Enable YakShaver Desktop App to run natively on Linux #1, if resolveMetadataStage throws before entering its own try/catch, UPDATING_METADATA would stay "not_started" forever, and since the UI's isWorkflowReadyForFinalOutput requires updating_metadata to reach a terminal status, the Final Result panel would still never appear — confirming change ✨ Native Linux Support - Enable YakShaver Desktop App to run natively on Linux #1 is genuinely necessary and complementary to change Copied the code from YakShaver #3, not overlapping. Also verified: no other call site duplicates the old !== "failed" pattern; isAuthenticated() cannot currently throw (fully swallows its own errors) so moving it into the try is defensive-only with no behavior change; cleanupTempFiles cannot escape to the outer catch today. No issues found.

Both lenses independently agree: the fix is structurally sound, addresses the actual root cause (not just the reported symptom), the new shouldFailStageOnUnexpectedError helper is a genuine WorkflowStatus-exhaustive fix (verified via the test file's exhaustiveness assertion) rather than a narrow patch, and the portal-submission refactor is behavior-preserving. The two changes (currentStage advancement + the new terminal-status guard) are complementary rather than redundant — each closes a distinct path to the same user-visible bug (missing Final Result panel).

Bottom line

No blocking, major, minor, or nit findings — review advisory only, nothing to act on.

Consolidated: blocking 0, major 0, minor 0, nit 0.

@tomek-i tomek-i removed the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 20, 2026
@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

✅ reviewed, addressed, green — awaiting human merge (auto-merge off).

@tomek-i tomek-i added the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 20, 2026
@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: ready-PR pipeline started — review → address → re-validate → gated merge.

@tomek-i tomek-i added armada:record Request an on-demand logbook walkthrough for this PR; crows-nest records it and removes the label and removed armada:record Request an on-demand logbook walkthrough for this PR; crows-nest records it and removes the label labels Jul 20, 2026
@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

muster review — PR #961

Two independent lenses reviewed this PR in parallel:

  • code-review (conventions + correctness): verified UPDATING_METADATA was the only stage in processVideoSource missing a currentStage advance (confirmed CONVERTING_AUDIO, TRANSCRIBING, OPTIMIZING_TRANSCRIPT, ANALYZING_TRANSCRIPT, SELECTING_PROMPT, EXECUTING_TASK all already do this correctly), confirmed the isAuthenticated() relocation into the try is behaviorally safe, and confirmed shouldFailStageOnUnexpectedError's "in_progress" | "not_started" set is correct given the state machine. No issues found.
  • codex-rescue (independent root-cause second opinion): hand-traced the full causal chain by hand and confirmed the claimed root cause is real and correctly diagnosed (not just a symptom patch) — pre-fix, a post-EXECUTING_TASK exception genuinely could silently re-fail the already-completed EXECUTING_TASK stage and permanently hide the Final Result panel. Verified the reordered auth check is behaviorally inert today (isAuthenticated() swallows its own errors). Checked for the same bug-class pattern elsewhere (workflow-retry-service.ts, RERUN_TASK handler, process-video-cloud360.ts) and found no other latent instance. No issues found.

Verdict

0 blocking, 0 major, 0 minor, 0 nit — no findings from either lens. The two lenses independently agree: the root-cause diagnosis is correct, the fix (advancing currentStage for UPDATING_METADATA, moving the auth check inside its own try, and generalizing the outer-catch guard via shouldFailStageOnUnexpectedError) closes the bug class rather than just today's call site, and the new unit tests meaningfully cover the new helper. No regressions or scope creep identified.

Bottom line: no blocking findings — review advisory only. Ready from a review-findings standpoint.


Note (minor, non-blocking observation from codex-rescue, not filed as a finding): the "not_started" branch of shouldFailStageOnUnexpectedError is not currently reachable via any code path in processVideoSource today — it's defensive future-proofing per the PR's own stated goal of closing the bug class, not the immediate call site. Flagged here for awareness only; does not affect the verdict.

@tomek-i tomek-i removed the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 20, 2026
@tomek-i

tomek-i commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

✅ crows-nest: reviewed — both lenses (code-review + codex-rescue) found zero blocking issues. Green and awaiting human merge (auto-merge off).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

armada Eligible for the ARMADA fleet to pick up Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error after creating issue: "failed after 3 attempts - cannot connect to API" and final result panel not shown

2 participants