返回 AiToEarn
channels-error-contract.spec.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / channels / channels-error-contract.spec.ts
1 import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
2 import { join, relative } from 'node:path'
3
4 const channelsRoot = resolveChannelsRoot()
5 const loggerObjectErrorPattern = /logger\.(?:error|warn|fatal)\(\s*\{[\s\S]{0,400}?[,{]\s*(?:error|err|[A-Za-z]+Error)(?:\s*:|\s*[,}])/g
6 const appExceptionLiteralMessagePattern = /new AppException\([^)\n]*,\s*['"`]/g
7 const appExceptionLiteralDataPattern = /new AppException\([^)\n]*,\s*\{[^)]{0,500}\b(?:reason|message|detail|error)\s*:\s*['"`]/g
8
9 describe('channels error contract', () => {
10 it('does not log errors through object fields that bypass pino error handling', () => {
11 const violations = listSourceFiles(channelsRoot)
12 .flatMap((file) => {
13 const content = readFileSync(file, 'utf8')
14 return collectMatches(file, content, loggerObjectErrorPattern)
15 })
16
17 expect(violations).toEqual([])
18 })
19
20 it('does not expose literal messages from AppException in channels', () => {
21 const violations = listSourceFiles(channelsRoot)
22 .flatMap((file) => {
23 const content = readFileSync(file, 'utf8')
24 return [
25 ...collectMatches(file, content, appExceptionLiteralMessagePattern),
26 ...collectMatches(file, content, appExceptionLiteralDataPattern),
27 ]
28 })
29
30 expect(violations).toEqual([])
31 })
32
33 it('does not throw bare Error in channels runtime code', () => {
34 const violations = listSourceFiles(channelsRoot)
35 .flatMap((file) => {
36 const content = readFileSync(file, 'utf8')
37 return collectMatches(file, content, /throw new Error\(/g)
38 })
39 .filter(match => !match.startsWith('platforms/platforms.registry.ts:'))
40
41 expect(violations).toEqual([])
42 })
43 })
44
45 function listSourceFiles(dir: string): string[] {
46 return readdirSync(dir).flatMap((name) => {
47 const path = join(dir, name)
48 const stat = statSync(path)
49 if (stat.isDirectory()) {
50 return listSourceFiles(path)
51 }
52 if (!name.endsWith('.ts') || name.endsWith('.spec.ts')) {
53 return []
54 }
55 return [path]
56 })
57 }
58
59 function collectMatches(file: string, content: string, pattern: RegExp): string[] {
60 return Array.from(content.matchAll(pattern), match => `${relative(channelsRoot, file)}:${lineOf(content, match.index ?? 0)}`)
61 }
62
63 function lineOf(content: string, index: number): number {
64 return content.slice(0, index).split('\n').length
65 }
66
67 function resolveChannelsRoot(): string {
68 const rootFromWorkspace = join(process.cwd(), 'apps/aitoearn-server/src/core/channels')
69 if (existsSync(rootFromWorkspace)) {
70 return rootFromWorkspace
71 }
72
73 return join(process.cwd(), 'src/core/channels')
74 }
75
75 lines TYPESCRIPT