完美像素补齐两处静默路径与缓存剥离的失败上报
A3-1906。对账分支发现权威快照里占位已被 completion 消费、本地占位也被用户 删除时直接静默 return。不应用快照是对的(删除意图胜出),但结论与相邻分支 一致——结果已落库,用户删掉的是占位不是素材,必须告诉他素材库多了一份。 同一形状的孪生分支在成功路径:!result.project 时提示被写在 if (hasCanvasGenerationDialogById(...)) 里面,占位不在就整段静默。改为无条件 提示、条件移除。 A4。会话缓存剥掉的占位数单独计,不并入提示计数——完美像素成功后缓存可能停留 在完成之前的版本,并入会报「上次处理未完成」的假告警。只在权威加载失败、没有 第二个来源纠正画面时才提示。已核实 isProjectReady 不挡画布渲染,该场景下用户 确实看得到一张静默少了占位的画布。 新增用例在去掉 1906 提示后失败。A4 无专用测试,理由记在 decision-log。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2748,6 +2748,63 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
).toBe('空闲');
|
||||
});
|
||||
|
||||
it('reports the asset-library outcome when reconciliation finds both placeholders gone', async () => {
|
||||
// 中文注释:对账发现权威快照里占位已被 completion 消费——同一分支下一步正是据此把结果
|
||||
// 当成功套用,所以结论一致:结果已落库。本地占位也没了(用户删的)时不应用快照是对的,
|
||||
// 但不能连话都不说:用户删掉的是占位而不是素材,不提示他就既看不到画布结果、也不知道
|
||||
// 素材库多了一份,进而重复执行。
|
||||
let rejectPerfectPixel: ((error: unknown) => void) | undefined;
|
||||
snapImageToPerfectPixelsMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectPerfectPixel = reject;
|
||||
}),
|
||||
);
|
||||
// 中文注释:对账 GET 返回的项目里不含该占位,模拟 completion 已消费。
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'project-1',
|
||||
title: '未命名画布',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
updatedAt: '2026-08-03T00:00:00.000Z',
|
||||
});
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
projectId="project-1"
|
||||
applyProjectSnapshot={vi.fn()}
|
||||
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('-');
|
||||
});
|
||||
|
||||
// 中文注释:裸 Error 表达 transport 异常(未知结果),这样才会进入对账分支。
|
||||
rejectPerfectPixel?.(new Error('网络中断'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'完美像素结果已保存到素材库,画布占位已不存在。',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not apply a completed project after the perfect-pixel placeholder was deleted', async () => {
|
||||
let resolvePerfectPixel:
|
||||
| ((value: {
|
||||
|
||||
@@ -1840,12 +1840,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
});
|
||||
upsertGeneratedAsset?.(result.asset);
|
||||
if (!result.project) {
|
||||
// 中文注释:提示不能挂在「占位还在」这个条件里。占位已被删除时结果同样已经落库,
|
||||
// 而且此时用户更需要知道——他删掉的是占位,不是已经生成的素材。原先提示与移除
|
||||
// 绑在同一个 if 里,占位不在就整段静默,用户不知道素材库多了一份。
|
||||
if (hasCanvasGenerationDialogById(perfectPixelDialogId)) {
|
||||
updateCanvasGenerationDialogById(perfectPixelDialogId, () => null);
|
||||
showGenerationWarning(
|
||||
'完美像素结果已保存到素材库,画布占位已不存在。',
|
||||
);
|
||||
}
|
||||
showGenerationWarning('完美像素结果已保存到素材库,画布占位已不存在。');
|
||||
return;
|
||||
}
|
||||
if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
|
||||
@@ -1904,6 +1905,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
// 契约要求此时不应用完成快照、不写历史,删除意图胜出。成功路径同一处有
|
||||
// 这道检查,对账路径不能漏。
|
||||
if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
|
||||
// 中文注释:不应用快照是对的(删除意图胜出),但不能连话都不说。走到这里
|
||||
// 意味着权威快照里占位已被 completion 消费——本分支下一步正是据此把结果
|
||||
// 当成功套用,所以结论一致:结果已落库。用户删掉的是占位而不是素材,必须
|
||||
// 告诉他素材库多了一份,否则他既看不到画布结果也不知道素材已生成。
|
||||
showGenerationWarning(
|
||||
'完美像素结果已保存到素材库,画布占位已不存在。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
applyProjectSnapshot(reconciled, {
|
||||
|
||||
@@ -1197,13 +1197,17 @@ export function useImageCanvasProjectPersistence({
|
||||
projectIdFromQuery,
|
||||
currentUserId,
|
||||
);
|
||||
// 中文注释:缓存里剥掉的数量单独记,不能直接并入提示计数。会话缓存可能停留在完美像素
|
||||
// 完成之前的版本(`applyProjectSnapshot` 置了 skipNext,成功后的第一次 effect 不落库),
|
||||
// 此时缓存里的占位是陈旧的而权威快照其实已经成功——并入就会报一条「上次处理未完成」的
|
||||
// 假告警。只有权威加载没能纠正这幅画面时,这个计数才代表真的有孤儿占位被静默清掉。
|
||||
let cachedDeadPlaceholderCount = 0;
|
||||
if (cachedProject) {
|
||||
// 中文注释:会话缓存写于占位落库之后,同样可能带着已死会话的 generating 占位,
|
||||
// 必须和权威快照走同一道剥离,否则首屏会先闪一个永远转圈的占位。
|
||||
applyProjectSnapshot(
|
||||
dropDeadInlineGenerationPlaceholders(cachedProject.project).project,
|
||||
{ authoritative: false },
|
||||
);
|
||||
const cached = dropDeadInlineGenerationPlaceholders(cachedProject.project);
|
||||
cachedDeadPlaceholderCount = cached.droppedCount;
|
||||
applyProjectSnapshot(cached.project, { authoritative: false });
|
||||
}
|
||||
const loadProject = projectIdFromQuery
|
||||
? loadEditorProject(projectIdFromQuery)
|
||||
@@ -1262,6 +1266,16 @@ export function useImageCanvasProjectPersistence({
|
||||
return;
|
||||
}
|
||||
replaceAppHistoryPath('/project');
|
||||
return;
|
||||
}
|
||||
// 中文注释:权威快照没能到达,缓存里剥掉的占位就没有第二个来源可以纠正。此时画布
|
||||
// 仍在渲染(isProjectReady 只管启动意图与自动保存,不挡画面),用户看到的是一张
|
||||
// 静默少了占位的画布——必须提示,否则他既不知道占位被清掉,也不知道素材库可能已有
|
||||
// 派生图。鉴权失败与项目失访这两条路径已各自跳转或弹窗,不在这里重复打扰。
|
||||
if (cachedDeadPlaceholderCount > 0) {
|
||||
setDeadInlinePlaceholderDropCount(
|
||||
(currentCount) => currentCount + cachedDeadPlaceholderCount,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user