Skip to content

Commit 911a790

Browse files
authored
Merge branch 'main' into tofik/cs-217-mime-type-validation
2 parents 53cf88a + 5b9387d commit 911a790

27 files changed

Lines changed: 3514 additions & 140 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [3.23.1](https://github.com/trycompai/comp/compare/v3.23.0...v3.23.1) (2026-04-16)
2+
3+
4+
### Bug Fixes
5+
6+
* **vendor:** harden firecrawl trust center crawling ([08a3786](https://github.com/trycompai/comp/commit/08a3786049a45617df2b98c3b88ca1ba6e712ce1))
7+
18
# [3.23.0](https://github.com/trycompai/comp/compare/v3.22.4...v3.23.0) (2026-04-16)
29

310

apps/api/src/integration-platform/repositories/check-run.repository.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,18 @@ export class CheckRunRepository {
124124
}
125125

126126
/**
127-
* Get check runs for a task
127+
* Get check runs for a task.
128+
*
129+
* CS-166: excludes runs from disconnected connections so the task's UI
130+
* panels don't render stale "failed" history after a user disconnects the
131+
* integration. The rows remain in the DB for audit.
128132
*/
129133
async findByTask(taskId: string, limit = 10) {
130134
return db.integrationCheckRun.findMany({
131-
where: { taskId },
135+
where: {
136+
taskId,
137+
connection: { status: { not: 'disconnected' } },
138+
},
132139
include: {
133140
results: true,
134141
connection: {
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { ConnectionService } from './connection.service';
3+
import { ConnectionRepository } from '../repositories/connection.repository';
4+
import { ProviderRepository } from '../repositories/provider.repository';
5+
import { ConnectionAuthTeardownService } from './connection-auth-teardown.service';
6+
7+
jest.mock('@db', () => ({
8+
db: {
9+
integrationCheckRun: {
10+
findMany: jest.fn(),
11+
},
12+
task: {
13+
findMany: jest.fn(),
14+
update: jest.fn(),
15+
},
16+
},
17+
}));
18+
19+
jest.mock('@trycompai/integration-platform', () => ({
20+
getManifest: jest.fn(),
21+
}));
22+
23+
import { db } from '@db';
24+
25+
const findRuns = (db.integrationCheckRun as unknown as { findMany: jest.Mock })
26+
.findMany;
27+
const findTasks = (db.task as unknown as { findMany: jest.Mock }).findMany;
28+
const updateTask = (db.task as unknown as { update: jest.Mock }).update;
29+
30+
describe('ConnectionService', () => {
31+
let service: ConnectionService;
32+
33+
const mockConnectionRepository = {
34+
update: jest.fn(),
35+
};
36+
const mockProviderRepository = {};
37+
const mockTeardown = {
38+
teardown: jest.fn(),
39+
};
40+
41+
const CONNECTION_ID = 'icn_1';
42+
43+
beforeEach(async () => {
44+
jest.clearAllMocks();
45+
46+
const module: TestingModule = await Test.createTestingModule({
47+
providers: [
48+
ConnectionService,
49+
{ provide: ConnectionRepository, useValue: mockConnectionRepository },
50+
{ provide: ProviderRepository, useValue: mockProviderRepository },
51+
{ provide: ConnectionAuthTeardownService, useValue: mockTeardown },
52+
],
53+
}).compile();
54+
55+
service = module.get(ConnectionService);
56+
57+
mockConnectionRepository.update.mockResolvedValue({
58+
id: CONNECTION_ID,
59+
status: 'disconnected',
60+
});
61+
mockTeardown.teardown.mockResolvedValue(undefined);
62+
});
63+
64+
describe('disconnectConnection (CS-166)', () => {
65+
it('re-evaluates failed tasks to "todo" when the only automation source was the disconnected connection', async () => {
66+
findRuns.mockResolvedValue([{ taskId: 'tsk_1' }]);
67+
findTasks.mockResolvedValue([
68+
{
69+
id: 'tsk_1',
70+
evidenceAutomations: [],
71+
integrationCheckRuns: [], // filtered query returns no active runs
72+
},
73+
]);
74+
75+
await service.disconnectConnection(CONNECTION_ID);
76+
77+
expect(findRuns).toHaveBeenCalledWith({
78+
where: { connectionId: CONNECTION_ID, taskId: { not: null } },
79+
select: { taskId: true },
80+
distinct: ['taskId'],
81+
});
82+
expect(findTasks).toHaveBeenCalledWith(
83+
expect.objectContaining({
84+
where: { id: { in: ['tsk_1'] }, status: 'failed' },
85+
}),
86+
);
87+
expect(updateTask).toHaveBeenCalledWith({
88+
where: { id: 'tsk_1' },
89+
data: { status: 'todo' },
90+
});
91+
});
92+
93+
it('re-evaluates failed task to "done" when remaining active automations are passing', async () => {
94+
findRuns.mockResolvedValue([{ taskId: 'tsk_2' }]);
95+
findTasks.mockResolvedValue([
96+
{
97+
id: 'tsk_2',
98+
evidenceAutomations: [],
99+
integrationCheckRuns: [
100+
{
101+
checkId: 'other_check',
102+
status: 'success',
103+
createdAt: new Date('2026-04-01'),
104+
},
105+
],
106+
},
107+
]);
108+
109+
await service.disconnectConnection(CONNECTION_ID);
110+
111+
expect(updateTask).toHaveBeenCalledWith({
112+
where: { id: 'tsk_2' },
113+
data: { status: 'done' },
114+
});
115+
});
116+
117+
it('leaves the task at "failed" when another active automation is still failing', async () => {
118+
findRuns.mockResolvedValue([{ taskId: 'tsk_3' }]);
119+
findTasks.mockResolvedValue([
120+
{
121+
id: 'tsk_3',
122+
evidenceAutomations: [],
123+
integrationCheckRuns: [
124+
{
125+
checkId: 'other_check',
126+
status: 'failed',
127+
createdAt: new Date('2026-04-01'),
128+
},
129+
],
130+
},
131+
]);
132+
133+
await service.disconnectConnection(CONNECTION_ID);
134+
135+
expect(updateTask).not.toHaveBeenCalled();
136+
});
137+
138+
it('picks the latest run per checkId when multiple exist', async () => {
139+
findRuns.mockResolvedValue([{ taskId: 'tsk_4' }]);
140+
findTasks.mockResolvedValue([
141+
{
142+
id: 'tsk_4',
143+
evidenceAutomations: [],
144+
integrationCheckRuns: [
145+
{
146+
checkId: 'check_a',
147+
status: 'success',
148+
createdAt: new Date('2026-04-05'),
149+
},
150+
{
151+
checkId: 'check_a',
152+
status: 'failed',
153+
createdAt: new Date('2026-04-01'),
154+
},
155+
{
156+
checkId: 'check_b',
157+
status: 'success',
158+
createdAt: new Date('2026-04-03'),
159+
},
160+
],
161+
},
162+
]);
163+
164+
await service.disconnectConnection(CONNECTION_ID);
165+
166+
expect(updateTask).toHaveBeenCalledWith({
167+
where: { id: 'tsk_4' },
168+
data: { status: 'done' },
169+
});
170+
});
171+
172+
it('picks the latest run per checkId even when the input is reverse-sorted', async () => {
173+
// Defensive test: if a future change breaks the query's orderBy,
174+
// the logic must still pick the newest run per checkId.
175+
findRuns.mockResolvedValue([{ taskId: 'tsk_reorder' }]);
176+
findTasks.mockResolvedValue([
177+
{
178+
id: 'tsk_reorder',
179+
evidenceAutomations: [],
180+
// Oldest first — the opposite of the query's orderBy desc.
181+
integrationCheckRuns: [
182+
{
183+
checkId: 'check_a',
184+
status: 'failed',
185+
createdAt: new Date('2026-04-01'),
186+
},
187+
{
188+
checkId: 'check_a',
189+
status: 'success',
190+
createdAt: new Date('2026-04-05'),
191+
},
192+
],
193+
},
194+
]);
195+
196+
await service.disconnectConnection(CONNECTION_ID);
197+
198+
// Latest run for check_a (2026-04-05) is success → task should become
199+
// done. If we naively picked the first-seen run, it would be failed
200+
// and the task would stay at 'failed'.
201+
expect(updateTask).toHaveBeenCalledWith({
202+
where: { id: 'tsk_reorder' },
203+
data: { status: 'done' },
204+
});
205+
});
206+
207+
it('swallows errors from the re-evaluation step so disconnect still succeeds', async () => {
208+
// The primary disconnect has already succeeded by the time re-evaluation
209+
// runs. A DB hiccup in the cleanup path must not surface to the caller.
210+
findRuns.mockRejectedValue(new Error('transient DB failure'));
211+
212+
await expect(
213+
service.disconnectConnection(CONNECTION_ID),
214+
).resolves.toEqual(
215+
expect.objectContaining({ id: CONNECTION_ID, status: 'disconnected' }),
216+
);
217+
});
218+
219+
it('does not touch a task that is not currently failed', async () => {
220+
findRuns.mockResolvedValue([{ taskId: 'tsk_5' }]);
221+
// findTasks filters by status: 'failed', so non-failed tasks are not returned
222+
findTasks.mockResolvedValue([]);
223+
224+
await service.disconnectConnection(CONNECTION_ID);
225+
226+
expect(updateTask).not.toHaveBeenCalled();
227+
});
228+
229+
it('skips re-evaluation when no runs exist for the connection', async () => {
230+
findRuns.mockResolvedValue([]);
231+
232+
await service.disconnectConnection(CONNECTION_ID);
233+
234+
expect(findTasks).not.toHaveBeenCalled();
235+
expect(updateTask).not.toHaveBeenCalled();
236+
});
237+
238+
it('handles evidenceAutomations — task with failing custom automation stays failed', async () => {
239+
findRuns.mockResolvedValue([{ taskId: 'tsk_6' }]);
240+
findTasks.mockResolvedValue([
241+
{
242+
id: 'tsk_6',
243+
evidenceAutomations: [
244+
{ runs: [{ evaluationStatus: 'fail' }] },
245+
],
246+
integrationCheckRuns: [],
247+
},
248+
]);
249+
250+
await service.disconnectConnection(CONNECTION_ID);
251+
252+
expect(updateTask).not.toHaveBeenCalled();
253+
});
254+
});
255+
});

0 commit comments

Comments
 (0)