合并master并保留双侧决策记录
合并 origin/master 的 UI 编辑器、GDD 审批与前端修复。 保留本分支 LLM Router、Direct 过程卡与私有路径相关实现和决策记录。 解决 decision-log 文档冲突,完整保留双方新增决策条目。
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
resetPlatformSessionStateForTests,
|
||||
} from '../../src/services/platformSession';
|
||||
import {
|
||||
act,
|
||||
AuthenticatedClient,
|
||||
expect,
|
||||
fireEvent,
|
||||
@@ -35,6 +36,41 @@ export function registerAuthTests() {
|
||||
delete window.__TAURI__;
|
||||
});
|
||||
|
||||
it('leaves startup loading with an actionable retry after auth service timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
let releaseRefresh: (() => void) | null = null;
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(
|
||||
async (input: RequestInfo | URL) => {
|
||||
if (String(input) === '/api/auth/refresh') {
|
||||
return new Promise<Response>((resolve) => {
|
||||
releaseRefresh = () => resolve(new Response('', { status: 401 }));
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${String(input)}`);
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
React.createElement(AuthenticatedClient, null, () =>
|
||||
React.createElement('main', { 'aria-label': '已登录' }),
|
||||
),
|
||||
);
|
||||
expect(screen.getByRole('main', { name: '登录状态检查' })).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
});
|
||||
|
||||
expect(screen.getByRole('main', { name: '登录' })).not.toBeNull();
|
||||
expect(screen.getByRole('alert')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByRole('button', { name: '重试登录状态检查' }),
|
||||
).not.toBeNull();
|
||||
releaseRefresh?.();
|
||||
await Promise.resolve();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders the unauthenticated client with the shared light platform theme and product image', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(
|
||||
async (input: RequestInfo | URL) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AGC_CLIENT_MARKER_VALUE,
|
||||
AGC_DEVELOPMENT_API_BASE_URL,
|
||||
AGC_RELEASE_API_BASE_URL,
|
||||
ClientHttpTimeoutError,
|
||||
fetchClientHttp,
|
||||
getClientServerBaseUrl,
|
||||
getClientServerSelection,
|
||||
@@ -25,6 +26,7 @@ vi.mock('@tauri-apps/plugin-http', () => ({
|
||||
|
||||
describe('AGC client HTTP transport', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
@@ -250,4 +252,62 @@ describe('AGC client HTTP transport', () => {
|
||||
url: `${AGC_DEVELOPMENT_API_BASE_URL}/api/auth/me`,
|
||||
});
|
||||
});
|
||||
|
||||
it('aborts a stalled Web request at the configured timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchMock = vi.fn(() => new Promise<Response>(() => {}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const request = fetchClientHttp('/api/auth/me', {}, { timeoutMs: 25 });
|
||||
const timeoutAssertion = expect(request).rejects.toMatchObject({
|
||||
name: 'ClientHttpTimeoutError',
|
||||
code: 'CLIENT_HTTP_TIMEOUT',
|
||||
timeoutMs: 25,
|
||||
url: '/api/auth/me',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(25);
|
||||
await timeoutAssertion;
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [, forwardedInit] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(forwardedInit.signal).toBeInstanceOf(AbortSignal);
|
||||
expect((forwardedInit.signal as AbortSignal).aborted).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves caller AbortError and does not report it as a timeout', async () => {
|
||||
const fetchMock = vi.fn(
|
||||
(_url: string, init: RequestInit) =>
|
||||
new Promise<Response>((_, reject) => {
|
||||
init.signal?.addEventListener('abort', () => {
|
||||
reject(
|
||||
new DOMException('The operation was aborted.', 'AbortError'),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const callerController = new AbortController();
|
||||
const request = fetchClientHttp(
|
||||
'/api/auth/me',
|
||||
{ signal: callerController.signal },
|
||||
{ timeoutMs: 10_000 },
|
||||
);
|
||||
|
||||
callerController.abort();
|
||||
|
||||
await expect(request).rejects.toMatchObject({ name: 'AbortError' });
|
||||
await expect(request).rejects.not.toBeInstanceOf(ClientHttpTimeoutError);
|
||||
});
|
||||
|
||||
it('allows explicitly disabling the timeout for long-running requests', async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Response(null, { status: 204 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await expect(
|
||||
fetchClientHttp('/api/agent/run', {}, { timeoutMs: null }),
|
||||
).resolves.toBeInstanceOf(Response);
|
||||
const [, forwardedInit] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(forwardedInit.signal).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,6 +137,7 @@ async function renderLoadedSession(state: State) {
|
||||
const stateStore: IUiDesignStateStore = {
|
||||
load: vi.fn().mockResolvedValue({ revision: 0, state }),
|
||||
save: vi.fn(),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
const hook = renderHook(() =>
|
||||
useUiEditorSession('/tmp/ui-editor', 'ui-resource', stateStore),
|
||||
@@ -150,6 +151,7 @@ describe('UiEditorPage', () => {
|
||||
const stateStore: IUiDesignStateStore = {
|
||||
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
|
||||
save: vi.fn(),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
|
||||
render(
|
||||
@@ -193,6 +195,7 @@ describe('UiEditorPage', () => {
|
||||
state: stateWithPages(['page']),
|
||||
}),
|
||||
save: vi.fn(),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
const hook = renderHook(
|
||||
({ resourceId }) =>
|
||||
@@ -248,6 +251,7 @@ describe('UiEditorPage', () => {
|
||||
const stateStore: IUiDesignStateStore = {
|
||||
load: vi.fn(() => new Promise(() => undefined)),
|
||||
save: vi.fn(),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
const hook = renderHook(() =>
|
||||
useUiEditorSession('/tmp/ui-editor', 'ui-resource', stateStore),
|
||||
@@ -355,6 +359,7 @@ describe('UiEditorPage', () => {
|
||||
state: stateWithPages(['gameplay-page']),
|
||||
}),
|
||||
save: vi.fn(),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
|
||||
render(
|
||||
@@ -594,6 +599,7 @@ describe('UiEditorPage', () => {
|
||||
const stateStore: IUiDesignStateStore = {
|
||||
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
|
||||
save: vi.fn().mockRejectedValue(new Error('临时存储不可用')),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
render(
|
||||
createElement(UiEditorPage, {
|
||||
@@ -627,6 +633,7 @@ describe('UiEditorPage', () => {
|
||||
status: 'conflict',
|
||||
currentRevision: 1,
|
||||
}),
|
||||
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
|
||||
};
|
||||
render(
|
||||
createElement(UiEditorPage, {
|
||||
|
||||
Reference in New Issue
Block a user