Add API error monitoring tests and error context snapshots for various browsers

- Created error context snapshots for Firefox, WebKit, and Chromium to capture UI state during API error monitoring.
- Implemented e2e tests for API error detection, including console errors, failed API requests, and proxy errors.
- Added functionality to ignore specific API patterns in monitoring assertions.
- Ensured tests validate the monitoring system's ability to detect and report errors effectively.
This commit is contained in:
catlog22
2026-01-31 00:15:59 +08:00
parent f1324a0bc8
commit a0f81f8841
66 changed files with 3112 additions and 3175 deletions

View File

@@ -5,8 +5,9 @@
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import type { AppStore, Theme, Locale, ViewMode, SessionFilter, LiteTaskType } from '../types/store';
import type { AppStore, Theme, ColorScheme, Locale, ViewMode, SessionFilter, LiteTaskType } from '../types/store';
import { getInitialLocale, updateIntl } from '../lib/i18n';
import { getThemeId } from '../lib/theme';
// Helper to resolve system theme
const getSystemTheme = (): 'light' | 'dark' => {
@@ -27,6 +28,7 @@ const initialState = {
// Theme
theme: 'system' as Theme,
resolvedTheme: 'light' as 'light' | 'dark',
colorScheme: 'blue' as ColorScheme, // New: default to blue scheme
// Locale
locale: getInitialLocale() as Locale,
@@ -61,9 +63,23 @@ export const useAppStore = create<AppStore>()(
// Apply theme to document
if (typeof document !== 'undefined') {
const { colorScheme } = get();
const themeId = getThemeId(colorScheme, resolved);
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolved);
document.documentElement.setAttribute('data-theme', resolved);
document.documentElement.setAttribute('data-theme', themeId);
}
},
setColorScheme: (colorScheme: ColorScheme) => {
set({ colorScheme }, false, 'setColorScheme');
// Apply color scheme to document
if (typeof document !== 'undefined') {
const { resolvedTheme } = get();
const themeId = getThemeId(colorScheme, resolvedTheme);
document.documentElement.setAttribute('data-theme', themeId);
document.documentElement.setAttribute('data-color-scheme', colorScheme);
}
},
@@ -131,6 +147,7 @@ export const useAppStore = create<AppStore>()(
// Only persist theme and locale preferences
partialize: (state) => ({
theme: state.theme,
colorScheme: state.colorScheme,
locale: state.locale,
sidebarCollapsed: state.sidebarCollapsed,
}),
@@ -139,10 +156,11 @@ export const useAppStore = create<AppStore>()(
if (state) {
const resolved = resolveTheme(state.theme);
state.resolvedTheme = resolved;
const themeId = getThemeId(state.colorScheme, resolved);
if (typeof document !== 'undefined') {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolved);
document.documentElement.setAttribute('data-theme', resolved);
document.documentElement.setAttribute('data-theme', themeId);
}
}
// Apply locale on rehydration
@@ -164,9 +182,10 @@ if (typeof window !== 'undefined') {
if (state.theme === 'system') {
const resolved = getSystemTheme();
useAppStore.setState({ resolvedTheme: resolved });
const themeId = getThemeId(state.colorScheme, resolved);
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolved);
document.documentElement.setAttribute('data-theme', resolved);
document.documentElement.setAttribute('data-theme', themeId);
}
});
}

View File

