@massu/core 0.1.0 → 0.1.1

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 (114) hide show
  1. package/LICENSE +71 -0
  2. package/dist/hooks/cost-tracker.js +127 -11493
  3. package/dist/hooks/post-edit-context.js +125 -11491
  4. package/dist/hooks/post-tool-use.js +127 -11493
  5. package/dist/hooks/pre-compact.js +127 -11493
  6. package/dist/hooks/pre-delete-check.js +126 -11492
  7. package/dist/hooks/quality-event.js +127 -11493
  8. package/dist/hooks/session-end.js +127 -11493
  9. package/dist/hooks/session-start.js +127 -11493
  10. package/dist/hooks/user-prompt.js +127 -11493
  11. package/package.json +9 -8
  12. package/src/__tests__/adr-generator.test.ts +260 -0
  13. package/src/__tests__/analytics.test.ts +282 -0
  14. package/src/__tests__/audit-trail.test.ts +382 -0
  15. package/src/__tests__/backfill-sessions.test.ts +690 -0
  16. package/src/__tests__/cli.test.ts +290 -0
  17. package/src/__tests__/cloud-sync.test.ts +261 -0
  18. package/src/__tests__/config-sections.test.ts +359 -0
  19. package/src/__tests__/config.test.ts +732 -0
  20. package/src/__tests__/cost-tracker.test.ts +348 -0
  21. package/src/__tests__/db.test.ts +177 -0
  22. package/src/__tests__/dependency-scorer.test.ts +325 -0
  23. package/src/__tests__/docs-integration.test.ts +178 -0
  24. package/src/__tests__/docs-tools.test.ts +199 -0
  25. package/src/__tests__/domains.test.ts +236 -0
  26. package/src/__tests__/hooks.test.ts +221 -0
  27. package/src/__tests__/import-resolver.test.ts +95 -0
  28. package/src/__tests__/integration/path-traversal.test.ts +134 -0
  29. package/src/__tests__/integration/pricing-consistency.test.ts +88 -0
  30. package/src/__tests__/integration/tool-registration.test.ts +146 -0
  31. package/src/__tests__/memory-db.test.ts +404 -0
  32. package/src/__tests__/memory-enhancements.test.ts +316 -0
  33. package/src/__tests__/memory-tools.test.ts +199 -0
  34. package/src/__tests__/middleware-tree.test.ts +177 -0
  35. package/src/__tests__/observability-tools.test.ts +595 -0
  36. package/src/__tests__/observability.test.ts +437 -0
  37. package/src/__tests__/observation-extractor.test.ts +167 -0
  38. package/src/__tests__/page-deps.test.ts +60 -0
  39. package/src/__tests__/prompt-analyzer.test.ts +298 -0
  40. package/src/__tests__/regression-detector.test.ts +295 -0
  41. package/src/__tests__/rules.test.ts +87 -0
  42. package/src/__tests__/schema-mapper.test.ts +29 -0
  43. package/src/__tests__/security-scorer.test.ts +238 -0
  44. package/src/__tests__/security-utils.test.ts +175 -0
  45. package/src/__tests__/sentinel-db.test.ts +491 -0
  46. package/src/__tests__/sentinel-scanner.test.ts +750 -0
  47. package/src/__tests__/sentinel-tools.test.ts +324 -0
  48. package/src/__tests__/sentinel-types.test.ts +750 -0
  49. package/src/__tests__/server.test.ts +452 -0
  50. package/src/__tests__/session-archiver.test.ts +524 -0
  51. package/src/__tests__/session-state-generator.test.ts +900 -0
  52. package/src/__tests__/team-knowledge.test.ts +327 -0
  53. package/src/__tests__/tools.test.ts +340 -0
  54. package/src/__tests__/transcript-parser.test.ts +195 -0
  55. package/src/__tests__/trpc-index.test.ts +25 -0
  56. package/src/__tests__/validate-features-runner.test.ts +517 -0
  57. package/src/__tests__/validation-engine.test.ts +300 -0
  58. package/src/adr-generator.ts +285 -0
  59. package/src/analytics.ts +367 -0
  60. package/src/audit-trail.ts +443 -0
  61. package/src/backfill-sessions.ts +180 -0
  62. package/src/cli.ts +105 -0
  63. package/src/cloud-sync.ts +194 -0
  64. package/src/commands/doctor.ts +300 -0
  65. package/src/commands/init.ts +399 -0
  66. package/src/commands/install-hooks.ts +26 -0
  67. package/src/config.ts +357 -0
  68. package/src/core-tools.ts +685 -0
  69. package/src/cost-tracker.ts +350 -0
  70. package/src/db.ts +233 -0
  71. package/src/dependency-scorer.ts +330 -0
  72. package/src/docs-map.json +100 -0
  73. package/src/docs-tools.ts +514 -0
  74. package/src/domains.ts +181 -0
  75. package/src/hooks/cost-tracker.ts +66 -0
  76. package/src/hooks/intent-suggester.ts +131 -0
  77. package/src/hooks/post-edit-context.ts +91 -0
  78. package/src/hooks/post-tool-use.ts +175 -0
  79. package/src/hooks/pre-compact.ts +146 -0
  80. package/src/hooks/pre-delete-check.ts +153 -0
  81. package/src/hooks/quality-event.ts +127 -0
  82. package/src/hooks/security-gate.ts +121 -0
  83. package/src/hooks/session-end.ts +467 -0
  84. package/src/hooks/session-start.ts +210 -0
  85. package/src/hooks/user-prompt.ts +91 -0
  86. package/src/import-resolver.ts +224 -0
  87. package/src/memory-db.ts +48 -0
  88. package/src/memory-queries.ts +804 -0
  89. package/src/memory-schema.ts +546 -0
  90. package/src/memory-tools.ts +392 -0
  91. package/src/middleware-tree.ts +70 -0
  92. package/src/observability-tools.ts +332 -0
  93. package/src/observation-extractor.ts +411 -0
  94. package/src/page-deps.ts +283 -0
  95. package/src/prompt-analyzer.ts +325 -0
  96. package/src/regression-detector.ts +313 -0
  97. package/src/rules.ts +57 -0
  98. package/src/schema-mapper.ts +232 -0
  99. package/src/security-scorer.ts +398 -0
  100. package/src/security-utils.ts +133 -0
  101. package/src/sentinel-db.ts +623 -0
  102. package/src/sentinel-scanner.ts +405 -0
  103. package/src/sentinel-tools.ts +515 -0
  104. package/src/sentinel-types.ts +140 -0
  105. package/src/server.ts +190 -0
  106. package/src/session-archiver.ts +112 -0
  107. package/src/session-state-generator.ts +174 -0
  108. package/src/team-knowledge.ts +400 -0
  109. package/src/tool-helpers.ts +41 -0
  110. package/src/tools.ts +111 -0
  111. package/src/transcript-parser.ts +458 -0
  112. package/src/trpc-index.ts +214 -0
  113. package/src/validate-features-runner.ts +107 -0
  114. package/src/validation-engine.ts +351 -0
