@orygn/opa-mcp 0.1.14 → 0.1.16

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 (44) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +3 -2
  3. package/dist/constants.d.ts +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/lib/opa-cli.d.ts +23 -0
  6. package/dist/lib/opa-cli.d.ts.map +1 -1
  7. package/dist/lib/opa-cli.js +8 -0
  8. package/dist/lib/opa-cli.js.map +1 -1
  9. package/dist/lib/rego-ast-walker.d.ts.map +1 -1
  10. package/dist/lib/rego-ast-walker.js +15 -0
  11. package/dist/lib/rego-ast-walker.js.map +1 -1
  12. package/dist/server.js +1 -1
  13. package/dist/server.js.map +1 -1
  14. package/dist/tools/authoring/format.d.ts.map +1 -1
  15. package/dist/tools/authoring/format.js +55 -5
  16. package/dist/tools/authoring/format.js.map +1 -1
  17. package/dist/tools/evaluation/index.d.ts.map +1 -1
  18. package/dist/tools/evaluation/index.js +2 -0
  19. package/dist/tools/evaluation/index.js.map +1 -1
  20. package/dist/tools/evaluation/test-multiroot.d.ts +37 -0
  21. package/dist/tools/evaluation/test-multiroot.d.ts.map +1 -0
  22. package/dist/tools/evaluation/test-multiroot.js +454 -0
  23. package/dist/tools/evaluation/test-multiroot.js.map +1 -0
  24. package/dist/tools/evaluation/test.d.ts +10 -1
  25. package/dist/tools/evaluation/test.d.ts.map +1 -1
  26. package/dist/tools/evaluation/test.js +47 -5
  27. package/dist/tools/evaluation/test.js.map +1 -1
  28. package/dist/tools/helpers/generate-test-skeleton.d.ts +2 -0
  29. package/dist/tools/helpers/generate-test-skeleton.d.ts.map +1 -1
  30. package/dist/tools/helpers/generate-test-skeleton.js +90 -14
  31. package/dist/tools/helpers/generate-test-skeleton.js.map +1 -1
  32. package/dist/tools/helpers/index.d.ts.map +1 -1
  33. package/dist/tools/helpers/index.js +2 -0
  34. package/dist/tools/helpers/index.js.map +1 -1
  35. package/dist/tools/helpers/playground-share.d.ts +25 -0
  36. package/dist/tools/helpers/playground-share.d.ts.map +1 -0
  37. package/dist/tools/helpers/playground-share.js +117 -0
  38. package/dist/tools/helpers/playground-share.js.map +1 -0
  39. package/dist/tools/index.d.ts +5 -3
  40. package/dist/tools/index.d.ts.map +1 -1
  41. package/dist/tools/index.js.map +1 -1
  42. package/dist/types.d.ts +1 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,454 @@
