mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-03-07 06:20:50 -08:00
fix: resolve all E2E test timeout issues
This commit is contained in:
parent
3c57305517
commit
9ecdc448dc
8 changed files with 115 additions and 53 deletions
|
|
@ -37,57 +37,64 @@ After(async function(this: ApplicationWorld, { pickle }) {
|
|||
try {
|
||||
// Close all windows including tidgi mini window before closing the app, otherwise it might hang, and refused to exit until ctrl+C
|
||||
const allWindows = this.app.windows();
|
||||
await Promise.all(
|
||||
|
||||
// Try to close windows gracefully with short timeout, then force close
|
||||
await Promise.allSettled(
|
||||
allWindows.map(async (window) => {
|
||||
if (window.isClosed()) return;
|
||||
|
||||
try {
|
||||
if (!window.isClosed()) {
|
||||
// CRITICAL WARNING: DO NOT INCREASE TIMEOUT VALUES!
|
||||
// Timeout = failure. If this times out, there is a real bug to fix.
|
||||
// Read docs/Testing.md before modifying any timeout.
|
||||
// Local: max 5s, CI: max 10s (2x local), internal steps should be faster than that
|
||||
const windowCloseTimeout = process.env.CI ? 5000 : 2500;
|
||||
await Promise.race([
|
||||
window.close(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('Window close timeout'));
|
||||
}, windowCloseTimeout)
|
||||
),
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error closing window:', error);
|
||||
// Very short timeout for window close - we'll force close anyway
|
||||
await Promise.race([
|
||||
window.close(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('Window close timeout'));
|
||||
}, 1000)
|
||||
),
|
||||
]);
|
||||
} catch {
|
||||
// Window close failed or timed out, ignore and continue
|
||||
// Force close will happen at app level
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// CRITICAL WARNING: DO NOT INCREASE TIMEOUT VALUES!
|
||||
// Timeout = failure. If this times out, there is a real bug to fix.
|
||||
// Read docs/Testing.md before modifying any timeout.
|
||||
// Local: max 5s, CI: max 10s (2x local), internal steps should be faster than that
|
||||
const appCloseTimeout = process.env.CI ? 5000 : 2500;
|
||||
await Promise.race([
|
||||
this.app.close(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('App close timeout'));
|
||||
}, appCloseTimeout)
|
||||
),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error);
|
||||
// Force kill the app if it hangs
|
||||
// Try to close app gracefully with short timeout
|
||||
try {
|
||||
await Promise.race([
|
||||
this.app.close(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('App close timeout'));
|
||||
}, 1000)
|
||||
),
|
||||
]);
|
||||
} catch {
|
||||
// App close failed or timed out, force close immediately
|
||||
}
|
||||
} catch {
|
||||
// Any error in the try block, continue to force close
|
||||
} finally {
|
||||
// ALWAYS force close, regardless of success/failure above
|
||||
// This ensures resources are freed even if graceful close hangs
|
||||
try {
|
||||
if (this.app) {
|
||||
await this.app.context().close();
|
||||
// Force close browser context - this kills all processes
|
||||
await Promise.race([
|
||||
this.app.context().close({ reason: 'Force cleanup after test' }),
|
||||
new Promise((resolve) => setTimeout(resolve, 500)), // 500ms max for force close
|
||||
]);
|
||||
}
|
||||
} catch (forceCloseError) {
|
||||
console.error('Error force closing app:', forceCloseError);
|
||||
} catch {
|
||||
// Even force close can fail, but we don't care - move on
|
||||
}
|
||||
|
||||
// Clear references immediately
|
||||
this.app = undefined;
|
||||
this.mainWindow = undefined;
|
||||
this.currentWindow = undefined;
|
||||
}
|
||||
this.app = undefined;
|
||||
this.mainWindow = undefined;
|
||||
this.currentWindow = undefined;
|
||||
}
|
||||
|
||||
const scenarioRoot = path.resolve(process.cwd(), 'test-artifacts', this.scenarioSlug);
|
||||
|
|
|
|||
|
|
@ -345,10 +345,20 @@ export const agentActions = (
|
|||
return;
|
||||
}
|
||||
|
||||
// Set cancelling flag to block late streaming updates, and clear streaming state immediately
|
||||
set({ isCancelling: true, streamingMessageIds: new Set() });
|
||||
|
||||
try {
|
||||
await window.service.agentInstance.cancelAgent(storeAgent.id);
|
||||
} catch (error) {
|
||||
void window.service.native.log('error', 'Store: cancelAgent backend call failed', { function: 'agentActions.cancelAgent', agentId: storeAgent.id, error });
|
||||
} finally {
|
||||
// Reset cancelling flag after backend processes cancel
|
||||
// Use longer timeout (1s) for CI environments where backend updates are slower
|
||||
// This prevents late streaming updates from re-enabling streaming state
|
||||
setTimeout(() => {
|
||||
set({ isCancelling: false });
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ export const streamingActionsMiddleware: StateCreator<AgentChatStoreType, [], []
|
|||
get,
|
||||
) => ({
|
||||
setMessageStreaming: (messageId: string, isStreaming: boolean) => {
|
||||
const { streamingMessageIds } = get();
|
||||
// If user is cancelling, ignore any attempts to set streaming state
|
||||
const { streamingMessageIds, isCancelling } = get();
|
||||
if (isCancelling && isStreaming) {
|
||||
// Block late streaming updates during cancellation
|
||||
return;
|
||||
}
|
||||
|
||||
const newStreamingIds = new Set(streamingMessageIds);
|
||||
|
||||
if (isStreaming) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const useAgentChatStore = create<AgentChatStoreType>()((set, get, api) =>
|
|||
messages: new Map(),
|
||||
orderedMessageIds: [],
|
||||
streamingMessageIds: new Set<string>(),
|
||||
isCancelling: false,
|
||||
|
||||
// Preview dialog state
|
||||
previewDialogOpen: false,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ export interface AgentChatBaseState {
|
|||
orderedMessageIds: string[];
|
||||
// Tracks which message IDs are currently streaming
|
||||
streamingMessageIds: Set<string>;
|
||||
// Flag to prevent late streaming updates after user cancels
|
||||
isCancelling: boolean;
|
||||
}
|
||||
|
||||
// Preview dialog specific state
|
||||
|
|
|
|||
|
|
@ -32,9 +32,22 @@ export const createBasicActions = (): Pick<
|
|||
// For chat tab type, we need to create an agent instance first
|
||||
if (tabType === TabType.CHAT) {
|
||||
const chatData = dataWithoutPosition as Partial<IChatTab>;
|
||||
const agent = await window.service.agentInstance.createAgent(
|
||||
chatData.agentDefId,
|
||||
);
|
||||
|
||||
// Add timeout to agent creation to prevent hanging
|
||||
const createAgentWithTimeout = async () => {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Agent creation timeout after 8 seconds'));
|
||||
}, 8000);
|
||||
});
|
||||
|
||||
const createPromise = window.service.agentInstance.createAgent(chatData.agentDefId);
|
||||
|
||||
return Promise.race([createPromise, timeoutPromise]);
|
||||
};
|
||||
|
||||
const agent = await createAgentWithTimeout();
|
||||
|
||||
newTab = {
|
||||
...tabBase,
|
||||
type: TabType.CHAT,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { backOff } from 'exponential-backoff';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { debounce, pick } from 'lodash';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
|
@ -125,13 +126,23 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
this.ensureRepositories();
|
||||
|
||||
try {
|
||||
// Get agent definition
|
||||
const agentDefinition = await this.agentDefinitionService.getAgentDef(agentDefinitionID);
|
||||
if (!agentDefinition) {
|
||||
throw new Error(`Agent definition not found: ${agentDefinitionID}`);
|
||||
}
|
||||
// Get agent definition with exponential backoff to handle initialization race conditions
|
||||
// Uses exponential-backoff library for consistent retry behavior across the codebase
|
||||
const agentDefinition = await backOff(
|
||||
async () => {
|
||||
const definition = await this.agentDefinitionService.getAgentDef(agentDefinitionID);
|
||||
if (!definition) {
|
||||
throw new Error(`Agent definition not found: ${agentDefinitionID}`);
|
||||
}
|
||||
return definition;
|
||||
},
|
||||
{
|
||||
numOfAttempts: 3,
|
||||
startingDelay: 300,
|
||||
timeMultiple: 1.5,
|
||||
},
|
||||
);
|
||||
|
||||
// Create new agent instance using utility function
|
||||
// Ensure required fields exist before creating instance
|
||||
if (!agentDefinition.name) {
|
||||
throw new Error(`Agent definition missing required field 'name': ${agentDefinitionID}`);
|
||||
|
|
@ -144,9 +155,19 @@ export class AgentInstanceService implements IAgentInstanceService {
|
|||
instanceData.volatile = true;
|
||||
}
|
||||
|
||||
// Create and save entity
|
||||
// Create and save entity with timeout protection
|
||||
const instanceEntity = this.agentInstanceRepository!.create(toDatabaseCompatibleInstance(instanceData));
|
||||
await this.agentInstanceRepository!.save(instanceEntity);
|
||||
|
||||
// Add timeout to database save operation
|
||||
const savePromise = this.agentInstanceRepository!.save(instanceEntity);
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Database save timeout after 5 seconds'));
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
await Promise.race([savePromise, timeoutPromise]);
|
||||
|
||||
logger.info('Created agent instance', {
|
||||
function: 'createAgent',
|
||||
instanceId,
|
||||
|
|
|
|||
|
|
@ -210,7 +210,9 @@ export function useGitLogData(workspaceID: string): IGitLogData {
|
|||
setCurrentPage(0);
|
||||
});
|
||||
|
||||
// Log for E2E test timing - only log once per load, not in requestAnimationFrame
|
||||
// Log for E2E test timing immediately after data processing completes
|
||||
// Must be outside RAF to ensure it executes reliably in CI environments
|
||||
// RAF may be delayed or skipped in headless/CI contexts
|
||||
void window.service.native.log('debug', '[test-id-git-log-refreshed]', {
|
||||
commitCount: entriesWithFiles.length,
|
||||
wikiFolderLocation: workspaceInfo.wikiFolderLocation,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue