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.
- package/README.md +9 -3
- package/cli/dist/index.cjs +960 -900
- package/cli/dist/index.cjs.map +1 -1
- package/hooks/main-guard.mjs +10 -1
- package/package.json +8 -3
- package/project-skills/py-quality-gate/.claude/hooks/quality-check.py +15 -2
- package/project-skills/service-skills-set/install-service-skills.py +41 -11
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test-data.ts +199 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test.ts +302 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.ts +201 -0
- package/project-skills/tdd-guard/reporters/jest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/jest/src/types.ts +42 -0
- package/project-skills/tdd-guard/reporters/jest/tsconfig.json +11 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test-data.ts +85 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test.ts +446 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.ts +110 -0
- package/project-skills/tdd-guard/reporters/vitest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/vitest/src/types.ts +39 -0
- package/project-skills/tdd-guard/reporters/vitest/tsconfig.json +11 -0
- package/project-skills/ts-quality-gate/.claude/hooks/quality-check.cjs +36 -1
package/hooks/main-guard.mjs
CHANGED
|
@@ -43,7 +43,16 @@ function deny(reason) {
|
|
|
43
43
|
process.exit(2);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const WRITE_TOOLS = new Set([
|
|
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.
|
|
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 --
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|