@matimo/bruno 0.1.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.
@@ -0,0 +1,119 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const logger = getGlobalMatimoLogger();
6
+
7
+ interface RequestInfo {
8
+ name: string;
9
+ method: string;
10
+ url: string;
11
+ path: string;
12
+ tags: string[];
13
+ has_tests: boolean;
14
+ }
15
+
16
+ async function scanBruFiles(dir: string, results: RequestInfo[]): Promise<void> {
17
+ let entries: string[];
18
+ try {
19
+ entries = await fs.readdir(dir);
20
+ } catch {
21
+ return;
22
+ }
23
+ for (const entry of entries) {
24
+ const fullPath = path.join(dir, entry);
25
+ try {
26
+ const stat = await fs.stat(fullPath);
27
+ if (stat.isDirectory() && entry !== 'node_modules') {
28
+ await scanBruFiles(fullPath, results);
29
+ } else if (!stat.isDirectory() && entry.endsWith('.bru')) {
30
+ try {
31
+ const content = await fs.readFile(fullPath, 'utf-8');
32
+ const methodMatch = content.match(/^(get|post|put|patch|delete|head|options)\s*\{/im);
33
+ const method = methodMatch ? methodMatch[1].toUpperCase() : 'UNKNOWN';
34
+ const nameMatch = content.match(/meta\s*\{[^}]*name:\s*(.+)/i);
35
+ const name = nameMatch ? nameMatch[1].trim() : path.basename(entry, '.bru');
36
+ const urlMatch = content.match(/^\s+url:\s*(.+)$/m);
37
+ const url = urlMatch ? urlMatch[1].trim() : '';
38
+ const tagsMatch = content.match(/tags:\s*\[([^\]]*)\]/);
39
+ const tags = tagsMatch
40
+ ? tagsMatch[1].split(',').map((t) => t.trim()).filter(Boolean)
41
+ : [];
42
+ const hasTests = /\btests\s*\{/.test(content);
43
+ results.push({ name, method, url, path: fullPath, tags, has_tests: hasTests });
44
+ } catch {
45
+ results.push({
46
+ name: path.basename(entry, '.bru'),
47
+ method: 'UNKNOWN',
48
+ url: '',
49
+ path: fullPath,
50
+ tags: [],
51
+ has_tests: false,
52
+ });
53
+ }
54
+ }
55
+ } catch {
56
+ // skip
57
+ }
58
+ }
59
+ }
60
+
61
+ export default async function execute(params: Record<string, unknown>): Promise<unknown> {
62
+ const collectionPath = params.collection_path as string;
63
+
64
+ if (!collectionPath) {
65
+ return {
66
+ success: false,
67
+ collection: undefined,
68
+ errors: ['collection_path parameter is required'],
69
+ };
70
+ }
71
+
72
+ try {
73
+ logger.info(`Getting collection info: ${collectionPath}`);
74
+ const absolutePath = path.resolve(collectionPath);
75
+
76
+ const brunoJsonPath = path.join(absolutePath, 'bruno.json');
77
+ let collectionName = path.basename(absolutePath);
78
+
79
+ // Verify the path exists before proceeding
80
+ try {
81
+ await fs.stat(absolutePath);
82
+ } catch {
83
+ return {
84
+ success: false,
85
+ collection: undefined,
86
+ errors: [`Collection path not found: ${collectionPath}`],
87
+ };
88
+ }
89
+
90
+ try {
91
+ const raw = await fs.readFile(brunoJsonPath, 'utf-8');
92
+ const parsed = JSON.parse(raw) as { name?: string };
93
+ if (parsed.name) collectionName = parsed.name;
94
+ } catch {
95
+ // bruno.json missing — use dirname as name
96
+ }
97
+
98
+ const requests: RequestInfo[] = [];
99
+ await scanBruFiles(absolutePath, requests);
100
+
101
+ return {
102
+ success: true,
103
+ collection: {
104
+ name: collectionName,
105
+ path: absolutePath,
106
+ requests,
107
+ },
108
+ };
109
+ } catch (error) {
110
+ logger.error(
111
+ `Get collection info failed: ${error instanceof Error ? error.message : String(error)}`
112
+ );
113
+ return {
114
+ success: false,
115
+ collection: undefined,
116
+ errors: [error instanceof Error ? error.message : String(error)],
117
+ };
118
+ }
119
+ }
@@ -0,0 +1,75 @@
1
+ name: bruno_import_openapi
2
+ description: Bootstrap a new Bruno collection from an OpenAPI 3.0 specification (YAML or JSON). Enables agents to auto-generate test collections from API specs without manual setup.
3
+ version: '1.0.0'
4
+ status: approved
5
+
6
+ parameters:
7
+ spec_source:
8
+ type: string
9
+ required: true
10
+ description: Path or URL to OpenAPI specification (YAML or JSON)
11
+
12
+ output_directory:
13
+ type: string
14
+ required: true
15
+ description: Directory where imported collection will be created
16
+
17
+ collection_name:
18
+ type: string
19
+ required: false
20
+ description: Name for imported collection (defaults to spec name)
21
+
22
+ collection_format:
23
+ type: string
24
+ required: false
25
+ default: bru
26
+ description: Format for collection files - 'bru' (classic) or 'opencollection' (YAML)
27
+
28
+ group_by:
29
+ type: string
30
+ required: false
31
+ default: tags
32
+ description: Group requests by 'tags' or 'path'
33
+
34
+ insecure:
35
+ type: boolean
36
+ required: false
37
+ default: false
38
+ description: Skip TLS verification when source is HTTPS URL
39
+
40
+ execution:
41
+ type: function
42
+ code: index.ts
43
+ timeout: 30000
44
+
45
+ output_schema:
46
+ type: object
47
+ properties:
48
+ success:
49
+ type: boolean
50
+ collection_path:
51
+ type: string
52
+ collection_name:
53
+ type: string
54
+ requests_created:
55
+ type: number
56
+ message:
57
+ type: string
58
+ errors:
59
+ type: array
60
+ items:
61
+ type: string
62
+
63
+
64
+ examples:
65
+ - name: "Import from local OpenAPI YAML"
66
+ params:
67
+ spec_source: "./specs/payment-api.yaml"
68
+ output_directory: "./collections"
69
+ collection_name: "Payment API"
70
+
71
+ - name: "Import from remote OpenAPI URL"
72
+ params:
73
+ spec_source: "https://api.example.com/openapi.json"
74
+ output_directory: "./collections"
75
+ group_by: "path"
@@ -0,0 +1,102 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core';
2
+ import { execFileSync } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import { checkBruVersion } from '../bru-utils';
6
+
7
+ const logger = getGlobalMatimoLogger();
8
+
9
+ /** Count .bru files recursively — compatible with Node 18+. */
10
+ async function countBruFilesRecursively(dir: string): Promise<number> {
11
+ let count = 0;
12
+ let entries: string[];
13
+ try {
14
+ entries = await fs.readdir(dir);
15
+ } catch {
16
+ return 0;
17
+ }
18
+ for (const entry of entries) {
19
+ const fullPath = path.join(dir, entry);
20
+ try {
21
+ const stat = await fs.stat(fullPath);
22
+ if (stat.isDirectory() && entry !== 'node_modules') {
23
+ count += await countBruFilesRecursively(fullPath);
24
+ } else if (!stat.isDirectory() && entry.endsWith('.bru')) {
25
+ count++;
26
+ }
27
+ } catch {
28
+ // skip
29
+ }
30
+ }
31
+ return count;
32
+ }
33
+
34
+ export default async function execute(params: Record<string, unknown>): Promise<unknown> {
35
+ const specSource = params.spec_source as string;
36
+ const outputDirectory = params.output_directory as string;
37
+
38
+ if (!specSource || !outputDirectory) {
39
+ return {
40
+ success: false,
41
+ collection_path: '',
42
+ collection_name: '',
43
+ requests_created: 0,
44
+ message: 'spec_source and output_directory parameters are required',
45
+ errors: ['spec_source and output_directory parameters are required'],
46
+ };
47
+ }
48
+
49
+ checkBruVersion();
50
+
51
+ const collectionName = (params.collection_name as string) || 'Imported Collection';
52
+ const absoluteOutput = path.resolve(outputDirectory);
53
+
54
+ try {
55
+ logger.info(`Importing OpenAPI from: ${specSource} to ${absoluteOutput}`);
56
+
57
+ const args: string[] = [
58
+ 'import', 'openapi',
59
+ '--source', specSource,
60
+ '--output', absoluteOutput,
61
+ '--collection-name', collectionName,
62
+ ];
63
+
64
+ if (params.group_by) args.push('--group-by', params.group_by as string);
65
+ if (params.insecure === true) args.push('--insecure');
66
+
67
+ logger.debug(`Executing: bru ${args.join(' ')}`);
68
+
69
+ execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe' });
70
+
71
+ logger.info('OpenAPI import completed');
72
+
73
+ // Count generated .bru files using a Node 18-compatible recursive walk
74
+ let requestsCreated = 0;
75
+ try {
76
+ requestsCreated = await countBruFilesRecursively(absoluteOutput);
77
+ } catch {
78
+ // best-effort count
79
+ }
80
+
81
+ return {
82
+ success: true,
83
+ collection_path: absoluteOutput,
84
+ collection_name: collectionName,
85
+ requests_created: requestsCreated,
86
+ message: `Collection "${collectionName}" imported from OpenAPI spec`,
87
+ errors: [],
88
+ };
89
+ } catch (error) {
90
+ logger.error(
91
+ `OpenAPI import failed: ${error instanceof Error ? error.message : String(error)}`
92
+ );
93
+ return {
94
+ success: false,
95
+ collection_path: absoluteOutput,
96
+ collection_name: collectionName,
97
+ requests_created: 0,
98
+ message: 'Import failed',
99
+ errors: [error instanceof Error ? error.message : String(error)],
100
+ };
101
+ }
102
+ }
@@ -0,0 +1,49 @@
1
+ name: bruno_list_collections
2
+ description: Discover all Bruno API collections in a workspace directory. Returns collection metadata including name, path, and request count.
3
+ version: '1.0.0'
4
+ status: approved
5
+
6
+ parameters:
7
+ workspace_path:
8
+ type: string
9
+ required: true
10
+ description: Path to Bruno workspace directory
11
+
12
+ filter:
13
+ type: string
14
+ required: false
15
+ description: Filter collections by name (substring match)
16
+
17
+ execution:
18
+ type: function
19
+ code: index.ts
20
+ timeout: 10000
21
+
22
+ output_schema:
23
+ type: object
24
+ properties:
25
+ success:
26
+ type: boolean
27
+ collections:
28
+ type: array
29
+ items:
30
+ type: object
31
+ properties:
32
+ name:
33
+ type: string
34
+ path:
35
+ type: string
36
+ request_count:
37
+ type: number
38
+ required: ["name", "path", "request_count"]
39
+
40
+
41
+ examples:
42
+ - name: "List all collections in workspace"
43
+ params:
44
+ workspace_path: "./bruno-collections"
45
+
46
+ - name: "Filter collections by name"
47
+ params:
48
+ workspace_path: "./bruno-collections"
49
+ filter: "api"
@@ -0,0 +1,101 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const logger = getGlobalMatimoLogger();
6
+
7
+ async function countBruFilesRecursively(dir: string): Promise<number> {
8
+ let count = 0;
9
+ let entries: string[];
10
+ try {
11
+ entries = await fs.readdir(dir);
12
+ } catch {
13
+ return 0;
14
+ }
15
+ for (const entry of entries) {
16
+ const fullPath = path.join(dir, entry);
17
+ try {
18
+ const stat = await fs.stat(fullPath);
19
+ if (stat.isDirectory() && entry !== 'node_modules') {
20
+ count += await countBruFilesRecursively(fullPath);
21
+ } else if (!stat.isDirectory() && entry.endsWith('.bru')) {
22
+ count++;
23
+ }
24
+ } catch {
25
+ // skip
26
+ }
27
+ }
28
+ return count;
29
+ }
30
+
31
+ async function findCollections(
32
+ dir: string
33
+ ): Promise<Array<{ name: string; path: string; request_count: number }>> {
34
+ const results: Array<{ name: string; path: string; request_count: number }> = [];
35
+ let entries: string[];
36
+ try {
37
+ entries = await fs.readdir(dir);
38
+ } catch {
39
+ return results;
40
+ }
41
+
42
+ if (entries.includes('bruno.json')) {
43
+ const brunoJsonPath = path.join(dir, 'bruno.json');
44
+ let name = path.basename(dir);
45
+ try {
46
+ const raw = await fs.readFile(brunoJsonPath, 'utf-8');
47
+ const parsed = JSON.parse(raw) as { name?: string };
48
+ if (parsed.name) name = parsed.name;
49
+ } catch {
50
+ // use dirname
51
+ }
52
+ // Count .bru files recursively so nested requests are included
53
+ const requestCount = await countBruFilesRecursively(dir);
54
+ results.push({ name, path: dir, request_count: requestCount });
55
+ }
56
+
57
+ for (const entry of entries) {
58
+ const fullPath = path.join(dir, entry);
59
+ try {
60
+ const stat = await fs.stat(fullPath);
61
+ if (stat.isDirectory() && entry !== 'node_modules') {
62
+ const nested = await findCollections(fullPath);
63
+ results.push(...nested);
64
+ }
65
+ } catch {
66
+ // skip
67
+ }
68
+ }
69
+
70
+ return results;
71
+ }
72
+
73
+ export default async function execute(params: Record<string, unknown>): Promise<unknown> {
74
+ const workspacePath = params.workspace_path as string;
75
+
76
+ if (!workspacePath) {
77
+ return { success: false, collections: [], errors: ['workspace_path parameter is required'] };
78
+ }
79
+
80
+ try {
81
+ logger.info(`Listing collections in: ${workspacePath}`);
82
+ const absolutePath = path.resolve(workspacePath);
83
+ let collections = await findCollections(absolutePath);
84
+
85
+ if (params.filter) {
86
+ const filter = (params.filter as string).toLowerCase();
87
+ collections = collections.filter((c) => c.name.toLowerCase().includes(filter));
88
+ }
89
+
90
+ return { success: true, collections };
91
+ } catch (error) {
92
+ logger.error(
93
+ `List collections failed: ${error instanceof Error ? error.message : String(error)}`
94
+ );
95
+ return {
96
+ success: false,
97
+ collections: [],
98
+ errors: [error instanceof Error ? error.message : String(error)],
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,131 @@
1
+ name: bruno_run_collection
2
+ description: Execute a Bruno API collection with configurable environment, data files, and reporting options. Returns structured JSON results with test assertions, metrics, and failure details.
3
+ version: '1.0.0'
4
+ status: approved
5
+
6
+ parameters:
7
+ collection_path:
8
+ type: string
9
+ required: true
10
+ description: Path to Bruno collection file (.bru) or collection directory
11
+
12
+ environment:
13
+ type: string
14
+ required: false
15
+ description: Environment name to use for execution (e.g., dev, staging, prod)
16
+
17
+ env_file:
18
+ type: string
19
+ required: false
20
+ description: Path to environment file (.bru or .json) to override collection environment
21
+
22
+ data_file:
23
+ type: string
24
+ required: false
25
+ description: Path to CSV or JSON data file for data-driven testing
26
+
27
+ iteration_count:
28
+ type: number
29
+ required: false
30
+ default: 1
31
+ description: Number of times to run the collection
32
+
33
+ delay_ms:
34
+ type: number
35
+ required: false
36
+ description: Delay between each request in milliseconds
37
+
38
+ tags:
39
+ type: string
40
+ required: false
41
+ description: Comma-separated tags - only run requests with ALL specified tags
42
+
43
+ exclude_tags:
44
+ type: string
45
+ required: false
46
+ description: Comma-separated tags - skip requests with ANY of these tags
47
+
48
+ tests_only:
49
+ type: boolean
50
+ required: false
51
+ default: false
52
+ description: Only run requests that have tests or active assertions
53
+
54
+ bail_on_failure:
55
+ type: boolean
56
+ required: false
57
+ default: false
58
+ description: Stop execution after first failure
59
+
60
+ parallel:
61
+ type: boolean
62
+ required: false
63
+ default: false
64
+ description: Run requests in parallel order
65
+
66
+ sandbox_mode:
67
+ type: string
68
+ required: false
69
+ default: safe
70
+ description: JavaScript execution mode - 'safe' or 'developer'
71
+
72
+ report_path:
73
+ type: string
74
+ required: false
75
+ description: Custom path for JSON report output (defaults to temp directory)
76
+
77
+ execution:
78
+ type: function
79
+ code: index.ts
80
+ timeout: 120000
81
+
82
+ output_schema:
83
+ type: object
84
+ properties:
85
+ success:
86
+ type: boolean
87
+ summary:
88
+ type: object
89
+ properties:
90
+ total_requests:
91
+ type: number
92
+ passed:
93
+ type: number
94
+ failed:
95
+ type: number
96
+ execution_time_ms:
97
+ type: number
98
+ results:
99
+ type: array
100
+ items:
101
+ type: object
102
+ errors:
103
+ type: array
104
+ items:
105
+ type: string
106
+ report_path:
107
+ type: string
108
+
109
+
110
+ examples:
111
+ - name: "Run entire collection against staging"
112
+ params:
113
+ collection_path: "./collections/my-api"
114
+ environment: "staging"
115
+ report_format: json
116
+ report_path: "./reports/staging-results.json"
117
+
118
+ - name: "Run data-driven tests with bail on failure"
119
+ params:
120
+ collection_path: "./collections/auth-flow.bru"
121
+ data_file: "./data/users.csv"
122
+ bail_on_failure: true
123
+ report_format: html
124
+ report_path: "./reports/auth-flow.html"
125
+
126
+ - name: "Run only tagged tests in parallel"
127
+ params:
128
+ collection_path: "./collections/api"
129
+ tags: "smoke,critical"
130
+ parallel: true
131
+ iteration_count: 3
@@ -0,0 +1,99 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core';
2
+ import { execFileSync } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import { checkBruVersion } from '../bru-utils';
7
+
8
+ const logger = getGlobalMatimoLogger();
9
+
10
+ export default async function execute(params: Record<string, unknown>): Promise<unknown> {
11
+ const collectionPath = params.collection_path as string;
12
+
13
+ if (!collectionPath) {
14
+ return {
15
+ success: false,
16
+ summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
17
+ results: [],
18
+ errors: ['collection_path parameter is required'],
19
+ };
20
+ }
21
+
22
+ checkBruVersion();
23
+
24
+ const absolutePath = path.resolve(collectionPath);
25
+ const reportPath = path.resolve((params.report_path as string | undefined) ?? path.join(os.tmpdir(), `bru-report-${Date.now()}.json`));
26
+
27
+ try {
28
+ logger.info(`Running Bruno collection: ${absolutePath}`);
29
+
30
+ const args: string[] = ['run', '.', '-r', '--reporter-json', reportPath];
31
+
32
+ if (params.environment) args.push('--env', params.environment as string);
33
+ if (params.env_file) args.push('--env-file', params.env_file as string);
34
+ if (params.data_file) args.push('--csv-file-path', params.data_file as string);
35
+ if (params.iteration_count) args.push('--iteration-count', String(params.iteration_count));
36
+ if (params.delay_ms) args.push('--delay', String(params.delay_ms));
37
+ if (params.tags) args.push('--tags', params.tags as string);
38
+ if (params.exclude_tags) args.push('--exclude-tags', params.exclude_tags as string);
39
+ if (params.tests_only === true) args.push('--tests-only');
40
+ if (params.bail_on_failure === true) args.push('--bail');
41
+ if (params.parallel === true) args.push('--parallel');
42
+ args.push('--sandbox', (params.sandbox_mode as string) || 'safe');
43
+
44
+ logger.debug(`Executing: bru ${args.join(' ')}`);
45
+
46
+ let exitCode = 0;
47
+ try {
48
+ execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe', cwd: absolutePath });
49
+ } catch (execError) {
50
+ // bru exits non-zero when tests fail — that's OK, we still read the report
51
+ exitCode = 1;
52
+ logger.warn(`bru run exited with non-zero status: ${execError instanceof Error ? execError.message : String(execError)}`);
53
+ }
54
+
55
+ // Read JSON report written by --reporter-json
56
+ let reportData: Record<string, unknown> = {};
57
+ try {
58
+ const raw = await fs.readFile(reportPath, 'utf-8');
59
+ reportData = JSON.parse(raw) as Record<string, unknown>;
60
+ } catch {
61
+ logger.warn('Could not read/parse JSON report');
62
+ }
63
+
64
+ // Map Bruno report fields to schema-declared keys
65
+ const summaryRaw = (reportData.summary as Record<string, number> | undefined) ?? {};
66
+ const totalRequests = (summaryRaw.totalRequests ?? 0) as number;
67
+ const passed = (summaryRaw.passedRequests ?? 0) as number;
68
+ const failed = (summaryRaw.failedRequests ?? 0) as number;
69
+ const executionTimeMs = (summaryRaw.totalTime ?? 0) as number;
70
+
71
+ const rawResults = (reportData.results as unknown[]) ?? [];
72
+ const results = rawResults.map((r) => {
73
+ const req = r as Record<string, unknown>;
74
+ return {
75
+ name: req.suiteName ?? req.name ?? 'unknown',
76
+ success: req.status === 'pass' || req.passed === true,
77
+ status: (req.response as Record<string, unknown> | undefined)?.status ?? 0,
78
+ };
79
+ });
80
+
81
+ return {
82
+ success: exitCode === 0,
83
+ summary: { total_requests: totalRequests, passed, failed, execution_time_ms: executionTimeMs },
84
+ results,
85
+ report_path: reportPath,
86
+ errors: [],
87
+ };
88
+ } catch (error) {
89
+ logger.error(
90
+ `Collection execution failed: ${error instanceof Error ? error.message : String(error)}`
91
+ );
92
+ return {
93
+ success: false,
94
+ summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
95
+ results: [],
96
+ errors: [error instanceof Error ? error.message : String(error)],
97
+ };
98
+ }
99
+ }