Skip to content

Commit a91f30f

Browse files
committed
feat(cloud-tests): enhance legacy integration filtering and add support for multiple connections
1 parent 75ddf65 commit a91f30f

5 files changed

Lines changed: 130 additions & 25 deletions

File tree

apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getIntegrationHandler } from '@comp/integrations';
55
import { db } from '@db';
66
import { Prisma } from '@prisma/client';
77
import { revalidatePath } from 'next/cache';
8-
import { cookies, headers } from 'next/headers';
8+
import { headers } from 'next/headers';
99
import { z } from 'zod';
1010
import { authActionClient } from '../../../../../actions/safe-action';
1111
import { runTests } from './run-tests';
@@ -113,12 +113,9 @@ export const connectCloudAction = authActionClient
113113
});
114114

115115
// Trigger immediate scan for only this new connection
116+
// runTests now waits for completion before returning
116117
const runResult = await runTests(newIntegration.id);
117118

118-
if (runResult.success && runResult.publicAccessToken) {
119-
(await cookies()).set('publicAccessToken', runResult.publicAccessToken);
120-
}
121-
122119
// Revalidate the path
123120
const headersList = await headers();
124121
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
@@ -130,7 +127,6 @@ export const connectCloudAction = authActionClient
130127
trigger: runResult.success
131128
? {
132129
taskId: runResult.taskId ?? undefined,
133-
publicAccessToken: runResult.publicAccessToken ?? undefined,
134130
}
135131
: undefined,
136132
runErrors: runResult.success ? undefined : (runResult.errors ?? undefined),

apps/app/src/app/(app)/[orgId]/cloud-tests/actions/run-tests.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import { runIntegrationTests } from '@/trigger/tasks/integration/run-integration-tests';
44
import { auth } from '@/utils/auth';
5-
import { tasks } from '@trigger.dev/sdk';
5+
import { runs, tasks } from '@trigger.dev/sdk';
66
import { revalidatePath } from 'next/cache';
77
import { headers } from 'next/headers';
88

9+
const MAX_POLL_ATTEMPTS = 60; // Max 2 minutes (60 * 2 seconds)
10+
const POLL_INTERVAL_MS = 2000;
11+
912
/**
10-
* Run integration tests.
13+
* Run integration tests and wait for completion.
1114
* @param integrationId - Optional. If provided, only run tests for this specific connection.
1215
* If not provided, run tests for all connections in the organization.
1316
*/
@@ -32,29 +35,72 @@ export const runTests = async (integrationId?: string) => {
3235
}
3336

3437
try {
38+
// Trigger the task
3539
const handle = await tasks.trigger<typeof runIntegrationTests>('run-integration-tests', {
3640
organizationId: orgId,
3741
...(integrationId ? { integrationId } : {}),
3842
});
3943

40-
const headersList = await headers();
41-
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
42-
path = path.replace(/\/[a-z]{2}\//, '/');
44+
// Poll for completion
45+
let attempts = 0;
46+
while (attempts < MAX_POLL_ATTEMPTS) {
47+
const run = await runs.retrieve(handle.id);
48+
49+
// Check if the run is in a terminal state
50+
if (run.isCompleted) {
51+
const headersList = await headers();
52+
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
53+
path = path.replace(/\/[a-z]{2}\//, '/');
54+
revalidatePath(path);
55+
56+
if (run.isSuccess) {
57+
const output = run.output as {
58+
success?: boolean;
59+
errors?: string[];
60+
failedIntegrations?: Array<{ name: string; error: string }>;
61+
} | null;
62+
63+
if (output?.success === false) {
64+
return {
65+
success: false,
66+
errors: output.errors || ['Scan completed with errors'],
67+
taskId: run.id,
68+
};
69+
}
4370

44-
revalidatePath(path);
71+
return {
72+
success: true,
73+
errors: null,
74+
taskId: run.id,
75+
};
76+
}
4577

78+
if (run.isFailed || run.isCancelled) {
79+
return {
80+
success: false,
81+
errors: ['Task failed or was canceled'],
82+
taskId: run.id,
83+
};
84+
}
85+
}
86+
87+
// Wait before polling again
88+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
89+
attempts++;
90+
}
91+
92+
// Timeout - task is taking too long
4693
return {
47-
success: true,
48-
errors: null,
94+
success: false,
95+
errors: ['Scan is taking longer than expected. Check the status in Trigger.dev dashboard.'],
4996
taskId: handle.id,
50-
publicAccessToken: handle.publicAccessToken,
5197
};
5298
} catch (error) {
53-
console.error('Error triggering integration tests:', error);
99+
console.error('Error running integration tests:', error);
54100

55101
return {
56102
success: false,
57-
errors: [error instanceof Error ? error.message : 'Failed to trigger integration tests'],
103+
errors: [error instanceof Error ? error.message : 'Failed to run integration tests'],
58104
};
59105
}
60106
};

apps/app/src/app/(app)/[orgId]/cloud-tests/components/TestsLayout.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
8383
},
8484
{
8585
fallbackData: initialProviders,
86+
refreshInterval: 10000, // Refresh providers every 10 seconds
8687
revalidateOnFocus: true,
88+
revalidateOnMount: true, // Always revalidate on mount to get fresh data
8789
},
8890
);
8991

