修复泥点消耗刷新不及时 (#138)
refactor: - 主站复用game agent的zustand store fix: - 修复旧请求覆盖问题 - 修复账号切换问题 - 主站原本已经每 4 秒轮询 external generation 任务状态, 在这里补充触发余额刷新的时机 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/138 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #138.
This commit is contained in:
@@ -474,6 +474,423 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('refreshes the wallet while an external task is running and after a failed refund settles', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTasksCompleted = vi.fn();
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
let activeRequestCount = 0;
|
||||
const runningTask = createExternalTask({
|
||||
jobId: 'wallet-running-task',
|
||||
requestLabel: '长耗时图片生成',
|
||||
status: 'running',
|
||||
priceMudPoints: 20,
|
||||
});
|
||||
const failedTask = createExternalTask({
|
||||
...runningTask,
|
||||
status: 'failed',
|
||||
progress: 0,
|
||||
phaseDetail: '生成失败。',
|
||||
error: '生成失败,泥点已退回。',
|
||||
completedAt: new Date().toISOString(),
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: activeRequestCount === 1 ? 1 : 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount === 1 ? 0 : 1,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount === 1 ? [runningTask] : [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount > 1 ? 1 : 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount > 1 ? [failedTask] : [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTasksCompleted={onExternalTasksCompleted}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(2);
|
||||
expect(onExternalTasksCompleted).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps wallet polling global when the active external task belongs to another project', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
let activeRequestCount = 0;
|
||||
const otherProjectTask = createExternalTask({
|
||||
jobId: 'other-project-wallet-task',
|
||||
sourceEntityId: 'project-other',
|
||||
status: 'running',
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: activeRequestCount === 1 ? 1 : 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount === 1 ? [otherProjectTask] : [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.queryByText('发光猫咪主视觉')).toBeNull();
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('retries a failed bootstrap and starts wallet polling from the recovered active task', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
const runningTask = createExternalTask({
|
||||
jobId: 'bootstrap-recovered-task',
|
||||
status: 'running',
|
||||
});
|
||||
let activeRequestCount = 0;
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(
|
||||
options: Parameters<typeof listExternalGenerationTasks>[0] = {},
|
||||
): ReturnType<typeof listExternalGenerationTasks> => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
if (activeRequestCount === 1) {
|
||||
return Promise.reject(new Error('temporary bootstrap failure'));
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 1,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: [runningTask],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(2);
|
||||
expect(activeRequestCount).toBe(3);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps a successful active task and wallet polling when only completed tasks fail', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
const runningTask = createExternalTask({
|
||||
jobId: 'active-while-completed-fails',
|
||||
requestLabel: '仍在生成的任务',
|
||||
status: 'running',
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 1,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
tasks: [runningTask],
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error('completed tasks unavailable'));
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.getByText('仍在生成的任务')).toBeTruthy();
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.getByText('仍在生成的任务')).toBeTruthy();
|
||||
expect(
|
||||
onExternalTaskWalletMayHaveChanged.mock.calls.length,
|
||||
).toBeGreaterThan(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('stops bootstrap retries after the bounded retry schedule is exhausted', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
listExternalGenerationTasksMock.mockRejectedValue(
|
||||
new Error('task list unavailable'),
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(listExternalGenerationTasksMock).toHaveBeenCalledTimes(8);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('clears stale tasks after a refreshed bootstrap exhausts its retries', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const staleTask = createExternalTask({
|
||||
jobId: 'stale-running-task',
|
||||
requestLabel: '旧生成任务',
|
||||
status: 'running',
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) =>
|
||||
Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: options.statuses?.includes('running') ? 1 : 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
tasks: options.statuses?.includes('running') ? [staleTask] : [],
|
||||
}),
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
refreshKey={0}
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.getByText('旧生成任务')).toBeTruthy();
|
||||
|
||||
listExternalGenerationTasksMock.mockRejectedValue(
|
||||
new Error('task list unavailable'),
|
||||
);
|
||||
rerender(
|
||||
<ImageCanvasTaskSidebarView
|
||||
refreshKey={1}
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('旧生成任务')).toBeNull();
|
||||
expect(screen.getByText('暂无任务')).toBeTruthy();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('aborts every bootstrap attempt when one branch fails and after retry unmounts', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const requestSignals: AbortSignal[] = [];
|
||||
let activeRequestCount = 0;
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
const signal = options.signal;
|
||||
if (!signal) {
|
||||
return Promise.reject(new Error('missing abort signal'));
|
||||
}
|
||||
requestSignals.push(signal);
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
if (activeRequestCount === 1) {
|
||||
return Promise.reject(new Error('active bootstrap failed'));
|
||||
}
|
||||
}
|
||||
return new Promise<
|
||||
Awaited<ReturnType<typeof listExternalGenerationTasks>>
|
||||
>((_, reject) => {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => reject(new DOMException('Aborted', 'AbortError')),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(requestSignals).toHaveLength(2);
|
||||
expect(requestSignals[0]).toBe(requestSignals[1]);
|
||||
expect(requestSignals[0]!.aborted).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(requestSignals).toHaveLength(4);
|
||||
expect(requestSignals[2]).toBe(requestSignals[3]);
|
||||
expect(requestSignals[2]!.aborted).toBe(false);
|
||||
|
||||
unmount();
|
||||
expect(requestSignals[2]!.aborted).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads more completed tasks while scrolling and keeps the request capped', async () => {
|
||||
const completedTasks = Array.from({ length: 120 }, (_, index) => {
|
||||
const completedAt = new Date(Date.now() - index * 1000).toISOString();
|
||||
|
||||
Reference in New Issue
Block a user