@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,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
+ });