xtrm-tools 2.1.4 → 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.
Files changed (29) hide show
  1. package/README.md +10 -4
  2. package/cli/dist/index.cjs +1562 -1392
  3. package/cli/dist/index.cjs.map +1 -1
  4. package/config/hooks.json +4 -4
  5. package/hooks/README.md +3 -3
  6. package/hooks/main-guard.mjs +10 -1
  7. package/package.json +8 -3
  8. package/project-skills/py-quality-gate/.claude/hooks/quality-check.py +15 -2
  9. package/project-skills/py-quality-gate/.claude/settings.json +1 -1
  10. package/project-skills/service-skills-set/.claude/settings.json +2 -2
  11. package/project-skills/service-skills-set/install-service-skills.py +43 -13
  12. package/project-skills/tdd-guard/.claude/hooks/tdd-guard-pretool-bridge.cjs +87 -0
  13. package/project-skills/tdd-guard/.claude/settings.json +2 -2
  14. package/project-skills/tdd-guard/README.md +6 -4
  15. package/project-skills/tdd-guard/docs/linting.md +2 -2
  16. package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test-data.ts +199 -0
  17. package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test.ts +302 -0
  18. package/project-skills/tdd-guard/reporters/jest/src/JestReporter.ts +201 -0
  19. package/project-skills/tdd-guard/reporters/jest/src/index.ts +4 -0
  20. package/project-skills/tdd-guard/reporters/jest/src/types.ts +42 -0
  21. package/project-skills/tdd-guard/reporters/jest/tsconfig.json +11 -0
  22. package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test-data.ts +85 -0
  23. package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test.ts +446 -0
  24. package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.ts +110 -0
  25. package/project-skills/tdd-guard/reporters/vitest/src/index.ts +4 -0
  26. package/project-skills/tdd-guard/reporters/vitest/src/types.ts +39 -0
  27. package/project-skills/tdd-guard/reporters/vitest/tsconfig.json +11 -0
  28. package/project-skills/ts-quality-gate/.claude/hooks/quality-check.cjs +36 -1
  29. package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
package/config/hooks.json CHANGED
@@ -13,12 +13,12 @@
13
13
  ],
14
14
  "PreToolUse": [
15
15
  {
16
- "matcher": "Write|Edit|MultiEdit",
16
+ "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
17
17
  "script": "main-guard.mjs",
18
18
  "timeout": 5000
19
19
  },
20
20
  {
21
- "matcher": "Read|Edit",
21
+ "matcher": "Read|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
22
22
  "script": "serena-workflow-reminder.py"
23
23
  },
24
24
  {
@@ -27,7 +27,7 @@
27
27
  "timeout": 30000
28
28
  },
29
29
  {
30
- "matcher": "Edit|Write",
30
+ "matcher": "Edit|Write|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
31
31
  "script": "type-safety-enforcement.py"
32
32
  },
33
33
  {
@@ -36,7 +36,7 @@
36
36
  "timeout": 8000
37
37
  },
38
38
  {
39
- "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__*",
39
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
40
40
  "script": "beads-edit-gate.mjs",
41
41
  "timeout": 5000
42
42
  },
package/hooks/README.md CHANGED
@@ -147,7 +147,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
147
147
 
148
148
  **Purpose**: Blocks direct file edits and dangerous git operations on protected branches (`main`/`master`). Enforces the feature-branch → PR workflow.
149
149
 
150
- **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|Bash`)
150
+ **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|Bash`)
151
151
 
152
152
  **Blocks**:
153
153
  - Write/Edit/MultiEdit/NotebookEdit on protected branches
@@ -159,7 +159,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
159
159
  {
160
160
  "hooks": {
161
161
  "PreToolUse": [{
162
- "matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash",
162
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|Bash",
163
163
  "hooks": [{ "type": "command", "command": "node \"~/.claude/hooks/main-guard.mjs\"", "timeout": 5000 }]
164
164
  }]
165
165
  }
@@ -182,7 +182,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
182
182
 
183
183
  **Purpose**: Blocks file edits when the current session has not claimed a beads issue via `bd kv`. Prevents free-riding in multi-agent and multi-session scenarios.
184
184
 
185
- **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__*`)
185
+ **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`)
186
186
 
187
187
  **Behavior**:
188
188
  - Session has claim (`bd kv get "claimed:<session_id>"`) → allow
