融合最新主线完美像素修复

保留完美像素逻辑分辨率和External v1契约更新。
合并无限画布恢复事务与共享记忆结论。
This commit is contained in:
2026-08-11 17:32:39 +08:00
15 changed files with 233 additions and 75 deletions
@@ -3160,20 +3160,31 @@ describe('useImageCanvasGenerationWorkflow', () => {
it('retries the exact persisted request without creating a second dialog or operation', async () => {
const flushProjectPersistence = vi.fn().mockResolvedValue(undefined);
const applyProjectSnapshot = vi.fn();
snapImageToPerfectPixelsMock
.mockImplementationOnce(async (request: EditorPixelArtSnapInput) =>
createMismatchedPerfectPixelResult(request),
)
.mockImplementationOnce(async (request: EditorPixelArtSnapInput) =>
createMismatchedPerfectPixelResult(request),
.mockRejectedValueOnce(
new ApiClientError({
message: '同一完美像素操作已有其它权威结果,请先读取项目状态对账。',
status: 409,
code: 'HTTP_409',
details: { operationResultAlreadyExists: true },
}),
);
loadEditorProjectMock.mockImplementation(async () => {
const request = snapImageToPerfectPixelsMock.mock.calls.at(-1)?.[0] as
| EditorPixelArtSnapInput
| undefined;
return createConflictingPerfectPixelProject(
request!.canvasCompletion.dialogId,
);
return snapImageToPerfectPixelsMock.mock.calls.length === 1
? createConflictingPerfectPixelProject(
request!.canvasCompletion.dialogId,
)
: createPerfectPixelProject(
request!.canvasCompletion.dialogId,
'applied',
);
});
render(
<GenerationWorkflowHarness
@@ -3184,7 +3195,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
src: '/generated-images/editor/source.png',
}),
]}
applyProjectSnapshot={vi.fn()}
applyProjectSnapshot={applyProjectSnapshot}
flushProjectPersistence={flushProjectPersistence}
/>,
);
@@ -3241,6 +3252,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(flushProjectPersistence).toHaveBeenNthCalledWith(2, {
preferLatestGenerationDialogs: true,
});
await waitFor(() => {
expect(applyProjectSnapshot).toHaveBeenCalledWith(
expect.objectContaining({ projectId: 'project-1' }),
{ type: 'perfect-pixel', count: 1 },
);
});
});
it('anchors the first submission reconciliation window at POST time when the pre-POST flush is slow', async () => {
@@ -3724,7 +3741,10 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('reconciles a responded failure that the server marked as post-persistence', async () => {
it.each([
['post-persistence', { resultPersistenceStarted: true }],
['an existing operation result', { operationResultAlreadyExists: true }],
])('reconciles a responded failure marked as %s', async (_label, details) => {
const applyProjectSnapshot = vi.fn();
const refreshAssetLibrary = vi.fn().mockResolvedValue(undefined);
let operationId = '';
@@ -3736,7 +3756,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
message: '画布版本冲突。',
status: 409,
code: 'HTTP_409',
details: { resultPersistenceStarted: true },
details,
});
},
);
@@ -2781,6 +2781,10 @@ export function useImageCanvasGenerationWorkflow({
error instanceof ApiClientError &&
(error.details as { resultPersistenceStarted?: unknown } | null)
?.resultPersistenceStarted === true;
const operationResultAlreadyExists =
error instanceof ApiClientError &&
(error.details as { operationResultAlreadyExists?: unknown } | null)
?.operationResultAlreadyExists === true;
const outcomeMayBePersisted =
perfectPixelPostAttempted &&
Boolean(perfectPixelDialogId) &&
@@ -2790,6 +2794,9 @@ export function useImageCanvasGenerationWorkflow({
// 而此时 api-server 可能已经走完 OSS PUT 与落库。必须算作未知结果去对账。
(!(error instanceof ApiClientError) ||
persistenceMayHaveStarted ||
// 中文注释:跨算法版本护栏在本请求 PUT 前发现同 operation 已有权威结果;它不应
// 冒充 resultPersistenceStarted,但同样必须读取项目事实完成收口。
operationResultAlreadyExists ||
isGatewayUnknownOutcomeError(error));
let reconciledMessage: string | undefined;
let keepPendingConfirmation = false;
@@ -3023,10 +3030,15 @@ export function useImageCanvasGenerationWorkflow({
error instanceof ApiClientError &&
(error.details as { resultPersistenceStarted?: unknown } | null)
?.resultPersistenceStarted === true;
const operationResultAlreadyExists =
error instanceof ApiClientError &&
(error.details as { operationResultAlreadyExists?: unknown } | null)
?.operationResultAlreadyExists === true;
const outcomeMayBePersisted =
postAttempted &&
(!(error instanceof ApiClientError) ||
persistenceMayHaveStarted ||
operationResultAlreadyExists ||
isGatewayUnknownOutcomeError(error));
if (outcomeMayBePersisted) {
const verdict = await reconcilePerfectPixelProject(