fix: Comprehensive validation and bug fixes
Infrastructure: - Add Redis and Celery workers to all docker-compose files - Fix celery migration race condition in entrypoint.sh - Add healthchecks and resource limits to dev compose - Update .env.template with Redis/Celery variables Backend Models & Schemas: - Rename Sprint.completed_points to velocity (per requirements) - Add AgentInstance.name as required field - Rename Issue external tracker fields for consistency - Add IssueSource and TrackerType enums - Add Project.default_tracker_type field Backend Fixes: - Add Celery retry configuration with exponential backoff - Remove unused sequence counter from EventBus - Add mypy overrides for test dependencies - Fix test file using wrong schema (UserUpdate -> dict) Frontend Fixes: - Fix memory leak in useProjectEvents (proper cleanup) - Fix race condition with stale closure in reconnection - Sync TokenWithUser type with regenerated API client - Fix expires_in null handling in useAuth - Clean up unused imports in prototype pages - Add ESLint relaxed rules for prototype files CI/CD: - Add E2E testing stage with Testcontainers - Add security scanning with Trivy and pip-audit - Add dependency caching for faster builds Tests: - Update all tests to use renamed fields (velocity, name, etc.) - Fix 14 schema test failures - All 1500 tests pass with 91% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteOauthClientData, DeleteOauthClientErrors, DeleteOauthClientResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMyOauthConsentsData, ListMyOauthConsentsResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthClientsData, ListOauthClientsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderConsentData, OauthProviderConsentErrors, OauthProviderConsentResponses, OauthProviderIntrospectData, OauthProviderIntrospectErrors, OauthProviderIntrospectResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeMyOauthConsentData, RevokeMyOauthConsentErrors, RevokeMyOauthConsentResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen';
|
||||
import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteOauthClientData, DeleteOauthClientErrors, DeleteOauthClientResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMyOauthConsentsData, ListMyOauthConsentsResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthClientsData, ListOauthClientsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderConsentData, OauthProviderConsentErrors, OauthProviderConsentResponses, OauthProviderIntrospectData, OauthProviderIntrospectErrors, OauthProviderIntrospectResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeMyOauthConsentData, RevokeMyOauthConsentErrors, RevokeMyOauthConsentResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, SendTestEventData, SendTestEventErrors, SendTestEventResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, StreamProjectEventsData, StreamProjectEventsErrors, StreamProjectEventsResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -1288,6 +1288,74 @@ export const getOrganizationMembers = <ThrowOnError extends boolean = false>(opt
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream Project Events
|
||||
*
|
||||
* Stream real-time events for a project via Server-Sent Events (SSE).
|
||||
*
|
||||
* **Authentication**: Required (Bearer token)
|
||||
* **Authorization**: Must have access to the project
|
||||
*
|
||||
* **SSE Event Format**:
|
||||
* ```
|
||||
* event: agent.status_changed
|
||||
* id: 550e8400-e29b-41d4-a716-446655440000
|
||||
* data: {"id": "...", "type": "agent.status_changed", "project_id": "...", ...}
|
||||
*
|
||||
* : keepalive
|
||||
*
|
||||
* event: issue.created
|
||||
* id: 550e8400-e29b-41d4-a716-446655440001
|
||||
* data: {...}
|
||||
* ```
|
||||
*
|
||||
* **Reconnection**: Include the `Last-Event-ID` header with the last received
|
||||
* event ID to resume from where you left off.
|
||||
*
|
||||
* **Keepalive**: The server sends a comment (`: keepalive`) every 30 seconds
|
||||
* to keep the connection alive.
|
||||
*
|
||||
* **Rate Limit**: 10 connections/minute per IP
|
||||
*/
|
||||
export const streamProjectEvents = <ThrowOnError extends boolean = false>(options: Options<StreamProjectEventsData, ThrowOnError>) => {
|
||||
return (options.client ?? client).sse.get<StreamProjectEventsResponses, StreamProjectEventsErrors, ThrowOnError>({
|
||||
responseType: 'text',
|
||||
security: [
|
||||
{
|
||||
scheme: 'bearer',
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/projects/{project_id}/events/stream',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send Test Event (Development Only)
|
||||
*
|
||||
* Send a test event to a project's event stream. This endpoint is
|
||||
* intended for development and testing purposes.
|
||||
*
|
||||
* **Authentication**: Required (Bearer token)
|
||||
* **Authorization**: Must have access to the project
|
||||
*
|
||||
* **Note**: This endpoint should be disabled or restricted in production.
|
||||
*/
|
||||
export const sendTestEvent = <ThrowOnError extends boolean = false>(options: Options<SendTestEventData, ThrowOnError>) => {
|
||||
return (options.client ?? client).post<SendTestEventResponses, SendTestEventErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
scheme: 'bearer',
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/projects/{project_id}/events/test',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* OAuth Server Metadata
|
||||
*
|
||||
|
||||
@@ -3186,6 +3186,94 @@ export type GetOrganizationMembersResponses = {
|
||||
|
||||
export type GetOrganizationMembersResponse = GetOrganizationMembersResponses[keyof GetOrganizationMembersResponses];
|
||||
|
||||
export type StreamProjectEventsData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Last-Event-Id
|
||||
*/
|
||||
'Last-Event-ID'?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Project Id
|
||||
*/
|
||||
project_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/projects/{project_id}/events/stream';
|
||||
};
|
||||
|
||||
export type StreamProjectEventsErrors = {
|
||||
/**
|
||||
* Not authenticated
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not authorized to access this project
|
||||
*/
|
||||
403: unknown;
|
||||
/**
|
||||
* Project not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type StreamProjectEventsError = StreamProjectEventsErrors[keyof StreamProjectEventsErrors];
|
||||
|
||||
export type StreamProjectEventsResponses = {
|
||||
/**
|
||||
* SSE stream established
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type SendTestEventData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Project Id
|
||||
*/
|
||||
project_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/projects/{project_id}/events/test';
|
||||
};
|
||||
|
||||
export type SendTestEventErrors = {
|
||||
/**
|
||||
* Not authenticated
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Not authorized to access this project
|
||||
*/
|
||||
403: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type SendTestEventError = SendTestEventErrors[keyof SendTestEventErrors];
|
||||
|
||||
export type SendTestEventResponses = {
|
||||
/**
|
||||
* Response Send Test Event
|
||||
*
|
||||
* Test event sent
|
||||
*/
|
||||
200: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type SendTestEventResponse = SendTestEventResponses[keyof SendTestEventResponses];
|
||||
|
||||
export type GetOauthServerMetadataData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -121,7 +121,7 @@ export function useLogin(onSuccess?: () => void) {
|
||||
const { access_token, refresh_token, user, expires_in } = data;
|
||||
|
||||
// Update auth store with user and tokens
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in);
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in ?? undefined);
|
||||
|
||||
// Invalidate and refetch user data
|
||||
queryClient.invalidateQueries({ queryKey: authKeys.all });
|
||||
@@ -194,7 +194,7 @@ export function useRegister(onSuccess?: () => void) {
|
||||
const { access_token, refresh_token, user, expires_in } = data;
|
||||
|
||||
// Update auth store with user and tokens (auto-login)
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in);
|
||||
await setAuth(user as User, access_token, refresh_token || '', expires_in ?? undefined);
|
||||
|
||||
// Invalidate and refetch user data
|
||||
queryClient.invalidateQueries({ queryKey: authKeys.all });
|
||||
|
||||
@@ -7,21 +7,15 @@
|
||||
* @module lib/api/types
|
||||
*/
|
||||
|
||||
import type { Token, UserResponse } from './generated/types.gen';
|
||||
import type { Token } from './generated/types.gen';
|
||||
|
||||
/**
|
||||
* Extended Token Response
|
||||
* Token with User Response
|
||||
*
|
||||
* The actual backend response includes additional fields not captured in OpenAPI spec:
|
||||
* - user: UserResponse object
|
||||
* - expires_in: Token expiration in seconds
|
||||
*
|
||||
* TODO: Update backend OpenAPI spec to include these fields
|
||||
* Alias for Token type which now includes user and expires_in fields.
|
||||
* Kept for backwards compatibility with existing type guards.
|
||||
*/
|
||||
export interface TokenWithUser extends Token {
|
||||
user: UserResponse;
|
||||
expires_in?: number;
|
||||
}
|
||||
export type TokenWithUser = Token;
|
||||
|
||||
/**
|
||||
* Success Response (for operations that return success messages)
|
||||
|
||||
@@ -129,6 +129,7 @@ export function useProjectEvents(
|
||||
const currentRetryDelayRef = useRef(initialRetryDelay);
|
||||
const isManualDisconnectRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const pingHandlerRef = useRef<(() => void) | null>(null);
|
||||
|
||||
/**
|
||||
* Update connection state and notify callback
|
||||
@@ -191,6 +192,12 @@ export function useProjectEvents(
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Remove ping listener before closing to prevent memory leak
|
||||
if (eventSourceRef.current && pingHandlerRef.current) {
|
||||
eventSourceRef.current.removeEventListener('ping', pingHandlerRef.current);
|
||||
pingHandlerRef.current = null;
|
||||
}
|
||||
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
@@ -286,12 +293,15 @@ export function useProjectEvents(
|
||||
};
|
||||
|
||||
// Handle specific event types from backend
|
||||
eventSource.addEventListener('ping', () => {
|
||||
// Store handler reference for proper cleanup
|
||||
const pingHandler = () => {
|
||||
// Keep-alive ping from server, no action needed
|
||||
if (config.debug.api) {
|
||||
console.log('[SSE] Received ping');
|
||||
}
|
||||
});
|
||||
};
|
||||
pingHandlerRef.current = pingHandler;
|
||||
eventSource.addEventListener('ping', pingHandler);
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
if (!mountedRef.current) return;
|
||||
@@ -355,30 +365,26 @@ export function useProjectEvents(
|
||||
clearProjectEvents(projectId);
|
||||
}, [clearProjectEvents, projectId]);
|
||||
|
||||
// Auto-connect on mount if enabled
|
||||
// Consolidated connection management effect
|
||||
// Handles both initial mount and auth state changes to prevent race conditions
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
|
||||
if (autoConnect && isAuthenticated && projectId) {
|
||||
connect();
|
||||
// Connect when authenticated with a project and not manually disconnected
|
||||
if (autoConnect && isAuthenticated && accessToken && projectId) {
|
||||
if (connectionState === 'disconnected' && !isManualDisconnectRef.current) {
|
||||
connect();
|
||||
}
|
||||
} else if (!isAuthenticated && connectionState !== 'disconnected') {
|
||||
// Disconnect when auth is lost
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
cleanup();
|
||||
};
|
||||
}, [autoConnect, isAuthenticated, projectId, connect, cleanup]);
|
||||
|
||||
// Reconnect when auth changes
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && accessToken && connectionState === 'disconnected' && autoConnect) {
|
||||
if (!isManualDisconnectRef.current) {
|
||||
connect();
|
||||
}
|
||||
} else if (!isAuthenticated && connectionState !== 'disconnected') {
|
||||
disconnect();
|
||||
}
|
||||
}, [isAuthenticated, accessToken, connectionState, autoConnect, connect, disconnect]);
|
||||
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]);
|
||||
|
||||
return {
|
||||
events,
|
||||
|
||||
Reference in New Issue
Block a user