@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,750 +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 } from 'vitest';
|
|
5
|
-
import type {
|
|
6
|
-
Feature,
|
|
7
|
-
FeatureInput,
|
|
8
|
-
FeatureComponent,
|
|
9
|
-
FeatureProcedure,
|
|
10
|
-
FeaturePage,
|
|
11
|
-
FeatureDep,
|
|
12
|
-
FeatureChangeLog,
|
|
13
|
-
FeatureWithCounts,
|
|
14
|
-
FeatureDetail,
|
|
15
|
-
ImpactItem,
|
|
16
|
-
ImpactReport,
|
|
17
|
-
ValidationItem,
|
|
18
|
-
ValidationReport,
|
|
19
|
-
ParityItem,
|
|
20
|
-
ParityReport,
|
|
21
|
-
FeatureStatus,
|
|
22
|
-
FeaturePriority,
|
|
23
|
-
ComponentRole,
|
|
24
|
-
DependencyType,
|
|
25
|
-
ChangeType,
|
|
26
|
-
} from '../sentinel-types.ts';
|
|
27
|
-
|
|
28
|
-
// ============================================================
|
|
29
|
-
// Sentinel Types Tests
|
|
30
|
-
// Verifies type shapes compile and runtime objects conform.
|
|
31
|
-
// ============================================================
|
|
32
|
-
|
|
33
|
-
// ------------------------------------
|
|
34
|
-
// Type guard helpers (runtime checks)
|
|
35
|
-
// ------------------------------------
|
|
36
|
-
|
|
37
|
-
function isFeatureStatus(value: unknown): value is FeatureStatus {
|
|
38
|
-
return (
|
|
39
|
-
typeof value === 'string' &&
|
|
40
|
-
['planned', 'active', 'deprecated', 'removed'].includes(value)
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isFeaturePriority(value: unknown): value is FeaturePriority {
|
|
45
|
-
return (
|
|
46
|
-
typeof value === 'string' &&
|
|
47
|
-
['critical', 'standard', 'nice-to-have'].includes(value)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isComponentRole(value: unknown): value is ComponentRole {
|
|
52
|
-
return (
|
|
53
|
-
typeof value === 'string' &&
|
|
54
|
-
['implementation', 'ui', 'data', 'utility'].includes(value)
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isDependencyType(value: unknown): value is DependencyType {
|
|
59
|
-
return (
|
|
60
|
-
typeof value === 'string' &&
|
|
61
|
-
['requires', 'enhances', 'replaces'].includes(value)
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isChangeType(value: unknown): value is ChangeType {
|
|
66
|
-
return (
|
|
67
|
-
typeof value === 'string' &&
|
|
68
|
-
['created', 'updated', 'deprecated', 'removed', 'restored'].includes(value)
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isFeature(obj: unknown): obj is Feature {
|
|
73
|
-
if (typeof obj !== 'object' || obj === null) return false;
|
|
74
|
-
const f = obj as Record<string, unknown>;
|
|
75
|
-
return (
|
|
76
|
-
typeof f.id === 'number' &&
|
|
77
|
-
typeof f.feature_key === 'string' &&
|
|
78
|
-
typeof f.domain === 'string' &&
|
|
79
|
-
(f.subdomain === null || typeof f.subdomain === 'string') &&
|
|
80
|
-
typeof f.title === 'string' &&
|
|
81
|
-
(f.description === null || typeof f.description === 'string') &&
|
|
82
|
-
isFeatureStatus(f.status) &&
|
|
83
|
-
isFeaturePriority(f.priority) &&
|
|
84
|
-
Array.isArray(f.portal_scope) &&
|
|
85
|
-
typeof f.created_at === 'string' &&
|
|
86
|
-
typeof f.updated_at === 'string' &&
|
|
87
|
-
(f.removed_at === null || typeof f.removed_at === 'string') &&
|
|
88
|
-
(f.removed_reason === null || typeof f.removed_reason === 'string')
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ------------------------------------
|
|
93
|
-
// Type literal union tests
|
|
94
|
-
// ------------------------------------
|
|
95
|
-
|
|
96
|
-
describe('FeatureStatus union type', () => {
|
|
97
|
-
it('accepts all valid status values', () => {
|
|
98
|
-
const statuses: FeatureStatus[] = ['planned', 'active', 'deprecated', 'removed'];
|
|
99
|
-
expect(statuses).toHaveLength(4);
|
|
100
|
-
for (const s of statuses) {
|
|
101
|
-
expect(isFeatureStatus(s)).toBe(true);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('rejects invalid status values at runtime', () => {
|
|
106
|
-
expect(isFeatureStatus('unknown')).toBe(false);
|
|
107
|
-
expect(isFeatureStatus('')).toBe(false);
|
|
108
|
-
expect(isFeatureStatus(null)).toBe(false);
|
|
109
|
-
expect(isFeatureStatus(42)).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('FeaturePriority union type', () => {
|
|
114
|
-
it('accepts all valid priority values', () => {
|
|
115
|
-
const priorities: FeaturePriority[] = ['critical', 'standard', 'nice-to-have'];
|
|
116
|
-
expect(priorities).toHaveLength(3);
|
|
117
|
-
for (const p of priorities) {
|
|
118
|
-
expect(isFeaturePriority(p)).toBe(true);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('rejects invalid priority values at runtime', () => {
|
|
123
|
-
expect(isFeaturePriority('low')).toBe(false);
|
|
124
|
-
expect(isFeaturePriority('high')).toBe(false);
|
|
125
|
-
expect(isFeaturePriority(null)).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('ComponentRole union type', () => {
|
|
130
|
-
it('accepts all valid role values', () => {
|
|
131
|
-
const roles: ComponentRole[] = ['implementation', 'ui', 'data', 'utility'];
|
|
132
|
-
expect(roles).toHaveLength(4);
|
|
133
|
-
for (const r of roles) {
|
|
134
|
-
expect(isComponentRole(r)).toBe(true);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('rejects invalid role values at runtime', () => {
|
|
139
|
-
expect(isComponentRole('service')).toBe(false);
|
|
140
|
-
expect(isComponentRole('')).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('DependencyType union type', () => {
|
|
145
|
-
it('accepts all valid dependency types', () => {
|
|
146
|
-
const types: DependencyType[] = ['requires', 'enhances', 'replaces'];
|
|
147
|
-
expect(types).toHaveLength(3);
|
|
148
|
-
for (const t of types) {
|
|
149
|
-
expect(isDependencyType(t)).toBe(true);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('rejects invalid dependency types at runtime', () => {
|
|
154
|
-
expect(isDependencyType('uses')).toBe(false);
|
|
155
|
-
expect(isDependencyType(null)).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('ChangeType union type', () => {
|
|
160
|
-
it('accepts all valid change types', () => {
|
|
161
|
-
const types: ChangeType[] = ['created', 'updated', 'deprecated', 'removed', 'restored'];
|
|
162
|
-
expect(types).toHaveLength(5);
|
|
163
|
-
for (const t of types) {
|
|
164
|
-
expect(isChangeType(t)).toBe(true);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('rejects invalid change types at runtime', () => {
|
|
169
|
-
expect(isChangeType('deleted')).toBe(false);
|
|
170
|
-
expect(isChangeType('modified')).toBe(false);
|
|
171
|
-
expect(isChangeType(null)).toBe(false);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ------------------------------------
|
|
176
|
-
// Interface conformance tests
|
|
177
|
-
// ------------------------------------
|
|
178
|
-
|
|
179
|
-
describe('Feature interface', () => {
|
|
180
|
-
it('accepts a fully populated Feature object', () => {
|
|
181
|
-
const feature: Feature = {
|
|
182
|
-
id: 1,
|
|
183
|
-
feature_key: 'auth.login',
|
|
184
|
-
domain: 'auth',
|
|
185
|
-
subdomain: 'session',
|
|
186
|
-
title: 'User Login',
|
|
187
|
-
description: 'Handles user authentication',
|
|
188
|
-
status: 'active',
|
|
189
|
-
priority: 'critical',
|
|
190
|
-
portal_scope: ['internal', 'external'],
|
|
191
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
192
|
-
updated_at: '2026-01-15T00:00:00Z',
|
|
193
|
-
removed_at: null,
|
|
194
|
-
removed_reason: null,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
expect(isFeature(feature)).toBe(true);
|
|
198
|
-
expect(feature.id).toBe(1);
|
|
199
|
-
expect(feature.feature_key).toBe('auth.login');
|
|
200
|
-
expect(feature.portal_scope).toEqual(['internal', 'external']);
|
|
201
|
-
expect(feature.removed_at).toBeNull();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('accepts a Feature with null optional fields', () => {
|
|
205
|
-
const feature: Feature = {
|
|
206
|
-
id: 2,
|
|
207
|
-
feature_key: 'product.list',
|
|
208
|
-
domain: 'product',
|
|
209
|
-
subdomain: null,
|
|
210
|
-
title: 'Product List',
|
|
211
|
-
description: null,
|
|
212
|
-
status: 'deprecated',
|
|
213
|
-
priority: 'standard',
|
|
214
|
-
portal_scope: [],
|
|
215
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
216
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
217
|
-
removed_at: '2026-02-01T00:00:00Z',
|
|
218
|
-
removed_reason: 'Feature deprecated in v2',
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
expect(isFeature(feature)).toBe(true);
|
|
222
|
-
expect(feature.subdomain).toBeNull();
|
|
223
|
-
expect(feature.description).toBeNull();
|
|
224
|
-
expect(feature.removed_at).toBe('2026-02-01T00:00:00Z');
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('FeatureInput interface', () => {
|
|
229
|
-
it('accepts a minimal FeatureInput (required fields only)', () => {
|
|
230
|
-
const input: FeatureInput = {
|
|
231
|
-
feature_key: 'auth.logout',
|
|
232
|
-
domain: 'auth',
|
|
233
|
-
title: 'User Logout',
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
expect(input.feature_key).toBe('auth.logout');
|
|
237
|
-
expect(input.domain).toBe('auth');
|
|
238
|
-
expect(input.title).toBe('User Logout');
|
|
239
|
-
expect(input.subdomain).toBeUndefined();
|
|
240
|
-
expect(input.status).toBeUndefined();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('accepts a fully populated FeatureInput', () => {
|
|
244
|
-
const input: FeatureInput = {
|
|
245
|
-
feature_key: 'product.search',
|
|
246
|
-
domain: 'product',
|
|
247
|
-
subdomain: 'catalog',
|
|
248
|
-
title: 'Product Search',
|
|
249
|
-
description: 'Full-text search for products',
|
|
250
|
-
status: 'active',
|
|
251
|
-
priority: 'standard',
|
|
252
|
-
portal_scope: ['internal'],
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
expect(input.subdomain).toBe('catalog');
|
|
256
|
-
expect(input.status).toBe('active');
|
|
257
|
-
expect(input.portal_scope).toEqual(['internal']);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('FeatureComponent interface', () => {
|
|
262
|
-
it('accepts a valid FeatureComponent object', () => {
|
|
263
|
-
const component: FeatureComponent = {
|
|
264
|
-
id: 10,
|
|
265
|
-
feature_id: 1,
|
|
266
|
-
component_file: 'src/components/Login.tsx',
|
|
267
|
-
component_name: 'LoginForm',
|
|
268
|
-
role: 'ui',
|
|
269
|
-
is_primary: true,
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
expect(component.id).toBe(10);
|
|
273
|
-
expect(component.feature_id).toBe(1);
|
|
274
|
-
expect(component.component_file).toBe('src/components/Login.tsx');
|
|
275
|
-
expect(isComponentRole(component.role)).toBe(true);
|
|
276
|
-
expect(component.is_primary).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('accepts a FeatureComponent with null component_name', () => {
|
|
280
|
-
const component: FeatureComponent = {
|
|
281
|
-
id: 11,
|
|
282
|
-
feature_id: 2,
|
|
283
|
-
component_file: 'src/utils/auth-helpers.ts',
|
|
284
|
-
component_name: null,
|
|
285
|
-
role: 'utility',
|
|
286
|
-
is_primary: false,
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
expect(component.component_name).toBeNull();
|
|
290
|
-
expect(component.is_primary).toBe(false);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('FeatureProcedure interface', () => {
|
|
295
|
-
it('accepts a valid FeatureProcedure object', () => {
|
|
296
|
-
const procedure: FeatureProcedure = {
|
|
297
|
-
id: 20,
|
|
298
|
-
feature_id: 1,
|
|
299
|
-
router_name: 'auth',
|
|
300
|
-
procedure_name: 'login',
|
|
301
|
-
procedure_type: 'mutation',
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
expect(procedure.router_name).toBe('auth');
|
|
305
|
-
expect(procedure.procedure_name).toBe('login');
|
|
306
|
-
expect(procedure.procedure_type).toBe('mutation');
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('accepts a FeatureProcedure with null procedure_type', () => {
|
|
310
|
-
const procedure: FeatureProcedure = {
|
|
311
|
-
id: 21,
|
|
312
|
-
feature_id: 1,
|
|
313
|
-
router_name: 'auth',
|
|
314
|
-
procedure_name: 'getSession',
|
|
315
|
-
procedure_type: null,
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
expect(procedure.procedure_type).toBeNull();
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe('FeaturePage interface', () => {
|
|
323
|
-
it('accepts a valid FeaturePage object', () => {
|
|
324
|
-
const page: FeaturePage = {
|
|
325
|
-
id: 30,
|
|
326
|
-
feature_id: 1,
|
|
327
|
-
page_route: '/login',
|
|
328
|
-
portal: 'external',
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
expect(page.page_route).toBe('/login');
|
|
332
|
-
expect(page.portal).toBe('external');
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('accepts a FeaturePage with null portal', () => {
|
|
336
|
-
const page: FeaturePage = {
|
|
337
|
-
id: 31,
|
|
338
|
-
feature_id: 1,
|
|
339
|
-
page_route: '/admin/dashboard',
|
|
340
|
-
portal: null,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
expect(page.portal).toBeNull();
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe('FeatureDep interface', () => {
|
|
348
|
-
it('accepts a valid FeatureDep object', () => {
|
|
349
|
-
const dep: FeatureDep = {
|
|
350
|
-
id: 40,
|
|
351
|
-
feature_id: 2,
|
|
352
|
-
depends_on_feature_id: 1,
|
|
353
|
-
dependency_type: 'requires',
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
expect(dep.feature_id).toBe(2);
|
|
357
|
-
expect(dep.depends_on_feature_id).toBe(1);
|
|
358
|
-
expect(isDependencyType(dep.dependency_type)).toBe(true);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('accepts all dependency types in FeatureDep', () => {
|
|
362
|
-
const types: DependencyType[] = ['requires', 'enhances', 'replaces'];
|
|
363
|
-
for (const depType of types) {
|
|
364
|
-
const dep: FeatureDep = {
|
|
365
|
-
id: 1,
|
|
366
|
-
feature_id: 1,
|
|
367
|
-
depends_on_feature_id: 2,
|
|
368
|
-
dependency_type: depType,
|
|
369
|
-
};
|
|
370
|
-
expect(isDependencyType(dep.dependency_type)).toBe(true);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe('FeatureChangeLog interface', () => {
|
|
376
|
-
it('accepts a valid FeatureChangeLog object', () => {
|
|
377
|
-
const log: FeatureChangeLog = {
|
|
378
|
-
id: 50,
|
|
379
|
-
feature_id: 1,
|
|
380
|
-
change_type: 'created',
|
|
381
|
-
changed_by: 'scanner',
|
|
382
|
-
change_detail: 'Auto-discovered feature',
|
|
383
|
-
commit_hash: 'abc123def456',
|
|
384
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
expect(isChangeType(log.change_type)).toBe(true);
|
|
388
|
-
expect(log.changed_by).toBe('scanner');
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('accepts a FeatureChangeLog with all nullable fields null', () => {
|
|
392
|
-
const log: FeatureChangeLog = {
|
|
393
|
-
id: 51,
|
|
394
|
-
feature_id: 1,
|
|
395
|
-
change_type: 'updated',
|
|
396
|
-
changed_by: null,
|
|
397
|
-
change_detail: null,
|
|
398
|
-
commit_hash: null,
|
|
399
|
-
created_at: '2026-02-01T00:00:00Z',
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
expect(log.changed_by).toBeNull();
|
|
403
|
-
expect(log.change_detail).toBeNull();
|
|
404
|
-
expect(log.commit_hash).toBeNull();
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
describe('FeatureWithCounts interface', () => {
|
|
409
|
-
it('extends Feature with count fields', () => {
|
|
410
|
-
const featureWithCounts: FeatureWithCounts = {
|
|
411
|
-
id: 1,
|
|
412
|
-
feature_key: 'auth.login',
|
|
413
|
-
domain: 'auth',
|
|
414
|
-
subdomain: null,
|
|
415
|
-
title: 'User Login',
|
|
416
|
-
description: null,
|
|
417
|
-
status: 'active',
|
|
418
|
-
priority: 'critical',
|
|
419
|
-
portal_scope: [],
|
|
420
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
421
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
422
|
-
removed_at: null,
|
|
423
|
-
removed_reason: null,
|
|
424
|
-
component_count: 3,
|
|
425
|
-
procedure_count: 2,
|
|
426
|
-
page_count: 1,
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
expect(isFeature(featureWithCounts)).toBe(true);
|
|
430
|
-
expect(featureWithCounts.component_count).toBe(3);
|
|
431
|
-
expect(featureWithCounts.procedure_count).toBe(2);
|
|
432
|
-
expect(featureWithCounts.page_count).toBe(1);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('accepts zero counts', () => {
|
|
436
|
-
const featureWithCounts: FeatureWithCounts = {
|
|
437
|
-
id: 2,
|
|
438
|
-
feature_key: 'empty.feature',
|
|
439
|
-
domain: 'test',
|
|
440
|
-
subdomain: null,
|
|
441
|
-
title: 'Empty Feature',
|
|
442
|
-
description: null,
|
|
443
|
-
status: 'planned',
|
|
444
|
-
priority: 'nice-to-have',
|
|
445
|
-
portal_scope: [],
|
|
446
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
447
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
448
|
-
removed_at: null,
|
|
449
|
-
removed_reason: null,
|
|
450
|
-
component_count: 0,
|
|
451
|
-
procedure_count: 0,
|
|
452
|
-
page_count: 0,
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
expect(featureWithCounts.component_count).toBe(0);
|
|
456
|
-
expect(featureWithCounts.procedure_count).toBe(0);
|
|
457
|
-
expect(featureWithCounts.page_count).toBe(0);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
describe('FeatureDetail interface', () => {
|
|
462
|
-
it('extends Feature with related entity arrays', () => {
|
|
463
|
-
const detail: FeatureDetail = {
|
|
464
|
-
id: 1,
|
|
465
|
-
feature_key: 'auth.login',
|
|
466
|
-
domain: 'auth',
|
|
467
|
-
subdomain: null,
|
|
468
|
-
title: 'User Login',
|
|
469
|
-
description: null,
|
|
470
|
-
status: 'active',
|
|
471
|
-
priority: 'critical',
|
|
472
|
-
portal_scope: [],
|
|
473
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
474
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
475
|
-
removed_at: null,
|
|
476
|
-
removed_reason: null,
|
|
477
|
-
components: [
|
|
478
|
-
{
|
|
479
|
-
id: 1,
|
|
480
|
-
feature_id: 1,
|
|
481
|
-
component_file: 'src/Login.tsx',
|
|
482
|
-
component_name: 'Login',
|
|
483
|
-
role: 'ui',
|
|
484
|
-
is_primary: true,
|
|
485
|
-
},
|
|
486
|
-
],
|
|
487
|
-
procedures: [
|
|
488
|
-
{
|
|
489
|
-
id: 1,
|
|
490
|
-
feature_id: 1,
|
|
491
|
-
router_name: 'auth',
|
|
492
|
-
procedure_name: 'login',
|
|
493
|
-
procedure_type: 'mutation',
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
pages: [
|
|
497
|
-
{
|
|
498
|
-
id: 1,
|
|
499
|
-
feature_id: 1,
|
|
500
|
-
page_route: '/login',
|
|
501
|
-
portal: 'external',
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
dependencies: [
|
|
505
|
-
{
|
|
506
|
-
id: 1,
|
|
507
|
-
feature_id: 1,
|
|
508
|
-
depends_on_feature_id: 2,
|
|
509
|
-
dependency_type: 'requires',
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
changelog: [
|
|
513
|
-
{
|
|
514
|
-
id: 1,
|
|
515
|
-
feature_id: 1,
|
|
516
|
-
change_type: 'created',
|
|
517
|
-
changed_by: 'scanner',
|
|
518
|
-
change_detail: null,
|
|
519
|
-
commit_hash: null,
|
|
520
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
expect(isFeature(detail)).toBe(true);
|
|
526
|
-
expect(detail.components).toHaveLength(1);
|
|
527
|
-
expect(detail.procedures).toHaveLength(1);
|
|
528
|
-
expect(detail.pages).toHaveLength(1);
|
|
529
|
-
expect(detail.dependencies).toHaveLength(1);
|
|
530
|
-
expect(detail.changelog).toHaveLength(1);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it('accepts empty arrays for all related entities', () => {
|
|
534
|
-
const detail: FeatureDetail = {
|
|
535
|
-
id: 2,
|
|
536
|
-
feature_key: 'new.feature',
|
|
537
|
-
domain: 'test',
|
|
538
|
-
subdomain: null,
|
|
539
|
-
title: 'New Feature',
|
|
540
|
-
description: null,
|
|
541
|
-
status: 'planned',
|
|
542
|
-
priority: 'standard',
|
|
543
|
-
portal_scope: [],
|
|
544
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
545
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
546
|
-
removed_at: null,
|
|
547
|
-
removed_reason: null,
|
|
548
|
-
components: [],
|
|
549
|
-
procedures: [],
|
|
550
|
-
pages: [],
|
|
551
|
-
dependencies: [],
|
|
552
|
-
changelog: [],
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
expect(detail.components).toHaveLength(0);
|
|
556
|
-
expect(detail.procedures).toHaveLength(0);
|
|
557
|
-
expect(detail.dependencies).toHaveLength(0);
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
describe('ImpactItem interface', () => {
|
|
562
|
-
it('accepts a valid ImpactItem with orphaned status', () => {
|
|
563
|
-
const baseFeature: Feature = {
|
|
564
|
-
id: 1,
|
|
565
|
-
feature_key: 'auth.login',
|
|
566
|
-
domain: 'auth',
|
|
567
|
-
subdomain: null,
|
|
568
|
-
title: 'User Login',
|
|
569
|
-
description: null,
|
|
570
|
-
status: 'active',
|
|
571
|
-
priority: 'critical',
|
|
572
|
-
portal_scope: [],
|
|
573
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
574
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
575
|
-
removed_at: null,
|
|
576
|
-
removed_reason: null,
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
const item: ImpactItem = {
|
|
580
|
-
feature: baseFeature,
|
|
581
|
-
affected_files: ['src/Login.tsx'],
|
|
582
|
-
remaining_files: [],
|
|
583
|
-
status: 'orphaned',
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
expect(item.status).toBe('orphaned');
|
|
587
|
-
expect(item.affected_files).toHaveLength(1);
|
|
588
|
-
expect(item.remaining_files).toHaveLength(0);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('accepts all ImpactItem status values', () => {
|
|
592
|
-
const statuses: ImpactItem['status'][] = ['orphaned', 'degraded', 'unaffected'];
|
|
593
|
-
for (const status of statuses) {
|
|
594
|
-
expect(['orphaned', 'degraded', 'unaffected'].includes(status)).toBe(true);
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
describe('ImpactReport interface', () => {
|
|
600
|
-
it('accepts a valid ImpactReport', () => {
|
|
601
|
-
const report: ImpactReport = {
|
|
602
|
-
files_analyzed: ['src/Login.tsx', 'src/helpers.ts'],
|
|
603
|
-
orphaned: [],
|
|
604
|
-
degraded: [],
|
|
605
|
-
unaffected: [],
|
|
606
|
-
blocked: false,
|
|
607
|
-
block_reason: null,
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
expect(report.files_analyzed).toHaveLength(2);
|
|
611
|
-
expect(report.blocked).toBe(false);
|
|
612
|
-
expect(report.block_reason).toBeNull();
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it('accepts a blocked ImpactReport', () => {
|
|
616
|
-
const report: ImpactReport = {
|
|
617
|
-
files_analyzed: ['src/Login.tsx'],
|
|
618
|
-
orphaned: [],
|
|
619
|
-
degraded: [],
|
|
620
|
-
unaffected: [],
|
|
621
|
-
blocked: true,
|
|
622
|
-
block_reason: 'Critical feature auth.login is orphaned',
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
expect(report.blocked).toBe(true);
|
|
626
|
-
expect(report.block_reason).toBe('Critical feature auth.login is orphaned');
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
describe('ValidationItem interface', () => {
|
|
631
|
-
it('accepts a valid ValidationItem', () => {
|
|
632
|
-
const baseFeature: Feature = {
|
|
633
|
-
id: 1,
|
|
634
|
-
feature_key: 'auth.login',
|
|
635
|
-
domain: 'auth',
|
|
636
|
-
subdomain: null,
|
|
637
|
-
title: 'User Login',
|
|
638
|
-
description: null,
|
|
639
|
-
status: 'active',
|
|
640
|
-
priority: 'critical',
|
|
641
|
-
portal_scope: [],
|
|
642
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
643
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
644
|
-
removed_at: null,
|
|
645
|
-
removed_reason: null,
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
const item: ValidationItem = {
|
|
649
|
-
feature: baseFeature,
|
|
650
|
-
missing_components: ['src/Login.tsx'],
|
|
651
|
-
missing_procedures: [{ router: 'auth', procedure: 'login' }],
|
|
652
|
-
missing_pages: ['/login'],
|
|
653
|
-
status: 'degraded',
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
expect(item.status).toBe('degraded');
|
|
657
|
-
expect(item.missing_components).toHaveLength(1);
|
|
658
|
-
expect(item.missing_procedures[0]).toEqual({ router: 'auth', procedure: 'login' });
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it('accepts all ValidationItem status values', () => {
|
|
662
|
-
const statuses: ValidationItem['status'][] = ['alive', 'orphaned', 'degraded'];
|
|
663
|
-
for (const status of statuses) {
|
|
664
|
-
expect(['alive', 'orphaned', 'degraded'].includes(status)).toBe(true);
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
describe('ValidationReport interface', () => {
|
|
670
|
-
it('accepts a valid ValidationReport', () => {
|
|
671
|
-
const report: ValidationReport = {
|
|
672
|
-
alive: 5,
|
|
673
|
-
orphaned: 1,
|
|
674
|
-
degraded: 2,
|
|
675
|
-
details: [],
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
expect(report.alive).toBe(5);
|
|
679
|
-
expect(report.orphaned).toBe(1);
|
|
680
|
-
expect(report.degraded).toBe(2);
|
|
681
|
-
expect(Array.isArray(report.details)).toBe(true);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
it('accepts a zero-count ValidationReport', () => {
|
|
685
|
-
const report: ValidationReport = {
|
|
686
|
-
alive: 0,
|
|
687
|
-
orphaned: 0,
|
|
688
|
-
degraded: 0,
|
|
689
|
-
details: [],
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
expect(report.alive + report.orphaned + report.degraded).toBe(0);
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
describe('ParityItem interface', () => {
|
|
697
|
-
it('accepts all ParityItem status values', () => {
|
|
698
|
-
const statuses: ParityItem['status'][] = ['DONE', 'GAP', 'NEW'];
|
|
699
|
-
for (const status of statuses) {
|
|
700
|
-
const item: ParityItem = {
|
|
701
|
-
feature_key: 'test.feature',
|
|
702
|
-
title: 'Test Feature',
|
|
703
|
-
status,
|
|
704
|
-
old_files: ['src/old.ts'],
|
|
705
|
-
new_files: ['src/new.ts'],
|
|
706
|
-
};
|
|
707
|
-
expect(['DONE', 'GAP', 'NEW'].includes(item.status)).toBe(true);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
it('accepts a ParityItem with empty file arrays', () => {
|
|
712
|
-
const item: ParityItem = {
|
|
713
|
-
feature_key: 'new.feature',
|
|
714
|
-
title: 'New Feature',
|
|
715
|
-
status: 'NEW',
|
|
716
|
-
old_files: [],
|
|
717
|
-
new_files: ['src/new-feature.ts'],
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
expect(item.old_files).toHaveLength(0);
|
|
721
|
-
expect(item.new_files).toHaveLength(1);
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
describe('ParityReport interface', () => {
|
|
726
|
-
it('accepts a valid ParityReport', () => {
|
|
727
|
-
const report: ParityReport = {
|
|
728
|
-
done: [],
|
|
729
|
-
gaps: [],
|
|
730
|
-
new_features: [],
|
|
731
|
-
parity_percentage: 100,
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
expect(report.parity_percentage).toBe(100);
|
|
735
|
-
expect(Array.isArray(report.done)).toBe(true);
|
|
736
|
-
expect(Array.isArray(report.gaps)).toBe(true);
|
|
737
|
-
expect(Array.isArray(report.new_features)).toBe(true);
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
it('accepts a partial parity percentage', () => {
|
|
741
|
-
const report: ParityReport = {
|
|
742
|
-
done: [],
|
|
743
|
-
gaps: [],
|
|
744
|
-
new_features: [],
|
|
745
|
-
parity_percentage: 66.67,
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
expect(report.parity_percentage).toBeCloseTo(66.67);
|
|
749
|
-
});
|
|
750
|
-
});
|