1
+ /**
2
+ * `rego_test_multiroot` -- run Rego tests across multiple independent package roots.
3
+ *
4
+ * OPA auto-recurses into subdirectories when given a directory path, which
5
+ * causes package-conflict errors in repos with multiple independent module
6
+ * namespaces (OPA issue #4724). This tool runs `opa test` once per root and
7
+ * aggregates the results, solving the problem at the MCP layer.
8
+ *
9
+ * Two modes:
10
+ * explicit -- caller supplies the root list; each root can carry per-root
11
+ * `include` paths (e.g., shared libraries) and an optional name.
12
+ * scan -- auto-discovers leaf test roots using the leaf rule: a directory
13
+ * is a root only if it directly contains `*_test.rego` files AND
14
+ * none of its eligible subdirectories do. Prevents OPA's automatic
15
+ * recursion from double-running descendant roots.
16
+ */
17
+ import * as fs from 'node:fs/promises';
18
+ import { join, sep } from 'node:path';
19
+ import { z } from 'zod';
20
+ import { OpaCli } from '../../lib/opa-cli.js';
21
+ import { err, ok } from '../../lib/errors.js';
22
+ import { tryParseJson, validatePaths, withToolEnvelope } from '../../lib/tool-helpers.js';
23
+ const ALWAYS_IGNORE = new Set([
24
+ '.git',
25
+ 'node_modules',
26
+ 'vendor',
27
+ '.next',
28
+ 'dist',
29
+ 'build',
30
+ 'target',
31
+ '__pycache__',
32
+ '.tox',
33
+ 'coverage',
34
+ ]);
35
+ // ─── Input schema ────────────────────────────────────────────────────────────
36
+ const RootEntrySchema = z.object({
37
+ path: z.string().describe('Absolute or allowed-relative path to the test root directory.'),
38
+ include: z
39
+ .array(z.string())
40
+ .optional()
41
+ .describe("Extra paths to add to this root's `opa test` invocation (e.g., shared library directories). These paths are passed after the root path so OPA can resolve imports."),
42
+ name: z.string().optional().describe('Human-readable label for this root (appears in output).'),
43
+ });
44
+ const RegoTestMultirootInput = {
45
+ roots: z
46
+ .array(RootEntrySchema)
47
+ .min(1)
48
+ .optional()
49
+ .describe('Explicit list of test root directories. Use when roots are known upfront or when scan mode cannot determine the correct roots. Mutually exclusive with `scanDir`.'),
50
+ scanDir: z
51
+ .string()
52
+ .optional()
53
+ .describe('Top-level directory to scan for test roots. Uses the leaf rule: a directory is a root only if it directly contains `*_test.rego` files and none of its eligible subdirectories do. Mutually exclusive with `roots`.'),
54
+ sharedPaths: z
55
+ .array(z.string())
56
+ .optional()
57
+ .describe("Paths added to every root's `opa test` invocation and excluded from auto-discovery. Use for shared library directories that all roots import from."),
58
+ maxDepth: z
59
+ .number()
60
+ .int()
61
+ .min(1)
62
+ .max(20)
63
+ .optional()
64
+ .describe('Maximum directory depth to scan. Default: 10. Only used with `scanDir`.'),
65
+ maxRoots: z
66
+ .number()
67
+ .int()
68
+ .min(1)
69
+ .max(200)
70
+ .optional()
71
+ .describe('Maximum number of test roots allowed. Returns INVALID_INPUT if scan finds more. Default: 50. Only used with `scanDir`.'),
72
+ ignorePatterns: z
73
+ .array(z.string())
74
+ .optional()
75
+ .describe('Additional directory name patterns to skip during scan (e.g., ["vendor", "*.generated"]). Supports `*` wildcards. Only used with `scanDir`.'),
76
+ verbose: z.boolean().optional().describe('Emit per-test pass/fail details for each root.'),
77
+ coverage: z
78
+ .boolean()
79
+ .optional()
80
+ .describe('Include per-line coverage data per root. Switches output to coverage-report mode: test record counts are not available, but `coverage`, `coveragePct`, and `overallCoveragePct` fields are populated.'),
81
+ runPattern: z
82
+ .string()
83
+ .optional()
84
+ .describe('Run only tests whose names match this regular expression (passed as `--run` to each root).'),
85
+ threshold: z
86
+ .number()
87
+ .min(0)
88
+ .max(100)
89
+ .optional()
90
+ .describe('Minimum coverage percentage required per root (0-100). Roots below threshold have `thresholdMet: false` in their result. Implicitly enables coverage-report output mode.'),
91
+ varValues: z
92
+ .boolean()
93
+ .optional()
94
+ .describe('Include local variable bindings in trace output (`--var-values`). Only useful with `verbose: true`.'),
95
+ };
96
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
97
+ function matchesIgnorePattern(name, patterns) {
98
+ return patterns.some((p) => {
99
+ if (p === name)
100
+ return true;
101
+ if (p.includes('*')) {
102
+ const escaped = p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/\\\\]*');
103
+ return new RegExp(`^${escaped}$`).test(name);
104
+ }
105
+ return false;
106
+ });
107
+ }
108
+ async function walk(dir, depth, opts, state) {
109
+ if (state.tooMany)
110
+ return false;
111
+ // Symlink escape guard: realpath the dir and confirm it stays under scanDirReal.
112
+ let realDir;
113
+ try {
114
+ realDir = await fs.realpath(dir);
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ if (realDir !== opts.scanDirReal && !realDir.startsWith(opts.scanDirReal + sep)) {
120
+ return false;
121
+ }
122
+ // Skip if this directory is or lives under a sharedPath (handled separately).
123
+ for (const sp of opts.sharedPathsResolved) {
124
+ if (dir === sp || dir.startsWith(sp + sep)) {
125
+ return false;
126
+ }
127
+ }
128
+ let entries;
129
+ try {
130
+ // Cast: readdir with withFileTypes always returns name-addressable Dirent objects;
131
+ // the as-unknown-as-DirEntry[] cast avoids the Dirent<Buffer> vs Dirent<string>
132
+ // overload ambiguity present in newer @types/node versions.
133
+ entries = await fs.readdir(dir, { withFileTypes: true });
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ const hasDirectTestFiles = entries.some((e) => e.isFile() && e.name.endsWith('_test.rego'));
139
+ const subdirs = entries.filter((e) => {
140
+ if (!e.isDirectory())
141
+ return false;
142
+ if (e.name.startsWith('.'))
143
+ return false;
144
+ if (ALWAYS_IGNORE.has(e.name))
145
+ return false;
146
+ if (matchesIgnorePattern(e.name, opts.ignorePatterns))
147
+ return false;
148
+ return true;
149
+ });
150
+ let descendantsHaveTestFiles = false;
151
+ if (depth < opts.maxDepth) {
152
+ for (const subdir of subdirs) {
153
+ if (state.tooMany)
154
+ break;
155
+ const subdirPath = join(dir, subdir.name);
156
+ const subHas = await walk(subdirPath, depth + 1, opts, state);
157
+ if (subHas)
158
+ descendantsHaveTestFiles = true;
159
+ }
160
+ }
161
+ if (hasDirectTestFiles) {
162
+ if (!descendantsHaveTestFiles) {
163
+ // Leaf rule: has test files directly; no eligible descendant has test files.
164
+ if (state.roots.length >= opts.maxRoots) {
165
+ state.tooMany = true;
166
+ return true;
167
+ }
168
+ state.roots.push(dir);
169
+ }
170
+ else {
171
+ // Ancestor: has test files alongside descendant test dirs. Running `opa test`
172
+ // from here would double-execute descendant tests -- record as skipped.
173
+ state.ancestorSkipped.push(dir);
174
+ }
175
+ }
176
+ return hasDirectTestFiles || descendantsHaveTestFiles;
177
+ }
178
+ async function discoverLeafTestRoots(scanDir, opts) {
179
+ let scanDirReal;
180
+ try {
181
+ scanDirReal = await fs.realpath(scanDir);
182
+ }
183
+ catch {
184
+ scanDirReal = scanDir;
185
+ }
186
+ const state = { roots: [], ancestorSkipped: [], tooMany: false };
187
+ await walk(scanDir, 0, { ...opts, scanDirReal }, state);
188
+ return { roots: state.roots, tooMany: state.tooMany, ancestorSkipped: state.ancestorSkipped };
189
+ }
190
+ function processRootOutput(result, coverageMode, threshold) {
191
+ if (coverageMode) {
192
+ if (result.exitCode === 0) {
193
+ const coverageData = tryParseJson(result.stdout);
194
+ return {
195
+ passed: 0,
196
+ failed: 0,
197
+ skipped: 0,
198
+ total: 0,
199
+ results: [],
200
+ coverage: coverageData,
201
+ coveragePct: coverageData?.coverage,
202
+ thresholdMet: threshold !== undefined ? true : undefined,
203
+ };
204
+ }
205
+ const stderrTrimmed = result.stderr.trim();
206
+ const thresholdMatch = /got\s+([\d.]+)\s+instead\s+of\s+([\d.]+)/i.exec(stderrTrimmed);
207
+ if (thresholdMatch) {
208
+ const actualCoverage = parseFloat(thresholdMatch[1]);
209
+ const requiredThreshold = parseFloat(thresholdMatch[2]);
210
+ return {
211
+ passed: 0,
212
+ failed: 0,
213
+ skipped: 0,
214
+ total: 0,
215
+ results: [],
216
+ coveragePct: actualCoverage,
217
+ thresholdMet: false,
218
+ error: {
219
+ code: 'COVERAGE_BELOW_THRESHOLD',
220
+ message: stderrTrimmed,
221
+ hint: `Increase test coverage to at least ${requiredThreshold}%. Currently at ${actualCoverage}%.`,
222
+ },
223
+ };
224
+ }
225
+ return {
226
+ passed: 0,
227
+ failed: 0,
228
+ skipped: 0,
229
+ total: 0,
230
+ results: [],
231
+ error: {
232
+ code: 'EVAL_ERROR',
233
+ message: stderrTrimmed || 'One or more tests failed.',
234
+ hint: 'Fix failing tests then re-run. Use verbose: true for trace output.',
235
+ },
236
+ };
237
+ }
238
+ // Normal mode -- parse test record array from stdout.
239
+ let records = [];
240
+ const arrayParsed = tryParseJson(result.stdout);
241
+ if (Array.isArray(arrayParsed)) {
242
+ records = arrayParsed;
243
+ }
244
+ else {
245
+ for (const line of result.stdout.split(/\r?\n/)) {
246
+ const trimmed = line.trim();
247
+ if (trimmed.length === 0)
248
+ continue;
249
+ const parsed = tryParseJson(trimmed);
250
+ if (parsed)
251
+ records.push(parsed);
252
+ }
253
+ }
254
+ if (records.length === 0) {
255
+ if (result.exitCode === 0) {
256
+ return {
257
+ passed: 0,
258
+ failed: 0,
259
+ skipped: 0,
260
+ total: 0,
261
+ results: [],
262
+ error: {
263
+ code: 'NO_TESTS_FOUND',
264
+ message: 'opa test did not discover any test rules in the provided paths.',
265
+ hint: 'Tests live in *_test.rego files with rules named test_*.',
266
+ },
267
+ };
268
+ }
269
+ // Non-zero exit with no records: package conflict, import error, parse error, etc.
270
+ return {
271
+ passed: 0,
272
+ failed: 0,
273
+ skipped: 0,
274
+ total: 0,
275
+ results: [],
276
+ error: {
277
+ code: 'EVAL_ERROR',
278
+ message: result.stderr.trim() || `opa test exited with code ${result.exitCode}.`,
279
+ hint: 'Check for package conflicts, import errors, or syntax errors in this root.',
280
+ },
281
+ };
282
+ }
283
+ const failed = records.filter((r) => r.fail).length;
284
+ const skipped = records.filter((r) => r.skip).length;
285
+ const passed = records.length - failed - skipped;
286
+ return { passed, failed, skipped, total: records.length, results: records };
287
+ }
288
+ function computeOverallCoveragePct(roots) {
289
+ const values = roots.filter((r) => r.coveragePct !== undefined).map((r) => r.coveragePct);
290
+ if (values.length === 0)
291
+ return undefined;
292
+ const mean = values.reduce((s, v) => s + v, 0) / values.length;
293
+ return Math.round(mean * 100) / 100;
294
+ }
295
+ // ─── Registration ─────────────────────────────────────────────────────────────
296
+ export function registerRegoTestMultiroot(server, config) {
297
+ const opa = new OpaCli(config);
298
+ server.registerTool('rego_test_multiroot', {
299
+ title: 'Run Rego tests across multiple roots',
300
+ description: "Run `opa test` once per root and aggregate results. Solves the package-conflict problem that occurs when `opa test .` is run on a repo with multiple independent package namespaces (OPA issue #4724). Two modes: `explicit` (supply root list with optional per-root `include` paths for shared libraries) and `scan` (auto-discover leaf test roots using the leaf rule -- a directory is a root only if it directly contains `*_test.rego` files and none of its eligible subdirectories do, preventing OPA's automatic recursion from double-running tests). Use `sharedPaths` in scan mode to add shared library directories to every root's invocation without including them in discovery. Coverage and threshold work per-root; `overallCoveragePct` is the mean across roots that have coverage data.",
301
+ inputSchema: RegoTestMultirootInput,
302
+ annotations: {
303
+ readOnlyHint: true,
304
+ destructiveHint: false,
305
+ idempotentHint: true,
306
+ openWorldHint: false,
307
+ },
308
+ }, async ({ roots, scanDir, sharedPaths, maxDepth, maxRoots, ignorePatterns, verbose, coverage, runPattern, threshold, varValues, }, { signal }) => {
309
+ return withToolEnvelope(config, async () => {
310
+ // Exactly one of roots or scanDir is required.
311
+ const hasRoots = roots !== undefined && roots.length > 0;
312
+ const hasScanDir = scanDir !== undefined && scanDir.length > 0;
313
+ if (!hasRoots && !hasScanDir) {
314
+ return err('INVALID_INPUT', 'rego_test_multiroot requires either `roots` or `scanDir`.', {
315
+ hint: 'Provide an explicit root list via `roots` or a top-level scan directory via `scanDir`.',
316
+ });
317
+ }
318
+ if (hasRoots && hasScanDir) {
319
+ return err('INVALID_INPUT', 'rego_test_multiroot accepts either `roots` or `scanDir`, not both.');
320
+ }
321
+ const coverageMode = coverage === true || threshold !== undefined;
322
+ const warnings = [];
323
+ let resolvedRoots;
324
+ let mode;
325
+ let ancestorSkippedPaths;
326
+ if (hasRoots) {
327
+ // Explicit mode: validate each root path and its include paths.
328
+ mode = 'explicit';
329
+ resolvedRoots = [];
330
+ for (const root of roots) {
331
+ const pathValidation = validatePaths([root.path], config, { mustExist: true });
332
+ if (!pathValidation.ok)
333
+ return pathValidation.error;
334
+ const resolvedPath = pathValidation.resolved[0];
335
+ const resolvedIncludes = [];
336
+ if (root.include?.length) {
337
+ const includeValidation = validatePaths(root.include, config, { mustExist: true });
338
+ if (!includeValidation.ok)
339
+ return includeValidation.error;
340
+ resolvedIncludes.push(...includeValidation.resolved);
341
+ }
342
+ resolvedRoots.push({ path: resolvedPath, name: root.name, include: resolvedIncludes });
343
+ }
344
+ }
345
+ else {
346
+ // Scan mode: validate scanDir and sharedPaths, then discover leaf roots.
347
+ mode = 'scan';
348
+ const scanDirValidation = validatePaths([scanDir], config, { mustExist: true });
349
+ if (!scanDirValidation.ok)
350
+ return scanDirValidation.error;
351
+ const resolvedScanDir = scanDirValidation.resolved[0];
352
+ const resolvedSharedPaths = [];
353
+ if (sharedPaths?.length) {
354
+ const spValidation = validatePaths(sharedPaths, config, { mustExist: true });
355
+ if (!spValidation.ok)
356
+ return spValidation.error;
357
+ resolvedSharedPaths.push(...spValidation.resolved);
358
+ }
359
+ const effectiveMaxRoots = maxRoots ?? 50;
360
+ const discovery = await discoverLeafTestRoots(resolvedScanDir, {
361
+ maxDepth: maxDepth ?? 10,
362
+ maxRoots: effectiveMaxRoots,
363
+ ignorePatterns: ignorePatterns ?? [],
364
+ sharedPathsResolved: resolvedSharedPaths,
365
+ });
366
+ if (discovery.tooMany) {
367
+ return err('INVALID_INPUT', `Scan found more than ${effectiveMaxRoots} test roots in ${scanDir}. Narrow the scan with a more specific scanDir, sharedPaths, or ignorePatterns, or raise maxRoots.`, { hint: 'Use explicit roots mode to enumerate roots manually.' });
368
+ }
369
+ if (discovery.roots.length === 0) {
370
+ return err('NO_TESTS_FOUND', `No test roots found under ${scanDir}. Ensure *_test.rego files exist.`, {
371
+ hint: 'Tests live in *_test.rego files with rules named test_*. If shared libraries hold tests, list them explicitly with `roots`.',
372
+ });
373
+ }
374
+ if (discovery.ancestorSkipped.length > 0) {
375
+ const count = discovery.ancestorSkipped.length;
376
+ warnings.push(`${count} director${count === 1 ? 'y has' : 'ies have'} test files alongside subdirectories that also have test files and were skipped to avoid double-running: ${discovery.ancestorSkipped.join(', ')}. Use explicit roots mode with per-root include paths to run these.`);
377
+ ancestorSkippedPaths = discovery.ancestorSkipped;
378
+ }
379
+ resolvedRoots = discovery.roots.map((r) => ({
380
+ path: r,
381
+ include: resolvedSharedPaths,
382
+ }));
383
+ }
384
+ // Sequential per-root execution.
385
+ const rootResults = [];
386
+ let abortedAt;
387
+ for (let i = 0; i < resolvedRoots.length; i++) {
388
+ if (signal?.aborted) {
389
+ abortedAt = i;
390
+ break;
391
+ }
392
+ const root = resolvedRoots[i];
393
+ const paths = [root.path, ...root.include];
394
+ const result = await opa.test({ paths, verbose, coverage: coverageMode, runPattern, varValues, threshold }, signal);
395
+ // Client cancellation: break loop and return partial results with a warning.
396
+ if (result.aborted) {
397
+ abortedAt = i;
398
+ break;
399
+ }
400
+ // Systemic failures (binary missing, timeout) abort the entire run.
401
+ if (result.exitCode === null) {
402
+ return err('OPA_BINARY_NOT_FOUND', `opa binary unreachable: ${result.stderr || 'spawn failed'}`, {
403
+ hint: 'Install OPA (https://www.openpolicyagent.org/docs/latest/) or set OPA_BINARY to the absolute path of the binary.',
404
+ });
405
+ }
406
+ if (result.timedOut) {
407
+ return err('TIMEOUT', 'opa subprocess exceeded the configured timeout (OPA_MCP_TIMEOUT_MS).', { details: { durationMs: result.durationMs } });
408
+ }
409
+ const outcome = processRootOutput(result, coverageMode, threshold);
410
+ const rootResult = {
411
+ path: root.path,
412
+ ...outcome,
413
+ };
414
+ if (root.name !== undefined)
415
+ rootResult.name = root.name;
416
+ if (root.include.length > 0)
417
+ rootResult.include = root.include;
418
+ rootResults.push(rootResult);
419
+ }
420
+ if (abortedAt !== undefined) {
421
+ warnings.push(`Run was cancelled after ${rootResults.length} of ${resolvedRoots.length} root${resolvedRoots.length === 1 ? '' : 's'}.`);
422
+ }
423
+ // Aggregate totals across all roots.
424
+ const totalPassed = rootResults.reduce((s, r) => s + (r.error ? 0 : r.passed), 0);
425
+ const totalFailed = rootResults.reduce((s, r) => s + (r.error ? 0 : r.failed), 0);
426
+ const totalSkipped = rootResults.reduce((s, r) => s + (r.error ? 0 : r.skipped), 0);
427
+ const totalTests = rootResults.reduce((s, r) => s + (r.error ? 0 : r.total), 0);
428
+ const rootsWithErrors = rootResults.filter((r) => r.error !== undefined).length;
429
+ const rootsWithFailures = rootResults.filter((r) => r.error === undefined && r.failed > 0).length;
430
+ const output = {
431
+ mode,
432
+ roots: rootResults,
433
+ totalPassed,
434
+ totalFailed,
435
+ totalSkipped,
436
+ totalTests,
437
+ rootsRun: rootResults.length,
438
+ rootsWithErrors,
439
+ rootsWithFailures,
440
+ };
441
+ if (coverageMode) {
442
+ const overallCoveragePct = computeOverallCoveragePct(rootResults);
443
+ if (overallCoveragePct !== undefined) {
444
+ output.overallCoveragePct = overallCoveragePct;
445
+ }
446
+ }
447
+ if (ancestorSkippedPaths !== undefined && ancestorSkippedPaths.length > 0) {
448
+ output.ancestorSkipped = ancestorSkippedPaths;
449
+ }
450
+ return ok(output, warnings.length > 0 ? warnings : undefined);
451
+ });
452
+ });
453
+ }
454
+ //# sourceMappingURL=test-multiroot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-multiroot.js","sourceRoot":"","sources":["../../../src/tools/evaluation/test-multiroot.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { Config } from '../../config.js';
3
- interface TestRecord {
3
+ export interface TestRecord {
4
4
  location?: {
5
5
  file?: string;
6
6
  row?: number;
@@ -57,6 +57,15 @@ export interface RegoTestOutput {
57
57
  coveragePct?: number;
58
58
  /** Present when `threshold` is set and the threshold was met. */
59
59
  thresholdMet?: boolean;
60
+ /**
61
+ * Groups of parameterized test cases. When OPA runs `test_X[case]`-style
62
+ * parametrized rules, each case appears as a separate record like
63
+ * `test_X[{"role":"admin"}]`. This field maps the base test name (e.g.
64
+ * `test_X`) to all of its case records, making it easy to see which specific
65
+ * inputs triggered a failure. Only present when at least one parametrized
66
+ * group is detected.
67
+ */
68
+ parameterizedGroups?: Record<string, TestRecord[]>;
60
69
  }
61
70
  export declare function registerRegoTest(server: McpServer, config: Config): void;
62
71
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/tools/evaluation/test.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AA4C9C,UAAU,UAAU;IAClB,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,aAAa;IACrB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,GAAG,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,aAAa,EAAE,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAiDxE"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/tools/evaluation/test.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAsE9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,aAAa;IACrB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,GAAG,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,aAAa,EAAE,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;CACpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAuExE"}
@@ -34,12 +34,30 @@ const RegoTestInput = {
34
34
  .boolean()
35
35
  .optional()
36
36
  .describe('Include local variable bindings in trace output (`--var-values`). When a table-driven test using `every tc in cases { ... }` fails, the trace shows which `tc` triggered the failure. Has no effect unless `verbose: true` is also set (OPA only emits trace entries in verbose mode).'),
37
+ ignorePatterns: z
38
+ .array(z.string())
39
+ .optional()
40
+ .describe('Glob patterns for files to exclude from the test run (`--ignore <pattern>`). Pass one pattern per array element. Useful for excluding generated or fixture files that contain no tests (e.g. `["*_generated.rego", "fixtures/**"]`).'),
41
+ bundle: z
42
+ .boolean()
43
+ .optional()
44
+ .describe('Load paths as OPA bundle roots (`--bundle`). Required when testing policies structured as bundles with a `manifest.json` at the root. Not needed for plain policy directories.'),
45
+ count: z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .optional()
50
+ .describe('Number of times to repeat each test (`--count N`). Default is 1. Useful for measuring repeatability or catching flaky tests under load.'),
51
+ timeout: z
52
+ .string()
53
+ .optional()
54
+ .describe('Per-test timeout as a Go duration string, e.g. `"30s"` or `"2m"` (`--timeout`). OPA\'s default is 5s. Increase for tests that load large policy sets or call slow built-ins.'),
37
55
  };
38
56
  export function registerRegoTest(server, config) {
39
57
  const opa = new OpaCli(config);
40
58
  server.registerTool('rego_test', {
41
59
  title: 'Run Rego tests',
42
- description: 'Run Rego unit tests with `opa test`. Returns aggregate pass/fail counts plus per-test records. Tests live in `*_test.rego` files; rule names beginning with `test_` are picked up. Use `runPattern` to filter by name regex. Use `threshold` to gate on a minimum coverage percentage (returns COVERAGE_BELOW_THRESHOLD on failure). Use `varValues: true` with `verbose: true` to include local variable bindings in the trace -- essential for debugging table-driven tests written with `every tc in cases { ... }` to identify which case caused a failure. Note: enabling `coverage` or `threshold` switches OPA to coverage-report output mode -- per-test counts are unavailable but `coverage` and `coveragePct` fields are populated.',
60
+ description: "Run Rego unit tests with `opa test`. Returns aggregate pass/fail counts plus per-test records. Tests live in `*_test.rego` files; rule names beginning with `test_` are picked up automatically. Use `runPattern` to filter by name regex; when no tests match, the error hint includes the pattern you supplied. Use `threshold` to gate on minimum coverage (returns COVERAGE_BELOW_THRESHOLD on failure). Use `varValues: true` with `verbose: true` to include local variable bindings in the trace -- essential for debugging table-driven tests written with `every tc in cases { ... }` to identify which case caused a failure. When tests use the `test_X[case]` parametrized form, the output includes `parameterizedGroups` mapping each base test name to its case records. Use `ignorePatterns` to exclude generated or fixture files. Use `bundle: true` when testing bundle-structured policy directories. Use `timeout` to raise the per-test limit beyond OPA's default 5s. Note: enabling `coverage` or `threshold` switches OPA to coverage-report output mode -- per-test counts are unavailable but `coverage` and `coveragePct` fields are populated.",
43
61
  inputSchema: RegoTestInput,
44
62
  annotations: {
45
63
  readOnlyHint: true,
@@ -47,11 +65,14 @@ export function registerRegoTest(server, config) {
47
65
  idempotentHint: true,
48
66
  openWorldHint: false,
49
67
  },
50
- }, async ({ paths, verbose, coverage, runPattern, threshold, varValues }, { signal }) => {
68
+ }, async ({ paths, verbose, coverage, runPattern, threshold, varValues, ignorePatterns, bundle, count, timeout, }, { signal }) => {
51
69
  return withToolEnvelope(config, async () => {
52
70
  const validation = validatePaths(paths, config, { mustExist: true });
53
71
  if (!validation.ok)
54
72
  return validation.error;
73
+ if (count !== undefined && count < 1) {
74
+ return err('INVALID_INPUT', '`count` must be at least 1.');
75
+ }
55
76
  // When coverage or threshold is set, OPA changes its output format:
56
77
  // stdout becomes a coverage JSON object instead of a test-record array.
57
78
  const coverageMode = coverage === true || threshold !== undefined;
@@ -62,6 +83,10 @@ export function registerRegoTest(server, config) {
62
83
  runPattern,
63
84
  varValues,
64
85
  threshold,
86
+ ignorePatterns,
87
+ bundle,
88
+ count,
89
+ timeout,
65
90
  }, signal);
66
91
  const subprocessFailure = mapSubprocessFailure(result, 'opa');
67
92
  if (subprocessFailure)
@@ -69,7 +94,7 @@ export function registerRegoTest(server, config) {
69
94
  if (coverageMode) {
70
95
  return handleCoverageMode(result.stdout, result.stderr, result.exitCode, threshold);
71
96
  }
72
- return handleTestRecordsMode(result.stdout, result.exitCode);
97
+ return handleTestRecordsMode(result.stdout, result.exitCode, runPattern);
73
98
  });
74
99
  });
75
100
  }
@@ -129,7 +154,7 @@ function handleCoverageMode(stdout, stderr, exitCode, threshold) {
129
154
  * 0 -- all tests pass
130
155
  * 2 -- one or more tests failed (failed records still appear in the JSON array)
131
156
  */
132
- function handleTestRecordsMode(stdout, exitCode) {
157
+ function handleTestRecordsMode(stdout, exitCode, runPattern) {
133
158
  let records = [];
134
159
  // OPA emits a JSON array. Older versions may emit NDJSON (one object per line).
135
160
  const arrayParsed = tryParseJson(stdout);
@@ -147,8 +172,11 @@ function handleTestRecordsMode(stdout, exitCode) {
147
172
  }
148
173
  }
149
174
  if (records.length === 0 && exitCode === 0) {
175
+ const hint = runPattern
176
+ ? `No tests matched the pattern "${runPattern}". Verify the regex against your test rule names. Tests live in *_test.rego files with rules named test_*.`
177
+ : 'Tests live in *_test.rego files with rules named test_*.';
150
178
  return err('NO_TESTS_FOUND', 'opa test did not discover any test rules in the provided paths.', {
151
- hint: 'Tests live in *_test.rego files with rules named test_*.',
179
+ hint,
152
180
  });
153
181
  }
154
182
  // OPA does NOT emit `pass: true` for passing tests; only `fail: true` for
@@ -156,12 +184,26 @@ function handleTestRecordsMode(stdout, exitCode) {
156
184
  const failed = records.filter((r) => r.fail).length;
157
185
  const skipped = records.filter((r) => r.skip).length;
158
186
  const passed = records.length - failed - skipped;
187
+ // Group parametrized test cases. OPA names them like `test_X[{"key":"val"}]`;
188
+ // extract the base name and bucket records for at-a-glance failure analysis.
189
+ const parameterizedGroups = {};
190
+ for (const record of records) {
191
+ if (record.name) {
192
+ const match = /^(test_[a-zA-Z0-9_]+)\[/.exec(record.name);
193
+ if (match) {
194
+ const baseName = match[1];
195
+ (parameterizedGroups[baseName] ??= []).push(record);
196
+ }
197
+ }
198
+ }
199
+ const hasGroups = Object.keys(parameterizedGroups).length > 0;
159
200
  return ok({
160
201
  passed,
161
202
  failed,
162
203
  skipped,
163
204
  total: records.length,
164
205
  results: records,
206
+ ...(hasGroups ? { parameterizedGroups } : {}),
165
207
  });
166
208
  }
167
209
  //# sourceMappingURL=test.js.map