@@ -0,0 +1,223 @@
// ========================================
// CLI Stream Store
// ========================================
// Zustand store for managing CLI streaming output
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
// ========== Types ==========
/**
* Output line type for CLI streaming
*/
export interface CliOutputLine {
type: 'stdout' | 'stderr' | 'metadata' | 'thought' | 'system' | 'tool_call';
content: string;
timestamp: number;
}
/**
* CLI execution status
*/
export type CliExecutionStatus = 'running' | 'completed' | 'error';
/**
* CLI execution state
*/
export interface CliExecutionState {
tool: string;
mode: string;
status: CliExecutionStatus;
output: CliOutputLine[];
startTime: number;
endTime?: number;
recovered?: boolean;
}
/**
* CLI stream state interface
*/
interface CliStreamState {
outputs: Record<string, CliOutputLine[]>;
executions: Record<string, CliExecutionState>;
currentExecutionId: string | null;
// Legacy methods
addOutput: (executionId: string, line: CliOutputLine) => void;
clearOutputs: (executionId: string) => void;
getOutputs: (executionId: string) => CliOutputLine[];
// Multi-execution methods
getAllExecutions: () => CliExecutionState[];
upsertExecution: (executionId: string, exec: Partial<CliExecutionState> & { tool?: string; mode?: string }) => void;
removeExecution: (executionId: string) => void;
setCurrentExecution: (executionId: string | null) => void;
}
// ========== Constants ==========
/**
* Maximum number of output lines to keep per execution
* Prevents memory issues for long-running executions
*/
const MAX_OUTPUT_LINES = 5000;
// ========== Store ==========
/**
* Zustand store for CLI streaming output
*
* @remarks
* Manages streaming output from CLI executions in memory.
* Each execution has its own output array, accessible by executionId.
*
* @example
* ```tsx
* const addOutput = useCliStreamStore(state => state.addOutput);
* addOutput('exec-123', { type: 'stdout', content: 'Hello', timestamp: Date.now() });
* ```
*/
export const useCliStreamStore = create<CliStreamState>()(
devtools(
(set, get) => ({
outputs: {},
executions: {},
currentExecutionId: null,
addOutput: (executionId: string, line: CliOutputLine) => {
set((state) => {
const current = state.outputs[executionId] || [];
const updated = [...current, line];
// Trim if too long to prevent memory issues
if (updated.length > MAX_OUTPUT_LINES) {
return {
outputs: {
...state.outputs,
[executionId]: updated.slice(-MAX_OUTPUT_LINES),
},
};
}
return {
outputs: {
...state.outputs,
[executionId]: updated,
},
};
}, false, 'cliStream/addOutput');
// Also update in executions
const state = get();
if (state.executions[executionId]) {
set((state) => ({
executions: {
...state.executions,
[executionId]: {
...state.executions[executionId],
output: [...state.executions[executionId].output, line],
},
},
}), false, 'cliStream/updateExecutionOutput');
}
},
clearOutputs: (executionId: string) => {
set(
(state) => ({
outputs: {
...state.outputs,
[executionId]: [],
},
}),
false,
'cliStream/clearOutputs'
);
},
getOutputs: (executionId: string) => {
return get().outputs[executionId] || [];
},
// Multi-execution methods
getAllExecutions: () => {
return Object.values(get().executions);
},
upsertExecution: (executionId: string, exec: Partial<CliExecutionState> & { tool?: string; mode?: string }) => {
set((state) => {
const existing = state.executions[executionId];
const updated: CliExecutionState = existing
? { ...existing, ...exec }
: {
tool: exec.tool || 'cli',
mode: exec.mode || 'analysis',
status: exec.status || 'running',
output: exec.output || [],
startTime: exec.startTime || Date.now(),
endTime: exec.endTime,
recovered: exec.recovered,
};
return {
executions: {
...state.executions,
[executionId]: updated,
},
};
}, false, 'cliStream/upsertExecution');
},
removeExecution: (executionId: string) => {
set((state) => {
const newExecutions = { ...state.executions };
delete newExecutions[executionId];
return {
executions: newExecutions,
currentExecutionId: state.currentExecutionId === executionId ? null : state.currentExecutionId,
};
}, false, 'cliStream/removeExecution');
},
setCurrentExecution: (executionId: string | null) => {
set({ currentExecutionId: executionId }, false, 'cliStream/setCurrentExecution');
},
}),
{ name: 'CliStreamStore' }
)
);
// ========== Selectors ==========
/**
* Selector for getting outputs by execution ID
*/
export const selectOutputs = (state: CliStreamState, executionId: string) =>
state.outputs[executionId] || [];
/**
* Selector for getting addOutput action
*/
export const selectAddOutput = (state: CliStreamState) => state.addOutput;
/**
* Selector for getting clearOutputs action
*/
export const selectClearOutputs = (state: CliStreamState) => state.clearOutputs;
/**
* Selector for getting all executions
*/
export const selectAllExecutions = (state: CliStreamState) => state.executions;
/**
* Selector for getting current execution ID
*/
export const selectCurrentExecutionId = (state: CliStreamState) => state.currentExecutionId;
/**
* Selector for getting active execution count
*/
export const selectActiveExecutionCount = (state: CliStreamState) =>
Object.values(state.executions).filter(e => e.status === 'running').length;