@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,517 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
// ============================================================
|
|
5
|
-
// validate-features-runner.ts tests
|
|
6
|
-
// The module is a standalone script (no exports). We test its
|
|
7
|
-
// logic by exercising the conditional branches directly in unit
|
|
8
|
-
// tests, mocking better-sqlite3, fs, and config.
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
-
|
|
13
|
-
// ------------------------------------
|
|
14
|
-
// Mock dependencies
|
|
15
|
-
// ------------------------------------
|
|
16
|
-
|
|
17
|
-
vi.mock('fs', async (importOriginal) => {
|
|
18
|
-
const actual = await importOriginal<typeof import('fs')>();
|
|
19
|
-
return {
|
|
20
|
-
...actual,
|
|
21
|
-
existsSync: vi.fn(),
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
vi.mock('../config.ts', () => ({
|
|
26
|
-
getProjectRoot: vi.fn(() => '/home/user/my-project'),
|
|
27
|
-
getResolvedPaths: vi.fn(() => ({
|
|
28
|
-
dataDbPath: '/home/user/my-project/.massu/data.db',
|
|
29
|
-
memoryDbPath: '/home/user/my-project/.massu/memory.db',
|
|
30
|
-
codegraphDbPath: '/home/user/my-project/.massu/codegraph.db',
|
|
31
|
-
srcDir: '/home/user/my-project/src',
|
|
32
|
-
pathAlias: {},
|
|
33
|
-
extensions: ['.ts'],
|
|
34
|
-
indexFiles: ['index.ts'],
|
|
35
|
-
patternsDir: '/home/user/my-project/.claude/patterns',
|
|
36
|
-
claudeMdPath: '/home/user/my-project/.claude/CLAUDE.md',
|
|
37
|
-
docsMapPath: '/home/user/my-project/.massu/docs-map.json',
|
|
38
|
-
helpSitePath: '/home/user/my-project/help',
|
|
39
|
-
prismaSchemaPath: '/home/user/my-project/prisma/schema.prisma',
|
|
40
|
-
rootRouterPath: '/home/user/my-project/src/server/api/root.ts',
|
|
41
|
-
routersDir: '/home/user/my-project/src/server/api/routers',
|
|
42
|
-
})),
|
|
43
|
-
getConfig: vi.fn(() => ({
|
|
44
|
-
toolPrefix: 'massu',
|
|
45
|
-
project: { name: 'my-project', root: '/home/user/my-project' },
|
|
46
|
-
})),
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
// Import mocks
|
|
50
|
-
import { existsSync } from 'fs';
|
|
51
|
-
import { getProjectRoot, getResolvedPaths } from '../config.ts';
|
|
52
|
-
|
|
53
|
-
const mockExistsSync = vi.mocked(existsSync);
|
|
54
|
-
const mockGetProjectRoot = vi.mocked(getProjectRoot);
|
|
55
|
-
const mockGetResolvedPaths = vi.mocked(getResolvedPaths);
|
|
56
|
-
|
|
57
|
-
// ------------------------------------
|
|
58
|
-
// Helper: build a mock DB statement
|
|
59
|
-
// ------------------------------------
|
|
60
|
-
|
|
61
|
-
function makeStmt(returnValue: unknown) {
|
|
62
|
-
return { get: vi.fn().mockReturnValue(returnValue), all: vi.fn().mockReturnValue([]) };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function makeAllStmt(returnValue: unknown[]) {
|
|
66
|
-
return { get: vi.fn(), all: vi.fn().mockReturnValue(returnValue) };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ------------------------------------
|
|
70
|
-
// Scenario: no data DB found
|
|
71
|
-
// ------------------------------------
|
|
72
|
-
|
|
73
|
-
describe('scenario: no data DB found', () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
vi.clearAllMocks();
|
|
76
|
-
mockExistsSync.mockReturnValue(false);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('exits 0 when dbPath does not exist', () => {
|
|
80
|
-
const dbPath = mockGetResolvedPaths().dataDbPath;
|
|
81
|
-
const dbExists = mockExistsSync(dbPath);
|
|
82
|
-
expect(dbExists).toBe(false);
|
|
83
|
-
|
|
84
|
-
// The script would call process.exit(0) here — we verify the condition
|
|
85
|
-
expect(dbPath).toBe('/home/user/my-project/.massu/data.db');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('prints skipping message when no db found', () => {
|
|
89
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
90
|
-
|
|
91
|
-
const dbPath = mockGetResolvedPaths().dataDbPath;
|
|
92
|
-
if (!mockExistsSync(dbPath)) {
|
|
93
|
-
console.log('Sentinel: No data DB found - skipping feature validation (run sync first)');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
97
|
-
'Sentinel: No data DB found - skipping feature validation (run sync first)'
|
|
98
|
-
);
|
|
99
|
-
consoleSpy.mockRestore();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ------------------------------------
|
|
104
|
-
// Scenario: sentinel tables do not exist
|
|
105
|
-
// ------------------------------------
|
|
106
|
-
|
|
107
|
-
describe('scenario: no sentinel tables', () => {
|
|
108
|
-
beforeEach(() => {
|
|
109
|
-
vi.clearAllMocks();
|
|
110
|
-
mockExistsSync.mockReturnValue(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('exits 0 when massu_sentinel table is missing', () => {
|
|
114
|
-
const tableExistsStmt = makeStmt(undefined); // undefined = no table found
|
|
115
|
-
const tableExists = tableExistsStmt.get();
|
|
116
|
-
expect(tableExists).toBeUndefined();
|
|
117
|
-
|
|
118
|
-
// The script would exit(0) and log skipping message
|
|
119
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
120
|
-
if (!tableExists) {
|
|
121
|
-
console.log('Sentinel: Feature registry not initialized - skipping (run sync first)');
|
|
122
|
-
}
|
|
123
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
124
|
-
'Sentinel: Feature registry not initialized - skipping (run sync first)'
|
|
125
|
-
);
|
|
126
|
-
consoleSpy.mockRestore();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('proceeds when massu_sentinel table exists', () => {
|
|
130
|
-
const tableExistsStmt = makeStmt({ name: 'massu_sentinel' });
|
|
131
|
-
const tableExists = tableExistsStmt.get();
|
|
132
|
-
expect(tableExists).toBeTruthy();
|
|
133
|
-
expect((tableExists as { name: string }).name).toBe('massu_sentinel');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// ------------------------------------
|
|
138
|
-
// Scenario: no active features
|
|
139
|
-
// ------------------------------------
|
|
140
|
-
|
|
141
|
-
describe('scenario: no active features', () => {
|
|
142
|
-
beforeEach(() => {
|
|
143
|
-
vi.clearAllMocks();
|
|
144
|
-
mockExistsSync.mockReturnValue(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('exits 0 when active feature count is 0', () => {
|
|
148
|
-
const countStmt = makeStmt({ count: 0 });
|
|
149
|
-
const totalActive = countStmt.get() as { count: number };
|
|
150
|
-
expect(totalActive.count).toBe(0);
|
|
151
|
-
|
|
152
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
153
|
-
if (totalActive.count === 0) {
|
|
154
|
-
console.log('Sentinel: No active features registered - skipping validation');
|
|
155
|
-
}
|
|
156
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
157
|
-
'Sentinel: No active features registered - skipping validation'
|
|
158
|
-
);
|
|
159
|
-
consoleSpy.mockRestore();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('proceeds when active features exist', () => {
|
|
163
|
-
const countStmt = makeStmt({ count: 5 });
|
|
164
|
-
const totalActive = countStmt.get() as { count: number };
|
|
165
|
-
expect(totalActive.count).toBe(5);
|
|
166
|
-
expect(totalActive.count > 0).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ------------------------------------
|
|
171
|
-
// Scenario: all primary components exist
|
|
172
|
-
// ------------------------------------
|
|
173
|
-
|
|
174
|
-
describe('scenario: all features have living primary components', () => {
|
|
175
|
-
beforeEach(() => {
|
|
176
|
-
vi.clearAllMocks();
|
|
177
|
-
mockExistsSync.mockReturnValue(true);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('exits 0 and prints PASS when all component files exist', () => {
|
|
181
|
-
const projectRoot = mockGetProjectRoot();
|
|
182
|
-
const rows = [
|
|
183
|
-
{ feature_key: 'auth.login', title: 'User Login', priority: 'critical', component_file: 'src/Login.tsx' },
|
|
184
|
-
{ feature_key: 'auth.register', title: 'User Register', priority: 'standard', component_file: 'src/Register.tsx' },
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
const allStmt = makeAllStmt(rows);
|
|
188
|
-
const orphanedRows = allStmt.all() as typeof rows;
|
|
189
|
-
|
|
190
|
-
// All files exist
|
|
191
|
-
mockExistsSync.mockImplementation(() => true);
|
|
192
|
-
|
|
193
|
-
const missingFeatures: typeof rows = [];
|
|
194
|
-
for (const row of orphanedRows) {
|
|
195
|
-
const absPath = `${projectRoot}/${row.component_file}`;
|
|
196
|
-
if (!mockExistsSync(absPath)) {
|
|
197
|
-
missingFeatures.push(row);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
expect(missingFeatures).toHaveLength(0);
|
|
202
|
-
|
|
203
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
204
|
-
if (missingFeatures.length === 0) {
|
|
205
|
-
console.log('Sentinel: All active features have living primary components. PASS');
|
|
206
|
-
}
|
|
207
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
208
|
-
'Sentinel: All active features have living primary components. PASS'
|
|
209
|
-
);
|
|
210
|
-
consoleSpy.mockRestore();
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ------------------------------------
|
|
215
|
-
// Scenario: orphaned features (non-critical)
|
|
216
|
-
// ------------------------------------
|
|
217
|
-
|
|
218
|
-
describe('scenario: orphaned features - non-critical only', () => {
|
|
219
|
-
beforeEach(() => {
|
|
220
|
-
vi.clearAllMocks();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('exits 0 with WARN when only non-critical features are orphaned', () => {
|
|
224
|
-
const projectRoot = '/home/user/my-project';
|
|
225
|
-
const rows = [
|
|
226
|
-
{ feature_key: 'product.search', title: 'Product Search', priority: 'standard', component_file: 'src/Search.tsx' },
|
|
227
|
-
{ feature_key: 'product.filter', title: 'Product Filter', priority: 'nice-to-have', component_file: 'src/Filter.tsx' },
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
// Files do not exist
|
|
231
|
-
mockExistsSync.mockImplementation((p) => {
|
|
232
|
-
// DB file itself exists
|
|
233
|
-
if (p === '/home/user/my-project/.massu/data.db') return true;
|
|
234
|
-
return false;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const missingFeatures: { feature_key: string; title: string; priority: string; missing_file: string }[] = [];
|
|
238
|
-
for (const row of rows) {
|
|
239
|
-
const absPath = `${projectRoot}/${row.component_file}`;
|
|
240
|
-
if (!mockExistsSync(absPath)) {
|
|
241
|
-
missingFeatures.push({
|
|
242
|
-
feature_key: row.feature_key,
|
|
243
|
-
title: row.title,
|
|
244
|
-
priority: row.priority,
|
|
245
|
-
missing_file: row.component_file,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
expect(missingFeatures).toHaveLength(2);
|
|
251
|
-
|
|
252
|
-
const criticalCount = missingFeatures.filter(f => f.priority === 'critical').length;
|
|
253
|
-
expect(criticalCount).toBe(0);
|
|
254
|
-
|
|
255
|
-
// Non-critical orphans are warnings, not blockers -> exit(0)
|
|
256
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
257
|
-
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
258
|
-
|
|
259
|
-
if (criticalCount > 0) {
|
|
260
|
-
console.error(`\nFAIL: ${criticalCount} CRITICAL features are orphaned. Fix before committing.`);
|
|
261
|
-
} else {
|
|
262
|
-
console.warn(`\nWARN: ${missingFeatures.length} features are orphaned (non-critical). Consider updating registry.`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
266
|
-
'\nWARN: 2 features are orphaned (non-critical). Consider updating registry.'
|
|
267
|
-
);
|
|
268
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
269
|
-
|
|
270
|
-
consoleSpy.mockRestore();
|
|
271
|
-
errorSpy.mockRestore();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('logs each orphaned feature with priority and missing file', () => {
|
|
275
|
-
const missingFeatures = [
|
|
276
|
-
{ feature_key: 'product.search', title: 'Product Search', priority: 'standard', missing_file: 'src/Search.tsx' },
|
|
277
|
-
];
|
|
278
|
-
|
|
279
|
-
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
280
|
-
|
|
281
|
-
console.error(`Sentinel: ${missingFeatures.length} features have MISSING primary components:`);
|
|
282
|
-
for (const f of missingFeatures) {
|
|
283
|
-
console.error(` [${f.priority}] ${f.feature_key}: ${f.title}`);
|
|
284
|
-
console.error(` Missing: ${f.missing_file}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
expect(errorSpy).toHaveBeenCalledWith('Sentinel: 1 features have MISSING primary components:');
|
|
288
|
-
expect(errorSpy).toHaveBeenCalledWith(' [standard] product.search: Product Search');
|
|
289
|
-
expect(errorSpy).toHaveBeenCalledWith(' Missing: src/Search.tsx');
|
|
290
|
-
|
|
291
|
-
errorSpy.mockRestore();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// ------------------------------------
|
|
296
|
-
// Scenario: critical features are orphaned
|
|
297
|
-
// ------------------------------------
|
|
298
|
-
|
|
299
|
-
describe('scenario: orphaned features - critical present', () => {
|
|
300
|
-
beforeEach(() => {
|
|
301
|
-
vi.clearAllMocks();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('exits 1 with FAIL message when critical features are orphaned', () => {
|
|
305
|
-
const projectRoot = '/home/user/my-project';
|
|
306
|
-
const rows = [
|
|
307
|
-
{ feature_key: 'auth.login', title: 'User Login', priority: 'critical', component_file: 'src/Login.tsx' },
|
|
308
|
-
{ feature_key: 'product.search', title: 'Product Search', priority: 'standard', component_file: 'src/Search.tsx' },
|
|
309
|
-
];
|
|
310
|
-
|
|
311
|
-
// All component files missing
|
|
312
|
-
mockExistsSync.mockImplementation((p) => {
|
|
313
|
-
if (p === '/home/user/my-project/.massu/data.db') return true;
|
|
314
|
-
return false;
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const missingFeatures: { feature_key: string; title: string; priority: string; missing_file: string }[] = [];
|
|
318
|
-
for (const row of rows) {
|
|
319
|
-
const absPath = `${projectRoot}/${row.component_file}`;
|
|
320
|
-
if (!mockExistsSync(absPath)) {
|
|
321
|
-
missingFeatures.push({
|
|
322
|
-
feature_key: row.feature_key,
|
|
323
|
-
title: row.title,
|
|
324
|
-
priority: row.priority,
|
|
325
|
-
missing_file: row.component_file,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const criticalCount = missingFeatures.filter(f => f.priority === 'critical').length;
|
|
331
|
-
expect(criticalCount).toBe(1);
|
|
332
|
-
|
|
333
|
-
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
334
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
335
|
-
|
|
336
|
-
if (criticalCount > 0) {
|
|
337
|
-
console.error(`\nFAIL: ${criticalCount} CRITICAL features are orphaned. Fix before committing.`);
|
|
338
|
-
} else {
|
|
339
|
-
console.warn(`\nWARN: ${missingFeatures.length} features are orphaned (non-critical). Consider updating registry.`);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
343
|
-
'\nFAIL: 1 CRITICAL features are orphaned. Fix before committing.'
|
|
344
|
-
);
|
|
345
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
346
|
-
|
|
347
|
-
errorSpy.mockRestore();
|
|
348
|
-
warnSpy.mockRestore();
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('counts critical orphaned features correctly', () => {
|
|
352
|
-
const missingFeatures = [
|
|
353
|
-
{ feature_key: 'auth.login', title: 'User Login', priority: 'critical', missing_file: 'src/Login.tsx' },
|
|
354
|
-
{ feature_key: 'auth.register', title: 'Registration', priority: 'critical', missing_file: 'src/Register.tsx' },
|
|
355
|
-
{ feature_key: 'product.search', title: 'Search', priority: 'standard', missing_file: 'src/Search.tsx' },
|
|
356
|
-
];
|
|
357
|
-
|
|
358
|
-
const criticalCount = missingFeatures.filter(f => f.priority === 'critical').length;
|
|
359
|
-
expect(criticalCount).toBe(2);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('distinguishes between critical and non-critical in mixed scenario', () => {
|
|
363
|
-
const missingFeatures = [
|
|
364
|
-
{ priority: 'critical' },
|
|
365
|
-
{ priority: 'standard' },
|
|
366
|
-
{ priority: 'nice-to-have' },
|
|
367
|
-
];
|
|
368
|
-
|
|
369
|
-
const criticalCount = missingFeatures.filter(f => f.priority === 'critical').length;
|
|
370
|
-
const nonCriticalCount = missingFeatures.filter(f => f.priority !== 'critical').length;
|
|
371
|
-
|
|
372
|
-
expect(criticalCount).toBe(1);
|
|
373
|
-
expect(nonCriticalCount).toBe(2);
|
|
374
|
-
expect(criticalCount > 0).toBe(true); // -> exit(1) path
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// ------------------------------------
|
|
379
|
-
// Status logging tests
|
|
380
|
-
// ------------------------------------
|
|
381
|
-
|
|
382
|
-
describe('status logging', () => {
|
|
383
|
-
it('logs active feature count before checking components', () => {
|
|
384
|
-
const totalActive = { count: 8 };
|
|
385
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
386
|
-
|
|
387
|
-
console.log(`Sentinel: ${totalActive.count} active features, checking primary components...`);
|
|
388
|
-
|
|
389
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
390
|
-
'Sentinel: 8 active features, checking primary components...'
|
|
391
|
-
);
|
|
392
|
-
consoleSpy.mockRestore();
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('formats singular vs plural active feature count correctly', () => {
|
|
396
|
-
// The script uses the raw count in the message - verify the message format
|
|
397
|
-
const count1 = 1;
|
|
398
|
-
const count5 = 5;
|
|
399
|
-
const msg1 = `Sentinel: ${count1} active features, checking primary components...`;
|
|
400
|
-
const msg5 = `Sentinel: ${count5} active features, checking primary components...`;
|
|
401
|
-
expect(msg1).toContain('1 active features');
|
|
402
|
-
expect(msg5).toContain('5 active features');
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ------------------------------------
|
|
407
|
-
// Path resolution tests
|
|
408
|
-
// ------------------------------------
|
|
409
|
-
|
|
410
|
-
describe('path resolution for component files', () => {
|
|
411
|
-
beforeEach(() => {
|
|
412
|
-
vi.clearAllMocks();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('resolves component_file relative to PROJECT_ROOT', () => {
|
|
416
|
-
const projectRoot = '/home/user/my-project';
|
|
417
|
-
const componentFile = 'src/components/Login.tsx';
|
|
418
|
-
|
|
419
|
-
// The script uses: resolve(PROJECT_ROOT, row.component_file)
|
|
420
|
-
// which is equivalent to path.join for relative paths
|
|
421
|
-
const absPath = `${projectRoot}/${componentFile}`;
|
|
422
|
-
expect(absPath).toBe('/home/user/my-project/src/components/Login.tsx');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('checks each component file with existsSync', () => {
|
|
426
|
-
const projectRoot = '/home/user/my-project';
|
|
427
|
-
const rows = [
|
|
428
|
-
{ component_file: 'src/Login.tsx' },
|
|
429
|
-
{ component_file: 'src/Register.tsx' },
|
|
430
|
-
];
|
|
431
|
-
|
|
432
|
-
const checkedPaths: string[] = [];
|
|
433
|
-
mockExistsSync.mockImplementation((p) => {
|
|
434
|
-
checkedPaths.push(p as string);
|
|
435
|
-
return true;
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
for (const row of rows) {
|
|
439
|
-
existsSync(`${projectRoot}/${row.component_file}`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
expect(checkedPaths).toEqual([
|
|
443
|
-
'/home/user/my-project/src/Login.tsx',
|
|
444
|
-
'/home/user/my-project/src/Register.tsx',
|
|
445
|
-
]);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('correctly identifies missing vs existing files', () => {
|
|
449
|
-
const existingFiles = new Set(['/home/user/my-project/src/Login.tsx']);
|
|
450
|
-
mockExistsSync.mockImplementation((p) => existingFiles.has(p as string));
|
|
451
|
-
|
|
452
|
-
expect(mockExistsSync('/home/user/my-project/src/Login.tsx')).toBe(true);
|
|
453
|
-
expect(mockExistsSync('/home/user/my-project/src/Search.tsx')).toBe(false);
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// ------------------------------------
|
|
458
|
-
// DB open/close correctness tests
|
|
459
|
-
// ------------------------------------
|
|
460
|
-
|
|
461
|
-
describe('database open and close', () => {
|
|
462
|
-
it('verifies readonly mode is used for data DB', () => {
|
|
463
|
-
// The script opens: new Database(dbPath, { readonly: true })
|
|
464
|
-
// Verify the options pattern
|
|
465
|
-
const options = { readonly: true };
|
|
466
|
-
expect(options.readonly).toBe(true);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('verifies WAL pragma is set', () => {
|
|
470
|
-
// The script calls: db.pragma('journal_mode = WAL')
|
|
471
|
-
const pragma = 'journal_mode = WAL';
|
|
472
|
-
expect(pragma).toBe('journal_mode = WAL');
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it('verifies sentinel table query uses correct table name', () => {
|
|
476
|
-
const query = "SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'";
|
|
477
|
-
expect(query).toContain('massu_sentinel');
|
|
478
|
-
expect(query).toContain("type='table'");
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('verifies active features query uses correct status filter', () => {
|
|
482
|
-
const query = "SELECT COUNT(*) as count FROM massu_sentinel WHERE status = 'active'";
|
|
483
|
-
expect(query).toContain("status = 'active'");
|
|
484
|
-
expect(query).toContain('COUNT(*)');
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it('verifies orphaned feature query joins correct tables', () => {
|
|
488
|
-
const query = `
|
|
489
|
-
SELECT s.feature_key, s.title, s.priority, c.component_file
|
|
490
|
-
FROM massu_sentinel s
|
|
491
|
-
JOIN massu_sentinel_components c ON c.feature_id = s.id AND c.is_primary = 1
|
|
492
|
-
WHERE s.status = 'active'
|
|
493
|
-
ORDER BY s.priority DESC, s.domain, s.feature_key
|
|
494
|
-
`;
|
|
495
|
-
expect(query).toContain('massu_sentinel s');
|
|
496
|
-
expect(query).toContain('massu_sentinel_components c');
|
|
497
|
-
expect(query).toContain('c.is_primary = 1');
|
|
498
|
-
expect(query).toContain("s.status = 'active'");
|
|
499
|
-
expect(query).toContain('s.priority DESC');
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
// ------------------------------------
|
|
504
|
-
// getResolvedPaths integration tests
|
|
505
|
-
// ------------------------------------
|
|
506
|
-
|
|
507
|
-
describe('config integration', () => {
|
|
508
|
-
it('reads dbPath from getResolvedPaths()', () => {
|
|
509
|
-
const paths = mockGetResolvedPaths();
|
|
510
|
-
expect(paths.dataDbPath).toBe('/home/user/my-project/.massu/data.db');
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('reads PROJECT_ROOT from getProjectRoot()', () => {
|
|
514
|
-
const root = mockGetProjectRoot();
|
|
515
|
-
expect(root).toBe('/home/user/my-project');
|
|
516
|
-
});
|
|
517
|
-
});
|