Repository navigation
Guard setup inputs on the real project load error and hydration state - #202
Conversation
Fixes SM-1 (HIGH) and SM-17 (LOW).
When a user archives the selected storyboard candidate, the candidate card
is hidden from the storyboard UI, but its index previously remained in
scene.selectedCandidateIndex. Because resolveSceneRenderClip lacked an
archive check, the hidden candidate was still fed into the combine workflow
and rendered into the exported video.
This implements a two-layer defense:
1. In Storyboard.toggleArchive (storyboard.ts), when archiving the candidate
currently pointed to by scene.selectedCandidateIndex, clear the selection
by setting scene.selectedCandidateIndex = undefined. Additionally, evict
candidate.referenceImage?.preview?.path along with the high quality
thumbnail and reference image paths from the thumbnail cache (SM-17).
2. As a defense-in-depth safety net, in resolveSceneRenderClip (config.ts),
check candidate.isArchived and return {state: 'not-selected'}. Returning
'not-selected' drops the unselected/archived scene from the rendered
composition without disabling the Render button for the entire project,
which returning 'invalid' would do.
Added regression tests in archived-candidate-render.spec.ts covering clip
resolution states, combine workflow submission filtering, and toggleArchive
selection clearing and cache eviction.
|
Still need readability approvals from:
|
e1506d3 to
18e36e9
Compare
The rxResource loader catches HTTP load errors and records them in projectLoadErrorValue rather than re-throwing, causing projectConfig.error() to remain permanently undefined. Consequently, the !projectConfig.error() conjunct in setupInputsLoaded was a dead guard. Switch the check to !this.projectLoadError(), guarding setup input readiness on the actual error signal. Additionally, add an explicit setupInputsHydrated signal tracking whether the full project input config was actually hydrated by a successful full GET. This closes the narrow data-loss path where a spurious 404 on Setup navigation with an unsettled editor save returns the local project without inputConfig; with setupInputsHydrated gating setupInputsLoaded, Setup will not prematurely synthesize a blank inputConfig or issue a destructive full-replacement PATCH /api/projects/:id. Cover with regression tests in config-mediated.spec.ts and setup.spec.ts asserting setupInputsLoaded is false on load errors and in the 404 unsettled-save edge case, and verifying that no full-replacement PATCH is issued.
18e36e9 to
b1972ff
Compare
|
Still need readability approvals from:
|
PR #202 — P2 — settled unhydrated state remains “Loading project…”Reviewed head: The hydration gate correctly prevents a destructive blank-input save, but the full-load 404/local-copy branch at A reachable case is shared-project deletion: a user edits an existing project, an editor save is pending, another admitted user deletes the project, and navigation to Setup plus the pending PATCH both return 404. The save-error snackbar retries the PATCH, not the Setup load. A probe using the real ConfigService and Setup DOM confirms the spinner remains after both requests settle. The broader GET-404/PATCH-200 example was not relied on as production evidence. Please keep the hydration safety gate and expose an explicit current-view error/deleted-project state with recovery/navigation. Acceptance: this sequence leaves the loading state, preserves unsaved local data as appropriate, and never synthesizes blank inputConfig or sends a full replacement before hydration. Reuse existing recovery UI; no new retry/save framework is needed. |
Review: merge after small fixes — one two-line template change closes the remaining gapMulti-agent review (reviewer → independent critique agent re-verifying each claim against the code). The critique pass rejected the reviewer's proposed fix and found a cheaper one; details below. The guard is correct and matches DEVELOPING.md's "absent Important In the scenario this PR fixes, the user now gets a permanent spinnerThe 404-with-local-copy branch (
This is still strictly better than the old behaviour (blank inputs + a full PATCH that wipes server state), so it's not a merge blocker. But the fix is small enough to land here. Cheapest correct fix — template-only, in Two edited lines, and it gives the Retry affordance DEVELOPING.md already promises. Smaller than the follow-up issue would be. Suggested deletions (~26 lines)
Keep the "normal successful load" control — that one guards against over-guarding, which would strand every user on the spinner. And keep the Nit: three unreachable
|
Consolidated ReviewVerdict: Fix one blocker, then merge. This note reconciles the 19:29 and 19:50 comments against head Do before merge
Optional, does not block
Rejected or superseded, do not re-litigate
Evidence
Way forward
|
Follow-up on Consolidated ReviewI addressed the Setup recovery blocker from the Consolidated Review on head
Verification: focused Setup and ConfigService suites passed with 90 tests; lint and spec typecheck passed. Recovery assertions failed before the fix and passed afterward, covering pre-load/pending states, the double-404 race, normal hydration, local creation, and retry. Chrome at 1280x1169 and 100% zoom checked the failure, Retry, and keyboard-navigation home states; the race remains covered by HTTP tests rather than a live cloud browser check. This branch includes the updated #199 parent. Because application workflows target PRs based on Parent #199 CI passed at |
…ders' into fix/setup-load-error-guard
…round empty scenes
… load normalization
|
Still need readability approvals from:
|
|
Still need readability approvals from:
|
| @@ -1510,6 +1533,7 @@ export class ConfigService { | |||
| // Not persisted yet: the first autosave POSTs /api/projects, where the | |||
| // server stamps createdBy from the verified identity. Left undefined here. | |||
| this.persistedProjectIds.delete(uuid); | |||
| this.setupInputsHydrated.set(true); | |||
There was a problem hiding this comment.
When leaving an editor route (/:id/storyboard, /:id/composition, or /:id/output-video where this.projectView() is 'editor') or leaving a failed Setup load via Back to projects ('/'), resetProjectConfig() and setNewProject() do not reset this.projectView to 'full' or clear this.projectLoadErrorValue.
Consequently, when creating a new project after visiting an editor route (loadProjectConfig('project-a', 'editor') -> resetProjectConfig() -> setNewProject('project-b') -> saveNow() -> loadProjectConfig('project-b', 'full')):
this.projectView()remains'editor', sothis.setupInputsLoaded()isfalseimmediately aftersetNewProject('project-b').- When
loadProjectConfig('project-b', 'full')runs on navigation to/:id/setup, the short-circuit guardview === this.projectView() || (view === 'editor' && alreadyFull)at line 1587 evaluates tofalsebecausethis.projectView()is still'editor'. loadProjectConfigthen setsthis.projectId.set('project-b'), which causes theprojectConfigloader to resetthis.setupInputsHydrated.set(false)and dispatch aGET /api/projects/project-bthat races the in-flightPOST /api/projects(and triggerssetupInputsError() === trueif theGETreturns404before thePOSTcommits).
Resetting projectLoadErrorValue, projectId, and projectView in resetProjectConfig() and setNewProject() ensures locally created projects remain hydrated and short-circuit loadProjectConfig regardless of the previous route's view mode.
| this.projectLoadErrorValue.set(undefined); | |
| this.setupInputsHydrated.set(false); | |
| this.projectId.set(null); | |
| this.projectView.set('full'); | |
| this.projectConfig.set({...this.DEFAULT_PROJECT_CONFIG()}); | |
| this.shouldSave = false; | |
| } | |
| updateProjectConfig(partial: Partial<ProjectConfig>) { | |
| this.shouldSave = true; | |
| this.projectConfig.update(config => { | |
| return { | |
| ...config, | |
| ...partial, | |
| }; | |
| }); | |
| } | |
| setNewProject(uuid: string) { | |
| // Not persisted yet: the first autosave POSTs /api/projects, where the | |
| // server stamps createdBy from the verified identity. Left undefined here. | |
| this.persistedProjectIds.delete(uuid); | |
| this.projectLoadErrorValue.set(undefined); | |
| this.setupInputsHydrated.set(true); | |
| this.projectId.set(null); | |
| this.projectView.set('full'); |
There was a problem hiding this comment.
Good catch, I reproduced it. After an editor route (or a failed load), projectView stayed 'editor' and the stale projectLoadErrorValue stayed set. So after "New project", setupInputsLoaded() was false, and Setup was stuck until a reload.
Fix (config.ts):
resetProjectConfig()now also clearsprojectLoadErrorValueand resetsprojectViewto'full'.setNewProject()now also clearsprojectLoadErrorValueand resetsprojectViewto'full'. Both happen beforeprojectConfig.set(project), so the resource can't reload and overwrite the local project.- I deliberately did not null
projectIdinsidesetNewProject(). A first attempt did, and it broke the existingdoes not mark a recreated project persisted from a stale load successtest, which relies on that contract.resetProjectConfig()already nulls it on the normal "leave project" path.
Test: keeps a locally created project ready after leaving an editor route or failed load in config-mediated.spec.ts. It covers: editor route, then a stale load error, then resetProjectConfig(), then setNewProject(). It then asserts that setupInputsLoaded() is true and stays true after loadProjectConfig(id, 'full'), with no GET /api/projects/<new>, no setupInputsError, and no projectLoadError.
Mutation check: I reverted only the config.ts hunk and this test fails (1 failed / 90 passed); with the fix restored it passes.
Full gate: compile, lint, typecheck:spec, the full UI test suite, and the Python suite are all green (details in the commit).
| const home = fixture.nativeElement.querySelector('a[href="/"]'); | ||
| expect(home?.textContent).toContain('Back to projects'); | ||
| expect(fixture.nativeElement.querySelector('.setup-container')).toBeNull(); | ||
| expect(config.projectConfig.value().inputConfig).toBeUndefined(); | ||
| http.expectNone('/api/projects/proj-unsettled'); | ||
| }); |
There was a problem hiding this comment.
In the double-404 recovery test (projectLoadError() === false with !setupInputsHydrated()), we verify that Back to projects is rendered, but we don't currently exercise clicking Retry (config.reloadProjectConfig()) from this state to confirm that a subsequent 200 OK response hydrates inputConfig, preserves the unsaved local editor edit (name: 'In-Flight'), clears setupInputsError(), and opens .setup-container.
| const home = fixture.nativeElement.querySelector('a[href="/"]'); | |
| expect(home?.textContent).toContain('Back to projects'); | |
| expect(fixture.nativeElement.querySelector('.setup-container')).toBeNull(); | |
| expect(config.projectConfig.value().inputConfig).toBeUndefined(); | |
| http.expectNone('/api/projects/proj-unsettled'); | |
| }); | |
| const home = fixture.nativeElement.querySelector('a[href="/"]'); | |
| expect(home?.textContent).toContain('Back to projects'); | |
| expect(fixture.nativeElement.querySelector('.setup-container')).toBeNull(); | |
| expect(config.projectConfig.value().inputConfig).toBeUndefined(); | |
| http.expectNone('/api/projects/proj-unsettled'); | |
| const retry = fixture.nativeElement.querySelector( | |
| '.loading-state button', | |
| ) as HTMLButtonElement | null; | |
| expect(retry?.textContent).toContain('Retry'); | |
| retry?.click(); | |
| TestBed.tick(); | |
| const retryGet = http.expectOne('/api/projects/proj-unsettled'); | |
| retryGet.flush({ | |
| id: 'proj-unsettled', | |
| name: 'Server Name', | |
| aspectRatio: '16:9', | |
| resolution: '720p', | |
| candidateDurationSeconds: 4, | |
| generateAudio: false, | |
| numberOfCandidates: 1, | |
| model: 'veo-default', | |
| inputConfig: {products: [], composition: 'Recovered composition'}, | |
| storyboard: [], | |
| audioTracks: [], | |
| visualOverlays: [], | |
| }); | |
| TestBed.tick(); | |
| await fixture.whenStable(); | |
| fixture.detectChanges(); | |
| expect(config.setupInputsError()).toBe(false); | |
| expect(config.setupInputsLoaded()).toBe(true); | |
| expect(config.projectConfig.value().name).toBe('In-Flight'); | |
| expect(config.projectConfig.value().inputConfig).toEqual({ | |
| products: [], | |
| composition: 'Recovered composition', | |
| }); | |
| expect( | |
| fixture.nativeElement.querySelector('.setup-container'), | |
| ).not.toBeNull(); | |
| }); |
There was a problem hiding this comment.
Agreed, and applied as suggested (unchanged). The double-404 recovery test now also clicks Retry and flushes a 200. It asserts:
setupInputsError()is false;setupInputsLoaded()is true;- the unsaved local edit
name: 'In-Flight'is kept; inputConfigis hydrated toRecovered composition;.setup-containerrenders.
Mutation check: I turned reloadProjectConfig() into a no-op. This test fails, along with 2 other retry tests; with the method restored they all pass.
resetProjectConfig() and setNewProject() now clear the stale projectLoadError and reset projectView to 'full', so a locally created project is not stuck behind setupInputsLoaded() === false after leaving an editor route or a failed load. Also extend the double-404 recovery test to click Retry and verify a 200 hydrates inputConfig, keeps the unsaved local edit, clears setupInputsError and renders the Setup form. Addresses review feedback from victor-paunescu on #202.
|
Readability approvals granted:
|
Summary
Keep Setup inputs closed until a full project load succeeds or the project is created locally (SM-10). Use the recorded load-error state rather than the resource error that the loader catches internally.
When a full load returns 404 while a local editor save is unsettled, retain the editor copy without synthesizing blank inputs or issuing a full replacement PATCH. Setup now shows recovery controls instead of an indefinite spinner. Retry supports a transient failure; Back to projects provides an exit for a deleted project. This recovery state is limited to Setup and does not replace the other editors with an error screen.
Validation
Stacked on #199. Its updated parent is included; merge after #199. Rebase and retarget to
mainafter the parent squash merge for fresh application CI.