修复更早历史读取失败消息为空时挂起态与错误行一起消失
- 失败消息统一收口成非空兜底文案,空 message 不再让 historyError 变 falsy - 新增控制器回归用例:宿主抛 new Error() 时挂起态仍成立 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
+48
-16
@@ -10,6 +10,7 @@ type SliceRequest = {
|
||||
projectPath: string;
|
||||
beforeItemId: string | undefined;
|
||||
resolve: (slice: HistorySlice) => void;
|
||||
reject: (error: unknown) => void;
|
||||
};
|
||||
|
||||
function userItem(itemId: string): ThreadItem {
|
||||
@@ -25,7 +26,7 @@ function slice(
|
||||
}
|
||||
|
||||
/**
|
||||
* 装一个可控的 Tauri invoke:历史切片读取全部挂起,用例自己决定哪一页何时落地。
|
||||
* 装一个可控的 Tauri invoke:历史切片读取全部挂起,用例自己决定哪一页何时落地 / 落空。
|
||||
*
|
||||
* `resolveTauriInvoke` 只读 `window.__TAURI__.core.invoke`,这里就是控制器唯一的外部接缝。
|
||||
*/
|
||||
@@ -34,11 +35,12 @@ function installDeferredHistoryInvoke() {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command !== 'read_direct_project_history_slice') return undefined;
|
||||
return new Promise<HistorySlice>((resolve) => {
|
||||
return new Promise<HistorySlice>((resolve, reject) => {
|
||||
requests.push({
|
||||
projectPath: String(args?.projectPath ?? ''),
|
||||
beforeItemId: args?.beforeItemId as string | undefined,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -64,6 +66,23 @@ function installDeferredHistoryInvoke() {
|
||||
};
|
||||
}
|
||||
|
||||
function renderChatController(initialProjectPath: string) {
|
||||
const allowed = async () => true;
|
||||
return renderHook(
|
||||
({ projectPath }: { projectPath: string }) =>
|
||||
useDirectProjectChatController({
|
||||
enabled: true,
|
||||
ensureConversationReadAllowed: allowed,
|
||||
ensureConversationWriteAllowed: allowed,
|
||||
onRuntimeError: () => undefined,
|
||||
projectId: 'project-1',
|
||||
projectPath,
|
||||
refreshManifest: () => undefined,
|
||||
}),
|
||||
{ initialProps: { projectPath: initialProjectPath } },
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
delete window.__TAURI__;
|
||||
@@ -72,20 +91,7 @@ afterEach(() => {
|
||||
describe('更早历史加载态的项目归属', () => {
|
||||
it('切项目后旧项目在飞的读取才落地,不能清掉新项目正在进行的加载态', async () => {
|
||||
const tauri = installDeferredHistoryInvoke();
|
||||
const allowed = async () => true;
|
||||
const { result, rerender } = renderHook(
|
||||
({ projectPath }: { projectPath: string }) =>
|
||||
useDirectProjectChatController({
|
||||
enabled: true,
|
||||
ensureConversationReadAllowed: allowed,
|
||||
ensureConversationWriteAllowed: allowed,
|
||||
onRuntimeError: () => undefined,
|
||||
projectId: 'project-1',
|
||||
projectPath,
|
||||
refreshManifest: () => undefined,
|
||||
}),
|
||||
{ initialProps: { projectPath: '/projects/a' } },
|
||||
);
|
||||
const { result, rerender } = renderChatController('/projects/a');
|
||||
|
||||
// 首屏:/a 的第一页落地,拿到「还有更早」。
|
||||
const firstScreenA = await tauri.take('/projects/a');
|
||||
@@ -129,3 +135,29 @@ describe('更早历史加载态的项目归属', () => {
|
||||
expect(result.current.historyLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('更早历史失败的挂起态', () => {
|
||||
it('宿主没给出失败消息时,挂起态仍是可判定的非空值', async () => {
|
||||
const tauri = installDeferredHistoryInvoke();
|
||||
const { result } = renderChatController('/projects/a');
|
||||
|
||||
const firstScreen = await tauri.take('/projects/a');
|
||||
await act(async () => {
|
||||
firstScreen.resolve(slice([userItem('a2')], true, 'a2'));
|
||||
});
|
||||
await waitFor(() => expect(result.current.historyHasMore).toBe(true));
|
||||
|
||||
await act(async () => {
|
||||
void result.current.loadEarlierHistory();
|
||||
});
|
||||
const failing = await tauri.take('/projects/a', 'a2');
|
||||
await act(async () => {
|
||||
failing.reject(new Error(''));
|
||||
});
|
||||
|
||||
// `historyError` 同时是「挂起自动加载」的闸门与内联错误行的显隐开关:空串是 falsy,
|
||||
// 两者会一起消失,滚到顶就变成反复重试同一个失败读取。
|
||||
expect(result.current.historyError).toBeTruthy();
|
||||
expect(result.current.historyLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+12
-1
@@ -44,6 +44,15 @@ import { useDirectThreadChatSubscription } from './useDirectThreadChatSubscripti
|
||||
export const MAX_CHAT_COMPOSER_ATTACHMENTS = 8;
|
||||
export const DIRECT_HISTORY_PAGE_SIZE = CONVERSATION_VISIBLE_STEP;
|
||||
|
||||
/**
|
||||
* 更早历史读取失败时的兜底文案。
|
||||
*
|
||||
* 宿主可能抛出空 message 的错误(裸 `new Error()`、宿主侧空串)。空串是 falsy,而
|
||||
* `historyError` 同时是「挂起自动加载」的闸门与内联错误行的显隐开关——空串会让两者一起
|
||||
* 消失,滚到顶就变成反复重试同一个失败读取。这里在源头收口成一句稳定文案。
|
||||
*/
|
||||
const DIRECT_HISTORY_LOAD_ERROR_FALLBACK = '读取更早的对话历史失败';
|
||||
|
||||
/**
|
||||
* 工作台壳注入的项目对话读权限门。返回 `false` 表示已入队确认:读取由 `onConfirmed`
|
||||
* 触发,壳自己负责取消时的状态文案。
|
||||
@@ -707,7 +716,9 @@ export function useDirectProjectChatController({
|
||||
} catch (error) {
|
||||
// 与加载成功路径同样按项目守卫:切项目后在飞的读取失败不能算到新项目头上。
|
||||
if (projectPathRef.current !== nextProjectPath) return;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message =
|
||||
(error instanceof Error ? error.message : String(error)) ||
|
||||
DIRECT_HISTORY_LOAD_ERROR_FALLBACK;
|
||||
historyErrorRef.current = message;
|
||||
setHistoryError(message);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user