Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jest.mock('@db', () => ({
},
integrationOAuthError: { findMany: jest.fn() },
dynamicIntegration: { findFirst: jest.fn() },
task: { findUnique: jest.fn(), updateMany: jest.fn() },
},
}));

Expand All @@ -20,6 +21,7 @@ import { db } from '@db';
import { InternalIntegrationDebugService } from './internal-integration-debug.service';
import type { ConnectionCheckRunnerService } from './connection-check-runner.service';
import type { CheckRunRepository } from '../repositories/check-run.repository';
import type { DynamicManifestLoaderService } from './dynamic-manifest-loader.service';

const encryptedBlob = {
encrypted: 'Y2lwaGVydGV4dA==',
Expand All @@ -38,15 +40,20 @@ const mockedDb = db as unknown as {
};
integrationOAuthError: { findMany: jest.Mock };
dynamicIntegration: { findFirst: jest.Mock };
task: { findUnique: jest.Mock; updateMany: jest.Mock };
};

const makeService = (
runner: Partial<ConnectionCheckRunnerService> = {},
checkRunRepo: Partial<CheckRunRepository> = {},
manifestLoader: Partial<DynamicManifestLoaderService> = {
loadDynamicManifests: jest.fn().mockResolvedValue(undefined),
},
) =>
new InternalIntegrationDebugService(
runner as ConnectionCheckRunnerService,
checkRunRepo as CheckRunRepository,
manifestLoader as DynamicManifestLoaderService,
);

describe('InternalIntegrationDebugService', () => {
Expand Down Expand Up @@ -471,6 +478,12 @@ describe('InternalIntegrationDebugService', () => {
complete: jest.fn().mockResolvedValue({}),
});

beforeEach(() => {
// The persisted re-run validates the taskId belongs to the connection's org
// (assertTaskBelongsToOrg). Default the task to the same org as the tests.
mockedDb.task.findUnique.mockResolvedValue({ organizationId: 'org_1' });
});

it('persists a fresh SUCCESS run when the fixed check now passes', async () => {
mockedDb.integrationConnection.findUnique.mockResolvedValue({
organizationId: 'org_1',
Expand Down Expand Up @@ -530,5 +543,55 @@ describe('InternalIntegrationDebugService', () => {
expect.objectContaining({ status: 'inconclusive', failedCount: 0 }),
);
});

it('refreshes the manifest cache BEFORE running (so a just-patched fix is live, not the 60s-stale code)', async () => {
mockedDb.integrationConnection.findUnique.mockResolvedValue({
organizationId: 'org_1',
provider: { slug: 'neon' },
});
mockedDb.dynamicIntegration.findFirst.mockResolvedValue({ id: 'din_neon' });
const runChecks = jest.fn().mockResolvedValue(runResult('success'));
const loadDynamicManifests = jest.fn().mockResolvedValue(undefined);

const service = makeService({ runChecks }, makeRepo(), {
loadDynamicManifests,
});
await service.rerunAndPersistCheck({
connectionId: 'icn_1',
checkId: 'neon_x',
taskId: 'task_1',
});

expect(loadDynamicManifests).toHaveBeenCalledTimes(1);
// Refresh MUST happen before the run — otherwise the run executes stale code.
expect(loadDynamicManifests.mock.invocationCallOrder[0]).toBeLessThan(
runChecks.mock.invocationCallOrder[0],
);
});

it('still runs (falls back to cached manifests) when the refresh throws', async () => {
mockedDb.integrationConnection.findUnique.mockResolvedValue({
organizationId: 'org_1',
provider: { slug: 'neon' },
});
mockedDb.dynamicIntegration.findFirst.mockResolvedValue({ id: 'din_neon' });
const runChecks = jest.fn().mockResolvedValue(runResult('success'));
const loadDynamicManifests = jest
.fn()
.mockRejectedValue(new Error('db blip'));

const service = makeService({ runChecks }, makeRepo(), {
loadDynamicManifests,
});
const out = await service.rerunAndPersistCheck({
connectionId: 'icn_1',
checkId: 'neon_x',
taskId: 'task_1',
});

// Refresh failure is swallowed; the re-run still executes + persists.
expect(out.status).toBe('success');
expect(runChecks).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './connection-check-runner.service';
import { CheckRunRepository } from '../repositories/check-run.repository';
import { decideRunStatus } from '../utils/task-check-evaluation';
import { DynamicManifestLoaderService } from './dynamic-manifest-loader.service';

/**
* Read-only/diagnostic toolkit for dynamic integrations, used by internal
Expand All @@ -32,6 +33,7 @@ export class InternalIntegrationDebugService {
constructor(
private readonly runner: ConnectionCheckRunnerService,
private readonly checkRunRepository: CheckRunRepository,
private readonly manifestLoader: DynamicManifestLoaderService,
) {}

private isEncryptedData(value: unknown): boolean {
Expand Down Expand Up @@ -499,6 +501,23 @@ export class InternalIntegrationDebugService {
}
await this.assertTaskBelongsToOrg(taskId, connection.organizationId, connectionId);

// The runner resolves dynamic check code from the in-memory manifest
// registry, which only refreshes from the DB every ~60s. A self-heal re-run
// fires seconds after the agent PATCHes a fix, so without a refresh here the
// re-run would execute the STALE (pre-fix) code and wrongly re-hold a check
// we just fixed. Refresh first so this persisted re-run reflects current DB.
// Best-effort: on a transient refresh failure, fall back to the cached
// manifests rather than failing the whole re-run.
try {
await this.manifestLoader.loadDynamicManifests();
} catch (err) {
this.logger.warn(
`Self-heal re-run: manifest refresh failed, using cached manifests: ${
err instanceof Error ? err.message : String(err)
}`,
);
}

// Execute on the real runtime in-process (runChecks never persists).
const result = await this.runner.runChecks({
connectionId,
Expand Down
Loading