@membranehq/cli 0.1.1 → 0.1.3

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.
Files changed (78) hide show
  1. package/dist/index.js +140 -140
  2. package/package.json +16 -4
  3. package/.turbo/turbo-build.log +0 -9
  4. package/CHANGELOG.md +0 -7
  5. package/scripts/add-shebang.sh +0 -6
  6. package/scripts/prepare-package-json.ts +0 -29
  7. package/src/agent.tsx +0 -50
  8. package/src/cli.ts +0 -72
  9. package/src/commands/open.command.ts +0 -51
  10. package/src/commands/pull.command.ts +0 -75
  11. package/src/commands/push.command.ts +0 -79
  12. package/src/commands/test.command.ts +0 -99
  13. package/src/components/AddMcpServerScreen.tsx +0 -215
  14. package/src/components/AgentStatus.tsx +0 -15
  15. package/src/components/Main.tsx +0 -64
  16. package/src/components/OverviewSection.tsx +0 -24
  17. package/src/components/PersonalAccessTokenInput.tsx +0 -56
  18. package/src/components/RecentChanges.tsx +0 -65
  19. package/src/components/SelectWorkspace.tsx +0 -112
  20. package/src/components/Setup.tsx +0 -121
  21. package/src/components/WorkspaceStatus.tsx +0 -61
  22. package/src/contexts/FileWatcherContext.tsx +0 -81
  23. package/src/index.ts +0 -27
  24. package/src/legacy/commands/pullWorkspace.ts +0 -70
  25. package/src/legacy/commands/pushWorkspace.ts +0 -246
  26. package/src/legacy/integrationElements.ts +0 -78
  27. package/src/legacy/push/types.ts +0 -17
  28. package/src/legacy/reader/index.ts +0 -113
  29. package/src/legacy/types.ts +0 -17
  30. package/src/legacy/util.ts +0 -149
  31. package/src/legacy/workspace-elements/connectors.ts +0 -397
  32. package/src/legacy/workspace-elements/index.ts +0 -27
  33. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +0 -70
  34. package/src/legacy/workspace-tools/integrationElements.ts +0 -78
  35. package/src/legacy/workspace-tools/util.ts +0 -149
  36. package/src/mcp/server-status.ts +0 -27
  37. package/src/mcp/server.ts +0 -36
  38. package/src/mcp/tools/getTestAccessToken.ts +0 -32
  39. package/src/modules/api/account-api-client.ts +0 -89
  40. package/src/modules/api/index.ts +0 -3
  41. package/src/modules/api/membrane-api-client.ts +0 -116
  42. package/src/modules/api/workspace-api-client.ts +0 -11
  43. package/src/modules/config/cwd-context.tsx +0 -11
  44. package/src/modules/config/project/getAgentVersion.ts +0 -16
  45. package/src/modules/config/project/index.ts +0 -8
  46. package/src/modules/config/project/paths.ts +0 -25
  47. package/src/modules/config/project/readProjectConfig.ts +0 -27
  48. package/src/modules/config/project/useProjectConfig.tsx +0 -103
  49. package/src/modules/config/system/index.ts +0 -35
  50. package/src/modules/file-watcher/index.ts +0 -166
  51. package/src/modules/file-watcher/types.ts +0 -14
  52. package/src/modules/setup/steps.ts +0 -9
  53. package/src/modules/setup/useSetup.ts +0 -16
  54. package/src/modules/status/useStatus.ts +0 -16
  55. package/src/modules/workspace-element-service/constants.ts +0 -121
  56. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +0 -69
  57. package/src/modules/workspace-element-service/index.ts +0 -304
  58. package/src/testing/environment.ts +0 -172
  59. package/src/testing/runners/base.runner.ts +0 -27
  60. package/src/testing/runners/test.runner.ts +0 -123
  61. package/src/testing/scripts/generate-test-report.ts +0 -757
  62. package/src/testing/test-suites/base.ts +0 -92
  63. package/src/testing/test-suites/data-collection.ts +0 -128
  64. package/src/testing/testers/base.ts +0 -115
  65. package/src/testing/testers/create.ts +0 -273
  66. package/src/testing/testers/delete.ts +0 -155
  67. package/src/testing/testers/find-by-id.ts +0 -135
  68. package/src/testing/testers/list.ts +0 -110
  69. package/src/testing/testers/match.ts +0 -149
  70. package/src/testing/testers/search.ts +0 -148
  71. package/src/testing/testers/spec.ts +0 -30
  72. package/src/testing/testers/update.ts +0 -284
  73. package/src/utils/auth.ts +0 -19
  74. package/src/utils/constants.ts +0 -27
  75. package/src/utils/fields.ts +0 -83
  76. package/src/utils/logger.ts +0 -106
  77. package/src/utils/templating.ts +0 -50
  78. package/tsconfig.json +0 -21
