@massu/core 0.1.1 → 0.1.2
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 +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -1,325 +0,0 @@
|
|
|
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
|
-
getDependencyToolDefinitions,
|
|
10
|
-
isDependencyTool,
|
|
11
|
-
calculateDepRisk,
|
|
12
|
-
getInstalledPackages,
|
|
13
|
-
storeAssessment,
|
|
14
|
-
getPreviousRemovals,
|
|
15
|
-
handleDependencyToolCall,
|
|
16
|
-
} from '../dependency-scorer.ts';
|
|
17
|
-
|
|
18
|
-
function createTestDb(): Database.Database {
|
|
19
|
-
const db = new Database(':memory:');
|
|
20
|
-
db.exec(`
|
|
21
|
-
CREATE TABLE dependency_assessments (
|
|
22
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
-
package_name TEXT NOT NULL,
|
|
24
|
-
version TEXT,
|
|
25
|
-
risk_score INTEGER NOT NULL DEFAULT 0,
|
|
26
|
-
vulnerabilities INTEGER NOT NULL DEFAULT 0,
|
|
27
|
-
last_publish_days INTEGER,
|
|
28
|
-
weekly_downloads INTEGER,
|
|
29
|
-
license TEXT,
|
|
30
|
-
bundle_size_kb INTEGER,
|
|
31
|
-
previous_removals INTEGER NOT NULL DEFAULT 0,
|
|
32
|
-
assessed_at TEXT DEFAULT (datetime('now'))
|
|
33
|
-
);
|
|
34
|
-
`);
|
|
35
|
-
return db;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('dependency-scorer', () => {
|
|
39
|
-
let db: Database.Database;
|
|
40
|
-
const testDir = '/tmp/dependency-scorer-test';
|
|
41
|
-
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
db = createTestDb();
|
|
44
|
-
try {
|
|
45
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
46
|
-
} catch { /* ignore */ }
|
|
47
|
-
mkdirSync(testDir, { recursive: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
db.close();
|
|
52
|
-
try {
|
|
53
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
54
|
-
} catch { /* ignore */ }
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('getDependencyToolDefinitions', () => {
|
|
58
|
-
it('returns 2 tool definitions', () => {
|
|
59
|
-
const tools = getDependencyToolDefinitions();
|
|
60
|
-
expect(tools).toHaveLength(2);
|
|
61
|
-
expect(tools.map(t => t.name.split('_').slice(-2).join('_'))).toEqual([
|
|
62
|
-
'dep_score',
|
|
63
|
-
'dep_alternatives',
|
|
64
|
-
]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('dep_score requires package_name', () => {
|
|
68
|
-
const tools = getDependencyToolDefinitions();
|
|
69
|
-
const scoreTool = tools.find(t => t.name.endsWith('_dep_score'));
|
|
70
|
-
expect(scoreTool?.inputSchema.required).toEqual(['package_name']);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('isDependencyTool', () => {
|
|
75
|
-
it('returns true for dependency tool names', () => {
|
|
76
|
-
expect(isDependencyTool('massu_dep_score')).toBe(true);
|
|
77
|
-
expect(isDependencyTool('massu_dep_alternatives')).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('returns false for non-dependency tool names', () => {
|
|
81
|
-
expect(isDependencyTool('massu_security_score')).toBe(false);
|
|
82
|
-
expect(isDependencyTool('massu_unknown')).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('calculateDepRisk', () => {
|
|
87
|
-
it('returns 0 for safe package', () => {
|
|
88
|
-
const risk = calculateDepRisk({
|
|
89
|
-
vulnerabilities: 0,
|
|
90
|
-
lastPublishDays: 30,
|
|
91
|
-
weeklyDownloads: 1000000,
|
|
92
|
-
license: 'MIT',
|
|
93
|
-
bundleSizeKb: 50,
|
|
94
|
-
previousRemovals: 0,
|
|
95
|
-
});
|
|
96
|
-
expect(risk).toBe(0);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('adds risk for vulnerabilities', () => {
|
|
100
|
-
const risk = calculateDepRisk({
|
|
101
|
-
vulnerabilities: 2,
|
|
102
|
-
lastPublishDays: null,
|
|
103
|
-
weeklyDownloads: null,
|
|
104
|
-
license: null,
|
|
105
|
-
bundleSizeKb: null,
|
|
106
|
-
previousRemovals: 0,
|
|
107
|
-
});
|
|
108
|
-
expect(risk).toBeGreaterThanOrEqual(30); // 2 * 15 = 30
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('adds risk for stale packages', () => {
|
|
112
|
-
const risk = calculateDepRisk({
|
|
113
|
-
vulnerabilities: 0,
|
|
114
|
-
lastPublishDays: 800, // Over 2 years
|
|
115
|
-
weeklyDownloads: null,
|
|
116
|
-
license: null,
|
|
117
|
-
bundleSizeKb: null,
|
|
118
|
-
previousRemovals: 0,
|
|
119
|
-
});
|
|
120
|
-
expect(risk).toBeGreaterThan(0);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('adds risk for low popularity', () => {
|
|
124
|
-
const risk = calculateDepRisk({
|
|
125
|
-
vulnerabilities: 0,
|
|
126
|
-
lastPublishDays: null,
|
|
127
|
-
weeklyDownloads: 50, // Very low
|
|
128
|
-
license: null,
|
|
129
|
-
bundleSizeKb: null,
|
|
130
|
-
previousRemovals: 0,
|
|
131
|
-
});
|
|
132
|
-
expect(risk).toBeGreaterThan(0);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('adds risk for restrictive licenses', () => {
|
|
136
|
-
const gplRisk = calculateDepRisk({
|
|
137
|
-
vulnerabilities: 0,
|
|
138
|
-
lastPublishDays: null,
|
|
139
|
-
weeklyDownloads: null,
|
|
140
|
-
license: 'GPL-3.0',
|
|
141
|
-
bundleSizeKb: null,
|
|
142
|
-
previousRemovals: 0,
|
|
143
|
-
});
|
|
144
|
-
expect(gplRisk).toBeGreaterThan(0);
|
|
145
|
-
|
|
146
|
-
const agplRisk = calculateDepRisk({
|
|
147
|
-
vulnerabilities: 0,
|
|
148
|
-
lastPublishDays: null,
|
|
149
|
-
weeklyDownloads: null,
|
|
150
|
-
license: 'AGPL',
|
|
151
|
-
bundleSizeKb: null,
|
|
152
|
-
previousRemovals: 0,
|
|
153
|
-
});
|
|
154
|
-
expect(agplRisk).toBeGreaterThan(0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('adds risk for unknown license', () => {
|
|
158
|
-
const risk = calculateDepRisk({
|
|
159
|
-
vulnerabilities: 0,
|
|
160
|
-
lastPublishDays: null,
|
|
161
|
-
weeklyDownloads: null,
|
|
162
|
-
license: null,
|
|
163
|
-
bundleSizeKb: null,
|
|
164
|
-
previousRemovals: 0,
|
|
165
|
-
});
|
|
166
|
-
expect(risk).toBe(5); // Unknown license penalty
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('adds risk for previous removals', () => {
|
|
170
|
-
const risk = calculateDepRisk({
|
|
171
|
-
vulnerabilities: 0,
|
|
172
|
-
lastPublishDays: null,
|
|
173
|
-
weeklyDownloads: null,
|
|
174
|
-
license: null,
|
|
175
|
-
bundleSizeKb: null,
|
|
176
|
-
previousRemovals: 3,
|
|
177
|
-
});
|
|
178
|
-
expect(risk).toBeGreaterThan(5); // 5 (unknown license) + 15 (3 * 5)
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('caps risk at 100', () => {
|
|
182
|
-
const risk = calculateDepRisk({
|
|
183
|
-
vulnerabilities: 10,
|
|
184
|
-
lastPublishDays: 1000,
|
|
185
|
-
weeklyDownloads: 10,
|
|
186
|
-
license: 'GPL',
|
|
187
|
-
bundleSizeKb: null,
|
|
188
|
-
previousRemovals: 5,
|
|
189
|
-
});
|
|
190
|
-
expect(risk).toBeLessThanOrEqual(100);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe('getInstalledPackages', () => {
|
|
195
|
-
it('returns empty map for missing package.json', () => {
|
|
196
|
-
const packages = getInstalledPackages(testDir);
|
|
197
|
-
expect(packages.size).toBe(0);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('parses dependencies from package.json', () => {
|
|
201
|
-
const pkgPath = join(testDir, 'package.json');
|
|
202
|
-
writeFileSync(pkgPath, JSON.stringify({
|
|
203
|
-
dependencies: {
|
|
204
|
-
'express': '^4.18.0',
|
|
205
|
-
'lodash': '^4.17.21',
|
|
206
|
-
},
|
|
207
|
-
devDependencies: {
|
|
208
|
-
'vitest': '^1.0.0',
|
|
209
|
-
},
|
|
210
|
-
}));
|
|
211
|
-
|
|
212
|
-
const packages = getInstalledPackages(testDir);
|
|
213
|
-
expect(packages.size).toBe(3);
|
|
214
|
-
expect(packages.get('express')).toBe('^4.18.0');
|
|
215
|
-
expect(packages.get('lodash')).toBe('^4.17.21');
|
|
216
|
-
expect(packages.get('vitest')).toBe('^1.0.0');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('handles invalid JSON gracefully', () => {
|
|
220
|
-
const pkgPath = join(testDir, 'package.json');
|
|
221
|
-
writeFileSync(pkgPath, '{ invalid json');
|
|
222
|
-
|
|
223
|
-
const packages = getInstalledPackages(testDir);
|
|
224
|
-
expect(packages.size).toBe(0);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('handles missing dependencies field', () => {
|
|
228
|
-
const pkgPath = join(testDir, 'package.json');
|
|
229
|
-
writeFileSync(pkgPath, JSON.stringify({ name: 'test' }));
|
|
230
|
-
|
|
231
|
-
const packages = getInstalledPackages(testDir);
|
|
232
|
-
expect(packages.size).toBe(0);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('storeAssessment', () => {
|
|
237
|
-
it('stores dependency assessment', () => {
|
|
238
|
-
storeAssessment(db, 'express', '4.18.0', 25, {
|
|
239
|
-
vulnerabilities: 1,
|
|
240
|
-
lastPublishDays: 120,
|
|
241
|
-
weeklyDownloads: 50000,
|
|
242
|
-
license: 'MIT',
|
|
243
|
-
bundleSizeKb: 200,
|
|
244
|
-
previousRemovals: 0,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const row = db.prepare('SELECT * FROM dependency_assessments WHERE package_name = ?').get('express') as Record<string, unknown>;
|
|
248
|
-
expect(row.package_name).toBe('express');
|
|
249
|
-
expect(row.version).toBe('4.18.0');
|
|
250
|
-
expect(row.risk_score).toBe(25);
|
|
251
|
-
expect(row.vulnerabilities).toBe(1);
|
|
252
|
-
expect(row.weekly_downloads).toBe(50000);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('getPreviousRemovals', () => {
|
|
257
|
-
it('returns 0 for package never assessed', () => {
|
|
258
|
-
const count = getPreviousRemovals(db, 'unknown-package');
|
|
259
|
-
expect(count).toBe(0);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('returns max previous_removals value', () => {
|
|
263
|
-
storeAssessment(db, 'moment', null, 50, {
|
|
264
|
-
vulnerabilities: 0,
|
|
265
|
-
lastPublishDays: null,
|
|
266
|
-
weeklyDownloads: null,
|
|
267
|
-
license: null,
|
|
268
|
-
bundleSizeKb: null,
|
|
269
|
-
previousRemovals: 2,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
storeAssessment(db, 'moment', null, 60, {
|
|
273
|
-
vulnerabilities: 0,
|
|
274
|
-
lastPublishDays: null,
|
|
275
|
-
weeklyDownloads: null,
|
|
276
|
-
license: null,
|
|
277
|
-
bundleSizeKb: null,
|
|
278
|
-
previousRemovals: 3,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const count = getPreviousRemovals(db, 'moment');
|
|
282
|
-
expect(count).toBe(3);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
describe('handleDependencyToolCall', () => {
|
|
287
|
-
it('handles dep_score for package', () => {
|
|
288
|
-
const result = handleDependencyToolCall('massu_dep_score', { package_name: 'express' }, db);
|
|
289
|
-
const text = result.content[0].text;
|
|
290
|
-
expect(text).toContain('Dependency Check: express');
|
|
291
|
-
expect(text).toContain('Risk Score');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('handles dep_score with installed package', () => {
|
|
295
|
-
const pkgPath = join(testDir, 'package.json');
|
|
296
|
-
writeFileSync(pkgPath, JSON.stringify({
|
|
297
|
-
dependencies: { 'express': '^4.18.0' },
|
|
298
|
-
}));
|
|
299
|
-
|
|
300
|
-
// Mock getConfig to return testDir
|
|
301
|
-
const result = handleDependencyToolCall('massu_dep_score', { package_name: 'express' }, db);
|
|
302
|
-
const text = result.content[0].text;
|
|
303
|
-
expect(text).toContain('express');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('handles dep_alternatives for known package', () => {
|
|
307
|
-
const result = handleDependencyToolCall('massu_dep_alternatives', { package_name: 'moment' }, db);
|
|
308
|
-
const text = result.content[0].text;
|
|
309
|
-
expect(text).toContain('Alternatives to: moment');
|
|
310
|
-
expect(text).toContain('date-fns');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('handles dep_alternatives for unknown package', () => {
|
|
314
|
-
const result = handleDependencyToolCall('massu_dep_alternatives', { package_name: 'unknown-package' }, db);
|
|
315
|
-
const text = result.content[0].text;
|
|
316
|
-
expect(text).toContain('No known alternative mappings');
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('handles unknown tool name', () => {
|
|
320
|
-
const result = handleDependencyToolCall('massu_dep_unknown', {}, db);
|
|
321
|
-
const text = result.content[0].text;
|
|
322
|
-
expect(text).toContain('Unknown dependency tool');
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
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, beforeAll } from 'vitest';
|
|
5
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
6
|
-
import { resolve, join } from 'path';
|
|
7
|
-
import { getResolvedPaths } from '../config.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Integration tests for docs tools against the real help site.
|
|
11
|
-
* These tests verify the tools work with actual MDX files.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const HELP_SITE_PATH = getResolvedPaths().helpSitePath;
|
|
15
|
-
const DOCS_MAP_PATH = getResolvedPaths().docsMapPath;
|
|
16
|
-
|
|
17
|
-
describe('docs integration: real help site', () => {
|
|
18
|
-
let docsMap: any;
|
|
19
|
-
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
// Skip tests if help site is not available
|
|
22
|
-
if (!existsSync(HELP_SITE_PATH)) {
|
|
23
|
-
console.warn('Help site not found at', HELP_SITE_PATH);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
docsMap = JSON.parse(readFileSync(DOCS_MAP_PATH, 'utf-8'));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('help site exists and has pages directory', () => {
|
|
30
|
-
if (!existsSync(HELP_SITE_PATH)) return; // Skip when help site not available
|
|
31
|
-
expect(existsSync(resolve(HELP_SITE_PATH, 'pages'))).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('all mapped help pages exist', () => {
|
|
35
|
-
if (!docsMap) return;
|
|
36
|
-
|
|
37
|
-
const missing: string[] = [];
|
|
38
|
-
for (const mapping of docsMap.mappings) {
|
|
39
|
-
const fullPath = resolve(HELP_SITE_PATH, mapping.helpPage);
|
|
40
|
-
if (!existsSync(fullPath)) {
|
|
41
|
-
missing.push(`${mapping.id}: ${mapping.helpPage}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
expect(missing).toEqual([]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('coverage report includes all 8+ top-level categories', () => {
|
|
48
|
-
if (!docsMap) return;
|
|
49
|
-
|
|
50
|
-
const topLevelPages = docsMap.mappings.filter((m: any) =>
|
|
51
|
-
m.helpPage.startsWith('pages/') && !m.helpPage.includes('/index.mdx') ||
|
|
52
|
-
m.helpPage.endsWith('/index.mdx')
|
|
53
|
-
);
|
|
54
|
-
expect(topLevelPages.length).toBeGreaterThanOrEqual(8);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('frontmatter parsing works on real MDX files', () => {
|
|
58
|
-
if (!docsMap) return;
|
|
59
|
-
|
|
60
|
-
// Test a few real MDX files
|
|
61
|
-
const testPages = ['pages/dashboard.mdx', 'pages/users.mdx', 'pages/billing.mdx'];
|
|
62
|
-
for (const page of testPages) {
|
|
63
|
-
const fullPath = resolve(HELP_SITE_PATH, page);
|
|
64
|
-
if (!existsSync(fullPath)) continue;
|
|
65
|
-
|
|
66
|
-
const content = readFileSync(fullPath, 'utf-8');
|
|
67
|
-
|
|
68
|
-
// Check frontmatter exists
|
|
69
|
-
expect(content.startsWith('---')).toBe(true);
|
|
70
|
-
|
|
71
|
-
// Extract frontmatter
|
|
72
|
-
const endIndex = content.indexOf('---', 3);
|
|
73
|
-
expect(endIndex).toBeGreaterThan(3);
|
|
74
|
-
|
|
75
|
-
const frontmatter = content.substring(3, endIndex).trim();
|
|
76
|
-
expect(frontmatter).toContain('lastVerified');
|
|
77
|
-
expect(frontmatter).toContain('status');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('known router change maps to correct help page', () => {
|
|
82
|
-
if (!docsMap) return;
|
|
83
|
-
|
|
84
|
-
// Simulate: dashboard.ts changed, should map to dashboard help page
|
|
85
|
-
const changedFile = 'src/server/api/routers/dashboard.ts';
|
|
86
|
-
const fileName = 'dashboard.ts';
|
|
87
|
-
|
|
88
|
-
let foundMapping = false;
|
|
89
|
-
for (const mapping of docsMap.mappings) {
|
|
90
|
-
if (mapping.routers.includes(fileName)) {
|
|
91
|
-
if (mapping.id === 'dashboard') {
|
|
92
|
-
foundMapping = true;
|
|
93
|
-
// Verify the help page exists
|
|
94
|
-
const helpPath = resolve(HELP_SITE_PATH, mapping.helpPage);
|
|
95
|
-
expect(existsSync(helpPath)).toBe(true);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
expect(foundMapping).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('non-user-facing changes return no affected mappings', () => {
|
|
103
|
-
if (!docsMap) return;
|
|
104
|
-
|
|
105
|
-
const nonUserFiles = [
|
|
106
|
-
'scripts/pattern-scanner.sh',
|
|
107
|
-
'package.json',
|
|
108
|
-
'.gitignore',
|
|
109
|
-
'tsconfig.json',
|
|
110
|
-
'jest.config.js',
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
for (const file of nonUserFiles) {
|
|
114
|
-
const fileName = file.split('/').pop() || '';
|
|
115
|
-
let matched = false;
|
|
116
|
-
|
|
117
|
-
for (const mapping of docsMap.mappings) {
|
|
118
|
-
if (mapping.routers.includes(fileName)) {
|
|
119
|
-
matched = true;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
expect(matched).toBe(false);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('changelog page exists', () => {
|
|
129
|
-
if (!existsSync(HELP_SITE_PATH)) return;
|
|
130
|
-
const changelogPath = resolve(HELP_SITE_PATH, 'pages/changelog.mdx');
|
|
131
|
-
expect(existsSync(changelogPath)).toBe(true);
|
|
132
|
-
|
|
133
|
-
const content = readFileSync(changelogPath, 'utf-8');
|
|
134
|
-
expect(content).toContain('lastVerified');
|
|
135
|
-
expect(content).toContain("What's New");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('_meta.json includes changelog', () => {
|
|
139
|
-
if (!existsSync(HELP_SITE_PATH)) return;
|
|
140
|
-
const metaPath = resolve(HELP_SITE_PATH, 'pages/_meta.json');
|
|
141
|
-
expect(existsSync(metaPath)).toBe(true);
|
|
142
|
-
|
|
143
|
-
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
144
|
-
expect(meta.changelog).toBeDefined();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('all MDX files have lastVerified frontmatter', () => {
|
|
148
|
-
if (!existsSync(HELP_SITE_PATH)) return;
|
|
149
|
-
|
|
150
|
-
function findMdxFiles(dir: string): string[] {
|
|
151
|
-
const files: string[] = [];
|
|
152
|
-
for (const entry of readdirSync(dir)) {
|
|
153
|
-
const fullPath = join(dir, entry);
|
|
154
|
-
const stat = statSync(fullPath);
|
|
155
|
-
if (stat.isDirectory() && entry !== 'node_modules' && entry !== '.next') {
|
|
156
|
-
files.push(...findMdxFiles(fullPath));
|
|
157
|
-
} else if (entry.endsWith('.mdx')) {
|
|
158
|
-
files.push(fullPath);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return files;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const mdxFiles = findMdxFiles(resolve(HELP_SITE_PATH, 'pages'));
|
|
165
|
-
const missingFrontmatter: string[] = [];
|
|
166
|
-
|
|
167
|
-
for (const file of mdxFiles) {
|
|
168
|
-
const content = readFileSync(file, 'utf-8');
|
|
169
|
-
if (!content.includes('lastVerified')) {
|
|
170
|
-
missingFrontmatter.push(file.replace(HELP_SITE_PATH + '/', ''));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
expect(missingFrontmatter).toEqual([]);
|
|
175
|
-
// Expect at least 5 MDX files for a meaningful docs site
|
|
176
|
-
expect(mdxFiles.length).toBeGreaterThanOrEqual(5);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
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, beforeAll } from 'vitest';
|
|
5
|
-
import { readFileSync, existsSync } from 'fs';
|
|
6
|
-
import { resolve } from 'path';
|
|
7
|
-
|
|
8
|
-
// Test the docs-tools module functions
|
|
9
|
-
// We import and test the exported functions directly
|
|
10
|
-
|
|
11
|
-
describe('docs-map.json', () => {
|
|
12
|
-
let docsMap: any;
|
|
13
|
-
|
|
14
|
-
beforeAll(() => {
|
|
15
|
-
const mapPath = resolve(__dirname, '../docs-map.json');
|
|
16
|
-
expect(existsSync(mapPath)).toBe(true);
|
|
17
|
-
docsMap = JSON.parse(readFileSync(mapPath, 'utf-8'));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('has correct version', () => {
|
|
21
|
-
expect(docsMap.version).toBe(1);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('has at least 8 mappings', () => {
|
|
25
|
-
expect(docsMap.mappings.length).toBeGreaterThanOrEqual(8);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('each mapping has required fields', () => {
|
|
29
|
-
for (const mapping of docsMap.mappings) {
|
|
30
|
-
expect(mapping).toHaveProperty('id');
|
|
31
|
-
expect(mapping).toHaveProperty('helpPage');
|
|
32
|
-
expect(mapping).toHaveProperty('appRoutes');
|
|
33
|
-
expect(mapping).toHaveProperty('routers');
|
|
34
|
-
expect(mapping).toHaveProperty('components');
|
|
35
|
-
expect(mapping).toHaveProperty('keywords');
|
|
36
|
-
expect(typeof mapping.id).toBe('string');
|
|
37
|
-
expect(typeof mapping.helpPage).toBe('string');
|
|
38
|
-
expect(Array.isArray(mapping.appRoutes)).toBe(true);
|
|
39
|
-
expect(Array.isArray(mapping.routers)).toBe(true);
|
|
40
|
-
expect(Array.isArray(mapping.components)).toBe(true);
|
|
41
|
-
expect(Array.isArray(mapping.keywords)).toBe(true);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('has unique mapping IDs', () => {
|
|
46
|
-
const ids = docsMap.mappings.map((m: any) => m.id);
|
|
47
|
-
expect(new Set(ids).size).toBe(ids.length);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('has user guide inheritance mapping', () => {
|
|
51
|
-
expect(docsMap.userGuideInheritance).toBeDefined();
|
|
52
|
-
expect(docsMap.userGuideInheritance.examples).toBeDefined();
|
|
53
|
-
expect(Object.keys(docsMap.userGuideInheritance.examples).length).toBeGreaterThanOrEqual(5);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('all user guide parent IDs reference valid mapping IDs', () => {
|
|
57
|
-
const validIds = new Set(docsMap.mappings.map((m: any) => m.id));
|
|
58
|
-
for (const [guide, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
|
|
59
|
-
expect(validIds.has(parentId as string)).toBe(true);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('maps key features correctly', () => {
|
|
64
|
-
const findMapping = (id: string) => docsMap.mappings.find((m: any) => m.id === id);
|
|
65
|
-
|
|
66
|
-
// Dashboard mapping should include dashboard routers
|
|
67
|
-
const dashboard = findMapping('dashboard');
|
|
68
|
-
expect(dashboard).toBeDefined();
|
|
69
|
-
expect(dashboard.routers).toContain('dashboard.ts');
|
|
70
|
-
expect(dashboard.appRoutes.some((r: string) => r.includes('dashboard'))).toBe(true);
|
|
71
|
-
|
|
72
|
-
// Users mapping
|
|
73
|
-
const users = findMapping('users');
|
|
74
|
-
expect(users).toBeDefined();
|
|
75
|
-
expect(users.routers).toContain('users.ts');
|
|
76
|
-
|
|
77
|
-
// Billing mapping
|
|
78
|
-
const billing = findMapping('billing');
|
|
79
|
-
expect(billing).toBeDefined();
|
|
80
|
-
expect(billing.routers).toContain('billing.ts');
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('matchesPattern (glob matching)', () => {
|
|
85
|
-
// Test the glob matching logic used in docs-tools
|
|
86
|
-
function matchesPattern(filePath: string, pattern: string): boolean {
|
|
87
|
-
const regexStr = pattern
|
|
88
|
-
.replace(/\./g, '\\.')
|
|
89
|
-
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
90
|
-
.replace(/\*/g, '[^/]*')
|
|
91
|
-
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
92
|
-
return new RegExp(`^${regexStr}$`).test(filePath);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
it('matches ** glob patterns', () => {
|
|
96
|
-
expect(matchesPattern('src/app/orders/page.tsx', 'src/app/orders/**')).toBe(true);
|
|
97
|
-
expect(matchesPattern('src/app/orders/[id]/page.tsx', 'src/app/orders/**')).toBe(true);
|
|
98
|
-
expect(matchesPattern('src/app/crm/page.tsx', 'src/app/orders/**')).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('matches component patterns', () => {
|
|
102
|
-
expect(matchesPattern('src/components/orders/OrderCard.tsx', 'src/components/orders/**')).toBe(true);
|
|
103
|
-
expect(matchesPattern('src/components/crm/LeadList.tsx', 'src/components/orders/**')).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('matches nested paths', () => {
|
|
107
|
-
expect(matchesPattern('src/app/admin/dashboard/page.tsx', 'src/app/admin/dashboard/**')).toBe(true);
|
|
108
|
-
expect(matchesPattern('src/app/admin/page.tsx', 'src/app/admin/**')).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('findAffectedMappings logic', () => {
|
|
113
|
-
let docsMap: any;
|
|
114
|
-
|
|
115
|
-
beforeAll(() => {
|
|
116
|
-
docsMap = JSON.parse(readFileSync(resolve(__dirname, '../docs-map.json'), 'utf-8'));
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
function findAffectedMappings(changedFiles: string[]): Map<string, string[]> {
|
|
120
|
-
const affected = new Map<string, string[]>();
|
|
121
|
-
const matchesPattern = (filePath: string, pattern: string): boolean => {
|
|
122
|
-
const regexStr = pattern
|
|
123
|
-
.replace(/\./g, '\\.')
|
|
124
|
-
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
125
|
-
.replace(/\*/g, '[^/]*')
|
|
126
|
-
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
127
|
-
return new RegExp(`^${regexStr}$`).test(filePath);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
for (const file of changedFiles) {
|
|
131
|
-
const fileName = file.split('/').pop() || '';
|
|
132
|
-
for (const mapping of docsMap.mappings) {
|
|
133
|
-
let matched = false;
|
|
134
|
-
for (const routePattern of mapping.appRoutes) {
|
|
135
|
-
if (matchesPattern(file, routePattern)) { matched = true; break; }
|
|
136
|
-
}
|
|
137
|
-
if (!matched) {
|
|
138
|
-
for (const router of mapping.routers) {
|
|
139
|
-
if (fileName === router || file.endsWith(`/routers/${router}`)) { matched = true; break; }
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (!matched) {
|
|
143
|
-
for (const compPattern of mapping.components) {
|
|
144
|
-
if (matchesPattern(file, compPattern)) { matched = true; break; }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (matched) {
|
|
148
|
-
const existing = affected.get(mapping.id) || [];
|
|
149
|
-
existing.push(file);
|
|
150
|
-
affected.set(mapping.id, existing);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return affected;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
it('maps router changes to correct help pages', () => {
|
|
158
|
-
const affected = findAffectedMappings(['src/server/api/routers/dashboard.ts']);
|
|
159
|
-
expect(affected.has('dashboard')).toBe(true);
|
|
160
|
-
expect(affected.has('users')).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('maps page changes to correct help pages', () => {
|
|
164
|
-
const affected = findAffectedMappings(['src/app/users/page.tsx']);
|
|
165
|
-
expect(affected.has('users')).toBe(true);
|
|
166
|
-
expect(affected.has('dashboard')).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('maps component changes to correct help pages', () => {
|
|
170
|
-
const affected = findAffectedMappings(['src/components/billing/PlanCard.tsx']);
|
|
171
|
-
expect(affected.has('billing')).toBe(true);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('returns empty for non-user-facing files', () => {
|
|
175
|
-
const affected = findAffectedMappings(['scripts/some-script.sh', 'package.json', '.gitignore']);
|
|
176
|
-
expect(affected.size).toBe(0);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('handles multiple affected mappings', () => {
|
|
180
|
-
const affected = findAffectedMappings([
|
|
181
|
-
'src/server/api/routers/dashboard.ts',
|
|
182
|
-
'src/server/api/routers/users.ts',
|
|
183
|
-
'src/app/billing/page.tsx',
|
|
184
|
-
]);
|
|
185
|
-
expect(affected.has('dashboard')).toBe(true);
|
|
186
|
-
expect(affected.has('users')).toBe(true);
|
|
187
|
-
expect(affected.has('billing')).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('maps notification routers correctly', () => {
|
|
191
|
-
const affected = findAffectedMappings(['src/server/api/routers/notifications.ts']);
|
|
192
|
-
expect(affected.has('notifications')).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('maps integration routers correctly', () => {
|
|
196
|
-
const affected = findAffectedMappings(['src/server/api/routers/integrations.ts']);
|
|
197
|
-
expect(affected.has('integrations')).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
});
|