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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { BaseReporter, Config } from '@jest/reporters'
|
|
2
|
+
import type { Test, TestResult, AggregatedResult } from '@jest/reporters'
|
|
3
|
+
import type { AssertionResult } from '@jest/test-result'
|
|
4
|
+
import type { TestContext } from '@jest/test-result'
|
|
5
|
+
import { Storage, FileStorage, Config as TDDConfig } from 'tdd-guard'
|
|
6
|
+
import type {
|
|
7
|
+
TDDGuardReporterOptions,
|
|
8
|
+
CapturedError,
|
|
9
|
+
CapturedTest,
|
|
10
|
+
CapturedTestRun,
|
|
11
|
+
CapturedUnhandledError,
|
|
12
|
+
CapturedModule,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
export class JestReporter extends BaseReporter {
|
|
16
|
+
private readonly storage: Storage
|
|
17
|
+
private readonly testModules: Map<
|
|
18
|
+
string,
|
|
19
|
+
{ test: Test; testResult: TestResult }
|
|
20
|
+
> = new Map()
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
_globalConfig?: Config.GlobalConfig,
|
|
24
|
+
reporterOptions?: TDDGuardReporterOptions
|
|
25
|
+
) {
|
|
26
|
+
super()
|
|
27
|
+
this.storage = this.initializeStorage(reporterOptions)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private initializeStorage(options?: TDDGuardReporterOptions): Storage {
|
|
31
|
+
if (options?.storage) {
|
|
32
|
+
return options.storage
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options?.projectRoot) {
|
|
36
|
+
const config = new TDDConfig({ projectRoot: options.projectRoot })
|
|
37
|
+
return new FileStorage(config)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new FileStorage()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override onTestResult(test: Test, testResult: TestResult): void {
|
|
44
|
+
this.testModules.set(test.path, { test, testResult })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override async onRunComplete(
|
|
48
|
+
_contexts: Set<TestContext>,
|
|
49
|
+
results: AggregatedResult
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const output: CapturedTestRun = {
|
|
52
|
+
testModules: this.buildTestModules(),
|
|
53
|
+
unhandledErrors: this.buildUnhandledErrors(results),
|
|
54
|
+
reason: this.determineTestRunReason(results),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await this.storage.saveTest(JSON.stringify(output, null, 2))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private determineTestRunReason(
|
|
61
|
+
results: AggregatedResult
|
|
62
|
+
): 'passed' | 'failed' | 'interrupted' {
|
|
63
|
+
if (results.wasInterrupted) {
|
|
64
|
+
return 'interrupted'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (results.numFailedTestSuites === 0 && results.numTotalTestSuites > 0) {
|
|
68
|
+
return 'passed'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 'failed'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private mapTestResult(test: AssertionResult): CapturedTest {
|
|
75
|
+
const result: CapturedTest = {
|
|
76
|
+
name: test.title,
|
|
77
|
+
fullName: test.fullName,
|
|
78
|
+
state: test.status,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Process failure details if present
|
|
82
|
+
if (test.failureMessages.length > 0) {
|
|
83
|
+
result.errors = this.processTestErrors(test)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private processTestErrors(test: AssertionResult): CapturedError[] {
|
|
90
|
+
if (test.failureDetails.length === 0) {
|
|
91
|
+
return test.failureMessages.map((message) => ({ message }))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return test.failureDetails.map((detail: unknown, index: number) => {
|
|
95
|
+
const message = test.failureMessages[index] || ''
|
|
96
|
+
const error: CapturedError = { message }
|
|
97
|
+
|
|
98
|
+
if (detail && typeof detail === 'object') {
|
|
99
|
+
this.extractErrorDetails(error, detail as Record<string, unknown>)
|
|
100
|
+
this.parseExpectedActualFromMessage(error, message)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return error
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private extractErrorDetails(
|
|
108
|
+
error: CapturedError,
|
|
109
|
+
obj: Record<string, unknown>
|
|
110
|
+
): void {
|
|
111
|
+
if ('actual' in obj) error.actual = String(obj.actual)
|
|
112
|
+
if ('expected' in obj) error.expected = String(obj.expected)
|
|
113
|
+
if ('showDiff' in obj) error.showDiff = Boolean(obj.showDiff)
|
|
114
|
+
if ('operator' in obj) error.operator = String(obj.operator)
|
|
115
|
+
if ('diff' in obj) error.diff = String(obj.diff)
|
|
116
|
+
if ('name' in obj) error.name = String(obj.name)
|
|
117
|
+
if ('ok' in obj) error.ok = Boolean(obj.ok)
|
|
118
|
+
if ('stack' in obj) error.stack = String(obj.stack)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private parseExpectedActualFromMessage(
|
|
122
|
+
error: CapturedError,
|
|
123
|
+
message: string
|
|
124
|
+
): void {
|
|
125
|
+
if (!error.expected || !error.actual) {
|
|
126
|
+
const expectedMatch = /Expected:\s*(\d+)/.exec(message)
|
|
127
|
+
const receivedMatch = /Received:\s*(\d+)/.exec(message)
|
|
128
|
+
if (expectedMatch && !error.expected) error.expected = expectedMatch[1]
|
|
129
|
+
if (receivedMatch && !error.actual) error.actual = receivedMatch[1]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private createTestFromExecError(execError: unknown): CapturedTest {
|
|
134
|
+
const errorObj = execError as Record<string, unknown>
|
|
135
|
+
const message = String(errorObj.message ?? 'Unknown error')
|
|
136
|
+
|
|
137
|
+
const error: CapturedError = {
|
|
138
|
+
message,
|
|
139
|
+
name: typeof errorObj.name === 'string' ? errorObj.name : 'Error',
|
|
140
|
+
stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract additional fields from Jest's SerializableError
|
|
144
|
+
if ('code' in errorObj && errorObj.code !== undefined) {
|
|
145
|
+
error.operator = String(errorObj.code)
|
|
146
|
+
}
|
|
147
|
+
if ('type' in errorObj && typeof errorObj.type === 'string') {
|
|
148
|
+
error.name = errorObj.type
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const errorType = error.name ?? 'Error'
|
|
152
|
+
const testName = `Module failed to load (${errorType})`
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: testName,
|
|
156
|
+
fullName: testName,
|
|
157
|
+
state: 'failed',
|
|
158
|
+
errors: [error],
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private buildTestModules(): CapturedModule[] {
|
|
163
|
+
return Array.from(this.testModules.entries()).map(([path, data]) => {
|
|
164
|
+
const { testResult } = data
|
|
165
|
+
|
|
166
|
+
// Handle module/import errors
|
|
167
|
+
if (testResult.testExecError && testResult.testResults.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
moduleId: path,
|
|
170
|
+
tests: [this.createTestFromExecError(testResult.testExecError)],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
moduleId: path,
|
|
176
|
+
tests: testResult.testResults.map((test: AssertionResult) =>
|
|
177
|
+
this.mapTestResult(test)
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private buildUnhandledErrors(
|
|
184
|
+
results: AggregatedResult
|
|
185
|
+
): CapturedUnhandledError[] {
|
|
186
|
+
if (!results.runExecError) {
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const error = results.runExecError
|
|
191
|
+
const errorObj = error as Record<string, unknown>
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
message: String(errorObj.message ?? 'Unknown error'),
|
|
196
|
+
name: typeof errorObj.name === 'string' ? errorObj.name : 'Error',
|
|
197
|
+
stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Storage } from 'tdd-guard'
|
|
2
|
+
|
|
3
|
+
export interface TDDGuardReporterOptions {
|
|
4
|
+
storage?: Storage
|
|
5
|
+
projectRoot?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CapturedError {
|
|
9
|
+
message: string
|
|
10
|
+
actual?: string
|
|
11
|
+
expected?: string
|
|
12
|
+
showDiff?: boolean
|
|
13
|
+
operator?: string
|
|
14
|
+
diff?: string
|
|
15
|
+
name?: string
|
|
16
|
+
ok?: boolean
|
|
17
|
+
stack?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CapturedTest {
|
|
21
|
+
name: string
|
|
22
|
+
fullName: string
|
|
23
|
+
state: string
|
|
24
|
+
errors?: CapturedError[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CapturedModule {
|
|
28
|
+
moduleId: string
|
|
29
|
+
tests: CapturedTest[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CapturedUnhandledError {
|
|
33
|
+
message: string
|
|
34
|
+
name: string
|
|
35
|
+
stack?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CapturedTestRun {
|
|
39
|
+
testModules: CapturedModule[]
|
|
40
|
+
unhandledErrors?: CapturedUnhandledError[]
|
|
41
|
+
reason?: 'passed' | 'failed' | 'interrupted'
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TestModule, TestCase, TestResult } from 'vitest/node'
|
|
2
|
+
import type { SerializedError } from '@vitest/utils'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MODULE_ID = '/test/example.test.ts'
|
|
5
|
+
const DEFAULT_TEST_NAME = 'should pass'
|
|
6
|
+
const DEFAULT_TEST_FULL_NAME = 'Example Suite > should pass'
|
|
7
|
+
|
|
8
|
+
// Helper to create a valid TestResult for a given state
|
|
9
|
+
export function createTestResult(state: TestResult['state']): TestResult {
|
|
10
|
+
switch (state) {
|
|
11
|
+
case 'failed':
|
|
12
|
+
return { state: 'failed', errors: [] }
|
|
13
|
+
case 'passed':
|
|
14
|
+
return { state: 'passed', errors: undefined }
|
|
15
|
+
case 'skipped':
|
|
16
|
+
return { state: 'skipped', errors: undefined, note: undefined }
|
|
17
|
+
case 'pending':
|
|
18
|
+
return { state: 'pending', errors: undefined }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Creates a minimal TestModule mock for testing
|
|
23
|
+
function createTestModule(props: {
|
|
24
|
+
moduleId: string
|
|
25
|
+
errors?: () => SerializedError[]
|
|
26
|
+
}): TestModule {
|
|
27
|
+
return {
|
|
28
|
+
moduleId: props.moduleId,
|
|
29
|
+
errors: props.errors ?? ((): SerializedError[] => []),
|
|
30
|
+
} as TestModule
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function testModule(overrides?: {
|
|
34
|
+
moduleId?: string
|
|
35
|
+
errors?: () => SerializedError[]
|
|
36
|
+
}): TestModule {
|
|
37
|
+
return createTestModule({
|
|
38
|
+
moduleId: overrides?.moduleId ?? DEFAULT_MODULE_ID,
|
|
39
|
+
errors: overrides?.errors,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createTestCase(overrides?: Partial<TestCase>): TestCase {
|
|
44
|
+
const defaultModule = createTestModule({ moduleId: DEFAULT_MODULE_ID })
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: DEFAULT_TEST_NAME,
|
|
48
|
+
fullName: DEFAULT_TEST_FULL_NAME,
|
|
49
|
+
module: defaultModule,
|
|
50
|
+
result: () => ({ state: 'passed', errors: [] }) as TestResult,
|
|
51
|
+
...overrides,
|
|
52
|
+
} as TestCase
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function failedTestCase(overrides?: Partial<TestCase>): TestCase {
|
|
56
|
+
return createTestCase({
|
|
57
|
+
name: 'should fail',
|
|
58
|
+
fullName: 'Example Suite > should fail',
|
|
59
|
+
result: () =>
|
|
60
|
+
({
|
|
61
|
+
state: 'failed',
|
|
62
|
+
errors: [
|
|
63
|
+
{
|
|
64
|
+
message: 'expected 2 to be 3',
|
|
65
|
+
stack: 'Error: expected 2 to be 3\n at test.ts:7:19',
|
|
66
|
+
expected: '3',
|
|
67
|
+
actual: '2',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}) as TestResult,
|
|
71
|
+
...overrides,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createUnhandledError(
|
|
76
|
+
overrides: Partial<SerializedError> = {}
|
|
77
|
+
): SerializedError {
|
|
78
|
+
return {
|
|
79
|
+
name: 'Error',
|
|
80
|
+
message: 'Cannot find module "./helpers"',
|
|
81
|
+
stack:
|
|
82
|
+
"Error: Cannot find module './helpers' imported from '/src/example.test.ts'",
|
|
83
|
+
...overrides,
|
|
84
|
+
}
|
|
85
|
+
}
|