@@ -1,304 +0,0 @@
1
- import { WorkspaceElementSpecs, WorkspaceElementType, type IntegrationAppClient } from '@integration-app/sdk'
2
- import { toCamelCase } from 'js-convert-case'
3
-
4
- import { getTypeAndKeyFromPath } from './getTypeAndKeyFromPath'
5
- import { membraneClient } from '../api'
6
- import { INTEGRATION_ELEMENTS } from './constants'
7
- import { FileChangeEventType, FileChangeEvent } from '../file-watcher/types'
8
-
9
- import type { FileWatcher } from '../file-watcher'
10
-
11
- interface WorkspaceDataCache {
12
- [key: string]: unknown[]
13
- integration?: Array<{ id: string; key: string; name: string }>
14
- }
15
-
16
- export class WorkspaceElementService {
17
- private client: IntegrationAppClient
18
- private workspaceDataCache: WorkspaceDataCache = {}
19
- private cacheExpirationTime = 5 * 60 * 1000 // 5 minutes
20
- private lastCacheUpdate = 0
21
-
22
- constructor(private readonly fileWatcher: FileWatcher) {
23
- this.setupEventHandlers()
24
- }
25
-
26
- async init(): Promise<void> {
27
- this.client = await membraneClient.getClient(this.fileWatcher.getCwd())
28
-
29
- // Perform initial pull (same as `membrane pull`).
30
- // 1. Get all remote elements.
31
- // 2. Match all local elements with all the remote elements.
32
- // 3. Compare local and remote elements. If there is a conflict (some local data will be overwritten) - ask for confirmation
33
- // 4. Compare the list of remote and local elements. If there are elements in local that are not in remote - ask for confirmation to delete.
34
- }
35
-
36
- private setupEventHandlers(): void {
37
- this.fileWatcher.on('added', (event) => this.processElementChange(event, 'added'))
38
- this.fileWatcher.on('changed', (event) => this.processElementChange(event, 'changed'))
39
- this.fileWatcher.on('deleted', (event) => this.processElementChange(event, 'deleted'))
40
- }
41
-
42
- private async processElementChange(event: FileChangeEvent, eventType: FileChangeEventType): Promise<void> {
43
- try {
44
- const ref = getTypeAndKeyFromPath(event.relativePath)
45
- if (!ref || !ref.key || !ref.type) {
46
- console.debug(`Skipping file change - could not extract element reference from: ${event.relativePath}`)
47
- return
48
- }
49
-
50
- console.debug(`Processing ${eventType} element: ${ref.key} of type ${ref.type}`)
51
-
52
- switch (eventType) {
53
- case 'added':
54
- case 'changed':
55
- await this.pushElement(ref.type, { ...event.data, key: ref.key })
56
- break
57
- case 'deleted':
58
- // ToDo: implement UI for confirming deleting of the remote elements
59
- break
60
- }
61
- } catch (e) {
62
- // ToDo: save errors and display them in the UI
63
- console.error(`Failed to process event ${eventType} for element ${event.relativePath}:`, e)
64
- }
65
- }
66
-
67
- private async pushElement(elementType: WorkspaceElementType, elementData: Record<string, unknown>): Promise<void> {
68
- const elementConfig = INTEGRATION_ELEMENTS[elementType]
69
-
70
- if (!elementConfig) {
71
- throw new Error(`Unknown element type: ${elementType}`)
72
- }
73
-
74
- const element = { ...elementData }
75
-
76
- const workspaceData = await this.getWorkspaceData()
77
-
78
- // Clean up element data
79
- delete element.integrationId
80
-
81
- const destinationElement = this.matchElement(element, elementType, workspaceData)
82
-
83
- if (destinationElement) {
84
- // Element exists - update or reset logic
85
- await this.handleExistingElement(elementType, element, destinationElement)
86
- } else {
87
- // Element doesn't exist - create logic
88
- await this.handleNewElement(elementType, element, workspaceData)
89
- }
90
- }
91
-
92
- private async handleExistingElement(
93
- elementType: WorkspaceElementType,
94
- element: Record<string, unknown>,
95
- destinationElement: Record<string, unknown>,
96
- ): Promise<void> {
97
- const elementConfig = INTEGRATION_ELEMENTS[elementType]
98
-
99
- const singleElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].name)
100
-
101
- if (!this.hasParent(element) || (this.elementIsCustomized(element) && this.hasParent(element))) {
102
- // Update the element without parent OR the integration-specific element that should be customized
103
- await this.client[singleElementAccessorKey](destinationElement.id).put(element)
104
- console.debug(`Updated ${element.integrationKey || 'universal'} ${elementConfig.element} ${element.key}`)
105
- } else if (this.hasParent(element)) {
106
- // Reset Integration-specific element if it has universal parent
107
- try {
108
- await this.client[singleElementAccessorKey](destinationElement.id).reset()
109
- console.debug(
110
- `Customization reset ${element.integrationKey || 'universal'} ${elementConfig.element} ${element.key}`,
111
- )
112
- } catch (error) {
113
- console.debug('Error resetting element:', destinationElement, element)
114
- throw error
115
- }
116
- } else {
117
- console.warn(
118
- `Corrupted element detected. Manual migration required: ${element.integrationKey || 'universal'} ${elementConfig.element} ${element.key}`,
119
- )
120
- }
121
- }
122
-
123
- private async handleNewElement(
124
- elementType: WorkspaceElementType,
125
- element: Record<string, unknown>,
126
- workspaceData: WorkspaceDataCache,
127
- ): Promise<void> {
128
- const elementConfig = INTEGRATION_ELEMENTS[elementType]
129
- const singleElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].name)
130
- const pluralElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].namePlural)
131
-
132
- if (this.hasParent(element)) {
133
- // Element has parent - apply universal to integration first
134
- try {
135
- await this.client[singleElementAccessorKey]({
136
- key: element.key,
137
- }).apply([element.integrationKey])
138
- } catch (_error) {
139
- // If apply fails, try direct put or create
140
- try {
141
- await this.client[singleElementAccessorKey]({
142
- key: element.key,
143
- integrationKey: element.integrationKey,
144
- }).put(element)
145
- } catch (_error) {
146
- // Last resort - create with integration ID
147
- const integrationId = this.findIntegrationId(element.integrationKey as string, workspaceData)
148
- await this.client[pluralElementAccessorKey].create({
149
- ...element,
150
- integrationId,
151
- })
152
- }
153
- }
154
-
155
- // If element is customized, apply customization
156
- if (this.elementIsCustomized(element)) {
157
- try {
158
- await this.client[singleElementAccessorKey]({
159
- key: element.key,
160
- integrationKey: element.integrationKey,
161
- }).put(element)
162
- console.debug(
163
- `Applied & Customized ${element.integrationKey || 'universal'} ${elementConfig.element} ${element.key}`,
164
- )
165
- } catch (_error) {
166
- console.debug('Error customizing element:', element)
167
- throw _error
168
- }
169
- } else {
170
- console.debug(`Applied universal ${elementConfig.element} ${element.key} to ${element.integrationKey}`)
171
- }
172
- } else {
173
- // Element has no parent - create directly
174
- try {
175
- if (element.integrationKey) {
176
- delete element.integration
177
- const integrationId = this.findIntegrationId(element.integrationKey as string, workspaceData)
178
- element.integrationId = integrationId
179
- }
180
- await this.client[pluralElementAccessorKey].create(element)
181
- console.debug(`Created ${element.integrationKey || 'universal'} ${elementConfig.element} ${element.key}`)
182
- } catch (_error) {
183
- console.debug(`Error creating element ${elementType}`, this.hasParent(element), element.integrationKey, element)
184
- throw _error
185
- }
186
- }
187
- }
188
-
189
- private hasParent(element: Record<string, unknown>): boolean {
190
- return Object.keys(element).some((key) => /universal.*Id/g.test(key) || /parentId/g.test(key))
191
- }
192
-
193
- private elementIsCustomized(element: Record<string, unknown>): boolean {
194
- return !!(element.integrationKey && (element.customized || element.isCustomized))
195
- }
196
-
197
- private matchElement(
198
- element: Record<string, unknown>,
199
- elementType: WorkspaceElementType,
200
- workspaceData: WorkspaceDataCache,
201
- ): Record<string, unknown> | undefined {
202
- const matchedElements = workspaceData[elementType]?.filter((item: any) => {
203
- return item.key === element.key && item.integrationKey === element.integrationKey && !item.archivedAt
204
- })
205
-
206
- if (!matchedElements || matchedElements.length === 0) {
207
- return undefined
208
- }
209
-
210
- if (matchedElements.length > 1) {
211
- throw new Error(
212
- `More than one ${element.integrationKey || 'universal'} ${elementType} with key ${element.key} found in the workspace`,
213
- )
214
- }
215
-
216
- return matchedElements[0] as Record<string, unknown>
217
- }
218
-
219
- private findIntegrationId(integrationKey: string, workspaceData: WorkspaceDataCache): string {
220
- const integration = workspaceData.integration?.find((item) => item.key === integrationKey)
221
- if (!integration) {
222
- throw new Error(`Integration with key ${integrationKey} not found`)
223
- }
224
- return integration.id
225
- }
226
-
227
- private async getWorkspaceData(): Promise<WorkspaceDataCache> {
228
- const now = Date.now()
229
-
230
- // Return cached data if it's still valid
231
- if (this.lastCacheUpdate && now - this.lastCacheUpdate < this.cacheExpirationTime) {
232
- return this.workspaceDataCache
233
- }
234
-
235
- // Refresh cache
236
- console.debug('Refreshing workspace data cache')
237
- const workspaceData: WorkspaceDataCache = {}
238
-
239
- try {
240
- // Get integrations first (needed for integration-specific elements)
241
- const integrations = await this.client.integrations.findAll()
242
- workspaceData.integration = integrations.map((item: any) => ({
243
- id: item.id,
244
- key: item.key,
245
- name: item.name,
246
- }))
247
-
248
- // Get all exportable element types
249
- for (const elementType of Object.keys(INTEGRATION_ELEMENTS)) {
250
- const elementConfig = INTEGRATION_ELEMENTS[elementType as WorkspaceElementType]
251
- if (elementConfig.exportable === false) continue
252
-
253
- try {
254
- const elements = []
255
-
256
- // Get all universal elements
257
- const universalElements = await this.client[elementConfig.elements].findAll()
258
- elements.push(...universalElements)
259
-
260
- // Get all integration-specific elements if applicable
261
- if (elementConfig.integrationSpecific) {
262
- for (const integration of integrations) {
263
- try {
264
- const integrationElements = await this.client[elementConfig.elements].findAll({
265
- integrationId: integration.id,
266
- })
267
- elements.push(
268
- ...integrationElements.map((item: any) => ({
269
- ...item,
270
- integrationKey: integration.key,
271
- })),
272
- )
273
- } catch (error) {
274
- console.error(`Failed to fetch ${elementType} for integration ${integration.key}:`, error)
275
- }
276
- }
277
- }
278
-
279
- workspaceData[elementType] = elements
280
- } catch (error) {
281
- console.error(`Failed to fetch ${elementType} elements:`, error)
282
- workspaceData[elementType] = []
283
- }
284
- }
285
-
286
- this.workspaceDataCache = workspaceData
287
- this.lastCacheUpdate = now
288
- console.debug('Workspace data cache refreshed successfully')
289
-
290
- return workspaceData
291
- } catch (error) {
292
- console.error('Failed to refresh workspace data cache:', error)
293
-
294
- // Return existing cache if available, otherwise empty cache
295
- if (Object.keys(this.workspaceDataCache).length > 0) {
296
- console.debug('Using existing workspace data cache due to refresh failure')
297
- return this.workspaceDataCache
298
- }
299
-
300
- // Return empty cache as fallback
301
- return { integration: [] }
302
- }
303
- }
304
- }
@@ -1,172 +0,0 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
-
4
- import Anthropic from '@anthropic-ai/sdk'
5
- import { IntegrationAppClient } from '@integration-app/sdk'
6
- import yaml from 'js-yaml'
7
- import jwt, { Algorithm } from 'jsonwebtoken'
8
- import merge from 'lodash/merge.js'
9
-
10
- import { BaseTestSuite } from './test-suites/base'
11
- import { readProjectConfig } from '../modules/config/project/readProjectConfig'
12
-
13
- import type { ProjectConfig } from '../modules/config/project/useProjectConfig'
14
-
15
- const LLM_MODEL = 'claude-sonnet-4-20250514'
16
-
17
- export interface TestRunOptions {
18
- fix?: boolean
19
- }
20
-
21
- export class TestEnvironment {
22
- client: IntegrationAppClient
23
-
24
- connectionId: string
25
-
26
- testsDir: string
27
-
28
- testBasePath: string
29
-
30
- options: TestRunOptions
31
-
32
- llm: {
33
- complete: (params: { prompt: string; maxTokens: number }) => Promise<string>
34
- }
35
-
36
- state: Record<string, unknown> = {}
37
-
38
- constructor({
39
- connectionId,
40
- testsDir,
41
- testBasePath,
42
- client,
43
- options,
44
- llm,
45
- }: {
46
- connectionId: string
47
- testsDir: string
48
- testBasePath: string
49
- options: TestRunOptions
50
- client: IntegrationAppClient
51
- llm: {
52
- complete: (params: { prompt: string; maxTokens: number }) => Promise<string>
53
- }
54
- }) {
55
- this.client = client
56
- this.connectionId = connectionId
57
- this.testsDir = testsDir
58
- this.testBasePath = testBasePath
59
- this.llm = llm
60
- this.options = options
61
- }
62
-
63
- static async create({
64
- connectionId,
65
- testBasePath,
66
- options,
67
- }: {
68
- connectionId: string
69
- testBasePath: string
70
- options: TestRunOptions
71
- }): Promise<TestEnvironment> {
72
- const workspaceConfig = readProjectConfig()
73
-
74
- if (!workspaceConfig) {
75
- throw new Error('No membrane.config.yml found. Please run `membrane init` first.')
76
- }
77
-
78
- if (!workspaceConfig.workspaceKey || !workspaceConfig.workspaceSecret) {
79
- throw new Error('Missing workspace credentials')
80
- }
81
-
82
- if (!workspaceConfig.anthropicApiKey) {
83
- throw new Error('Anthropic API key not configured. Run `membrane init` to set up testing.')
84
- }
85
-
86
- const client = new IntegrationAppClient({
87
- token: await this.createMembraneToken(workspaceConfig),
88
- apiUri: workspaceConfig.apiUri,
89
- })
90
-
91
- const anthropicClient = new Anthropic({
92
- apiKey: workspaceConfig.anthropicApiKey,
93
- })
94
-
95
- const llm = {
96
- complete: async ({ prompt, maxTokens }) => {
97
- const response = await anthropicClient.messages.create({
98
- model: LLM_MODEL,
99
- max_tokens: maxTokens,
100
- messages: [
101
- {
102
- role: 'user',
103
- content: prompt,
104
- },
105
- ],
106
- })
107
- return response.content[0].type === 'text' ? response.content[0].text : ''
108
- },
109
- }
110
-
111
- return new TestEnvironment({
112
- client,
113
- options,
114
- connectionId,
115
- testsDir: `src/testing/tests`,
116
- testBasePath,
117
- llm,
118
- })
119
- }
120
-
121
- async run(suites: BaseTestSuite[]) {
122
- this.state = {}
123
- const allResults = {}
124
- for (const suite of suites) {
125
- await suite.run()
126
- merge(allResults, suite.getResult())
127
- }
128
-
129
- this.writeResults(allResults)
130
- }
131
-
132
- async readYaml<T>(yamlPath: string): Promise<T | undefined> {
133
- const fullConfigPath = path.join(this.testsDir, this.testBasePath, this.connectionId, yamlPath)
134
- if (!fs.existsSync(fullConfigPath)) {
135
- return undefined
136
- }
137
-
138
- return yaml.load(fs.readFileSync(fullConfigPath, 'utf8')) as T
139
- }
140
-
141
- async writeYaml(yamlPath: string, config: any) {
142
- const fullConfigPath = path.join(this.testsDir, this.testBasePath, this.connectionId, yamlPath)
143
- fs.mkdirSync(path.dirname(fullConfigPath), { recursive: true })
144
- fs.writeFileSync(fullConfigPath, yaml.dump(config, { noRefs: true }))
145
- }
146
-
147
- writeResults(results: any) {
148
- const connectionTestsDir = path.join(this.testsDir, this.testBasePath, this.connectionId)
149
- fs.mkdirSync(connectionTestsDir, { recursive: true })
150
-
151
- const resultsPath = path.join(connectionTestsDir, 'test-results.yaml')
152
- fs.writeFileSync(resultsPath, yaml.dump(results, { noRefs: true }))
153
- console.debug(`[TestRunner] Results written to: ${resultsPath}`)
154
- }
155
-
156
- private static async createMembraneToken(workspaceConfig: ProjectConfig) {
157
- const customerId = workspaceConfig.testCustomerId || 'test-customer'
158
-
159
- const tokenData = {
160
- id: customerId,
161
- name: customerId,
162
- }
163
-
164
- const options = {
165
- issuer: workspaceConfig.workspaceKey,
166
- expiresIn: 7200,
167
- algorithm: 'HS512' as Algorithm,
168
- }
169
-
170
- return jwt.sign(tokenData, workspaceConfig.workspaceSecret, options)
171
- }
172
- }
@@ -1,27 +0,0 @@
1
- import { getPaths } from '../../utils/constants'
2
-
3
- import type { IntegrationAppClient } from '@integration-app/sdk'
4
-
5
- export abstract class BaseRunner<T> {
6
- protected client!: IntegrationAppClient
7
- protected workspace?: string
8
-
9
- protected fsPaths: ReturnType<typeof getPaths>
10
-
11
- constructor(protected options?: T) {
12
- this.fsPaths = getPaths()
13
-
14
- if (options && typeof options === 'object') {
15
- const opts = options as Record<string, unknown>
16
- if ('client' in opts && opts.client) {
17
- this.client = opts.client as IntegrationAppClient
18
- }
19
- if ('workspace' in opts) {
20
- this.workspace = opts.workspace as string
21
- }
22
- }
23
- }
24
-
25
- abstract initialize(): Promise<void>
26
- abstract run(): Promise<void>
27
- }
@@ -1,123 +0,0 @@
1
- import { ConnectorDataCollection } from '@integration-app/sdk'
2
- import merge from 'lodash/merge.js'
3
-
4
- import { BaseRunner } from './base.runner'
5
- import { Logger } from '../../utils/logger'
6
- import { TestEnvironment } from '../environment'
7
- import { DataCollectionTestSuite } from '../test-suites/data-collection'
8
-
9
- interface TestOptions {
10
- testPath: string
11
- path?: string
12
- fix?: boolean
13
- }
14
-
15
- export class TestRunner extends BaseRunner<TestOptions> {
16
- constructor(options: TestOptions) {
17
- super(options)
18
- }
19
-
20
- async initialize(): Promise<void> {
21
- Logger.debug('Initializing test runner', {
22
- prefix: 'TestRunner',
23
- })
24
- }
25
-
26
- async run(): Promise<void> {
27
- try {
28
- const { testPath, path, fix } = this.options
29
-
30
- // Parse the test path (e.g., "connectors/netsuite/data/contacts/create")
31
- const pathParts = testPath.split('/')
32
-
33
- if (pathParts.length < 2) {
34
- Logger.error('Invalid test path. Expected format: <type>/<name>[/additional/path][/method]')
35
- process.exit(1)
36
- }
37
-
38
- const [testType, testName, ...additionalPath] = pathParts
39
-
40
- // For now, only connectors are fully supported
41
- if (testType !== 'connectors') {
42
- Logger.error(
43
- `Test type "${testType}" is not yet fully implemented. Currently only "connectors" is fully supported.`,
44
- )
45
- Logger.error('Supported test types: connectors')
46
- process.exit(1)
47
- }
48
-
49
- const connectionId = testName
50
-
51
- const testBasePath = testType
52
-
53
- const combinedPath = [...additionalPath, ...(path ? path.split('/') : [])].join('/')
54
-
55
- const environment = await TestEnvironment.create({
56
- connectionId,
57
- testBasePath,
58
- options: { fix },
59
- })
60
-
61
- const client = environment.client
62
-
63
- const dataCollections = (await client.get(`connections/${connectionId}/data`)) as ConnectorDataCollection[]
64
-
65
- const suites = []
66
-
67
- const pathSegments = combinedPath ? combinedPath.split('/') : []
68
-
69
- if (pathSegments.length === 0 || pathSegments[0] === 'data') {
70
- if (pathSegments[0] === 'data') {
71
- pathSegments.shift()
72
- }
73
-
74
- // Extract test method if present (e.g., "contacts/create" -> dataCollectionKey="contacts", testMethod="create")
75
- let testMethod: string | undefined
76
- let dataCollectionKey: string | undefined
77
-
78
- if (pathSegments.length >= 1) {
79
- dataCollectionKey = pathSegments[0]
80
-
81
- if (pathSegments.length >= 2 && pathSegments[1].trim() !== '') {
82
- testMethod = pathSegments[1]
83
- }
84
- }
85
-
86
- for (const dataCollection of dataCollections) {
87
- if (dataCollectionKey && dataCollectionKey !== dataCollection.key) {
88
- continue
89
- }
90
-
91
- const suite = new DataCollectionTestSuite({
92
- environment,
93
- dataCollectionKey: dataCollection.key,
94
- testMethod,
95
- })
96
-
97
- suites.push(suite)
98
- }
99
- }
100
-
101
- if (suites.length === 0) {
102
- Logger.error(`No test suites found for path: ${testPath}${combinedPath ? '/' + combinedPath : ''}`)
103
- process.exit(1)
104
- }
105
-
106
- // Run suites while preserving state between data collections
107
- const allResults = {}
108
- for (const suite of suites) {
109
- await suite.run()
110
- const suiteResult = suite.getResult()
111
- Logger.debug(`Suite ${suite.constructor.name} result:`, { prefix: 'TestRunner' })
112
- merge(allResults, suiteResult)
113
- }
114
-
115
- console.debug(`[TestRunner] All results collected:`, Object.keys(allResults))
116
- // Write the collected results
117
- environment.writeResults(allResults)
118
- } catch (error) {
119
- console.error('Error in TestRunner.run():', error)
120
- throw error
121
- }
122
- }
123
- }