补齐完美像素第三处静默分支并区分两条结果文案

链路复查发现两处。

遗留:成功路径拿到 result.project 后,本地占位已被删除时直接 return。不套用是
对的(删除意图胜出),但什么都不说。同一形状此前已修两处,这是第三处;既有用例
只断言「不套用」,没断言「要说话」,所以没挡住。

新引入:上次让「对账已收口 + 本地已删」沿用了「只在素材库」的文案。两种情况事实
不同——快照里没有 dialog 时服务端画布确实没有结果;dialog 已收口时服务端画布上
有结果,只是本地未同步,重新加载即可见。混用会让用户以为画布上没有而重做。

两条文案抽成常量按事实分派。

第一次回归验证只有 1 条变红,暴露出「对账已收口 + 本地已删」这条分支根本没有
用例;补上后再破坏,2 条同时变红。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 10:00:40 +00:00
parent 925238e221
commit c9db36b177
3 changed files with 111 additions and 7 deletions
@@ -2610,6 +2610,76 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(screen.getByTestId('dialog').textContent).toBe('-');
});
it('tells the user the reconciled result already landed remotely after they deleted the placeholder', async () => {
// 中文注释:对账发现服务端已收口(dialog 保留且写了 generatedLayerId),说明结果**已经在
// 服务端画布上**;而本地占位被用户删掉,按删除意图不套用快照。此时不能用「只在素材库」
// 那条文案——它会让用户以为画布上没有,再做一遍造出第二份。必须说清重新加载后可见。
const applyProjectSnapshot = vi.fn();
const reconciledProject = {
projectId: 'project-1',
title: '未命名画布',
viewport: { x: 0, y: 0, scale: 1 },
layers: [
{
itemType: 'generation-dialog',
layerId: 'generation-dialog:generation-dialog-1',
resourceId: 'generation-dialog:generation-dialog-1',
dialog: {
id: 'generation-dialog-1',
mode: 'quick-edit',
prompt: '完美像素',
status: 'idle',
composerOpen: false,
generatedLayerId: 'layer-perfect-pixel-1',
},
},
],
resources: [],
updatedAt: '2026-08-01T00:00:00.000Z',
};
let rejectPerfectPixel: ((error: unknown) => void) | undefined;
snapImageToPerfectPixelsMock.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectPerfectPixel = reject;
}),
);
loadEditorProjectMock.mockResolvedValueOnce(reconciledProject);
render(
<GenerationWorkflowHarness
projectId="project-1"
applyProjectSnapshot={applyProjectSnapshot}
flushProjectPersistence={vi.fn().mockResolvedValue(undefined)}
initialLayers={[
createLayer({
objectKey: 'generated-images/editor/source.png',
src: '/generated-images/editor/source.png',
}),
]}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '完美像素' }));
await waitFor(() => {
expect(screen.getByTestId('dialog').textContent).toBe(
'quick-edit:generating:closed:-:placeholder',
);
});
fireEvent.click(screen.getByRole('button', { name: '删除处理占位' }));
await waitFor(() => {
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
});
rejectPerfectPixel?.(new Error('网络中断'));
await waitFor(() => {
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
'完美像素结果已生成并保存到素材库。画布占位已被删除,结果未落入当前画布,重新加载后可见。',
);
});
expect(applyProjectSnapshot).not.toHaveBeenCalled();
});
it('treats a gateway-synthesized timeout as an unknown outcome and reconciles', async () => {
// 中文注释:网关自己合成的 504 只有 code / message,没有 details,所以既不是 transport
// 异常也拿不到 resultPersistenceStarted。按「有响应即结果已知」会跳过对账,而此时
@@ -2779,6 +2849,11 @@ describe('useImageCanvasGenerationWorkflow', () => {
'完美像素结果已保存到素材库,画布占位已不存在。',
);
});
// 中文注释:本例服务端 completion 返回 Ok(None),画布上确实没有结果图层,所以只能用
// 「只在素材库」这条;用「重新加载后可见」会是谎报。两条文案对应两种不同的事实。
expect(screen.getByTestId('reference-pick-warning').textContent).not.toContain(
'重新加载后可见',
);
expect(applyProjectSnapshot).not.toHaveBeenCalled();
expect(screen.getByTestId('generation-dialogs').textContent).toBe('-');
});
@@ -3099,6 +3174,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
});
expect(applyProjectSnapshot).not.toHaveBeenCalled();
expect(screen.getByTestId('dialog').textContent).toBe('-');
// 中文注释:不套用是对的,但不能静默——服务端已经完成并把结果落到画布上,只是本地按
// 删除意图没同步。文案必须说清「重新加载后可见」,不能沿用「只在素材库」那条:后者会让
// 用户以为画布上没有,再做一遍。
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
'完美像素结果已生成并保存到素材库。画布占位已被删除,结果未落入当前画布,重新加载后可见。',
);
});
it('opens UI design extraction as a mark selection state before submitting', () => {
@@ -1752,6 +1752,18 @@ export function useImageCanvasGenerationWorkflow({
],
);
// 中文注释:两条文案对应两种**不同的事实**,不能共用。
// 「未落入画布」:服务端 completion 返回 Ok(None)——占位在处理期间被删掉,服务端画布上
// 没有结果图层,结果只在素材库。
// 「未落入当前画布」:服务端已经完成回填,画布上**有**结果图层,只是本地占位已被用户删除、
// 按删除意图不套用快照,所以本地看不到;重新加载即可见。
// 早先这两种情况共用前一条文案,会让「结果其实已在画布上」的用户以为只进了素材库,
// 进而重做一遍——正是本链路要消除的重复创建。
const PERFECT_PIXEL_ASSET_ONLY_NOTICE =
'完美像素结果已保存到素材库,画布占位已不存在。';
const PERFECT_PIXEL_APPLIED_REMOTELY_NOTICE =
'完美像素结果已生成并保存到素材库。画布占位已被删除,结果未落入当前画布,重新加载后可见。';
const snapSelectedLayerToPerfectPixels = useCallback(
async (sourceLayer: CanvasLayer) => {
const normalizedProjectId = projectId?.trim();
@@ -1852,10 +1864,14 @@ export function useImageCanvasGenerationWorkflow({
if (hasCanvasGenerationDialogById(perfectPixelDialogId)) {
updateCanvasGenerationDialogById(perfectPixelDialogId, () => null);
}
showGenerationWarning('完美像素结果已保存到素材库,画布占位已不存在。');
showGenerationWarning(PERFECT_PIXEL_ASSET_ONLY_NOTICE);
return;
}
if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
// 中文注释:不套用是对的(删除意图胜出),但不能静默。服务端已经完成并把结果落到了
// 画布上,只是本地按用户的删除意图没有同步——必须说清楚,否则用户既看不到结果、
// 也不知道它已经存在,会再做一遍。这是本链路同一形状的第三处,前两处已修。
showGenerationWarning(PERFECT_PIXEL_APPLIED_REMOTELY_NOTICE);
return;
}
applyProjectSnapshot(result.project, {
@@ -1923,18 +1939,14 @@ export function useImageCanvasGenerationWorkflow({
() => null,
);
}
showGenerationWarning(
'完美像素结果已保存到素材库,画布占位已不存在。',
);
showGenerationWarning(PERFECT_PIXEL_ASSET_ONLY_NOTICE);
return;
}
if (!isUnresolvedCanvasGenerationDialogRecord(reconciledDialog)) {
// 中文注释:服务端已经完成并回填,只是响应没回来。这是真成功。
// 但本地占位若已被用户删除,删除意图仍然胜出:不套用快照、不写历史。
if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
showGenerationWarning(
'完美像素结果已保存到素材库,画布占位已不存在。',
);
showGenerationWarning(PERFECT_PIXEL_APPLIED_REMOTELY_NOTICE);
return;
}
applyProjectSnapshot(reconciled, {