@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.
- package/LICENSE +71 -0
- package/dist/hooks/cost-tracker.js +127 -11493
- package/dist/hooks/post-edit-context.js +125 -11491
- package/dist/hooks/post-tool-use.js +127 -11493
- package/dist/hooks/pre-compact.js +127 -11493
- package/dist/hooks/pre-delete-check.js +126 -11492
- package/dist/hooks/quality-event.js +127 -11493
- package/dist/hooks/session-end.js +127 -11493
- package/dist/hooks/session-start.js +127 -11493
- package/dist/hooks/user-prompt.js +127 -11493
- package/package.json +9 -8
- package/src/__tests__/adr-generator.test.ts +260 -0
- package/src/__tests__/analytics.test.ts +282 -0
- package/src/__tests__/audit-trail.test.ts +382 -0
- package/src/__tests__/backfill-sessions.test.ts +690 -0
- package/src/__tests__/cli.test.ts +290 -0
- package/src/__tests__/cloud-sync.test.ts +261 -0
- package/src/__tests__/config-sections.test.ts +359 -0
- package/src/__tests__/config.test.ts +732 -0
- package/src/__tests__/cost-tracker.test.ts +348 -0
- package/src/__tests__/db.test.ts +177 -0
- package/src/__tests__/dependency-scorer.test.ts +325 -0
- package/src/__tests__/docs-integration.test.ts +178 -0
- package/src/__tests__/docs-tools.test.ts +199 -0
- package/src/__tests__/domains.test.ts +236 -0
- package/src/__tests__/hooks.test.ts +221 -0
- package/src/__tests__/import-resolver.test.ts +95 -0
- package/src/__tests__/integration/path-traversal.test.ts +134 -0
- package/src/__tests__/integration/pricing-consistency.test.ts +88 -0
- package/src/__tests__/integration/tool-registration.test.ts +146 -0
- package/src/__tests__/memory-db.test.ts +404 -0
- package/src/__tests__/memory-enhancements.test.ts +316 -0
- package/src/__tests__/memory-tools.test.ts +199 -0
- package/src/__tests__/middleware-tree.test.ts +177 -0
- package/src/__tests__/observability-tools.test.ts +595 -0
- package/src/__tests__/observability.test.ts +437 -0
- package/src/__tests__/observation-extractor.test.ts +167 -0
- package/src/__tests__/page-deps.test.ts +60 -0
- package/src/__tests__/prompt-analyzer.test.ts +298 -0
- package/src/__tests__/regression-detector.test.ts +295 -0
- package/src/__tests__/rules.test.ts +87 -0
- package/src/__tests__/schema-mapper.test.ts +29 -0
- package/src/__tests__/security-scorer.test.ts +238 -0
- package/src/__tests__/security-utils.test.ts +175 -0
- package/src/__tests__/sentinel-db.test.ts +491 -0
- package/src/__tests__/sentinel-scanner.test.ts +750 -0
- package/src/__tests__/sentinel-tools.test.ts +324 -0
- package/src/__tests__/sentinel-types.test.ts +750 -0
- package/src/__tests__/server.test.ts +452 -0
- package/src/__tests__/session-archiver.test.ts +524 -0
- package/src/__tests__/session-state-generator.test.ts +900 -0
- package/src/__tests__/team-knowledge.test.ts +327 -0
- package/src/__tests__/tools.test.ts +340 -0
- package/src/__tests__/transcript-parser.test.ts +195 -0
- package/src/__tests__/trpc-index.test.ts +25 -0
- package/src/__tests__/validate-features-runner.test.ts +517 -0
- package/src/__tests__/validation-engine.test.ts +300 -0
- package/src/adr-generator.ts +285 -0
- package/src/analytics.ts +367 -0
- package/src/audit-trail.ts +443 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +194 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +399 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/core-tools.ts +685 -0
- package/src/cost-tracker.ts +350 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +330 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +514 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +48 -0
- package/src/memory-queries.ts +804 -0
- package/src/memory-schema.ts +546 -0
- package/src/memory-tools.ts +392 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +332 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +325 -0
- package/src/regression-detector.ts +313 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +398 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +623 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +515 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +190 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +400 -0
- package/src/tool-helpers.ts +41 -0
- package/src/tools.ts +111 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +107 -0
- package/src/validation-engine.ts +351 -0
|
@@ -0,0 +1,732 @@
|
|
|
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 { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { getConfig, getProjectRoot, getResolvedPaths, resetConfig } from '../config.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core config function tests: getConfig(), getProjectRoot(),
|
|
11
|
+
* getResolvedPaths(), resetConfig(), and findProjectRoot() behavior.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: config-sections.test.ts covers detailed section parsing.
|
|
14
|
+
* This file focuses on core mechanics: caching, defaults, resolved paths, etc.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TEST_DIR = resolve(__dirname, '../test-config-core-tmp');
|
|
18
|
+
const CONFIG_PATH = resolve(TEST_DIR, 'massu.config.yaml');
|
|
19
|
+
|
|
20
|
+
function writeConfig(yaml: string) {
|
|
21
|
+
if (!existsSync(TEST_DIR)) mkdirSync(TEST_DIR, { recursive: true });
|
|
22
|
+
writeFileSync(CONFIG_PATH, yaml, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Core Config Functions', () => {
|
|
26
|
+
const originalCwd = process.cwd();
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetConfig();
|
|
30
|
+
if (!existsSync(TEST_DIR)) mkdirSync(TEST_DIR, { recursive: true });
|
|
31
|
+
process.chdir(TEST_DIR);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
process.chdir(originalCwd);
|
|
36
|
+
resetConfig();
|
|
37
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// -------------------------------------------------------
|
|
41
|
+
// 1. getConfig() returns sensible defaults when no config file exists
|
|
42
|
+
// -------------------------------------------------------
|
|
43
|
+
describe('getConfig() defaults (no config file)', () => {
|
|
44
|
+
it('returns sensible defaults when an empty config is provided', () => {
|
|
45
|
+
// Write an empty config so findProjectRoot stays in TEST_DIR
|
|
46
|
+
// and the Zod defaults kick in for all fields.
|
|
47
|
+
writeConfig('');
|
|
48
|
+
const config = getConfig();
|
|
49
|
+
expect(config.project.name).toBe('my-project');
|
|
50
|
+
expect(config.framework.type).toBe('typescript');
|
|
51
|
+
expect(config.framework.router).toBe('none');
|
|
52
|
+
expect(config.framework.orm).toBe('none');
|
|
53
|
+
expect(config.framework.ui).toBe('none');
|
|
54
|
+
expect(config.paths.source).toBe('src');
|
|
55
|
+
expect(config.toolPrefix).toBe('massu');
|
|
56
|
+
expect(config.domains).toEqual([]);
|
|
57
|
+
expect(config.rules).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns default paths.aliases with @ -> src', () => {
|
|
61
|
+
writeConfig('');
|
|
62
|
+
const config = getConfig();
|
|
63
|
+
expect(config.paths.aliases).toEqual({ '@': 'src' });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// -------------------------------------------------------
|
|
68
|
+
// 2. getConfig() caching (second call returns same object)
|
|
69
|
+
// -------------------------------------------------------
|
|
70
|
+
describe('getConfig() caching', () => {
|
|
71
|
+
it('returns the same object reference on consecutive calls', () => {
|
|
72
|
+
writeConfig(`
|
|
73
|
+
project:
|
|
74
|
+
name: cache-test
|
|
75
|
+
toolPrefix: ct
|
|
76
|
+
`);
|
|
77
|
+
const first = getConfig();
|
|
78
|
+
const second = getConfig();
|
|
79
|
+
expect(first).toBe(second);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// -------------------------------------------------------
|
|
84
|
+
// 3. resetConfig() clears cache
|
|
85
|
+
// -------------------------------------------------------
|
|
86
|
+
describe('resetConfig()', () => {
|
|
87
|
+
it('clears the config cache so next call re-reads from disk', () => {
|
|
88
|
+
writeConfig(`
|
|
89
|
+
project:
|
|
90
|
+
name: before-reset
|
|
91
|
+
`);
|
|
92
|
+
const before = getConfig();
|
|
93
|
+
expect(before.project.name).toBe('before-reset');
|
|
94
|
+
|
|
95
|
+
// Write new config and reset
|
|
96
|
+
writeConfig(`
|
|
97
|
+
project:
|
|
98
|
+
name: after-reset
|
|
99
|
+
`);
|
|
100
|
+
resetConfig();
|
|
101
|
+
const after = getConfig();
|
|
102
|
+
expect(after.project.name).toBe('after-reset');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('clears project root cache', () => {
|
|
106
|
+
const root1 = getProjectRoot();
|
|
107
|
+
resetConfig();
|
|
108
|
+
const root2 = getProjectRoot();
|
|
109
|
+
// Both should resolve to a valid directory, but the cache was cleared
|
|
110
|
+
// (the result may be the same path, but it was recomputed)
|
|
111
|
+
expect(typeof root1).toBe('string');
|
|
112
|
+
expect(typeof root2).toBe('string');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// -------------------------------------------------------
|
|
117
|
+
// 4. getProjectRoot() caches the result
|
|
118
|
+
// -------------------------------------------------------
|
|
119
|
+
describe('getProjectRoot() caching', () => {
|
|
120
|
+
it('returns the same string on consecutive calls', () => {
|
|
121
|
+
writeConfig(`
|
|
122
|
+
project:
|
|
123
|
+
name: root-cache
|
|
124
|
+
`);
|
|
125
|
+
const first = getProjectRoot();
|
|
126
|
+
const second = getProjectRoot();
|
|
127
|
+
expect(first).toBe(second);
|
|
128
|
+
expect(typeof first).toBe('string');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('finds the directory containing massu.config.yaml', () => {
|
|
132
|
+
writeConfig(`
|
|
133
|
+
project:
|
|
134
|
+
name: root-find
|
|
135
|
+
`);
|
|
136
|
+
const root = getProjectRoot();
|
|
137
|
+
expect(existsSync(resolve(root, 'massu.config.yaml'))).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------
|
|
142
|
+
// 5. getResolvedPaths() returns all expected keys
|
|
143
|
+
// -------------------------------------------------------
|
|
144
|
+
describe('getResolvedPaths() keys', () => {
|
|
145
|
+
it('returns an object with all expected path keys', () => {
|
|
146
|
+
writeConfig(`
|
|
147
|
+
project:
|
|
148
|
+
name: paths-test
|
|
149
|
+
`);
|
|
150
|
+
const paths = getResolvedPaths();
|
|
151
|
+
const expectedKeys = [
|
|
152
|
+
'codegraphDbPath',
|
|
153
|
+
'dataDbPath',
|
|
154
|
+
'prismaSchemaPath',
|
|
155
|
+
'rootRouterPath',
|
|
156
|
+
'routersDir',
|
|
157
|
+
'srcDir',
|
|
158
|
+
'pathAlias',
|
|
159
|
+
'extensions',
|
|
160
|
+
'indexFiles',
|
|
161
|
+
'patternsDir',
|
|
162
|
+
'claudeMdPath',
|
|
163
|
+
'docsMapPath',
|
|
164
|
+
'helpSitePath',
|
|
165
|
+
'memoryDbPath',
|
|
166
|
+
];
|
|
167
|
+
for (const key of expectedKeys) {
|
|
168
|
+
expect(paths).toHaveProperty(key);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// -------------------------------------------------------
|
|
174
|
+
// 6. getResolvedPaths() uses config.paths.source for srcDir
|
|
175
|
+
// -------------------------------------------------------
|
|
176
|
+
describe('getResolvedPaths() srcDir resolution', () => {
|
|
177
|
+
it('resolves srcDir from config.paths.source', () => {
|
|
178
|
+
writeConfig(`
|
|
179
|
+
project:
|
|
180
|
+
name: src-test
|
|
181
|
+
paths:
|
|
182
|
+
source: lib
|
|
183
|
+
`);
|
|
184
|
+
const paths = getResolvedPaths();
|
|
185
|
+
const root = getProjectRoot();
|
|
186
|
+
expect(paths.srcDir).toBe(resolve(root, 'lib'));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('uses default src when paths.source not specified', () => {
|
|
190
|
+
writeConfig(`
|
|
191
|
+
project:
|
|
192
|
+
name: src-default
|
|
193
|
+
`);
|
|
194
|
+
const paths = getResolvedPaths();
|
|
195
|
+
const root = getProjectRoot();
|
|
196
|
+
expect(paths.srcDir).toBe(resolve(root, 'src'));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------
|
|
201
|
+
// 7. Config with custom toolPrefix
|
|
202
|
+
// -------------------------------------------------------
|
|
203
|
+
describe('custom toolPrefix', () => {
|
|
204
|
+
it('reads custom toolPrefix from config', () => {
|
|
205
|
+
writeConfig(`
|
|
206
|
+
project:
|
|
207
|
+
name: prefix-test
|
|
208
|
+
toolPrefix: myprefix
|
|
209
|
+
`);
|
|
210
|
+
const config = getConfig();
|
|
211
|
+
expect(config.toolPrefix).toBe('myprefix');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// -------------------------------------------------------
|
|
216
|
+
// 8. Config with custom project name
|
|
217
|
+
// -------------------------------------------------------
|
|
218
|
+
describe('custom project name', () => {
|
|
219
|
+
it('reads custom project name', () => {
|
|
220
|
+
writeConfig(`
|
|
221
|
+
project:
|
|
222
|
+
name: my-awesome-project
|
|
223
|
+
`);
|
|
224
|
+
const config = getConfig();
|
|
225
|
+
expect(config.project.name).toBe('my-awesome-project');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('uses project name in helpSitePath', () => {
|
|
229
|
+
writeConfig(`
|
|
230
|
+
project:
|
|
231
|
+
name: cool-app
|
|
232
|
+
`);
|
|
233
|
+
const paths = getResolvedPaths();
|
|
234
|
+
expect(paths.helpSitePath).toContain('cool-app-help');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// -------------------------------------------------------
|
|
239
|
+
// 9. Config with domains array
|
|
240
|
+
// -------------------------------------------------------
|
|
241
|
+
describe('domains array', () => {
|
|
242
|
+
it('parses domains from config', () => {
|
|
243
|
+
writeConfig(`
|
|
244
|
+
project:
|
|
245
|
+
name: domains-test
|
|
246
|
+
domains:
|
|
247
|
+
- name: auth
|
|
248
|
+
routers:
|
|
249
|
+
- auth.ts
|
|
250
|
+
pages:
|
|
251
|
+
- login
|
|
252
|
+
tables:
|
|
253
|
+
- users
|
|
254
|
+
allowedImportsFrom:
|
|
255
|
+
- shared
|
|
256
|
+
- name: billing
|
|
257
|
+
routers:
|
|
258
|
+
- billing.ts
|
|
259
|
+
`);
|
|
260
|
+
const config = getConfig();
|
|
261
|
+
expect(config.domains).toHaveLength(2);
|
|
262
|
+
expect(config.domains[0].name).toBe('auth');
|
|
263
|
+
expect(config.domains[0].routers).toEqual(['auth.ts']);
|
|
264
|
+
expect(config.domains[0].pages).toEqual(['login']);
|
|
265
|
+
expect(config.domains[0].tables).toEqual(['users']);
|
|
266
|
+
expect(config.domains[0].allowedImportsFrom).toEqual(['shared']);
|
|
267
|
+
expect(config.domains[1].name).toBe('billing');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('defaults to empty array when domains not specified', () => {
|
|
271
|
+
writeConfig(`
|
|
272
|
+
project:
|
|
273
|
+
name: no-domains
|
|
274
|
+
`);
|
|
275
|
+
const config = getConfig();
|
|
276
|
+
expect(config.domains).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// -------------------------------------------------------
|
|
281
|
+
// 10. Config with rules array
|
|
282
|
+
// -------------------------------------------------------
|
|
283
|
+
describe('rules array', () => {
|
|
284
|
+
it('parses rules from config', () => {
|
|
285
|
+
writeConfig(`
|
|
286
|
+
project:
|
|
287
|
+
name: rules-test
|
|
288
|
+
rules:
|
|
289
|
+
- pattern: "src/server/**"
|
|
290
|
+
rules:
|
|
291
|
+
- no-client-imports
|
|
292
|
+
- must-have-tests
|
|
293
|
+
- pattern: "src/components/**"
|
|
294
|
+
rules:
|
|
295
|
+
- no-direct-db-access
|
|
296
|
+
`);
|
|
297
|
+
const config = getConfig();
|
|
298
|
+
expect(config.rules).toHaveLength(2);
|
|
299
|
+
expect(config.rules[0].pattern).toBe('src/server/**');
|
|
300
|
+
expect(config.rules[0].rules).toEqual(['no-client-imports', 'must-have-tests']);
|
|
301
|
+
expect(config.rules[1].pattern).toBe('src/components/**');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('defaults to empty array when rules not specified', () => {
|
|
305
|
+
writeConfig(`
|
|
306
|
+
project:
|
|
307
|
+
name: no-rules
|
|
308
|
+
`);
|
|
309
|
+
const config = getConfig();
|
|
310
|
+
expect(config.rules).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// -------------------------------------------------------
|
|
315
|
+
// 11. Config with analytics section
|
|
316
|
+
// -------------------------------------------------------
|
|
317
|
+
describe('analytics section', () => {
|
|
318
|
+
it('populates analytics when present in config', () => {
|
|
319
|
+
writeConfig(`
|
|
320
|
+
project:
|
|
321
|
+
name: analytics-test
|
|
322
|
+
analytics:
|
|
323
|
+
quality:
|
|
324
|
+
weights:
|
|
325
|
+
clean_commit: 10
|
|
326
|
+
cost:
|
|
327
|
+
currency: GBP
|
|
328
|
+
prompts:
|
|
329
|
+
max_turns_for_success: 5
|
|
330
|
+
`);
|
|
331
|
+
const config = getConfig();
|
|
332
|
+
expect(config.analytics).toBeDefined();
|
|
333
|
+
expect(config.analytics!.quality!.weights!.clean_commit).toBe(10);
|
|
334
|
+
expect(config.analytics!.cost!.currency).toBe('GBP');
|
|
335
|
+
expect(config.analytics!.prompts!.max_turns_for_success).toBe(5);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('is undefined when analytics not specified', () => {
|
|
339
|
+
writeConfig(`
|
|
340
|
+
project:
|
|
341
|
+
name: no-analytics
|
|
342
|
+
`);
|
|
343
|
+
const config = getConfig();
|
|
344
|
+
expect(config.analytics).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// -------------------------------------------------------
|
|
349
|
+
// 12. Config with governance section
|
|
350
|
+
// -------------------------------------------------------
|
|
351
|
+
describe('governance section', () => {
|
|
352
|
+
it('populates governance when present in config', () => {
|
|
353
|
+
writeConfig(`
|
|
354
|
+
project:
|
|
355
|
+
name: gov-test
|
|
356
|
+
governance:
|
|
357
|
+
audit:
|
|
358
|
+
retention_days: 180
|
|
359
|
+
validation:
|
|
360
|
+
realtime: true
|
|
361
|
+
adr:
|
|
362
|
+
template: custom
|
|
363
|
+
storage: filesystem
|
|
364
|
+
`);
|
|
365
|
+
const config = getConfig();
|
|
366
|
+
expect(config.governance).toBeDefined();
|
|
367
|
+
expect(config.governance!.audit!.retention_days).toBe(180);
|
|
368
|
+
expect(config.governance!.validation!.realtime).toBe(true);
|
|
369
|
+
expect(config.governance!.adr!.template).toBe('custom');
|
|
370
|
+
expect(config.governance!.adr!.storage).toBe('filesystem');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('is undefined when governance not specified', () => {
|
|
374
|
+
writeConfig(`
|
|
375
|
+
project:
|
|
376
|
+
name: no-gov
|
|
377
|
+
`);
|
|
378
|
+
const config = getConfig();
|
|
379
|
+
expect(config.governance).toBeUndefined();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// -------------------------------------------------------
|
|
384
|
+
// 13. Config with security section
|
|
385
|
+
// -------------------------------------------------------
|
|
386
|
+
describe('security section', () => {
|
|
387
|
+
it('populates security when present in config', () => {
|
|
388
|
+
writeConfig(`
|
|
389
|
+
project:
|
|
390
|
+
name: sec-test
|
|
391
|
+
security:
|
|
392
|
+
auto_score_on_edit: false
|
|
393
|
+
score_threshold_alert: 80
|
|
394
|
+
patterns:
|
|
395
|
+
- pattern: 'eval\('
|
|
396
|
+
severity: critical
|
|
397
|
+
category: injection
|
|
398
|
+
description: No eval
|
|
399
|
+
`);
|
|
400
|
+
const config = getConfig();
|
|
401
|
+
expect(config.security).toBeDefined();
|
|
402
|
+
expect(config.security!.auto_score_on_edit).toBe(false);
|
|
403
|
+
expect(config.security!.score_threshold_alert).toBe(80);
|
|
404
|
+
expect(config.security!.patterns).toHaveLength(1);
|
|
405
|
+
expect(config.security!.patterns![0].severity).toBe('critical');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('is undefined when security not specified', () => {
|
|
409
|
+
writeConfig(`
|
|
410
|
+
project:
|
|
411
|
+
name: no-sec
|
|
412
|
+
`);
|
|
413
|
+
const config = getConfig();
|
|
414
|
+
expect(config.security).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// -------------------------------------------------------
|
|
419
|
+
// 14. Config with team section
|
|
420
|
+
// -------------------------------------------------------
|
|
421
|
+
describe('team section', () => {
|
|
422
|
+
it('populates team when present in config', () => {
|
|
423
|
+
writeConfig(`
|
|
424
|
+
project:
|
|
425
|
+
name: team-test
|
|
426
|
+
team:
|
|
427
|
+
enabled: true
|
|
428
|
+
sync_backend: supabase
|
|
429
|
+
developer_id: dev-42
|
|
430
|
+
share_by_default: true
|
|
431
|
+
`);
|
|
432
|
+
const config = getConfig();
|
|
433
|
+
expect(config.team).toBeDefined();
|
|
434
|
+
expect(config.team!.enabled).toBe(true);
|
|
435
|
+
expect(config.team!.sync_backend).toBe('supabase');
|
|
436
|
+
expect(config.team!.developer_id).toBe('dev-42');
|
|
437
|
+
expect(config.team!.share_by_default).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('is undefined when team not specified', () => {
|
|
441
|
+
writeConfig(`
|
|
442
|
+
project:
|
|
443
|
+
name: no-team
|
|
444
|
+
`);
|
|
445
|
+
const config = getConfig();
|
|
446
|
+
expect(config.team).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// -------------------------------------------------------
|
|
451
|
+
// 15. Config with regression section
|
|
452
|
+
// -------------------------------------------------------
|
|
453
|
+
describe('regression section', () => {
|
|
454
|
+
it('populates regression when present in config', () => {
|
|
455
|
+
writeConfig(`
|
|
456
|
+
project:
|
|
457
|
+
name: reg-test
|
|
458
|
+
regression:
|
|
459
|
+
test_runner: vitest
|
|
460
|
+
test_patterns:
|
|
461
|
+
- "**/*.spec.ts"
|
|
462
|
+
health_thresholds:
|
|
463
|
+
healthy: 95
|
|
464
|
+
warning: 70
|
|
465
|
+
`);
|
|
466
|
+
const config = getConfig();
|
|
467
|
+
expect(config.regression).toBeDefined();
|
|
468
|
+
expect(config.regression!.test_runner).toBe('vitest');
|
|
469
|
+
expect(config.regression!.test_patterns).toEqual(['**/*.spec.ts']);
|
|
470
|
+
expect(config.regression!.health_thresholds!.healthy).toBe(95);
|
|
471
|
+
expect(config.regression!.health_thresholds!.warning).toBe(70);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('is undefined when regression not specified', () => {
|
|
475
|
+
writeConfig(`
|
|
476
|
+
project:
|
|
477
|
+
name: no-reg
|
|
478
|
+
`);
|
|
479
|
+
const config = getConfig();
|
|
480
|
+
expect(config.regression).toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// -------------------------------------------------------
|
|
485
|
+
// 16. Config with cloud section
|
|
486
|
+
// -------------------------------------------------------
|
|
487
|
+
describe('cloud section', () => {
|
|
488
|
+
it('populates cloud when present in config', () => {
|
|
489
|
+
writeConfig(`
|
|
490
|
+
project:
|
|
491
|
+
name: cloud-test
|
|
492
|
+
cloud:
|
|
493
|
+
enabled: true
|
|
494
|
+
endpoint: https://api.example.com
|
|
495
|
+
sync:
|
|
496
|
+
memory: true
|
|
497
|
+
analytics: false
|
|
498
|
+
audit: true
|
|
499
|
+
`);
|
|
500
|
+
const config = getConfig();
|
|
501
|
+
expect(config.cloud).toBeDefined();
|
|
502
|
+
expect(config.cloud!.enabled).toBe(true);
|
|
503
|
+
expect(config.cloud!.endpoint).toBe('https://api.example.com');
|
|
504
|
+
expect(config.cloud!.sync!.memory).toBe(true);
|
|
505
|
+
expect(config.cloud!.sync!.analytics).toBe(false);
|
|
506
|
+
expect(config.cloud!.sync!.audit).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('is undefined when cloud not specified', () => {
|
|
510
|
+
writeConfig(`
|
|
511
|
+
project:
|
|
512
|
+
name: no-cloud
|
|
513
|
+
`);
|
|
514
|
+
const config = getConfig();
|
|
515
|
+
expect(config.cloud).toBeUndefined();
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// -------------------------------------------------------
|
|
520
|
+
// 17. Config with paths.aliases
|
|
521
|
+
// -------------------------------------------------------
|
|
522
|
+
describe('paths.aliases', () => {
|
|
523
|
+
it('parses custom path aliases', () => {
|
|
524
|
+
writeConfig(`
|
|
525
|
+
project:
|
|
526
|
+
name: alias-test
|
|
527
|
+
paths:
|
|
528
|
+
source: src
|
|
529
|
+
aliases:
|
|
530
|
+
"@": src
|
|
531
|
+
"@components": src/components
|
|
532
|
+
"@utils": src/utils
|
|
533
|
+
`);
|
|
534
|
+
const config = getConfig();
|
|
535
|
+
expect(config.paths.aliases).toEqual({
|
|
536
|
+
'@': 'src',
|
|
537
|
+
'@components': 'src/components',
|
|
538
|
+
'@utils': 'src/utils',
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('resolves aliases in getResolvedPaths().pathAlias', () => {
|
|
543
|
+
writeConfig(`
|
|
544
|
+
project:
|
|
545
|
+
name: alias-resolve
|
|
546
|
+
paths:
|
|
547
|
+
source: src
|
|
548
|
+
aliases:
|
|
549
|
+
"@lib": lib
|
|
550
|
+
"@shared": packages/shared
|
|
551
|
+
`);
|
|
552
|
+
const paths = getResolvedPaths();
|
|
553
|
+
const root = getProjectRoot();
|
|
554
|
+
expect(paths.pathAlias['@lib']).toBe(resolve(root, 'lib'));
|
|
555
|
+
expect(paths.pathAlias['@shared']).toBe(resolve(root, 'packages/shared'));
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// -------------------------------------------------------
|
|
560
|
+
// 18. getResolvedPaths() includes extensions and indexFiles
|
|
561
|
+
// -------------------------------------------------------
|
|
562
|
+
describe('getResolvedPaths() extensions and indexFiles', () => {
|
|
563
|
+
it('includes the standard file extensions', () => {
|
|
564
|
+
writeConfig(`
|
|
565
|
+
project:
|
|
566
|
+
name: ext-test
|
|
567
|
+
`);
|
|
568
|
+
const paths = getResolvedPaths();
|
|
569
|
+
expect(paths.extensions).toEqual(['.ts', '.tsx', '.js', '.jsx']);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('includes the standard index file names', () => {
|
|
573
|
+
writeConfig(`
|
|
574
|
+
project:
|
|
575
|
+
name: idx-test
|
|
576
|
+
`);
|
|
577
|
+
const paths = getResolvedPaths();
|
|
578
|
+
expect(paths.indexFiles).toEqual(['index.ts', 'index.tsx', 'index.js', 'index.jsx']);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('extensions and indexFiles are readonly tuples', () => {
|
|
582
|
+
writeConfig(`
|
|
583
|
+
project:
|
|
584
|
+
name: tuple-test
|
|
585
|
+
`);
|
|
586
|
+
const paths = getResolvedPaths();
|
|
587
|
+
expect(paths.extensions).toHaveLength(4);
|
|
588
|
+
expect(paths.indexFiles).toHaveLength(4);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// -------------------------------------------------------
|
|
593
|
+
// 19. Config with knownMismatches
|
|
594
|
+
// -------------------------------------------------------
|
|
595
|
+
describe('knownMismatches', () => {
|
|
596
|
+
it('parses knownMismatches from config', () => {
|
|
597
|
+
writeConfig(`
|
|
598
|
+
project:
|
|
599
|
+
name: mismatch-test
|
|
600
|
+
knownMismatches:
|
|
601
|
+
imports:
|
|
602
|
+
"src/legacy.ts": "Uses old import style intentionally"
|
|
603
|
+
naming:
|
|
604
|
+
"src/XMLParser.ts": "Third-party convention"
|
|
605
|
+
`);
|
|
606
|
+
const config = getConfig();
|
|
607
|
+
expect(config.knownMismatches).toBeDefined();
|
|
608
|
+
expect(config.knownMismatches!.imports['src/legacy.ts']).toBe('Uses old import style intentionally');
|
|
609
|
+
expect(config.knownMismatches!.naming['src/XMLParser.ts']).toBe('Third-party convention');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('is undefined when knownMismatches not specified', () => {
|
|
613
|
+
writeConfig(`
|
|
614
|
+
project:
|
|
615
|
+
name: no-mismatches
|
|
616
|
+
`);
|
|
617
|
+
const config = getConfig();
|
|
618
|
+
expect(config.knownMismatches).toBeUndefined();
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// -------------------------------------------------------
|
|
623
|
+
// 20. Config with accessScopes
|
|
624
|
+
// -------------------------------------------------------
|
|
625
|
+
describe('accessScopes', () => {
|
|
626
|
+
it('parses accessScopes from config', () => {
|
|
627
|
+
writeConfig(`
|
|
628
|
+
project:
|
|
629
|
+
name: scopes-test
|
|
630
|
+
accessScopes:
|
|
631
|
+
- read:code
|
|
632
|
+
- write:config
|
|
633
|
+
- admin:users
|
|
634
|
+
`);
|
|
635
|
+
const config = getConfig();
|
|
636
|
+
expect(config.accessScopes).toBeDefined();
|
|
637
|
+
expect(config.accessScopes).toEqual(['read:code', 'write:config', 'admin:users']);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('is undefined when accessScopes not specified', () => {
|
|
641
|
+
writeConfig(`
|
|
642
|
+
project:
|
|
643
|
+
name: no-scopes
|
|
644
|
+
`);
|
|
645
|
+
const config = getConfig();
|
|
646
|
+
expect(config.accessScopes).toBeUndefined();
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// -------------------------------------------------------
|
|
651
|
+
// Additional: getResolvedPaths() fixed path resolution
|
|
652
|
+
// -------------------------------------------------------
|
|
653
|
+
describe('getResolvedPaths() fixed paths', () => {
|
|
654
|
+
it('resolves codegraphDbPath under .codegraph/', () => {
|
|
655
|
+
writeConfig(`
|
|
656
|
+
project:
|
|
657
|
+
name: fixed-paths
|
|
658
|
+
`);
|
|
659
|
+
const paths = getResolvedPaths();
|
|
660
|
+
const root = getProjectRoot();
|
|
661
|
+
expect(paths.codegraphDbPath).toBe(resolve(root, '.codegraph/codegraph.db'));
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('resolves dataDbPath under .massu/', () => {
|
|
665
|
+
writeConfig(`
|
|
666
|
+
project:
|
|
667
|
+
name: fixed-paths
|
|
668
|
+
`);
|
|
669
|
+
const paths = getResolvedPaths();
|
|
670
|
+
const root = getProjectRoot();
|
|
671
|
+
expect(paths.dataDbPath).toBe(resolve(root, '.massu/data.db'));
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('resolves memoryDbPath under .massu/', () => {
|
|
675
|
+
writeConfig(`
|
|
676
|
+
project:
|
|
677
|
+
name: fixed-paths
|
|
678
|
+
`);
|
|
679
|
+
const paths = getResolvedPaths();
|
|
680
|
+
const root = getProjectRoot();
|
|
681
|
+
expect(paths.memoryDbPath).toBe(resolve(root, '.massu/memory.db'));
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('resolves patternsDir and claudeMdPath under .claude/', () => {
|
|
685
|
+
writeConfig(`
|
|
686
|
+
project:
|
|
687
|
+
name: fixed-paths
|
|
688
|
+
`);
|
|
689
|
+
const paths = getResolvedPaths();
|
|
690
|
+
const root = getProjectRoot();
|
|
691
|
+
expect(paths.patternsDir).toBe(resolve(root, '.claude/patterns'));
|
|
692
|
+
expect(paths.claudeMdPath).toBe(resolve(root, '.claude/CLAUDE.md'));
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('resolves docsMapPath under .massu/', () => {
|
|
696
|
+
writeConfig(`
|
|
697
|
+
project:
|
|
698
|
+
name: fixed-paths
|
|
699
|
+
`);
|
|
700
|
+
const paths = getResolvedPaths();
|
|
701
|
+
const root = getProjectRoot();
|
|
702
|
+
expect(paths.docsMapPath).toBe(resolve(root, '.massu/docs-map.json'));
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// -------------------------------------------------------
|
|
707
|
+
// Additional: project.root resolution
|
|
708
|
+
// -------------------------------------------------------
|
|
709
|
+
describe('project.root resolution', () => {
|
|
710
|
+
it('uses auto root when project.root is auto', () => {
|
|
711
|
+
writeConfig(`
|
|
712
|
+
project:
|
|
713
|
+
name: auto-root
|
|
714
|
+
root: auto
|
|
715
|
+
`);
|
|
716
|
+
const config = getConfig();
|
|
717
|
+
const root = getProjectRoot();
|
|
718
|
+
expect(config.project.root).toBe(root);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('resolves relative project.root against project root', () => {
|
|
722
|
+
writeConfig(`
|
|
723
|
+
project:
|
|
724
|
+
name: relative-root
|
|
725
|
+
root: packages/core
|
|
726
|
+
`);
|
|
727
|
+
const config = getConfig();
|
|
728
|
+
const root = getProjectRoot();
|
|
729
|
+
expect(config.project.root).toBe(resolve(root, 'packages/core'));
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
});
|