@@ -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.4",
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"""
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PostToolUse": [
4
4
  {
5
- "matcher": "Write|Edit|MultiEdit",
5
+ "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "PreToolUse": [
14
14
  {
15
- "matcher": "Read|Write|Edit|Glob|Grep|Bash",
15
+ "matcher": "Read|Write|Edit|Glob|Grep|Bash|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "PostToolUse": [
25
25
  {
26
- "matcher": "Write|Edit",
26
+ "matcher": "Write|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
27
27
  "hooks": [
28
28
  {
29
29
  "type": "command",
@@ -41,12 +41,12 @@ SETTINGS_HOOKS = {
41
41
  "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/cataloger.py\""}]}
42
42
  ],
43
43
  "PreToolUse": [
44
- {"matcher": "Read|Write|Edit|Glob|Grep|Bash",
44
+ {"matcher": "Read|Write|Edit|Glob|Grep|Bash|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
45
45
  "hooks": [{"type": "command",
46
46
  "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/skill_activator.py\""}]}
47
47
  ],
48
48
  "PostToolUse": [
49
- {"matcher": "Write|Edit",
49
+ {"matcher": "Write|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
50
50
  "hooks": [{"type": "command",
51
51
  "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/updating-service-skills/scripts/drift_detector.py\" check-hook",
52
52
  "timeout": 10}]}
@@ -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,87 @@
1
+ #!/usr/bin/env node
2
+ const { readFileSync } = require('node:fs');
3
+ const { spawnSync } = require('node:child_process');
4
+ const path = require('node:path');
5
+
6
+ const CODE_EXTENSIONS = new Set([
7
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
8
+ '.py', '.php', '.go', '.rs', '.java', '.kt', '.kts',
9
+ '.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp',
10
+ '.cs', '.rb', '.swift', '.scala', '.lua', '.sh', '.zsh', '.bash',
11
+ ]);
12
+
13
+ const SERENA_EDIT_TOOLS = new Set([
14
+ 'mcp__serena__rename_symbol',
15
+ 'mcp__serena__replace_symbol_body',
16
+ 'mcp__serena__insert_after_symbol',
17
+ 'mcp__serena__insert_before_symbol',
18
+ ]);
19
+
20
+ function isEditLikeTool(toolName) {
21
+ if (!toolName) return false;
22
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'MultiEdit' || toolName === 'TodoWrite') {
23
+ return true;
24
+ }
25
+ return SERENA_EDIT_TOOLS.has(toolName);
26
+ }
27
+
28
+ function pickFilePath(toolInput) {
29
+ if (!toolInput || typeof toolInput !== 'object') return null;
30
+
31
+ const direct = toolInput.file_path || toolInput.filePath || toolInput.path ||
32
+ toolInput.relative_path || toolInput.relativePath;
33
+ if (typeof direct === 'string' && direct.trim()) return direct;
34
+
35
+ if (Array.isArray(toolInput.edits)) {
36
+ for (const edit of toolInput.edits) {
37
+ const p = edit?.file_path || edit?.filePath || edit?.path || edit?.relative_path || edit?.relativePath;
38
+ if (typeof p === 'string' && p.trim()) return p;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function isCodeFile(filePath) {
46
+ if (!filePath) return true; // fail-open when no path is available
47
+ return CODE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
48
+ }
49
+
50
+ let payloadText = '';
51
+ try {
52
+ payloadText = readFileSync(0, 'utf8');
53
+ } catch {
54
+ process.exit(0);
55
+ }
56
+
57
+ let payload;
58
+ try {
59
+ payload = JSON.parse(payloadText);
60
+ } catch {
61
+ process.exit(0);
62
+ }
63
+
64
+ const toolName = payload.tool_name || '';
65
+ if (!isEditLikeTool(toolName)) {
66
+ process.exit(0);
67
+ }
68
+
69
+ const filePath = pickFilePath(payload.tool_input);
70
+ if (!isCodeFile(filePath)) {
71
+ process.exit(0);
72
+ }
73
+
74
+ const result = spawnSync('tdd-guard', {
75
+ input: payloadText,
76
+ encoding: 'utf8',
77
+ stdio: ['pipe', 'pipe', 'pipe'],
78
+ });
79
+
80
+ if (result.stdout) process.stdout.write(result.stdout);
81
+ if (result.stderr) process.stderr.write(result.stderr);
82
+
83
+ if (result.error) {
84
+ process.exit(0);
85
+ }
86
+
87
+ process.exit(result.status ?? 0);
@@ -2,11 +2,11 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "Write|Edit|MultiEdit|TodoWrite",
5
+ "matcher": "Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "tdd-guard",
9
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs\"",
10
10
  "timeout": 30
11
11
  }
12
12
  ]
@@ -276,8 +276,8 @@ Type `/hooks` in Claude Code to open the hooks menu, then configure each hook. U
276
276
  **PreToolUse Hook**
277
277
 
278
278
  1. Select `PreToolUse - Before tool execution`
279
- 2. Choose `+ Add new matcher...` and enter: `Write|Edit|MultiEdit|TodoWrite`
280
- 3. Select `+ Add new hook...` and enter: `tdd-guard`
279
+ 2. Choose `+ Add new matcher...` and enter: `Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`
280
+ 3. Select `+ Add new hook...` and enter: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs"`
281
281
  4. Choose where to save
282
282
 
283
283
  **UserPromptSubmit Hook**
@@ -305,11 +305,11 @@ If you prefer to edit settings files directly, add all three hooks to your chose
305
305
  "hooks": {
306
306
  "PreToolUse": [
307
307
  {
308
- "matcher": "Write|Edit|MultiEdit|TodoWrite",
308
+ "matcher": "Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
309
309
  "hooks": [
310
310
  {
311
311
  "type": "command",
312
- "command": "tdd-guard"
312
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs\""
313
313
  }
314
314
  ]
315
315
  }
@@ -341,6 +341,8 @@ If you prefer to edit settings files directly, add all three hooks to your chose
341
341
 
342
342
  </details>
343
343
 
344
+ **Note**: The pretool bridge skips non-code files (for example `.md`) and forwards code edits to `tdd-guard`, which avoids false positives on documentation-only changes.
345
+
344
346
  ## Additional Configuration
345
347
 
346
348
  - [Custom instructions](docs/custom-instructions.md) - Customize TDD validation rules
@@ -42,7 +42,7 @@ The refactoring support helps by:
42
42
  1. Type `/hooks` in Claude Code
43
43
  2. Select `PostToolUse - After tool execution`
44
44
  3. Choose `+ Add new matcher...`
45
- 4. Enter: `Write|Edit|MultiEdit`
45
+ 4. Enter: `Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`
46
46
  5. Select `+ Add new hook...`
47
47
  6. Enter command: `tdd-guard`
48
48
  7. Choose where to save
@@ -56,7 +56,7 @@ The refactoring support helps by:
56
56
  "hooks": {
57
57
  "PostToolUse": [
58
58
  {
59
- "matcher": "Write|Edit|MultiEdit",
59
+ "matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
60
60
  "hooks": [
61
61
  {
62
62
  "type": "command",
@@ -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
+ }