@@ -0,0 +1,29 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import { describe, it, expect } from 'vitest';
5
+ import { existsSync } from 'fs';
6
+ import { parsePrismaSchema } from '../schema-mapper.ts';
7
+ import { getResolvedPaths } from '../config.ts';
8
+
9
+ const schemaExists = existsSync(getResolvedPaths().prismaSchemaPath);
10
+
11
+ describe('parsePrismaSchema', () => {
12
+ it('parses the Prisma schema file', () => {
13
+ if (!schemaExists) {
14
+ // No schema in this project - verify it throws gracefully
15
+ expect(() => parsePrismaSchema()).toThrow('Prisma schema not found');
16
+ return;
17
+ }
18
+ const models = parsePrismaSchema();
19
+ expect(models.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it('finds models with fields', () => {
23
+ if (!schemaExists) return;
24
+ const models = parsePrismaSchema();
25
+ for (const model of models) {
26
+ expect(model.fields.length).toBeGreaterThan(0);
27
+ }
28
+ });
29
+ });
@@ -0,0 +1,238 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import Database from 'better-sqlite3';
6
+ import { writeFileSync, mkdirSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import {
9
+ getSecurityToolDefinitions,
10
+ isSecurityTool,
11
+ scoreFileSecurity,
12
+ storeSecurityScore,
13
+ handleSecurityToolCall,
14
+ } from '../security-scorer.ts';
15
+
16
+ function createTestDb(): Database.Database {
17
+ const db = new Database(':memory:');
18
+ db.exec(`
19
+ CREATE TABLE security_scores (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ session_id TEXT NOT NULL,
22
+ file_path TEXT NOT NULL,
23
+ risk_score INTEGER NOT NULL DEFAULT 0,
24
+ findings TEXT,
25
+ created_at TEXT DEFAULT (datetime('now'))
26
+ );
27
+
28
+ CREATE TABLE sessions (
29
+ session_id TEXT PRIMARY KEY,
30
+ status TEXT,
31
+ started_at_epoch INTEGER
32
+ );
33
+ `);
34
+ return db;
35
+ }
36
+
37
+ describe('security-scorer', () => {
38
+ let db: Database.Database;
39
+ const testDir = '/tmp/security-scorer-test';
40
+
41
+ beforeEach(() => {
42
+ db = createTestDb();
43
+ try {
44
+ rmSync(testDir, { recursive: true, force: true });
45
+ } catch { /* ignore */ }
46
+ mkdirSync(testDir, { recursive: true });
47
+ });
48
+
49
+ afterEach(() => {
50
+ db.close();
51
+ try {
52
+ rmSync(testDir, { recursive: true, force: true });
53
+ } catch { /* ignore */ }
54
+ });
55
+
56
+ describe('getSecurityToolDefinitions', () => {
57
+ it('returns 3 tool definitions', () => {
58
+ const tools = getSecurityToolDefinitions();
59
+ expect(tools).toHaveLength(3);
60
+ expect(tools.map(t => t.name.split('_').slice(-2).join('_'))).toEqual([
61
+ 'security_score',
62
+ 'security_heatmap',
63
+ 'security_trend',
64
+ ]);
65
+ });
66
+
67
+ it('has required fields in tool definitions', () => {
68
+ const tools = getSecurityToolDefinitions();
69
+ tools.forEach(tool => {
70
+ expect(tool.name).toBeTruthy();
71
+ expect(tool.description).toBeTruthy();
72
+ expect(tool.inputSchema).toBeDefined();
73
+ });
74
+ });
75
+ });
76
+
77
+ describe('isSecurityTool', () => {
78
+ it('returns true for security tool names', () => {
79
+ expect(isSecurityTool('massu_security_score')).toBe(true);
80
+ expect(isSecurityTool('massu_security_heatmap')).toBe(true);
81
+ expect(isSecurityTool('massu_security_trend')).toBe(true);
82
+ });
83
+
84
+ it('returns false for non-security tool names', () => {
85
+ expect(isSecurityTool('massu_adr_list')).toBe(false);
86
+ expect(isSecurityTool('massu_unknown')).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('scoreFileSecurity', () => {
91
+ it('returns 0 for non-existent file', () => {
92
+ const result = scoreFileSecurity('nonexistent.ts', testDir);
93
+ expect(result.riskScore).toBe(0);
94
+ expect(result.findings).toEqual([]);
95
+ });
96
+
97
+ it('detects hardcoded credentials', () => {
98
+ const filePath = join(testDir, 'test.ts');
99
+ writeFileSync(filePath, `const api_key = "sk-1234567890abcdef";\n`);
100
+
101
+ const result = scoreFileSecurity(filePath, testDir);
102
+ expect(result.riskScore).toBeGreaterThan(0);
103
+ expect(result.findings.length).toBeGreaterThan(0);
104
+ expect(result.findings[0].severity).toBe('critical');
105
+ expect(result.findings[0].description).toContain('credential');
106
+ });
107
+
108
+ it('detects publicProcedure.mutation vulnerability', () => {
109
+ const filePath = join(testDir, 'router.ts');
110
+ writeFileSync(filePath, `export const router = publicProcedure.mutation(async () => {});\n`);
111
+
112
+ const result = scoreFileSecurity(filePath, testDir);
113
+ expect(result.riskScore).toBeGreaterThan(0);
114
+ const mutation = result.findings.find(f => f.description.includes('Mutation without authentication'));
115
+ expect(mutation).toBeDefined();
116
+ expect(mutation?.severity).toBe('critical');
117
+ });
118
+
119
+ it('detects eval usage', () => {
120
+ const filePath = join(testDir, 'dangerous.ts');
121
+ writeFileSync(filePath, `const result = eval(userInput);\n`);
122
+
123
+ const result = scoreFileSecurity(filePath, testDir);
124
+ expect(result.riskScore).toBeGreaterThan(0);
125
+ const evalFinding = result.findings.find(f => f.description.includes('eval()'));
126
+ expect(evalFinding).toBeDefined();
127
+ expect(evalFinding?.severity).toBe('high');
128
+ });
129
+
130
+ it('detects dangerouslySetInnerHTML in tsx files', () => {
131
+ const filePath = join(testDir, 'component.tsx');
132
+ writeFileSync(filePath, `<div dangerouslySetInnerHTML={{ __html: html }} />\n`);
133
+
134
+ const result = scoreFileSecurity(filePath, testDir);
135
+ expect(result.riskScore).toBeGreaterThan(0);
136
+ const xss = result.findings.find(f => f.description.includes('XSS risk'));
137
+ expect(xss).toBeDefined();
138
+ expect(xss?.severity).toBe('high');
139
+ });
140
+
141
+ it('returns 0 for clean file', () => {
142
+ const filePath = join(testDir, 'clean.ts');
143
+ writeFileSync(filePath, `export function add(a: number, b: number) { return a + b; }\n`);
144
+
145
+ const result = scoreFileSecurity(filePath, testDir);
146
+ expect(result.riskScore).toBe(0);
147
+ expect(result.findings).toEqual([]);
148
+ });
149
+
150
+ it('caps risk score at 100', () => {
151
+ const filePath = join(testDir, 'critical.ts');
152
+ writeFileSync(filePath, `
153
+ const password = "hardcoded-secret-12345";
154
+ const token = "sk-1234567890abcdef";
155
+ const apiKey = "another-secret-key-abc";
156
+ publicProcedure.mutation(async () => {});
157
+ eval(userInput);
158
+ exec(\`rm -rf \${dir}\`);
159
+ `);
160
+
161
+ const result = scoreFileSecurity(filePath, testDir);
162
+ expect(result.riskScore).toBeLessThanOrEqual(100);
163
+ });
164
+
165
+ it('blocks path traversal attacks', () => {
166
+ const result = scoreFileSecurity('../../../etc/passwd', testDir);
167
+ expect(result.riskScore).toBe(100);
168
+ expect(result.findings[0].severity).toBe('critical');
169
+ expect(result.findings[0].description).toContain('Path traversal blocked');
170
+ });
171
+ });
172
+
173
+ describe('storeSecurityScore', () => {
174
+ it('stores security score in database', () => {
175
+ storeSecurityScore(db, 'session-123', 'src/test.ts', 42, [
176
+ { pattern: 'test', severity: 'high', line: 10, description: 'Test finding' },
177
+ ]);
178
+
179
+ const row = db.prepare('SELECT * FROM security_scores WHERE session_id = ?').get('session-123') as Record<string, unknown>;
180
+ expect(row.file_path).toBe('src/test.ts');
181
+ expect(row.risk_score).toBe(42);
182
+
183
+ const findings = JSON.parse(row.findings as string) as Array<Record<string, unknown>>;
184
+ expect(findings).toHaveLength(1);
185
+ expect(findings[0].description).toBe('Test finding');
186
+ });
187
+ });
188
+
189
+ describe('handleSecurityToolCall', () => {
190
+ it('handles security_score for file', () => {
191
+ const filePath = join(testDir, 'test.ts');
192
+ writeFileSync(filePath, `export const x = 1;\n`);
193
+
194
+ // Create active session
195
+ db.prepare(`INSERT INTO sessions (session_id, status, started_at_epoch) VALUES (?, ?, ?)`).run(
196
+ 'test-session',
197
+ 'active',
198
+ Math.floor(Date.now() / 1000)
199
+ );
200
+
201
+ const result = handleSecurityToolCall('massu_security_score', { file_path: filePath }, db);
202
+ const text = result.content[0].text;
203
+ expect(text).toContain('Security Score');
204
+ expect(text).toContain(filePath);
205
+ });
206
+
207
+ it('handles security_heatmap with no data', () => {
208
+ const result = handleSecurityToolCall('massu_security_heatmap', {}, db);
209
+ const text = result.content[0].text;
210
+ expect(text).toContain('No files with risk score');
211
+ });
212
+
213
+ it('handles security_heatmap with threshold', () => {
214
+ storeSecurityScore(db, 'session-1', 'file1.ts', 50, []);
215
+ storeSecurityScore(db, 'session-1', 'file2.ts', 80, []);
216
+ storeSecurityScore(db, 'session-1', 'file3.ts', 20, []);
217
+
218
+ const result = handleSecurityToolCall('massu_security_heatmap', { threshold: 30 }, db);
219
+ const text = result.content[0].text;
220
+ expect(text).toContain('Security Heat Map');
221
+ expect(text).toContain('file1.ts');
222
+ expect(text).toContain('file2.ts');
223
+ expect(text).not.toContain('file3.ts');
224
+ });
225
+
226
+ it('handles security_trend with no data', () => {
227
+ const result = handleSecurityToolCall('massu_security_trend', {}, db);
228
+ const text = result.content[0].text;
229
+ expect(text).toContain('No security scan data');
230
+ });
231
+
232
+ it('handles unknown tool name', () => {
233
+ const result = handleSecurityToolCall('massu_security_unknown', {}, db);
234
+ const text = result.content[0].text;
235
+ expect(text).toContain('Unknown security tool');
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,175 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ ensureWithinRoot,
7
+ escapeRegex,
8
+ safeRegex,
9
+ globToSafeRegex,
10
+ redactSensitiveContent,
11
+ enforceSeverityFloors,
12
+ MINIMUM_SEVERITY_WEIGHTS,
13
+ } from '../security-utils.ts';
14
+
15
+ describe('ensureWithinRoot', () => {
16
+ const root = '/projects/my-app';
17
+
18
+ it('allows paths within root', () => {
19
+ expect(ensureWithinRoot('src/index.ts', root)).toBe('/projects/my-app/src/index.ts');
20
+ });
21
+
22
+ it('allows nested paths', () => {
23
+ expect(ensureWithinRoot('src/lib/utils/helpers.ts', root)).toBe('/projects/my-app/src/lib/utils/helpers.ts');
24
+ });
25
+
26
+ it('blocks path traversal with ../', () => {
27
+ expect(() => ensureWithinRoot('../../etc/passwd', root)).toThrow('Path traversal blocked');
28
+ });
29
+
30
+ it('blocks path traversal with encoded sequences', () => {
31
+ expect(() => ensureWithinRoot('../../../etc/shadow', root)).toThrow('Path traversal blocked');
32
+ });
33
+
34
+ it('normalizes paths with ./ segments', () => {
35
+ expect(ensureWithinRoot('./src/../src/index.ts', root)).toBe('/projects/my-app/src/index.ts');
36
+ });
37
+
38
+ it('blocks traversal that resolves outside after normalization', () => {
39
+ expect(() => ensureWithinRoot('src/../../../../etc/passwd', root)).toThrow('Path traversal blocked');
40
+ });
41
+ });
42
+
43
+ describe('escapeRegex', () => {
44
+ it('escapes special regex characters', () => {
45
+ expect(escapeRegex('hello.world')).toBe('hello\\.world');
46
+ expect(escapeRegex('a+b*c')).toBe('a\\+b\\*c');
47
+ expect(escapeRegex('foo(bar)')).toBe('foo\\(bar\\)');
48
+ });
49
+
50
+ it('leaves normal strings unchanged', () => {
51
+ expect(escapeRegex('hello world')).toBe('hello world');
52
+ });
53
+
54
+ it('escapes all PCRE special chars', () => {
55
+ const special = '.*+?^${}()|[]\\';
56
+ const escaped = escapeRegex(special);
57
+ // Every char should be escaped
58
+ expect(escaped).toBe('\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\');
59
+ });
60
+ });
61
+
62
+ describe('safeRegex', () => {
63
+ it('compiles simple patterns', () => {
64
+ const re = safeRegex('hello|world');
65
+ expect(re).toBeInstanceOf(RegExp);
66
+ expect(re!.test('hello')).toBe(true);
67
+ });
68
+
69
+ it('rejects nested quantifiers (ReDoS)', () => {
70
+ expect(safeRegex('(a+)+')).toBeNull();
71
+ expect(safeRegex('(a*)*')).toBeNull();
72
+ expect(safeRegex('(a+){2,}')).toBeNull();
73
+ });
74
+
75
+ it('rejects excessively long patterns', () => {
76
+ expect(safeRegex('a'.repeat(501))).toBeNull();
77
+ });
78
+
79
+ it('rejects invalid regex syntax', () => {
80
+ expect(safeRegex('(?P<invalid')).toBeNull();
81
+ });
82
+
83
+ it('accepts reasonable patterns', () => {
84
+ expect(safeRegex('\\bctx\\.prisma\\b')).toBeInstanceOf(RegExp);
85
+ expect(safeRegex('import.*from')).toBeInstanceOf(RegExp);
86
+ });
87
+
88
+ it('supports flags', () => {
89
+ const re = safeRegex('hello', 'i');
90
+ expect(re!.test('HELLO')).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('globToSafeRegex', () => {
95
+ it('converts simple glob with single star', () => {
96
+ const re = globToSafeRegex('src/**/*.ts');
97
+ expect(re.test('src/lib/utils.ts')).toBe(true);
98
+ expect(re.test('src/deep/nested/file.ts')).toBe(true);
99
+ });
100
+
101
+ it('does not match across path separators with single star', () => {
102
+ const re = globToSafeRegex('src/*.ts');
103
+ expect(re.test('src/index.ts')).toBe(true);
104
+ expect(re.test('src/lib/index.ts')).toBe(false);
105
+ });
106
+
107
+ it('escapes special regex chars in the glob', () => {
108
+ const re = globToSafeRegex('src/components/(portal)/*.tsx');
109
+ expect(re.test('src/components/(portal)/page.tsx')).toBe(true);
110
+ });
111
+ });
112
+
113
+ describe('redactSensitiveContent', () => {
114
+ it('redacts API keys', () => {
115
+ expect(redactSensitiveContent('key: sk-abc123def456ghij')).toContain('[REDACTED_KEY]');
116
+ expect(redactSensitiveContent('token: ghp_1234567890abcdef')).toContain('[REDACTED_KEY]');
117
+ });
118
+
119
+ it('redacts email addresses', () => {
120
+ expect(redactSensitiveContent('contact user@example.com')).toContain('[REDACTED_EMAIL]');
121
+ });
122
+
123
+ it('redacts Bearer tokens', () => {
124
+ expect(redactSensitiveContent('Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig'))
125
+ .toContain('[REDACTED_TOKEN]');
126
+ });
127
+
128
+ it('redacts absolute file paths', () => {
129
+ expect(redactSensitiveContent('file at /Users/john/secrets/key.pem')).toContain('[REDACTED_PATH]');
130
+ });
131
+
132
+ it('redacts connection strings', () => {
133
+ expect(redactSensitiveContent('postgres://admin:s3cret@db.host.com/mydb'))
134
+ .toContain('[REDACTED_CREDENTIALS]');
135
+ });
136
+
137
+ it('preserves non-sensitive content', () => {
138
+ const safe = 'This is a normal prompt about implementing a feature';
139
+ expect(redactSensitiveContent(safe)).toBe(safe);
140
+ });
141
+ });
142
+
143
+ describe('enforceSeverityFloors', () => {
144
+ const defaults = { critical: 25, high: 15, medium: 8, low: 3 };
145
+
146
+ it('uses config values when above floor', () => {
147
+ const result = enforceSeverityFloors({ critical: 30, high: 20 }, defaults);
148
+ expect(result.critical).toBe(30);
149
+ expect(result.high).toBe(20);
150
+ });
151
+
152
+ it('enforces minimum floors', () => {
153
+ const result = enforceSeverityFloors({ critical: 0, high: 0, medium: 0, low: 0 }, defaults);
154
+ expect(result.critical).toBe(MINIMUM_SEVERITY_WEIGHTS.critical);
155
+ expect(result.high).toBe(MINIMUM_SEVERITY_WEIGHTS.high);
156
+ expect(result.medium).toBe(MINIMUM_SEVERITY_WEIGHTS.medium);
157
+ expect(result.low).toBe(MINIMUM_SEVERITY_WEIGHTS.low);
158
+ });
159
+
160
+ it('preserves defaults for missing config keys', () => {
161
+ const result = enforceSeverityFloors({ critical: 50 }, defaults);
162
+ expect(result.critical).toBe(50);
163
+ expect(result.high).toBe(15); // default preserved
164
+ expect(result.medium).toBe(8); // default preserved
165
+ });
166
+
167
+ it('prevents complete disabling of security scoring', () => {
168
+ const result = enforceSeverityFloors(
169
+ { critical: 0, high: 0, medium: 0, low: 0 },
170
+ defaults
171
+ );
172
+ const totalWeight = Object.values(result).reduce((sum, v) => sum + v, 0);
173
+ expect(totalWeight).toBeGreaterThan(0);
174
+ });
175
+ });