xtrm-tools 2.1.5 → 2.1.6

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.
@@ -43,7 +43,16 @@ function deny(reason) {
43
43
  process.exit(2);
44
44
  }
45
45
 
46
- const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
46
+ const WRITE_TOOLS = new Set([
47
+ 'Edit',
48
+ 'Write',
49
+ 'MultiEdit',
50
+ 'NotebookEdit',
51
+ 'mcp__serena__rename_symbol',
52
+ 'mcp__serena__replace_symbol_body',
53
+ 'mcp__serena__insert_after_symbol',
54
+ 'mcp__serena__insert_before_symbol',
55
+ ]);
47
56
 
48
57
  if (WRITE_TOOLS.has(tool)) {
49
58
  deny(
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "workspaces": [
8
+ "cli"
9
+ ],
7
10
  "bin": {
8
11
  "xtrm": "cli/dist/index.cjs"
9
12
  },
@@ -29,10 +32,12 @@
29
32
  "url": "https://github.com/Jaggerxtrm/xtrm-tools/issues"
30
33
  },
31
34
  "scripts": {
32
- "build": "npm run build --prefix cli",
35
+ "build": "npm run build --workspace cli",
36
+ "typecheck": "npm run typecheck --workspace cli",
33
37
  "start": "node cli/dist/index.cjs",
34
38
  "lint": "echo 'No linting configured'",
35
- "test": "echo 'No tests configured'"
39
+ "test": "npm test --workspace cli",
40
+ "prepublishOnly": "npm run build"
36
41
  },
37
42
  "engines": {
38
43
  "node": ">=18.0.0"
@@ -218,9 +218,22 @@ def parse_json_input() -> dict:
218
218
  sys.exit(1)
219
219
 
220
220
  def extract_file_path(input_data: dict) -> str | None:
221
- """Extract file path from tool input"""
221
+ """Extract file path from tool input, including Serena relative_path."""
222
222
  tool_input = input_data.get('tool_input', {})
223
- return tool_input.get('file_path') or tool_input.get('path')
223
+ file_path = (
224
+ tool_input.get('file_path')
225
+ or tool_input.get('path')
226
+ or tool_input.get('relative_path')
227
+ )
228
+ if not file_path:
229
+ return None
230
+
231
+ # Serena tools pass relative_path relative to the project root.
232
+ if not os.path.isabs(file_path):
233
+ project_root = os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd()
234
+ return str(Path(project_root) / file_path)
235
+
236
+ return file_path
224
237
 
225
238
  def main():
226
239
  """Main entry point"""
@@ -55,6 +55,7 @@ SETTINGS_HOOKS = {
55
55
 
56
56
  MARKER_DOC = "# [jaggers] doc-reminder"
57
57
  MARKER_STALENESS = "# [jaggers] skill-staleness"
58
+ MARKER_CHAIN = "# [jaggers] chain-githooks"
58
59
 
59
60
 
60
61
  def get_project_root() -> Path:
@@ -126,25 +127,54 @@ def install_git_hooks(project_root: Path) -> None:
126
127
  f"\n{MARKER_STALENESS}\nif command -v python3 &>/dev/null && [ -f \"{staleness_script}\" ]; then\n python3 \"{staleness_script}\" || true\nfi\n"),
127
128
  ]
128
129
 
129
- changed = False
130
130
  for hook_path, marker, snippet in snippets:
131
131
  content = hook_path.read_text(encoding="utf-8")
132
132
  if marker not in content:
133
133
  hook_path.write_text(content + snippet, encoding="utf-8")
134
134
  print(f"{GREEN} ✓{NC} {hook_path.relative_to(project_root)}")
135
- changed = True
136
135
  else:
137
136
  print(f"{YELLOW} ○{NC} already installed: {hook_path.name}")
138
137
 
139
- if changed:
140
- git_dir = project_root / ".git" / "hooks"
141
- git_dir.mkdir(parents=True, exist_ok=True)
142
- for src, name in ((pre_commit, "pre-commit"), (pre_push, "pre-push")):
143
- if src.exists():
144
- dest = git_dir / name
145
- shutil.copy2(src, dest)
146
- dest.chmod(0o755)
147
- print(f"{GREEN} ✓{NC} activated in .git/hooks/")
138
+ hooks_path = ""
139
+ try:
140
+ r = subprocess.run(
141
+ ["git", "config", "--get", "core.hooksPath"],
142
+ cwd=project_root,
143
+ capture_output=True,
144
+ text=True,
145
+ timeout=5,
146
+ check=False,
147
+ )
148
+ if r.returncode == 0:
149
+ hooks_path = r.stdout.strip()
150
+ except Exception:
151
+ hooks_path = ""
152
+
153
+ active_hooks_dir = (Path(hooks_path) if Path(hooks_path).is_absolute() else project_root / hooks_path) if hooks_path else (project_root / ".git" / "hooks")
154
+ activation_targets = {project_root / ".git" / "hooks", active_hooks_dir}
155
+
156
+ for hooks_dir in activation_targets:
157
+ hooks_dir.mkdir(parents=True, exist_ok=True)
158
+ for name, source_hook in (("pre-commit", pre_commit), ("pre-push", pre_push)):
159
+ target_hook = hooks_dir / name
160
+ if not target_hook.exists():
161
+ target_hook.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
162
+ target_hook.chmod(0o755)
163
+
164
+ if target_hook.resolve() == source_hook.resolve():
165
+ continue
166
+
167
+ chain_snippet = (
168
+ f"\n{MARKER_CHAIN}\n"
169
+ f"if [ -x \"{source_hook}\" ]; then\n"
170
+ f" \"{source_hook}\" \"$@\"\n"
171
+ "fi\n"
172
+ )
173
+ target_content = target_hook.read_text(encoding="utf-8")
174
+ if MARKER_CHAIN not in target_content:
175
+ target_hook.write_text(target_content + chain_snippet, encoding="utf-8")
176
+
177
+ print(f"{GREEN} ✓{NC} activated in .git/hooks/")
148
178
 
149
179
 
150
180
  def main() -> None:
@@ -0,0 +1,199 @@
1
+ import type { Test, TestResult, AggregatedResult } from '@jest/reporters'
2
+ import type { Config } from '@jest/types'
3
+
4
+ // Create a minimal snapshot object that satisfies the type requirements
5
+ const createSnapshot = (): TestResult['snapshot'] =>
6
+ ({
7
+ added: 0,
8
+ didUpdate: false,
9
+ failure: false,
10
+ filesAdded: 0,
11
+ filesRemoved: 0,
12
+ filesRemovedList: [],
13
+ filesUnmatched: 0,
14
+ filesUpdated: 0,
15
+ matched: 0,
16
+ total: 0,
17
+ unchecked: 0,
18
+ uncheckedKeysByFile: [],
19
+ unmatched: 0,
20
+ updated: 0,
21
+ // Additional properties that might be required by different versions
22
+ fileDeleted: false,
23
+ uncheckedKeys: [],
24
+ }) as TestResult['snapshot']
25
+
26
+ // Create a minimal snapshot summary for AggregatedResult
27
+ const createSnapshotSummary = (): AggregatedResult['snapshot'] =>
28
+ ({
29
+ added: 0,
30
+ didUpdate: false,
31
+ failure: false,
32
+ filesAdded: 0,
33
+ filesRemoved: 0,
34
+ filesRemovedList: [],
35
+ filesUnmatched: 0,
36
+ filesUpdated: 0,
37
+ matched: 0,
38
+ total: 0,
39
+ unchecked: 0,
40
+ uncheckedKeysByFile: [],
41
+ unmatched: 0,
42
+ updated: 0,
43
+ }) as AggregatedResult['snapshot']
44
+
45
+ // Create a minimal Test object
46
+ export function createTest(overrides?: Partial<Test>): Test {
47
+ // For test purposes, we create minimal mock implementations
48
+ const mockContext = {
49
+ config: {} as Config.ProjectConfig,
50
+ hasteFS: {} as never, // Using never since we don't access these properties
51
+ moduleMap: {} as never, // Using never since we don't access these properties
52
+ resolver: {} as never, // Using never since we don't access these properties
53
+ }
54
+
55
+ return {
56
+ context: mockContext,
57
+ duration: 100,
58
+ path: '/test/example.test.ts',
59
+ ...overrides,
60
+ } as Test
61
+ }
62
+
63
+ // Create a minimal TestResult object
64
+ export function createTestResult(overrides?: Partial<TestResult>): TestResult {
65
+ const base: TestResult = {
66
+ leaks: false,
67
+ numFailingTests: 0,
68
+ numPassingTests: 1,
69
+ numPendingTests: 0,
70
+ numTodoTests: 0,
71
+ openHandles: [],
72
+ perfStats: {
73
+ end: 1000,
74
+ runtime: 100,
75
+ slow: false,
76
+ start: 900,
77
+ loadTestEnvironmentEnd: 950,
78
+ loadTestEnvironmentStart: 920,
79
+ setupAfterEnvEnd: 980,
80
+ setupAfterEnvStart: 960,
81
+ setupFilesEnd: 940,
82
+ setupFilesStart: 930,
83
+ },
84
+ skipped: false,
85
+ snapshot: createSnapshot(),
86
+ testExecError: undefined,
87
+ testFilePath: '/test/example.test.ts',
88
+ testResults: [
89
+ {
90
+ ancestorTitles: ['Example Suite'],
91
+ duration: 5,
92
+ failureDetails: [],
93
+ failureMessages: [],
94
+ fullName: 'Example Suite should pass',
95
+ invocations: 1,
96
+ location: undefined,
97
+ numPassingAsserts: 0,
98
+ retryReasons: [],
99
+ status: 'passed',
100
+ title: 'should pass',
101
+ },
102
+ ],
103
+ ...overrides,
104
+ }
105
+
106
+ // If test is failing, update the test results
107
+ if (overrides?.numFailingTests && overrides.numFailingTests > 0) {
108
+ base.testResults = [
109
+ {
110
+ ancestorTitles: ['Example Suite'],
111
+ duration: 5,
112
+ failureDetails: [{}],
113
+ failureMessages: ['expected 2 to be 3'],
114
+ fullName: 'Example Suite should fail',
115
+ invocations: 1,
116
+ location: undefined,
117
+ numPassingAsserts: 0,
118
+ retryReasons: [],
119
+ status: 'failed',
120
+ title: 'should fail',
121
+ },
122
+ ]
123
+ }
124
+
125
+ return base
126
+ }
127
+
128
+ // Create a minimal AggregatedResult object
129
+ export function createAggregatedResult(
130
+ overrides?: Partial<AggregatedResult>
131
+ ): AggregatedResult {
132
+ return {
133
+ numFailedTestSuites: 0,
134
+ numFailedTests: 0,
135
+ numPassedTestSuites: 1,
136
+ numPassedTests: 1,
137
+ numPendingTestSuites: 0,
138
+ numPendingTests: 0,
139
+ numRuntimeErrorTestSuites: 0,
140
+ numTodoTests: 0,
141
+ numTotalTestSuites: 1,
142
+ numTotalTests: 1,
143
+ openHandles: [],
144
+ runExecError: undefined,
145
+ snapshot: createSnapshotSummary(),
146
+ startTime: Date.now(),
147
+ success: true,
148
+ testResults: [],
149
+ wasInterrupted: false,
150
+ ...overrides,
151
+ }
152
+ }
153
+
154
+ export function createUnhandledError(
155
+ overrides: Partial<{ name: string; message: string; stack: string }> = {}
156
+ ): AggregatedResult['runExecError'] {
157
+ return {
158
+ message: overrides.message ?? 'Cannot find module "./helpers"',
159
+ stack:
160
+ overrides.stack ??
161
+ "Error: Cannot find module './helpers' imported from '/src/example.test.ts'",
162
+ ...(overrides.name && { name: overrides.name }),
163
+ // SerializableError might have additional properties but these are the required ones
164
+ }
165
+ }
166
+
167
+ // Create a module error (testExecError) for import failures
168
+ export function createModuleError(
169
+ overrides: Partial<{
170
+ name: string
171
+ message: string
172
+ stack: string
173
+ type: string
174
+ code: string
175
+ }> = {}
176
+ ): TestResult['testExecError'] {
177
+ return {
178
+ message: overrides.message ?? "Cannot find module './non-existent-module'",
179
+ stack:
180
+ overrides.stack ??
181
+ "Error: Cannot find module './non-existent-module'\n at Resolver.resolveModule",
182
+ ...(overrides.name && { name: overrides.name }),
183
+ ...(overrides.type && { type: overrides.type }),
184
+ ...(overrides.code && { code: overrides.code }),
185
+ }
186
+ }
187
+
188
+ // Create a TestResult with module import error
189
+ export function createTestResultWithModuleError(
190
+ overrides?: Partial<TestResult>
191
+ ): TestResult {
192
+ return createTestResult({
193
+ testExecError: createModuleError(),
194
+ testResults: [], // No test results when module fails to load
195
+ numFailingTests: 0,
196
+ numPassingTests: 0,
197
+ ...overrides,
198
+ })
199
+ }
@@ -0,0 +1,302 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { JestReporter } from './JestReporter'
3
+ import {
4
+ FileStorage,
5
+ MemoryStorage,
6
+ Config as TDDConfig,
7
+ DEFAULT_DATA_DIR,
8
+ } from 'tdd-guard'
9
+ import path from 'node:path'
10
+ import {
11
+ createTest,
12
+ createTestResult,
13
+ createAggregatedResult,
14
+ createUnhandledError,
15
+ createModuleError,
16
+ createTestResultWithModuleError,
17
+ } from './JestReporter.test-data'
18
+
19
+ describe('JestReporter', () => {
20
+ let sut: ReturnType<typeof setupJestReporter>
21
+
22
+ beforeEach(() => {
23
+ sut = setupJestReporter()
24
+ })
25
+
26
+ describe('constructor', () => {
27
+ it('uses FileStorage by default', () => {
28
+ const reporter = new JestReporter()
29
+ expect(reporter['storage']).toBeInstanceOf(FileStorage)
30
+ })
31
+
32
+ it('accepts Storage instance in reporterOptions', () => {
33
+ const storage = new MemoryStorage()
34
+ const globalConfig = undefined
35
+ const reporterOptions = { storage }
36
+ const reporter = new JestReporter(globalConfig, reporterOptions)
37
+ expect(reporter['storage']).toBe(storage)
38
+ })
39
+
40
+ it('accepts projectRoot string in reporterOptions', () => {
41
+ const rootPath = '/some/project/root'
42
+ const globalConfig = undefined
43
+ const reporterOptions = { projectRoot: rootPath }
44
+ const reporter = new JestReporter(globalConfig, reporterOptions)
45
+
46
+ // Verify the storage is configured with the correct path
47
+ const fileStorage = reporter['storage'] as FileStorage
48
+ const config = fileStorage['config'] as TDDConfig
49
+ const expectedDataDir = path.join(
50
+ rootPath,
51
+ ...DEFAULT_DATA_DIR.split('/')
52
+ )
53
+ expect(config.dataDir).toBe(expectedDataDir)
54
+ })
55
+ })
56
+
57
+ describe('onTestResult', () => {
58
+ it('collects test results', () => {
59
+ const test = createTest()
60
+ const testResult = createTestResult()
61
+
62
+ sut.reporter.onTestResult(test, testResult)
63
+
64
+ expect(sut.reporter['testModules'].size).toBe(1)
65
+ })
66
+ })
67
+
68
+ describe('onRunComplete', () => {
69
+ it('saves test results to storage', async () => {
70
+ const test = createTest()
71
+ const testResult = createTestResult()
72
+ const aggregatedResult = createAggregatedResult()
73
+
74
+ // Collect test results first
75
+ sut.reporter.onTestResult(test, testResult)
76
+
77
+ // Run complete
78
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
79
+
80
+ // Verify results were saved
81
+ const parsed = await sut.getParsedData()
82
+ expect(parsed).toBeTruthy()
83
+ expect(parsed.testModules).toHaveLength(1)
84
+ })
85
+
86
+ it('includes test case details in output', async () => {
87
+ const test = createTest()
88
+ const testResult = createTestResult()
89
+ const aggregatedResult = createAggregatedResult()
90
+
91
+ // Collect test results
92
+ sut.reporter.onTestResult(test, testResult)
93
+
94
+ // Run complete
95
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
96
+
97
+ // Verify test details are included
98
+ const parsed = await sut.getParsedData()
99
+ const module = parsed.testModules[0]
100
+ expect(module.tests).toHaveLength(1)
101
+ expect(module.tests[0].name).toBe('should pass')
102
+ expect(module.tests[0].fullName).toBe('Example Suite should pass')
103
+ expect(module.tests[0].state).toBe('passed')
104
+ })
105
+
106
+ it('includes error details for failed tests', async () => {
107
+ const test = createTest()
108
+ const failedTestResult = createTestResult({ numFailingTests: 1 })
109
+ const aggregatedResult = createAggregatedResult()
110
+
111
+ // Collect test results
112
+ sut.reporter.onTestResult(test, failedTestResult)
113
+
114
+ // Run complete
115
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
116
+
117
+ // Verify error details are included
118
+ const parsed = await sut.getParsedData()
119
+ const module = parsed.testModules[0]
120
+ const failedTest = module.tests[0]
121
+ expect(failedTest.state).toBe('failed')
122
+ expect(failedTest.errors).toBeDefined()
123
+ expect(failedTest.errors).toHaveLength(1)
124
+ expect(failedTest.errors[0].message).toBe('expected 2 to be 3')
125
+ })
126
+
127
+ it('handles empty test runs', async () => {
128
+ // Run complete without any tests
129
+ await sut.reporter.onRunComplete(new Set(), createAggregatedResult())
130
+
131
+ // Verify empty output
132
+ const parsed = await sut.getParsedData()
133
+ expect(parsed.testModules).toEqual([])
134
+ })
135
+
136
+ it('includes unhandled errors in output', async () => {
137
+ const error = createUnhandledError()
138
+ const aggregatedResult = createAggregatedResult({
139
+ runExecError: error,
140
+ })
141
+
142
+ // Run complete with unhandled error
143
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
144
+
145
+ // Verify unhandled errors are included
146
+ const parsed = await sut.getParsedData()
147
+ expect(parsed.unhandledErrors).toBeDefined()
148
+ expect(parsed.unhandledErrors).toHaveLength(1)
149
+ expect(parsed.unhandledErrors[0].message).toBe(
150
+ 'Cannot find module "./helpers"'
151
+ )
152
+ expect(parsed.unhandledErrors[0].name).toBe('Error')
153
+ expect(parsed.unhandledErrors[0].stack).toContain('imported from')
154
+ })
155
+
156
+ it('includes test run reason when tests pass', async () => {
157
+ const test = createTest()
158
+ const testResult = createTestResult()
159
+ const aggregatedResult = createAggregatedResult({
160
+ success: true,
161
+ numFailedTests: 0,
162
+ })
163
+
164
+ sut.reporter.onTestResult(test, testResult)
165
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
166
+
167
+ const parsed = await sut.getParsedData()
168
+ expect(parsed.reason).toBe('passed')
169
+ })
170
+
171
+ it('handles SerializableError without name property', async () => {
172
+ const aggregatedResult = createAggregatedResult({
173
+ runExecError: {
174
+ message: 'Module not found',
175
+ stack: 'at test.js:1:1',
176
+ },
177
+ })
178
+
179
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
180
+
181
+ const parsed = await sut.getParsedData()
182
+ expect(parsed.unhandledErrors[0].message).toBe('Module not found')
183
+ expect(parsed.unhandledErrors[0].name).toBe('Error')
184
+ expect(parsed.unhandledErrors[0].stack).toBe('at test.js:1:1')
185
+ })
186
+
187
+ it('includes module import errors as failed tests', async () => {
188
+ const test = createTest()
189
+ const testResult = createTestResultWithModuleError()
190
+ const aggregatedResult = createAggregatedResult()
191
+
192
+ sut.reporter.onTestResult(test, testResult)
193
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
194
+
195
+ const parsed = await sut.getParsedData()
196
+ const module = parsed.testModules[0]
197
+ expect(module.tests).toHaveLength(1)
198
+
199
+ const importErrorTest = module.tests[0]
200
+ expect(importErrorTest.name).toBe('Module failed to load (Error)')
201
+ expect(importErrorTest.fullName).toBe('Module failed to load (Error)')
202
+ expect(importErrorTest.state).toBe('failed')
203
+ expect(importErrorTest.errors).toHaveLength(1)
204
+ expect(importErrorTest.errors[0].message).toBe(
205
+ "Cannot find module './non-existent-module'"
206
+ )
207
+ })
208
+
209
+ it('preserves error stack trace from module import errors', async () => {
210
+ const test = createTest()
211
+ const moduleError = createModuleError({
212
+ message: "Cannot find module './helpers'",
213
+ stack:
214
+ "Error: Cannot find module './helpers'\n at Function.Module._resolveFilename",
215
+ name: 'Error',
216
+ })
217
+ const testResult = createTestResultWithModuleError({
218
+ testExecError: moduleError,
219
+ })
220
+ const aggregatedResult = createAggregatedResult()
221
+
222
+ sut.reporter.onTestResult(test, testResult)
223
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
224
+
225
+ const parsed = await sut.getParsedData()
226
+ const importErrorTest = parsed.testModules[0].tests[0]
227
+
228
+ expect(importErrorTest.errors[0].stack).toBe(
229
+ "Error: Cannot find module './helpers'\n at Function.Module._resolveFilename"
230
+ )
231
+ expect(importErrorTest.errors[0].name).toBe('Error')
232
+ })
233
+
234
+ it('uses error type in test name for module import errors', async () => {
235
+ const test = createTest()
236
+ const testResult = createTestResultWithModuleError({
237
+ testExecError: createModuleError({
238
+ message: 'Module parse failed',
239
+ stack: 'SyntaxError: Unexpected token',
240
+ name: 'SyntaxError',
241
+ }),
242
+ })
243
+ const aggregatedResult = createAggregatedResult()
244
+
245
+ sut.reporter.onTestResult(test, testResult)
246
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
247
+
248
+ const parsed = await sut.getParsedData()
249
+ const importErrorTest = parsed.testModules[0].tests[0]
250
+
251
+ expect(importErrorTest.name).toBe('Module failed to load (SyntaxError)')
252
+ expect(importErrorTest.fullName).toBe(
253
+ 'Module failed to load (SyntaxError)'
254
+ )
255
+ })
256
+
257
+ it('handles SerializableError with type field for module import errors', async () => {
258
+ const test = createTest()
259
+ const testResult = createTestResultWithModuleError({
260
+ testExecError: createModuleError({
261
+ message: 'Module error',
262
+ stack: 'at test.js:1',
263
+ type: 'ReferenceError',
264
+ code: 'ERR_MODULE_NOT_FOUND',
265
+ }),
266
+ })
267
+ const aggregatedResult = createAggregatedResult()
268
+
269
+ sut.reporter.onTestResult(test, testResult)
270
+ await sut.reporter.onRunComplete(new Set(), aggregatedResult)
271
+
272
+ const parsed = await sut.getParsedData()
273
+ const importErrorTest = parsed.testModules[0].tests[0]
274
+
275
+ expect(importErrorTest.name).toBe(
276
+ 'Module failed to load (ReferenceError)'
277
+ )
278
+ expect(importErrorTest.errors[0].name).toBe('ReferenceError')
279
+ expect(importErrorTest.errors[0].operator).toBe('ERR_MODULE_NOT_FOUND')
280
+ })
281
+ })
282
+ })
283
+
284
+ // Test setup helper function
285
+ function setupJestReporter() {
286
+ const storage = new MemoryStorage()
287
+ const globalConfig = undefined
288
+ const reporterOptions = { storage }
289
+ const reporter = new JestReporter(globalConfig, reporterOptions)
290
+
291
+ // Helper to get parsed test data
292
+ const getParsedData = async () => {
293
+ const savedData = await storage.getTest()
294
+ return savedData ? JSON.parse(savedData) : null
295
+ }
296
+
297
+ return {
298
+ reporter,
299
+ storage,
300
+ getParsedData,
301
+ }
302
+ }