@@ -134,16 +136,22 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
134136
}
135137

136138
setIsScanning(true);
139+
const startTime = Date.now();
137140
toast.message(`Starting ${targetProvider.name} security scan...`);
138141

139142
try {
140143
if (targetProvider.isLegacy) {
141144
// Run legacy check for this specific connection
145+
// runTests now waits for completion (uses triggerAndPoll)
142146
const { runTests } = await import('../actions/run-tests');
143-
// Pass the unique connection ID to only scan this specific connection
144147
const result = await runTests(targetProvider.id);
148+
145149
if (!result.success) {
146150
console.error('Legacy scan error:', result.errors);
151+
toast.error(
152+
`Scan failed: ${result.errors?.join(', ') || 'Unknown error'}`,
153+
);
154+
return null;
147155
}
148156
} else {
149157
// Use dedicated cloud security endpoint
@@ -155,9 +163,11 @@ export function TestsLayout({ initialFindings, initialProviders, orgId }: TestsL
155163
}
156164
}
157165

158-
toast.success('Scan completed! Results updated.');
159-
await mutateProviders(); // Refresh to get updated lastRunAt
160-
await mutateFindings();
166+
// Refresh data to get updated results
167+
await Promise.all([mutateProviders(), mutateFindings()]);
168+
169+
const elapsed = Math.round((Date.now() - startTime) / 1000);
170+
toast.success(`Scan completed in ${elapsed}s! Results updated.`);
161171
return 'completed';
162172
} catch (error) {
163173
console.error('Scan error:', error);

apps/app/src/app/(app)/[orgId]/cloud-tests/page.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
107107
requiredVariables: string[];
108108
accountId?: string;
109109
regions?: string[];
110+
supportsMultipleConnections?: boolean;
110111
};
111112

112113
const newProviders: Provider[] = newConnections.map((conn) => {
@@ -117,6 +118,7 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
117118
const regions = Array.isArray(metadata.regions)
118119
? metadata.regions.filter((region): region is string => typeof region === 'string')
119120
: undefined;
121+
const manifest = getManifest(conn.provider.slug);
120122

121123
return {
122124
id: conn.id,
@@ -133,6 +135,7 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
133135
requiredVariables: getRequiredVariables(conn.provider.slug),
134136
accountId,
135137
regions,
138+
supportsMultipleConnections: manifest?.supportsMultipleConnections ?? false,
136139
};
137140
});
138141

@@ -144,6 +147,7 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
144147
const regions = Array.isArray(settings.regions)
145148
? settings.regions.filter((region): region is string => typeof region === 'string')
146149
: undefined;
150+
const manifest = getManifest(integration.integrationId);
147151

148152
return {
149153
id: integration.id,
@@ -160,6 +164,7 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
160164
requiredVariables: getRequiredVariables(integration.integrationId),
161165
accountId,
162166
regions,
167+
supportsMultipleConnections: manifest?.supportsMultipleConnections ?? false,
163168
};
164169
});
165170

