补齐完美像素第三处静默分支并区分两条结果文案
链路复查发现两处。 遗留:成功路径拿到 result.project 后,本地占位已被删除时直接 return。不套用是 对的(删除意图胜出),但什么都不说。同一形状此前已修两处,这是第三处;既有用例 只断言「不套用」,没断言「要说话」,所以没挡住。 新引入:上次让「对账已收口 + 本地已删」沿用了「只在素材库」的文案。两种情况事实 不同——快照里没有 dialog 时服务端画布确实没有结果;dialog 已收口时服务端画布上 有结果,只是本地未同步,重新加载即可见。混用会让用户以为画布上没有而重做。 两条文案抽成常量按事实分派。 第一次回归验证只有 1 条变红,暴露出「对账已收口 + 本地已删」这条分支根本没有 用例;补上后再破坏,2 条同时变红。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6096,3 +6096,14 @@
|
||||
- 两条共性:都是用代理信号代替事实——用「占位在不在」代替「操作完成没有」,用「有没有 HTTP 响应」代替「应用层有没有表态」。两处都是没有去读被代理的那个事实的真实形状。
|
||||
- 验证:新增四条用例(网关 504 触发对账、网关 429 不触发、应用层无标记 502 不触发、快照无 dialog 时不套用快照)。同时破坏两处修复后,四条精确变红。后两条是对照用例,专门守住「放宽判据不得退回反向谎报」。`vitest src/components/image-editor` 907 通过 / 72 文件,typecheck、eslint、check:encoding 通过。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 2026-08-03 完美像素链路复查:补齐第三处静默分支并区分两条结果文案
|
||||
|
||||
- 背景:对账判据与网关分类修完之后做的整链复查,目标是找遗留与新引入的问题,不是重复已知项。
|
||||
- 遗留(第三处静默):成功路径拿到 `result.project` 后,若本地占位已被用户删除则直接 `return`。不套用快照是对的(删除意图胜出),但什么都不说。此前已修过同一形状的两处(`!result.project` 分支、对账早返回),这是第三处,复查才发现。既有用例 `does not apply a completed project after the perfect-pixel placeholder was deleted` 只断言「不套用」,没断言「要说话」,所以也没挡住。
|
||||
- 新引入(文案混用):上一次修复让「对账发现已收口 + 本地占位已删」这一支沿用了「结果已保存到素材库,画布占位已不存在」。**两种情况的事实不同**——快照里没有 dialog 时服务端 completion 返回 `Ok(None)`,画布上确实没有结果图层;而 dialog 已收口时服务端画布上**有**结果图层,只是本地按删除意图没套用,重新加载即可见。用前一条文案会让用户以为画布上没有,再做一遍,正是本链路要消除的重复创建。
|
||||
- 决策:两条文案抽成常量并按事实分派——`PERFECT_PIXEL_ASSET_ONLY_NOTICE` 用于「服务端画布也没有」,`PERFECT_PIXEL_APPLIED_REMOTELY_NOTICE` 用于「服务端画布已有、本地未同步」。四个使用点各归其位。
|
||||
- 测试补位过程值得记:第一次回归验证只有 1 条变红——说明「对账已收口 + 本地已删」这条分支根本没有用例,文案改动是无覆盖的。补上该用例后再破坏,2 条同时变红。**如果止步于第一次验证,就会把一处无覆盖的改动当成已验证。**
|
||||
- 复查中核过、确认不是问题的两点:其一,网关放宽的作用域正确——`/api/*` 走 `is_generic_api_proxy_path`,读超时默认 `3600` 秒,远高于服务端 90 秒预算与客户端 120 秒,次序是 `90 < 120 < 3600`,网关不会在服务端合法工作期间截断,它合成 502/504 只可能是连接失败或进程不可用,确属未知结果;唯一变数是有人把 `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS` 调到 90 秒以下。其二,`projectHasUnresolvedGenerationDialog` 由 `some(...)` 改为「取首个匹配再判」存在极低风险的语义收窄,同 id 多 dialog 时行为不同,但 id 唯一,实际不可达。
|
||||
- 验证:`vitest src/components/image-editor` 908 通过 / 72 文件,typecheck、eslint、check:encoding 通过。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user