From 11b19832a35477d2136c6ea953dd9c408fd84816 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 18 Aug 2026 11:32:38 +0800 Subject: [PATCH] fix(canvas): block invalid video duration confirmation --- .../20260818-video-duration-client-a7c42f.md | 5 ++++ src/pages/ImageCanvas/index.tsx | 26 ++++++++++++++++++- tests/unit/image-canvas-page.test.tsx | 9 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md b/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md index 5a01280..528bd7b 100644 --- a/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md +++ b/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md @@ -29,6 +29,8 @@ - The confirmation card now filters enabled server-provided video durations to the inclusive 2-6 second client boundary. - A legacy Quote whose selected duration is outside that boundary renders an explicit disabled `请选择时长` placeholder instead of visually selecting one allowed value while retaining a different hidden draft value. - Selecting 6 seconds updates the controlled draft, survives the debounced server re-quote, and is used by final confirmation. +- Final-review P1 follow-up: a video Quote with `null`, an out-of-range duration, a disabled duration, or a duration absent from the filtered server options now disables the confirmation button and is rejected again inside the shared `handleConfirm` path used by direct, quick-reply, and text confirmation. +- Only an explicit allowed selection followed by a successful server re-quote re-enables confirmation. - Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square. ## Verification @@ -40,6 +42,9 @@ - `corepack pnpm exec eslint src/pages/ImageCanvas/index.tsx tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — passed with no output. - `corepack pnpm run build:vite` — passed for Renderer, Electron Main, and Preload; only the existing dynamic-import and chunk-size warnings were reported. - `git diff --check` — passed; Git reported only the repository's LF-to-CRLF checkout warning. +- Final-review P1 red test: the legacy 10-second Quote initially left `确认并开始生成` enabled. +- Final-review P1 focused regression: legacy 10-second initial state disables confirmation and text confirmation sends no request; selecting 6 seconds, completing re-quote, and confirming sends 6 — passed. +- Post-P1 focused suite: 3 files / 92 tests passed; typecheck and scoped ESLint passed; Renderer/Main/Preload `build:vite` passed with the same existing warnings. ## Follow-ups diff --git a/src/pages/ImageCanvas/index.tsx b/src/pages/ImageCanvas/index.tsx index 721b641..5c9dd77 100644 --- a/src/pages/ImageCanvas/index.tsx +++ b/src/pages/ImageCanvas/index.tsx @@ -268,6 +268,20 @@ type EditableGenerationParameters = Partial= MIN_VIDEO_DURATION_SECONDS + && durationSeconds <= MAX_VIDEO_DURATION_SECONDS + && quote.generationOptions.durations.some((option) => ( + !option.disabled && option.value === durationSeconds + )); +} + function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft { return { quoteId: quote.quoteId, @@ -740,6 +754,10 @@ function QuoteCard({ const selectedDuration = availableDurations.some( (option) => option.value === draft.generationParameters.durationSeconds, ) ? String(draft.generationParameters.durationSeconds) : ''; + const durationSelectionValid = hasSelectableVideoDuration( + quote, + draft.generationParameters, + ); const updateParameters = (next: EditableGenerationParameters) => { onDraftChange({ ...draft, @@ -867,7 +885,7 @@ function QuoteCard({ size="sm" className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground" onClick={onConfirm} - disabled={busy || dirty || repricing} + disabled={busy || dirty || repricing || !durationSelectionValid} > {busy || repricing ? @@ -1153,6 +1171,7 @@ export function ImageCanvas() { setQuoteDirty(true); setRepricingQuoteId(quote.quoteId); setQuoteUpdateError(null); + setActionError(null); quoteDraftVersionRef.current += 1; const version = quoteDraftVersionRef.current; if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current); @@ -1189,6 +1208,11 @@ export function ImageCanvas() { setActionError('当前报价正在更新,请等待重新报价完成后再确认。'); return; } + if (!quote || quote.quoteId !== quoteId + || !hasSelectableVideoDuration(quote, draft.generationParameters)) { + setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。'); + return; + } const requestedWorkspaceId = workspace.workspaceId; const requestedConversationId = conversation.conversationId; setConfirmingQuoteId(quoteId); diff --git a/tests/unit/image-canvas-page.test.tsx b/tests/unit/image-canvas-page.test.tsx index 3428ab7..086ae82 100644 --- a/tests/unit/image-canvas-page.test.tsx +++ b/tests/unit/image-canvas-page.test.tsx @@ -544,6 +544,15 @@ describe('ImageCanvas Workspace-first design experience', () => { expect(durationSelect).toHaveValue(''); expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value'))) .toEqual(['', '2', '6']); + expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled(); + + fireEvent.change(screen.getByLabelText('设计需求'), { + target: { value: '确认生成' }, + }); + fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' })); + + expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled(); + expect(await screen.findByRole('alert')).toHaveTextContent('请选择 2 至 6 秒的视频时长'); fireEvent.change(durationSelect, { target: { value: '6' } });