@@ -231,9 +236,17 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
231236

232237
// ====================================================================
233238
// Fetch findings from OLD platform (IntegrationResult)
239+
// Only show results from the most recent scan for each integration
234240
// ====================================================================
235241
const legacyIntegrationIds = activeLegacyIntegrations.map((i) => i.id);
236242

243+
// Create a map of integration ID to lastRunAt for filtering
244+
const integrationLastRunMap = new Map(
245+
activeLegacyIntegrations
246+
.filter((i) => i.lastRunAt)
247+
.map((i) => [i.id, i.lastRunAt!]),
248+
);
249+
237250
const legacyResults =
238251
legacyIntegrationIds.length > 0
239252
? await db.integrationResult.findMany({
@@ -254,17 +267,33 @@ export default async function CloudTestsPage({ params }: { params: Promise<{ org
254267
select: {
255268
integrationId: true,
256269
id: true,
270+
lastRunAt: true,
257271
},
258272
},
259273
},
260274
orderBy: {
261275
completedAt: 'desc',
262276
},
263-
take: 500,
264277
})
265278
: [];
266279

267-
const legacyFindings = legacyResults.map((result) => ({
280+
// Filter to only include results from the most recent scan
281+
// Results are considered from the "latest scan" if they were completed
282+
// within 5 minutes of the integration's lastRunAt
283+
const SCAN_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
284+
285+
const filteredLegacyResults = legacyResults.filter((result) => {
286+
const lastRunAt = integrationLastRunMap.get(result.integration.id);
287+
if (!lastRunAt || !result.completedAt) return false;
288+
289+
const lastRunTime = lastRunAt.getTime();
290+
const completedTime = result.completedAt.getTime();
291+
292+
// Include if completed within the scan window of the last run
293+
return Math.abs(completedTime - lastRunTime) <= SCAN_WINDOW_MS;
294+
});
295+
296+
const legacyFindings = filteredLegacyResults.map((result) => ({
268297
id: result.id,
269298
title: result.title,
270299
description: result.description,

apps/app/src/app/api/cloud-tests/findings/route.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,17 @@ export async function GET(request: NextRequest) {
136136

137137
// ====================================================================
138138
// Fetch findings from OLD platform (IntegrationResult)
139+
// Only show results from the most recent scan for each integration
139140
// ====================================================================
140141
const legacyIntegrationIds = activeLegacyIntegrations.map((i) => i.id);
141142

143+
// Create a map of integration ID to lastRunAt for filtering
144+
const integrationLastRunMap = new Map(
145+
activeLegacyIntegrations
146+
.filter((i) => i.lastRunAt)
147+
.map((i) => [i.id, i.lastRunAt!]),
148+
);
149+
142150
const legacyResults =
143151
legacyIntegrationIds.length > 0
144152
? await db.integrationResult.findMany({
@@ -159,17 +167,33 @@ export async function GET(request: NextRequest) {
159167
select: {
160168
integrationId: true,
161169
id: true,
170+
lastRunAt: true,
162171
},
163172
},
164173
},
165174
orderBy: {
166175
completedAt: 'desc',
167176
},
168-
take: 500,
169177
})
170178
: [];
171179

172-
const legacyFindings = legacyResults.map((result) => ({
180+
// Filter to only include results from the most recent scan
181+
// Results are considered from the "latest scan" if they were completed
182+
// within 5 minutes of the integration's lastRunAt
183+
const SCAN_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
184+
185+
const filteredLegacyResults = legacyResults.filter((result) => {
186+
const lastRunAt = integrationLastRunMap.get(result.integration.id);
187+
if (!lastRunAt || !result.completedAt) return false;
188+
189+
const lastRunTime = lastRunAt.getTime();
190+
const completedTime = result.completedAt.getTime();
191+
192+
// Include if completed within the scan window of the last run
193+
return Math.abs(completedTime - lastRunTime) <= SCAN_WINDOW_MS;
194+
});
195+
196+
const legacyFindings = filteredLegacyResults.map((result) => ({
173197
id: result.id,
174198
title: result.title,
175199
description: result.description,

0 commit comments

Comments
 (0)