@shipfox/api-integration-core 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +44 -0
- package/dist/core/event-catalogs.d.ts +18 -0
- package/dist/core/event-catalogs.d.ts.map +1 -0
- package/dist/core/event-catalogs.js +27 -0
- package/dist/core/event-catalogs.js.map +1 -0
- package/dist/core/providers/registry.d.ts +1 -0
- package/dist/core/providers/registry.d.ts.map +1 -1
- package/dist/core/providers/registry.js +4 -1
- package/dist/core/providers/registry.js.map +1 -1
- package/dist/core/providers/source-control.d.ts +1 -1
- package/dist/core/providers/source-control.d.ts.map +1 -1
- package/dist/core/providers/source-control.js.map +1 -1
- package/dist/core/source-control-service.d.ts +5 -1
- package/dist/core/source-control-service.d.ts.map +1 -1
- package/dist/core/source-control-service.js +12 -0
- package/dist/core/source-control-service.js.map +1 -1
- package/dist/db/connections.d.ts +4 -1
- package/dist/db/connections.d.ts.map +1 -1
- package/dist/db/connections.js +15 -6
- package/dist/db/connections.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +2 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +27 -5
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +1 -1
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
- package/dist/presentation/routes/manage-connections.js +7 -3
- package/dist/presentation/routes/manage-connections.js.map +1 -1
- package/dist/providers/gitea.d.ts.map +1 -1
- package/dist/providers/gitea.js +5 -1
- package/dist/providers/gitea.js.map +1 -1
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +6 -5
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +7 -2
- package/dist/providers/jira.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +6 -4
- package/dist/providers/linear.js.map +1 -1
- package/dist/providers/sentry.d.ts.map +1 -1
- package/dist/providers/sentry.js +3 -1
- package/dist/providers/sentry.js.map +1 -1
- package/dist/providers/slack.d.ts.map +1 -1
- package/dist/providers/slack.js +6 -4
- package/dist/providers/slack.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/core/agent-tool-selection.test.ts +4 -0
- package/src/core/event-catalogs.test.ts +16 -0
- package/src/core/event-catalogs.ts +34 -0
- package/src/core/providers/registry.test.ts +4 -0
- package/src/core/providers/registry.ts +9 -3
- package/src/core/providers/source-control.ts +2 -0
- package/src/core/secret-cleanup.test.ts +1 -0
- package/src/core/source-control-service.test.ts +48 -0
- package/src/core/source-control-service.ts +16 -0
- package/src/db/connections.test.ts +167 -0
- package/src/db/connections.ts +21 -4
- package/src/db/secret-cleanups.test.ts +1 -0
- package/src/index.ts +2 -0
- package/src/metrics/instance.ts +2 -0
- package/src/presentation/inter-module.test.ts +219 -0
- package/src/presentation/inter-module.ts +28 -3
- package/src/presentation/routes/errors.ts +3 -0
- package/src/presentation/routes/list-connections.test.ts +13 -1
- package/src/presentation/routes/list-repositories.test.ts +9 -0
- package/src/presentation/routes/manage-connections.test.ts +52 -0
- package/src/presentation/routes/manage-connections.ts +6 -2
- package/src/providers/gitea.ts +5 -0
- package/src/providers/github.ts +6 -1
- package/src/providers/jira.test.ts +3 -0
- package/src/providers/jira.ts +6 -0
- package/src/providers/linear.test.ts +3 -0
- package/src/providers/linear.ts +6 -1
- package/src/providers/modules.test.ts +64 -1
- package/src/providers/sentry.ts +3 -0
- package/src/providers/slack.test.ts +4 -0
- package/src/providers/slack.ts +6 -1
- package/test/route-utils.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"ajv": "^8.20.0",
|
|
25
25
|
"drizzle-orm": "^0.45.2",
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@shipfox/api-agent-dto": "
|
|
27
|
+
"@shipfox/api-agent-dto": "14.0.0",
|
|
28
28
|
"@shipfox/api-auth-context": "12.2.0",
|
|
29
|
-
"@shipfox/api-workflows-dto": "
|
|
30
|
-
"@shipfox/api-integration-core-dto": "
|
|
31
|
-
"@shipfox/api-integration-spi": "
|
|
29
|
+
"@shipfox/api-workflows-dto": "14.0.0",
|
|
30
|
+
"@shipfox/api-integration-core-dto": "14.0.0",
|
|
31
|
+
"@shipfox/api-integration-spi": "2.0.0",
|
|
32
32
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
33
|
-
"@shipfox/api-integration-gitea": "
|
|
34
|
-
"@shipfox/api-integration-github": "
|
|
35
|
-
"@shipfox/api-integration-jira": "
|
|
36
|
-
"@shipfox/api-integration-linear": "
|
|
37
|
-
"@shipfox/api-integration-sentry": "
|
|
38
|
-
"@shipfox/api-integration-slack": "
|
|
39
|
-
"@shipfox/api-integration-webhook": "
|
|
33
|
+
"@shipfox/api-integration-gitea": "14.0.0",
|
|
34
|
+
"@shipfox/api-integration-github": "14.0.0",
|
|
35
|
+
"@shipfox/api-integration-jira": "14.0.0",
|
|
36
|
+
"@shipfox/api-integration-linear": "14.0.0",
|
|
37
|
+
"@shipfox/api-integration-sentry": "14.0.0",
|
|
38
|
+
"@shipfox/api-integration-slack": "14.0.0",
|
|
39
|
+
"@shipfox/api-integration-webhook": "14.0.0",
|
|
40
40
|
"@shipfox/config": "1.2.4",
|
|
41
41
|
"@shipfox/inter-module": "0.2.3",
|
|
42
42
|
"@shipfox/node-drizzle": "0.3.5",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {createIntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
2
|
+
import {buildFixedEventProviders} from './event-catalogs.js';
|
|
3
|
+
|
|
4
|
+
describe('buildFixedEventProviders', () => {
|
|
5
|
+
it('omits fixed providers that are not registered', () => {
|
|
6
|
+
expect(buildFixedEventProviders(createIntegrationProviderRegistry([]))).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('keeps a registered fixed provider in the context metadata', () => {
|
|
10
|
+
expect(
|
|
11
|
+
buildFixedEventProviders(
|
|
12
|
+
createIntegrationProviderRegistry([{provider: 'webhook', displayName: 'Webhook'}]),
|
|
13
|
+
),
|
|
14
|
+
).toEqual(['webhook']);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Providers whose single event name is minted by Shipfox. An explicit event on
|
|
5
|
+
* one of these providers is provably wrong unless it is that one name; every
|
|
6
|
+
* other provider's event names are provider-minted and never treated as a
|
|
7
|
+
* closed set. `manual` and `cron` are registered built-in trigger sources;
|
|
8
|
+
* sync classifies them by literal, before slug resolution.
|
|
9
|
+
*/
|
|
10
|
+
export const FIXED_EVENT_PROVIDERS = ['webhook'] as const;
|
|
11
|
+
|
|
12
|
+
export interface ProviderEventCatalog {
|
|
13
|
+
provider: string;
|
|
14
|
+
events: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Keeps fixed-provider metadata aligned with the enabled provider registry. */
|
|
18
|
+
export function buildFixedEventProviders(
|
|
19
|
+
registry: IntegrationProviderRegistry,
|
|
20
|
+
): Array<(typeof FIXED_EVENT_PROVIDERS)[number]> {
|
|
21
|
+
const registeredProviders = new Set(registry.list().map(({provider}) => provider));
|
|
22
|
+
return FIXED_EVENT_PROVIDERS.filter((provider) => registeredProviders.has(provider));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Collects the event names each registered provider documents. */
|
|
26
|
+
export function buildProviderEventCatalogs(
|
|
27
|
+
registry: IntegrationProviderRegistry,
|
|
28
|
+
): ProviderEventCatalog[] {
|
|
29
|
+
return registry.list().flatMap((provider) => {
|
|
30
|
+
const catalog = provider.eventCatalog;
|
|
31
|
+
if (catalog === undefined) return [];
|
|
32
|
+
return [{provider: provider.provider, events: catalog.events.map((event) => event.name)}];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -83,8 +83,14 @@ function normalizeProvider(provider: IntegrationProvider): RegisteredIntegration
|
|
|
83
83
|
return {
|
|
84
84
|
...provider,
|
|
85
85
|
adapters,
|
|
86
|
-
capabilities:
|
|
87
|
-
.filter(([, adapter]) => Boolean(adapter))
|
|
88
|
-
.map(([capability]) => capability as IntegrationCapability),
|
|
86
|
+
capabilities: getIntegrationProviderCapabilities(adapters),
|
|
89
87
|
};
|
|
90
88
|
}
|
|
89
|
+
|
|
90
|
+
export function getIntegrationProviderCapabilities(
|
|
91
|
+
adapters: IntegrationProvider['adapters'],
|
|
92
|
+
): IntegrationCapability[] {
|
|
93
|
+
return Object.entries(adapters ?? {})
|
|
94
|
+
.filter(([, adapter]) => Boolean(adapter))
|
|
95
|
+
.map(([capability]) => capability as IntegrationCapability);
|
|
96
|
+
}
|
|
@@ -119,6 +119,7 @@ async function createCleanupConnection(
|
|
|
119
119
|
externalAccountId: overrides.externalAccountId ?? crypto.randomUUID(),
|
|
120
120
|
slug: `${overrides.provider ?? 'slack'}_${crypto.randomUUID()}`,
|
|
121
121
|
displayName: overrides.provider ?? 'Slack',
|
|
122
|
+
capabilities: overrides.provider === 'gitea' ? ['source_control'] : ['agent_tools'],
|
|
122
123
|
});
|
|
123
124
|
await enqueueIntegrationSecretCleanup({connection});
|
|
124
125
|
return connection;
|
|
@@ -52,6 +52,10 @@ describe('integration source-control service', () => {
|
|
|
52
52
|
await Promise.resolve();
|
|
53
53
|
return {path: '.shipfox/workflows/ci.yml', ref: 'main', content: 'name: CI'};
|
|
54
54
|
},
|
|
55
|
+
resolveRef: async (input) => {
|
|
56
|
+
await Promise.resolve();
|
|
57
|
+
return {ref: input.ref, commit: 'a'.repeat(40)};
|
|
58
|
+
},
|
|
55
59
|
resolveTriggerReference: () => null,
|
|
56
60
|
createCheckoutSpec: async (input) => {
|
|
57
61
|
await Promise.resolve();
|
|
@@ -116,6 +120,50 @@ describe('integration source-control service', () => {
|
|
|
116
120
|
expect(resolveTriggerReference).toHaveBeenCalledWith({ref: 'refs/heads/main'});
|
|
117
121
|
});
|
|
118
122
|
|
|
123
|
+
it('resolves a ref to a pinned commit through an active source-control connection', async () => {
|
|
124
|
+
const service = createService();
|
|
125
|
+
|
|
126
|
+
const result = await service.resolveSourceRef({
|
|
127
|
+
workspaceId,
|
|
128
|
+
connectionId: connection.id,
|
|
129
|
+
externalRepositoryId: 'gitea:gitea-owner/platform',
|
|
130
|
+
ref: 'refs/heads/main',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual({ref: 'refs/heads/main', commit: 'a'.repeat(40)});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('surfaces provider ref failures', async () => {
|
|
137
|
+
const service = createService({
|
|
138
|
+
resolveRef: async () => {
|
|
139
|
+
await Promise.resolve();
|
|
140
|
+
throw new IntegrationProviderError('ref-not-found', 'Ref not found');
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = service.resolveSourceRef({
|
|
145
|
+
workspaceId,
|
|
146
|
+
connectionId: connection.id,
|
|
147
|
+
externalRepositoryId: 'gitea:gitea-owner/platform',
|
|
148
|
+
ref: 'refs/heads/missing',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await expect(result).rejects.toMatchObject({reason: 'ref-not-found'});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects source ref resolution for a connection in another workspace', async () => {
|
|
155
|
+
const service = createService();
|
|
156
|
+
|
|
157
|
+
const result = service.resolveSourceRef({
|
|
158
|
+
workspaceId: crypto.randomUUID(),
|
|
159
|
+
connectionId: connection.id,
|
|
160
|
+
externalRepositoryId: 'gitea:gitea-owner/platform',
|
|
161
|
+
ref: 'refs/heads/main',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await expect(result).rejects.toThrow('requested workspace');
|
|
165
|
+
});
|
|
166
|
+
|
|
119
167
|
it('rejects a missing connection', async () => {
|
|
120
168
|
const service = createService();
|
|
121
169
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
FileSnapshot,
|
|
14
14
|
RepositoryPage,
|
|
15
15
|
RepositorySnapshot,
|
|
16
|
+
ResolvedRef,
|
|
16
17
|
TriggerReference,
|
|
17
18
|
} from './providers/source-control.js';
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ export interface IntegrationSourceControlService {
|
|
|
21
22
|
listRepositories(input: ListSourceRepositoriesInput): Promise<RepositoryPage>;
|
|
22
23
|
resolveRepository(input: ResolveSourceRepositoryInput): Promise<ResolvedSourceRepository>;
|
|
23
24
|
resolveTriggerReference(input: ResolveTriggerReferenceInput): Promise<TriggerReference | null>;
|
|
25
|
+
resolveSourceRef(input: ResolveSourceRefInput): Promise<ResolvedRef>;
|
|
24
26
|
listFiles(input: ListSourceFilesInput): Promise<FilePage>;
|
|
25
27
|
fetchFile(input: FetchSourceFileInput): Promise<FileSnapshot>;
|
|
26
28
|
createCheckoutSpec(input: CreateSourceCheckoutSpecInput): Promise<CheckoutSpec>;
|
|
@@ -45,6 +47,10 @@ export interface ResolveTriggerReferenceInput {
|
|
|
45
47
|
payload: unknown;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export interface ResolveSourceRefInput extends ResolveSourceRepositoryInput {
|
|
51
|
+
ref: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
export interface ListSourceFilesInput extends ResolveSourceRepositoryInput {
|
|
49
55
|
ref: string;
|
|
50
56
|
prefix: string;
|
|
@@ -129,6 +135,16 @@ export function createSourceControlIntegrationService({
|
|
|
129
135
|
return sourceControl?.resolveTriggerReference(payload) ?? null;
|
|
130
136
|
},
|
|
131
137
|
|
|
138
|
+
async resolveSourceRef({workspaceId, connectionId, externalRepositoryId, ref}) {
|
|
139
|
+
const connection = await getConnection(connectionId);
|
|
140
|
+
if (connection.workspaceId !== workspaceId) {
|
|
141
|
+
throw new IntegrationConnectionWorkspaceMismatchError(connectionId);
|
|
142
|
+
}
|
|
143
|
+
const sourceControl = registry.getAdapter(connection.provider, 'source_control');
|
|
144
|
+
|
|
145
|
+
return await sourceControl.resolveRef({connection, externalRepositoryId, ref});
|
|
146
|
+
},
|
|
147
|
+
|
|
132
148
|
async listFiles({workspaceId, connectionId, externalRepositoryId, ref, prefix, limit, cursor}) {
|
|
133
149
|
const connection = await getConnection(connectionId);
|
|
134
150
|
if (connection.workspaceId !== workspaceId) {
|
|
@@ -40,6 +40,7 @@ describe('integration connection queries', () => {
|
|
|
40
40
|
externalAccountId: 'gitea-owner',
|
|
41
41
|
slug: 'gitea_owner',
|
|
42
42
|
displayName: 'Gitea',
|
|
43
|
+
capabilities: ['source_control'],
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
const second = await upsertIntegrationConnection({
|
|
@@ -48,6 +49,7 @@ describe('integration connection queries', () => {
|
|
|
48
49
|
externalAccountId: 'gitea-owner',
|
|
49
50
|
slug: 'debug_renamed',
|
|
50
51
|
displayName: 'Renamed Debug',
|
|
52
|
+
capabilities: ['source_control'],
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
expect(second.id).toBe(first.id);
|
|
@@ -64,6 +66,7 @@ describe('integration connection queries', () => {
|
|
|
64
66
|
slug: 'linear_acme',
|
|
65
67
|
displayName: 'Linear Acme',
|
|
66
68
|
lifecycleStatus: 'disabled',
|
|
69
|
+
capabilities: ['agent_tools'],
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
expect(await connectionEvents(connection.id)).toHaveLength(0);
|
|
@@ -75,6 +78,7 @@ describe('integration connection queries', () => {
|
|
|
75
78
|
slug: 'linear_acme',
|
|
76
79
|
displayName: 'Linear Acme',
|
|
77
80
|
lifecycleStatus: 'active',
|
|
81
|
+
capabilities: ['agent_tools'],
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
expect(await connectionEvents(connection.id)).toHaveLength(1);
|
|
@@ -87,6 +91,7 @@ describe('integration connection queries', () => {
|
|
|
87
91
|
externalAccountId: 'debug-1',
|
|
88
92
|
slug: 'debug_1',
|
|
89
93
|
displayName: 'Debug One',
|
|
94
|
+
capabilities: ['source_control'],
|
|
90
95
|
});
|
|
91
96
|
await upsertIntegrationConnection({
|
|
92
97
|
workspaceId,
|
|
@@ -94,6 +99,7 @@ describe('integration connection queries', () => {
|
|
|
94
99
|
externalAccountId: 'debug-2',
|
|
95
100
|
slug: 'debug_2',
|
|
96
101
|
displayName: 'Debug Two',
|
|
102
|
+
capabilities: ['source_control'],
|
|
97
103
|
});
|
|
98
104
|
|
|
99
105
|
const result = await listIntegrationConnections({workspaceId});
|
|
@@ -108,6 +114,7 @@ describe('integration connection queries', () => {
|
|
|
108
114
|
externalAccountId: 'debug-1',
|
|
109
115
|
slug: 'github_main',
|
|
110
116
|
displayName: 'GitHub',
|
|
117
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
111
118
|
});
|
|
112
119
|
|
|
113
120
|
await expect(
|
|
@@ -125,6 +132,7 @@ describe('integration connection queries', () => {
|
|
|
125
132
|
externalAccountId: 'debug-1',
|
|
126
133
|
slug: 'gitea_owner',
|
|
127
134
|
displayName: 'Debug One',
|
|
135
|
+
capabilities: ['source_control'],
|
|
128
136
|
});
|
|
129
137
|
await upsertIntegrationConnection({
|
|
130
138
|
workspaceId,
|
|
@@ -132,6 +140,7 @@ describe('integration connection queries', () => {
|
|
|
132
140
|
externalAccountId: 'debug-2',
|
|
133
141
|
slug: 'debug_2',
|
|
134
142
|
displayName: 'Debug Two',
|
|
143
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
135
144
|
});
|
|
136
145
|
|
|
137
146
|
const result = await resolveUniqueConnectionSlug({
|
|
@@ -144,6 +153,83 @@ describe('integration connection queries', () => {
|
|
|
144
153
|
expect(result).toBe('debug');
|
|
145
154
|
});
|
|
146
155
|
|
|
156
|
+
it.each([
|
|
157
|
+
'github',
|
|
158
|
+
'gitea',
|
|
159
|
+
'sentry',
|
|
160
|
+
'slack',
|
|
161
|
+
'jira',
|
|
162
|
+
'linear',
|
|
163
|
+
'webhook',
|
|
164
|
+
] as const)('refuses the reserved source slugs for provider %s', async (provider) => {
|
|
165
|
+
// Sync classifies a trigger source by literal: `manual` and `cron` are
|
|
166
|
+
// built-in sources, never connection slugs, so every provider must refuse
|
|
167
|
+
// to allocate them.
|
|
168
|
+
for (const baseSlug of ['manual', 'cron'] as const) {
|
|
169
|
+
await expect(
|
|
170
|
+
resolveUniqueConnectionSlug({
|
|
171
|
+
workspaceId,
|
|
172
|
+
provider,
|
|
173
|
+
externalAccountId: 'external-account',
|
|
174
|
+
baseSlug,
|
|
175
|
+
}),
|
|
176
|
+
).rejects.toBeInstanceOf(ConnectionSlugConflictError);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('still allocates provider-prefixed slugs derived from reserved words', async () => {
|
|
181
|
+
const result = await resolveUniqueConnectionSlug({
|
|
182
|
+
workspaceId,
|
|
183
|
+
provider: 'github',
|
|
184
|
+
externalAccountId: 'external-account',
|
|
185
|
+
baseSlug: 'github_manual',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result).toBe('github_manual');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it.each([
|
|
192
|
+
'manual_',
|
|
193
|
+
'MANUAL',
|
|
194
|
+
'cron-',
|
|
195
|
+
] as const)('refuses slugs that normalize to a reserved source slug: %s', async (baseSlug) => {
|
|
196
|
+
await expect(
|
|
197
|
+
resolveUniqueConnectionSlug({
|
|
198
|
+
workspaceId,
|
|
199
|
+
provider: 'github',
|
|
200
|
+
externalAccountId: `normalized-${baseSlug}`,
|
|
201
|
+
baseSlug,
|
|
202
|
+
}),
|
|
203
|
+
).rejects.toBeInstanceOf(ConnectionSlugConflictError);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it.each([
|
|
207
|
+
'manual',
|
|
208
|
+
'cron',
|
|
209
|
+
] as const)('refuses direct writes for reserved slug %s', async (slug) => {
|
|
210
|
+
await expect(
|
|
211
|
+
createIntegrationConnection({
|
|
212
|
+
workspaceId,
|
|
213
|
+
provider: 'github',
|
|
214
|
+
externalAccountId: `create-${slug}`,
|
|
215
|
+
slug,
|
|
216
|
+
displayName: 'GitHub',
|
|
217
|
+
capabilities: ['source_control'],
|
|
218
|
+
}),
|
|
219
|
+
).rejects.toBeInstanceOf(ConnectionSlugConflictError);
|
|
220
|
+
|
|
221
|
+
await expect(
|
|
222
|
+
upsertIntegrationConnection({
|
|
223
|
+
workspaceId,
|
|
224
|
+
provider: 'gitea',
|
|
225
|
+
externalAccountId: `upsert-${slug}`,
|
|
226
|
+
slug,
|
|
227
|
+
displayName: 'Gitea',
|
|
228
|
+
capabilities: ['source_control'],
|
|
229
|
+
}),
|
|
230
|
+
).rejects.toBeInstanceOf(ConnectionSlugConflictError);
|
|
231
|
+
});
|
|
232
|
+
|
|
147
233
|
it('keeps the existing slug when resolving a reconnect', async () => {
|
|
148
234
|
await upsertIntegrationConnection({
|
|
149
235
|
workspaceId,
|
|
@@ -151,6 +237,7 @@ describe('integration connection queries', () => {
|
|
|
151
237
|
externalAccountId: 'install-uuid',
|
|
152
238
|
slug: 'sentry_prod',
|
|
153
239
|
displayName: 'Sentry',
|
|
240
|
+
capabilities: [],
|
|
154
241
|
});
|
|
155
242
|
|
|
156
243
|
const result = await resolveUniqueConnectionSlug({
|
|
@@ -170,6 +257,7 @@ describe('integration connection queries', () => {
|
|
|
170
257
|
externalAccountId: 'stripe',
|
|
171
258
|
slug: 'stripe',
|
|
172
259
|
displayName: 'Stripe',
|
|
260
|
+
capabilities: [],
|
|
173
261
|
});
|
|
174
262
|
|
|
175
263
|
const result = createIntegrationConnection({
|
|
@@ -178,6 +266,7 @@ describe('integration connection queries', () => {
|
|
|
178
266
|
externalAccountId: 'stripe',
|
|
179
267
|
slug: 'stripe_renamed',
|
|
180
268
|
displayName: 'Renamed Stripe',
|
|
269
|
+
capabilities: [],
|
|
181
270
|
});
|
|
182
271
|
|
|
183
272
|
await expect(result).rejects.toBeInstanceOf(IntegrationConnectionAlreadyExistsError);
|
|
@@ -195,6 +284,7 @@ describe('integration connection queries', () => {
|
|
|
195
284
|
externalAccountId: 'stripe',
|
|
196
285
|
slug: 'stripe',
|
|
197
286
|
displayName: 'Stripe',
|
|
287
|
+
capabilities: [],
|
|
198
288
|
});
|
|
199
289
|
|
|
200
290
|
const result = createIntegrationConnection({
|
|
@@ -203,6 +293,7 @@ describe('integration connection queries', () => {
|
|
|
203
293
|
externalAccountId: 'stripe-prod',
|
|
204
294
|
slug: 'stripe',
|
|
205
295
|
displayName: 'Stripe prod',
|
|
296
|
+
capabilities: [],
|
|
206
297
|
});
|
|
207
298
|
|
|
208
299
|
await expect(result).rejects.toBeInstanceOf(ConnectionSlugConflictError);
|
|
@@ -215,6 +306,7 @@ describe('integration connection queries', () => {
|
|
|
215
306
|
externalAccountId: 'gitea-owner',
|
|
216
307
|
slug: 'gitea_owner',
|
|
217
308
|
displayName: 'Gitea',
|
|
309
|
+
capabilities: ['source_control'],
|
|
218
310
|
});
|
|
219
311
|
await upsertIntegrationConnection({
|
|
220
312
|
workspaceId,
|
|
@@ -222,6 +314,7 @@ describe('integration connection queries', () => {
|
|
|
222
314
|
externalAccountId: 'team-1',
|
|
223
315
|
slug: 'github_team_1',
|
|
224
316
|
displayName: 'GitHub',
|
|
317
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
225
318
|
});
|
|
226
319
|
await upsertIntegrationConnection({
|
|
227
320
|
workspaceId,
|
|
@@ -230,6 +323,7 @@ describe('integration connection queries', () => {
|
|
|
230
323
|
slug: 'github_installation_1',
|
|
231
324
|
displayName: 'GitHub',
|
|
232
325
|
lifecycleStatus: 'disabled',
|
|
326
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
233
327
|
});
|
|
234
328
|
|
|
235
329
|
const result = await listIntegrationConnections({workspaceId});
|
|
@@ -249,6 +343,7 @@ describe('integration connection queries', () => {
|
|
|
249
343
|
externalAccountId: 'gitea-owner',
|
|
250
344
|
slug: 'gitea_owner',
|
|
251
345
|
displayName: 'Gitea',
|
|
346
|
+
capabilities: ['source_control'],
|
|
252
347
|
});
|
|
253
348
|
const debugB = await upsertIntegrationConnection({
|
|
254
349
|
workspaceId: otherWorkspaceId,
|
|
@@ -256,6 +351,7 @@ describe('integration connection queries', () => {
|
|
|
256
351
|
externalAccountId: 'gitea-owner',
|
|
257
352
|
slug: 'gitea_owner',
|
|
258
353
|
displayName: 'Gitea',
|
|
354
|
+
capabilities: ['source_control'],
|
|
259
355
|
});
|
|
260
356
|
const github = await upsertIntegrationConnection({
|
|
261
357
|
workspaceId,
|
|
@@ -263,6 +359,7 @@ describe('integration connection queries', () => {
|
|
|
263
359
|
externalAccountId: 'gh-1',
|
|
264
360
|
slug: 'github_gh_1',
|
|
265
361
|
displayName: 'GitHub',
|
|
362
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
266
363
|
});
|
|
267
364
|
|
|
268
365
|
const result = await listIntegrationConnectionsByProvider({provider: 'gitea'});
|
|
@@ -280,11 +377,13 @@ describe('integration connection queries', () => {
|
|
|
280
377
|
externalAccountId: 'install-uuid',
|
|
281
378
|
slug: 'sentry_install_uuid',
|
|
282
379
|
displayName: 'Sentry acme',
|
|
380
|
+
capabilities: [],
|
|
283
381
|
});
|
|
284
382
|
|
|
285
383
|
const updated = await updateIntegrationConnectionLifecycleStatus({
|
|
286
384
|
id: connection.id,
|
|
287
385
|
lifecycleStatus: 'disabled',
|
|
386
|
+
capabilities: [],
|
|
288
387
|
});
|
|
289
388
|
|
|
290
389
|
expect(updated?.lifecycleStatus).toBe('disabled');
|
|
@@ -301,6 +400,7 @@ describe('integration connection queries', () => {
|
|
|
301
400
|
slug: 'linear_acme',
|
|
302
401
|
displayName: 'Linear Acme',
|
|
303
402
|
lifecycleStatus: 'disabled',
|
|
403
|
+
capabilities: ['agent_tools'],
|
|
304
404
|
});
|
|
305
405
|
|
|
306
406
|
expect(await connectionEvents(connection.id)).toHaveLength(0);
|
|
@@ -308,6 +408,7 @@ describe('integration connection queries', () => {
|
|
|
308
408
|
await updateIntegrationConnectionLifecycleStatus({
|
|
309
409
|
id: connection.id,
|
|
310
410
|
lifecycleStatus: 'active',
|
|
411
|
+
capabilities: ['agent_tools'],
|
|
311
412
|
});
|
|
312
413
|
|
|
313
414
|
const events = await connectionEvents(connection.id);
|
|
@@ -317,6 +418,7 @@ describe('integration connection queries', () => {
|
|
|
317
418
|
workspaceId,
|
|
318
419
|
connectionId: connection.id,
|
|
319
420
|
slug: 'linear_acme',
|
|
421
|
+
capabilities: ['agent_tools'],
|
|
320
422
|
});
|
|
321
423
|
});
|
|
322
424
|
|
|
@@ -327,11 +429,13 @@ describe('integration connection queries', () => {
|
|
|
327
429
|
externalAccountId: 'linear-acme',
|
|
328
430
|
slug: 'linear_acme',
|
|
329
431
|
displayName: 'Linear Acme',
|
|
432
|
+
capabilities: ['agent_tools'],
|
|
330
433
|
});
|
|
331
434
|
|
|
332
435
|
await updateIntegrationConnectionLifecycleStatus({
|
|
333
436
|
id: connection.id,
|
|
334
437
|
lifecycleStatus: 'active',
|
|
438
|
+
capabilities: ['agent_tools'],
|
|
335
439
|
});
|
|
336
440
|
|
|
337
441
|
expect(await connectionEvents(connection.id)).toHaveLength(1);
|
|
@@ -341,6 +445,7 @@ describe('integration connection queries', () => {
|
|
|
341
445
|
const result = await updateIntegrationConnectionLifecycleStatus({
|
|
342
446
|
id: crypto.randomUUID(),
|
|
343
447
|
lifecycleStatus: 'disabled',
|
|
448
|
+
capabilities: [],
|
|
344
449
|
});
|
|
345
450
|
|
|
346
451
|
expect(result).toBeUndefined();
|
|
@@ -353,6 +458,7 @@ describe('integration connection queries', () => {
|
|
|
353
458
|
externalAccountId: 'stripe',
|
|
354
459
|
slug: 'stripe',
|
|
355
460
|
displayName: 'Stripe',
|
|
461
|
+
capabilities: [],
|
|
356
462
|
});
|
|
357
463
|
|
|
358
464
|
const deleted = await deleteIntegrationConnection({id: connection.id});
|
|
@@ -372,6 +478,7 @@ describe('integration connection queries', () => {
|
|
|
372
478
|
externalAccountId: '123',
|
|
373
479
|
slug: 'github_123',
|
|
374
480
|
displayName: 'GitHub shipfox',
|
|
481
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
375
482
|
},
|
|
376
483
|
{tx},
|
|
377
484
|
);
|
|
@@ -401,4 +508,64 @@ describe('integration connection queries', () => {
|
|
|
401
508
|
);
|
|
402
509
|
expect(events).toHaveLength(0);
|
|
403
510
|
});
|
|
511
|
+
|
|
512
|
+
it('publishes source-control and tool capabilities for a GitHub connection', async () => {
|
|
513
|
+
const connection = await upsertIntegrationConnection({
|
|
514
|
+
workspaceId,
|
|
515
|
+
provider: 'github',
|
|
516
|
+
externalAccountId: 'gh-capabilities',
|
|
517
|
+
slug: 'github_capabilities',
|
|
518
|
+
displayName: 'GitHub',
|
|
519
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const [event] = await connectionEvents(connection.id);
|
|
523
|
+
expect(event?.payload).toEqual({
|
|
524
|
+
provider: 'github',
|
|
525
|
+
workspaceId,
|
|
526
|
+
connectionId: connection.id,
|
|
527
|
+
slug: 'github_capabilities',
|
|
528
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('publishes agent-tool capabilities for a Linear connection', async () => {
|
|
533
|
+
const connection = await upsertIntegrationConnection({
|
|
534
|
+
workspaceId,
|
|
535
|
+
provider: 'linear',
|
|
536
|
+
externalAccountId: 'linear-capabilities',
|
|
537
|
+
slug: 'linear_capabilities',
|
|
538
|
+
displayName: 'Linear',
|
|
539
|
+
capabilities: ['agent_tools'],
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const [event] = await connectionEvents(connection.id);
|
|
543
|
+
expect(event?.payload).toEqual({
|
|
544
|
+
provider: 'linear',
|
|
545
|
+
workspaceId,
|
|
546
|
+
connectionId: connection.id,
|
|
547
|
+
slug: 'linear_capabilities',
|
|
548
|
+
capabilities: ['agent_tools'],
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('publishes an empty capabilities array for a webhook connection', async () => {
|
|
553
|
+
const connection = await createIntegrationConnection({
|
|
554
|
+
workspaceId,
|
|
555
|
+
provider: 'webhook',
|
|
556
|
+
externalAccountId: 'stripe-capabilities',
|
|
557
|
+
slug: 'stripe_capabilities',
|
|
558
|
+
displayName: 'Stripe',
|
|
559
|
+
capabilities: [],
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const [event] = await connectionEvents(connection.id);
|
|
563
|
+
expect(event?.payload).toEqual({
|
|
564
|
+
provider: 'webhook',
|
|
565
|
+
workspaceId,
|
|
566
|
+
connectionId: connection.id,
|
|
567
|
+
slug: 'stripe_capabilities',
|
|
568
|
+
capabilities: [],
|
|
569
|
+
});
|
|
570
|
+
});
|
|
404
571
|
});
|