Merge remote-tracking branch 'web/master' into feat/five_min_design
冲突两处,均为两侧独立新增: - provider_tool_plan.rs:本分支的 force_autonomous_owner_artifact_delivery 与 master 的 force_root_goal_contract 是各自独立的 let 绑定,两者都保留。 - provider_request_builders.rs:本分支新增测试 full_dag_pre_code_owner_requests_do_not_advertise_manual_verification, master 把紧随其后的 trusted_root_supervisor_receives_dynamic_goal_control_tools 改名为 trusted_root_supervisor_first_turn_only_receives_goal_contract_tool。 保留本分支新增的测试,共享的那个测试采用 master 的新名(其函数体已随 master 自动合并)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1396,6 +1396,71 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
).toMatchObject({ kind: 'pending', project: foreignProject });
|
||||
});
|
||||
|
||||
it('rejects an incomplete matching task resource instead of treating it as an applied result', () => {
|
||||
const operationId = 'perfect-pixel-corrupted-resource';
|
||||
const project = createPerfectPixelProject(operationId, 'applied');
|
||||
project.resources[0] = {
|
||||
...project.resources[0]!,
|
||||
objectKey: null,
|
||||
};
|
||||
|
||||
expect(
|
||||
inspectPerfectPixelProjectSnapshot(project, {
|
||||
operationId,
|
||||
taskId: `pixel-art-snap-${operationId}`,
|
||||
}),
|
||||
).toMatchObject({
|
||||
kind: 'conflict',
|
||||
project,
|
||||
message: '完美像素任务资源记录不完整或不属于当前项目,无法自动确认结果。',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the server-confirmed stable resource id to reject a corrupted task id', () => {
|
||||
const operationId = 'perfect-pixel-corrupted-task';
|
||||
const project = createPerfectPixelProject(operationId, 'dialog-missing');
|
||||
project.resources[0] = {
|
||||
...project.resources[0]!,
|
||||
taskId: 'pixel-art-snap-another-operation',
|
||||
objectKey: null,
|
||||
};
|
||||
|
||||
expect(
|
||||
inspectPerfectPixelProjectSnapshot(
|
||||
project,
|
||||
{
|
||||
operationId,
|
||||
taskId: `pixel-art-snap-${operationId}`,
|
||||
},
|
||||
project.resources[0]!.resourceId,
|
||||
),
|
||||
).toMatchObject({
|
||||
kind: 'conflict',
|
||||
project,
|
||||
message: '完美像素任务资源记录不完整或不属于当前项目,无法自动确认结果。',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a settled dialog whose generated layer points at another resource', () => {
|
||||
const operationId = 'perfect-pixel-mismatched-layer';
|
||||
const project = createPerfectPixelProject(operationId, 'applied');
|
||||
project.layers[0] = {
|
||||
...project.layers[0]!,
|
||||
resourceId: 'resource-other',
|
||||
};
|
||||
|
||||
expect(
|
||||
inspectPerfectPixelProjectSnapshot(project, {
|
||||
operationId,
|
||||
taskId: `pixel-art-snap-${operationId}`,
|
||||
}),
|
||||
).toMatchObject({
|
||||
kind: 'conflict',
|
||||
project,
|
||||
message: '完美像素占位与任务资源的画布关联不一致,无法自动应用。',
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a movable canvas generation placeholder and keeps toolbar state active', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
@@ -3265,6 +3330,81 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reconciles a retried existing-result marker against its corrupted stable resource as a conflict', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
let corruptedProject: EditorProjectSnapshot | undefined;
|
||||
snapImageToPerfectPixelsMock
|
||||
.mockImplementationOnce(async (request: EditorPixelArtSnapInput) =>
|
||||
createMismatchedPerfectPixelResult(request),
|
||||
)
|
||||
.mockImplementationOnce(async (request: EditorPixelArtSnapInput) => {
|
||||
const operationId = request.canvasCompletion.dialogId;
|
||||
corruptedProject = createPerfectPixelProject(
|
||||
operationId,
|
||||
'dialog-missing',
|
||||
);
|
||||
corruptedProject.resources[0] = {
|
||||
...corruptedProject.resources[0]!,
|
||||
taskId: 'pixel-art-snap-corrupted-task',
|
||||
objectKey: null,
|
||||
};
|
||||
throw new ApiClientError({
|
||||
message: '同一完美像素操作已有权威结果,请先读取项目状态对账。',
|
||||
status: 409,
|
||||
code: 'HTTP_409',
|
||||
details: {
|
||||
operationResultAlreadyExists: true,
|
||||
resultResourceId: corruptedProject.resources[0]!.resourceId,
|
||||
},
|
||||
});
|
||||
});
|
||||
loadEditorProjectMock.mockImplementation(async () => {
|
||||
const request = snapImageToPerfectPixelsMock.mock.calls.at(-1)?.[0] as
|
||||
| EditorPixelArtSnapInput
|
||||
| undefined;
|
||||
return snapImageToPerfectPixelsMock.mock.calls.length === 1
|
||||
? createConflictingPerfectPixelProject(
|
||||
request!.canvasCompletion.dialogId,
|
||||
)
|
||||
: corruptedProject!;
|
||||
});
|
||||
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
projectId="project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
objectKey: 'generated-images/editor/source.png',
|
||||
src: '/generated-images/editor/source.png',
|
||||
}),
|
||||
]}
|
||||
applyProjectSnapshot={applyProjectSnapshot}
|
||||
flushProjectPersistence={vi.fn(async () => {})}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '完美像素' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'pending-confirmation',
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试完美像素' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog-error').textContent).toContain(
|
||||
'完美像素任务资源记录不完整或不属于当前项目,无法自动确认结果。',
|
||||
);
|
||||
});
|
||||
|
||||
expect(snapImageToPerfectPixelsMock).toHaveBeenCalledTimes(2);
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledTimes(2);
|
||||
expect(applyProjectSnapshot).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'pending-confirmation',
|
||||
);
|
||||
});
|
||||
|
||||
it('anchors the first submission reconciliation window at POST time when the pre-POST flush is slow', async () => {
|
||||
// 中文注释:pre-POST flush 是服务端硬前置(占位未持久化会被 409 拒收),且没有整体
|
||||
// 上限。窗口若锚在 flush 之前,慢保存会让 POST 带着已过期的 reconciliation deadline
|
||||
@@ -3801,6 +3941,65 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(screen.getByTestId('dialog').textContent).not.toContain('failed');
|
||||
});
|
||||
|
||||
it('uses the server-confirmed stable resource id during existing-result reconciliation', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
let reconciledProject: EditorProjectSnapshot | undefined;
|
||||
snapImageToPerfectPixelsMock.mockImplementationOnce(
|
||||
async (request: EditorPixelArtSnapInput) => {
|
||||
const operationId = request.canvasCompletion.dialogId;
|
||||
reconciledProject = createPerfectPixelProject(
|
||||
operationId,
|
||||
'dialog-missing',
|
||||
);
|
||||
reconciledProject.resources[0] = {
|
||||
...reconciledProject.resources[0]!,
|
||||
taskId: 'pixel-art-snap-corrupted-task',
|
||||
objectKey: null,
|
||||
};
|
||||
throw new ApiClientError({
|
||||
message: '同一完美像素操作已有权威结果,请先读取项目状态对账。',
|
||||
status: 409,
|
||||
code: 'HTTP_409',
|
||||
details: {
|
||||
operationResultAlreadyExists: true,
|
||||
resultResourceId: reconciledProject.resources[0]!.resourceId,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
loadEditorProjectMock.mockImplementationOnce(async () => {
|
||||
expect(reconciledProject).toBeDefined();
|
||||
return reconciledProject!;
|
||||
});
|
||||
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
projectId="project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
objectKey: 'generated-images/editor/source.png',
|
||||
src: '/generated-images/editor/source.png',
|
||||
}),
|
||||
]}
|
||||
applyProjectSnapshot={applyProjectSnapshot}
|
||||
flushProjectPersistence={vi.fn(async () => {})}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '完美像素' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog-error').textContent).toContain(
|
||||
'完美像素任务资源记录不完整或不属于当前项目,无法自动确认结果。',
|
||||
);
|
||||
});
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledTimes(1);
|
||||
expect(applyProjectSnapshot).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('dialog').textContent).toContain(
|
||||
'pending-confirmation',
|
||||
);
|
||||
});
|
||||
|
||||
it('skips reconciliation for a responded failure the server did not mark', async () => {
|
||||
// 中文注释:纯校验失败发生在任何 IO 之前,不可能留下对象或素材。服务端不置
|
||||
// resultPersistenceStarted,客户端就不该多打两次读取,也不该附上「请核对素材库」
|
||||
|
||||
@@ -334,9 +334,14 @@ export type PerfectPixelProjectVerdict =
|
||||
export function inspectPerfectPixelProjectSnapshot(
|
||||
project: EditorProjectSnapshot,
|
||||
operation: Pick<PerfectPixelOperationSnapshot, 'operationId' | 'taskId'>,
|
||||
resultResourceId?: string | null,
|
||||
): PerfectPixelProjectVerdict {
|
||||
const normalizedResultResourceId = resultResourceId?.trim() ?? '';
|
||||
const matchingResources = project.resources.filter(
|
||||
(resource) => resource.taskId?.trim() === operation.taskId,
|
||||
(resource) =>
|
||||
resource.taskId?.trim() === operation.taskId ||
|
||||
(normalizedResultResourceId !== '' &&
|
||||
resource.resourceId.trim() === normalizedResultResourceId),
|
||||
);
|
||||
const matchingDialogs = findCanvasGenerationDialogRecords(
|
||||
project,
|
||||
@@ -369,6 +374,20 @@ export function inspectPerfectPixelProjectSnapshot(
|
||||
message: '完美像素占位已收口,但权威项目缺少对应任务资源。',
|
||||
};
|
||||
}
|
||||
if (
|
||||
(normalizedResultResourceId !== '' &&
|
||||
resource.resourceId.trim() !== normalizedResultResourceId) ||
|
||||
resource.taskId?.trim() !== operation.taskId ||
|
||||
resource.projectId !== project.projectId ||
|
||||
!resource.objectKey?.trim() ||
|
||||
!resource.imageSrc.trim()
|
||||
) {
|
||||
return {
|
||||
kind: 'conflict',
|
||||
project,
|
||||
message: '完美像素任务资源记录不完整或不属于当前项目,无法自动确认结果。',
|
||||
};
|
||||
}
|
||||
if (!dialog) {
|
||||
return { kind: 'dialog-missing', project, resource };
|
||||
}
|
||||
@@ -436,7 +455,7 @@ function waitForPerfectPixelReconciliationDelay(
|
||||
async function reconcilePerfectPixelProject(
|
||||
projectId: string,
|
||||
operation: PerfectPixelOperationSnapshot,
|
||||
options: { signal?: AbortSignal } = {},
|
||||
options: { signal?: AbortSignal; resultResourceId?: string | null } = {},
|
||||
): Promise<PerfectPixelProjectVerdict> {
|
||||
let attempt = 0;
|
||||
let hasAttemptedRead = false;
|
||||
@@ -478,6 +497,7 @@ async function reconcilePerfectPixelProject(
|
||||
const verdict = inspectPerfectPixelProjectSnapshot(
|
||||
latestProject,
|
||||
operation,
|
||||
options.resultResourceId,
|
||||
);
|
||||
if (verdict.kind !== 'pending') {
|
||||
return verdict;
|
||||
@@ -2785,6 +2805,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
error instanceof ApiClientError &&
|
||||
(error.details as { operationResultAlreadyExists?: unknown } | null)
|
||||
?.operationResultAlreadyExists === true;
|
||||
const existingResultResourceId =
|
||||
operationResultAlreadyExists && error instanceof ApiClientError
|
||||
? (error.details as { resultResourceId?: unknown } | null)
|
||||
?.resultResourceId
|
||||
: null;
|
||||
const outcomeMayBePersisted =
|
||||
perfectPixelPostAttempted &&
|
||||
Boolean(perfectPixelDialogId) &&
|
||||
@@ -2808,6 +2833,12 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const verdict = await reconcilePerfectPixelProject(
|
||||
normalizedProjectId,
|
||||
perfectPixelOperation,
|
||||
{
|
||||
resultResourceId:
|
||||
typeof existingResultResourceId === 'string'
|
||||
? existingResultResourceId
|
||||
: null,
|
||||
},
|
||||
);
|
||||
if (!isPerfectPixelAuthorityCurrent(operationAuthority)) {
|
||||
return;
|
||||
@@ -3034,6 +3065,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
error instanceof ApiClientError &&
|
||||
(error.details as { operationResultAlreadyExists?: unknown } | null)
|
||||
?.operationResultAlreadyExists === true;
|
||||
const existingResultResourceId =
|
||||
operationResultAlreadyExists && error instanceof ApiClientError
|
||||
? (error.details as { resultResourceId?: unknown } | null)
|
||||
?.resultResourceId
|
||||
: null;
|
||||
const outcomeMayBePersisted =
|
||||
postAttempted &&
|
||||
(!(error instanceof ApiClientError) ||
|
||||
@@ -3044,6 +3080,12 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const verdict = await reconcilePerfectPixelProject(
|
||||
normalizedProjectId,
|
||||
retriedOperation,
|
||||
{
|
||||
resultResourceId:
|
||||
typeof existingResultResourceId === 'string'
|
||||
? existingResultResourceId
|
||||
: null,
|
||||
},
|
||||
);
|
||||
if (!isPerfectPixelAuthorityCurrent(operationAuthority)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user