@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,260 +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 {
|
|
7
|
-
getAdrToolDefinitions,
|
|
8
|
-
isAdrTool,
|
|
9
|
-
detectDecisionPatterns,
|
|
10
|
-
extractAlternatives,
|
|
11
|
-
storeDecision,
|
|
12
|
-
handleAdrToolCall,
|
|
13
|
-
} from '../adr-generator.ts';
|
|
14
|
-
|
|
15
|
-
function createTestDb(): Database.Database {
|
|
16
|
-
const db = new Database(':memory:');
|
|
17
|
-
db.exec(`
|
|
18
|
-
CREATE TABLE architecture_decisions (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
session_id TEXT,
|
|
21
|
-
title TEXT NOT NULL,
|
|
22
|
-
context TEXT,
|
|
23
|
-
decision TEXT NOT NULL,
|
|
24
|
-
status TEXT NOT NULL DEFAULT 'accepted' CHECK(status IN ('accepted', 'superseded', 'deprecated')),
|
|
25
|
-
alternatives TEXT,
|
|
26
|
-
consequences TEXT,
|
|
27
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
28
|
-
);
|
|
29
|
-
`);
|
|
30
|
-
return db;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('adr-generator', () => {
|
|
34
|
-
let db: Database.Database;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
db = createTestDb();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
db.close();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('getAdrToolDefinitions', () => {
|
|
45
|
-
it('returns 3 tool definitions with correct names', () => {
|
|
46
|
-
const tools = getAdrToolDefinitions();
|
|
47
|
-
expect(tools).toHaveLength(3);
|
|
48
|
-
expect(tools.map(t => t.name.split('_').slice(-2).join('_'))).toEqual([
|
|
49
|
-
'adr_list',
|
|
50
|
-
'adr_detail',
|
|
51
|
-
'adr_create',
|
|
52
|
-
]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('has required fields in tool definitions', () => {
|
|
56
|
-
const tools = getAdrToolDefinitions();
|
|
57
|
-
tools.forEach(tool => {
|
|
58
|
-
expect(tool.name).toBeTruthy();
|
|
59
|
-
expect(tool.description).toBeTruthy();
|
|
60
|
-
expect(tool.inputSchema).toBeDefined();
|
|
61
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('adr_create requires title and decision', () => {
|
|
66
|
-
const tools = getAdrToolDefinitions();
|
|
67
|
-
const createTool = tools.find(t => t.name.endsWith('_adr_create'));
|
|
68
|
-
expect(createTool?.inputSchema.required).toEqual(['title', 'decision']);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('isAdrTool', () => {
|
|
73
|
-
it('returns true for ADR tool names', () => {
|
|
74
|
-
expect(isAdrTool('massu_adr_list')).toBe(true);
|
|
75
|
-
expect(isAdrTool('massu_adr_detail')).toBe(true);
|
|
76
|
-
expect(isAdrTool('massu_adr_create')).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns false for non-ADR tool names', () => {
|
|
80
|
-
expect(isAdrTool('massu_security_score')).toBe(false);
|
|
81
|
-
expect(isAdrTool('massu_unknown')).toBe(false);
|
|
82
|
-
expect(isAdrTool('adr_list')).toBe(true); // base name without prefix
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('detectDecisionPatterns', () => {
|
|
87
|
-
it('detects decision phrases in text', () => {
|
|
88
|
-
expect(detectDecisionPatterns('We chose Redis for caching')).toBe(true);
|
|
89
|
-
expect(detectDecisionPatterns('Decided to use TypeScript')).toBe(true);
|
|
90
|
-
expect(detectDecisionPatterns('switching to ESM modules')).toBe(true);
|
|
91
|
-
expect(detectDecisionPatterns('moving from CommonJS to ESM')).toBe(true);
|
|
92
|
-
expect(detectDecisionPatterns('going with Fastify')).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('is case insensitive', () => {
|
|
96
|
-
expect(detectDecisionPatterns('DECIDED to refactor')).toBe(true);
|
|
97
|
-
expect(detectDecisionPatterns('Chose the new architecture')).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('returns false when no decision pattern found', () => {
|
|
101
|
-
expect(detectDecisionPatterns('This is a regular sentence')).toBe(false);
|
|
102
|
-
expect(detectDecisionPatterns('No patterns here')).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('extractAlternatives', () => {
|
|
107
|
-
it('extracts alternatives from "X over Y" pattern', () => {
|
|
108
|
-
const result = extractAlternatives('We chose Redis over Memcached for caching');
|
|
109
|
-
// The regex captures from the last word before "over" to next word, so "chose Redis" and "Memcached for caching"
|
|
110
|
-
expect(result.length).toBeGreaterThan(0);
|
|
111
|
-
// Just verify the function works and returns alternatives
|
|
112
|
-
expect(result.some(alt => alt.includes('Redis'))).toBe(true);
|
|
113
|
-
expect(result.some(alt => alt.includes('Memcached'))).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('extracts alternatives from "X instead of Y" pattern', () => {
|
|
117
|
-
const result = extractAlternatives('Use TypeScript instead of JavaScript');
|
|
118
|
-
expect(result.length).toBeGreaterThan(0);
|
|
119
|
-
expect(result.some(alt => alt.includes('TypeScript'))).toBe(true);
|
|
120
|
-
expect(result.some(alt => alt.includes('JavaScript'))).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('extracts alternatives from "switching from X to Y" pattern', () => {
|
|
124
|
-
const result = extractAlternatives('switching from CommonJS to ESM modules');
|
|
125
|
-
expect(result.length).toBeGreaterThan(0);
|
|
126
|
-
expect(result.some(alt => alt.includes('ESM'))).toBe(true);
|
|
127
|
-
expect(result.some(alt => alt.includes('CommonJS'))).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('returns unique alternatives only', () => {
|
|
131
|
-
const result = extractAlternatives('Redis over Memcached instead of Redis');
|
|
132
|
-
const unique = [...new Set(result)];
|
|
133
|
-
expect(result.length).toBe(unique.length);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('returns empty array when no alternatives found', () => {
|
|
137
|
-
const result = extractAlternatives('This has no alternatives');
|
|
138
|
-
expect(result).toEqual([]);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('storeDecision', () => {
|
|
143
|
-
it('stores a decision and returns ID', () => {
|
|
144
|
-
const id = storeDecision(db, {
|
|
145
|
-
title: 'Use Redis for caching',
|
|
146
|
-
context: 'Need fast cache',
|
|
147
|
-
decision: 'Redis over Memcached',
|
|
148
|
-
alternatives: ['Redis', 'Memcached'],
|
|
149
|
-
consequences: 'Better performance',
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(id).toBeGreaterThan(0);
|
|
153
|
-
|
|
154
|
-
const row = db.prepare('SELECT * FROM architecture_decisions WHERE id = ?').get(id) as Record<string, unknown>;
|
|
155
|
-
expect(row.title).toBe('Use Redis for caching');
|
|
156
|
-
expect(row.decision).toBe('Redis over Memcached');
|
|
157
|
-
expect(JSON.parse(row.alternatives as string)).toEqual(['Redis', 'Memcached']);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('uses default status when not provided', () => {
|
|
161
|
-
const id = storeDecision(db, {
|
|
162
|
-
title: 'Test Decision',
|
|
163
|
-
context: 'Context',
|
|
164
|
-
decision: 'Decision text',
|
|
165
|
-
alternatives: [],
|
|
166
|
-
consequences: 'None',
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const row = db.prepare('SELECT status FROM architecture_decisions WHERE id = ?').get(id) as { status: string };
|
|
170
|
-
expect(row.status).toBe('accepted');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('accepts custom status', () => {
|
|
174
|
-
const id = storeDecision(db, {
|
|
175
|
-
title: 'Test Decision',
|
|
176
|
-
context: 'Context',
|
|
177
|
-
decision: 'Decision text',
|
|
178
|
-
alternatives: [],
|
|
179
|
-
consequences: 'None',
|
|
180
|
-
status: 'superseded',
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const row = db.prepare('SELECT status FROM architecture_decisions WHERE id = ?').get(id) as { status: string };
|
|
184
|
-
expect(row.status).toBe('superseded');
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe('handleAdrToolCall', () => {
|
|
189
|
-
it('handles adr_list with no decisions', () => {
|
|
190
|
-
const result = handleAdrToolCall('massu_adr_list', {}, db);
|
|
191
|
-
expect(result.content[0].type).toBe('text');
|
|
192
|
-
const text = result.content[0].text;
|
|
193
|
-
expect(text).toContain('No architecture decisions found');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('handles adr_list with decisions', () => {
|
|
197
|
-
storeDecision(db, {
|
|
198
|
-
title: 'Test Decision',
|
|
199
|
-
context: 'Context',
|
|
200
|
-
decision: 'Decision',
|
|
201
|
-
alternatives: [],
|
|
202
|
-
consequences: 'None',
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const result = handleAdrToolCall('massu_adr_list', {}, db);
|
|
206
|
-
const text = result.content[0].text;
|
|
207
|
-
expect(text).toContain('Architecture Decisions (1)');
|
|
208
|
-
expect(text).toContain('Test Decision');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('handles adr_detail for existing decision', () => {
|
|
212
|
-
const id = storeDecision(db, {
|
|
213
|
-
title: 'Test Decision',
|
|
214
|
-
context: 'Test Context',
|
|
215
|
-
decision: 'Test Decision Text',
|
|
216
|
-
alternatives: ['Option A', 'Option B'],
|
|
217
|
-
consequences: 'Test Consequences',
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const result = handleAdrToolCall('massu_adr_detail', { id }, db);
|
|
221
|
-
const text = result.content[0].text;
|
|
222
|
-
expect(text).toContain(`ADR-${id}: Test Decision`);
|
|
223
|
-
expect(text).toContain('Test Context');
|
|
224
|
-
expect(text).toContain('Option A');
|
|
225
|
-
expect(text).toContain('Option B');
|
|
226
|
-
expect(text).toContain('Test Consequences');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('handles adr_detail for non-existent decision', () => {
|
|
230
|
-
const result = handleAdrToolCall('massu_adr_detail', { id: 999 }, db);
|
|
231
|
-
const text = result.content[0].text;
|
|
232
|
-
expect(text).toContain('ADR #999 not found');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('handles adr_create and extracts alternatives', () => {
|
|
236
|
-
const result = handleAdrToolCall('massu_adr_create', {
|
|
237
|
-
title: 'Use Redis',
|
|
238
|
-
decision: 'Redis over Memcached',
|
|
239
|
-
context: 'Need caching',
|
|
240
|
-
consequences: 'Better performance',
|
|
241
|
-
}, db);
|
|
242
|
-
|
|
243
|
-
const text = result.content[0].text;
|
|
244
|
-
expect(text).toContain('ADR-1 Created');
|
|
245
|
-
expect(text).toContain('Redis');
|
|
246
|
-
expect(text).toContain('Memcached');
|
|
247
|
-
|
|
248
|
-
const row = db.prepare('SELECT * FROM architecture_decisions WHERE id = 1').get() as Record<string, unknown>;
|
|
249
|
-
const alternatives = JSON.parse(row.alternatives as string) as string[];
|
|
250
|
-
expect(alternatives).toContain('Redis');
|
|
251
|
-
expect(alternatives).toContain('Memcached');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('handles unknown tool name', () => {
|
|
255
|
-
const result = handleAdrToolCall('massu_adr_unknown', {}, db);
|
|
256
|
-
const text = result.content[0].text;
|
|
257
|
-
expect(text).toContain('Unknown ADR tool');
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
});
|
|
@@ -1,282 +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 {
|
|
7
|
-
getAnalyticsToolDefinitions,
|
|
8
|
-
isAnalyticsTool,
|
|
9
|
-
calculateQualityScore,
|
|
10
|
-
storeQualityScore,
|
|
11
|
-
backfillQualityScores,
|
|
12
|
-
handleAnalyticsToolCall,
|
|
13
|
-
type QualityBreakdown,
|
|
14
|
-
} from '../analytics.ts';
|
|
15
|
-
|
|
16
|
-
function createTestDb(): Database.Database {
|
|
17
|
-
const db = new Database(':memory:');
|
|
18
|
-
db.pragma('journal_mode = WAL');
|
|
19
|
-
|
|
20
|
-
// Create minimal required schema for analytics
|
|
21
|
-
db.exec(`
|
|
22
|
-
CREATE TABLE sessions (
|
|
23
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
-
session_id TEXT UNIQUE NOT NULL,
|
|
25
|
-
started_at TEXT NOT NULL,
|
|
26
|
-
started_at_epoch INTEGER NOT NULL
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
CREATE TABLE observations (
|
|
30
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
-
session_id TEXT NOT NULL,
|
|
32
|
-
type TEXT NOT NULL,
|
|
33
|
-
detail TEXT,
|
|
34
|
-
created_at TEXT NOT NULL
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
CREATE TABLE session_quality_scores (
|
|
38
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
-
session_id TEXT NOT NULL UNIQUE,
|
|
40
|
-
score INTEGER NOT NULL,
|
|
41
|
-
security_score INTEGER NOT NULL DEFAULT 0,
|
|
42
|
-
architecture_score INTEGER NOT NULL DEFAULT 0,
|
|
43
|
-
coupling_score INTEGER NOT NULL DEFAULT 0,
|
|
44
|
-
test_score INTEGER NOT NULL DEFAULT 0,
|
|
45
|
-
rule_compliance_score INTEGER NOT NULL DEFAULT 0,
|
|
46
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
47
|
-
);
|
|
48
|
-
`);
|
|
49
|
-
|
|
50
|
-
return db;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('analytics', () => {
|
|
54
|
-
let db: Database.Database;
|
|
55
|
-
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
db = createTestDb();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
db.close();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('getAnalyticsToolDefinitions', () => {
|
|
65
|
-
it('should return tool definitions for analytics tools', () => {
|
|
66
|
-
const defs = getAnalyticsToolDefinitions();
|
|
67
|
-
expect(defs.length).toBe(3);
|
|
68
|
-
|
|
69
|
-
const names = defs.map(d => d.name);
|
|
70
|
-
expect(names.some(n => n.includes('quality_score'))).toBe(true);
|
|
71
|
-
expect(names.some(n => n.includes('quality_trend'))).toBe(true);
|
|
72
|
-
expect(names.some(n => n.includes('quality_report'))).toBe(true);
|
|
73
|
-
|
|
74
|
-
// Check structure
|
|
75
|
-
for (const def of defs) {
|
|
76
|
-
expect(def.name).toBeTruthy();
|
|
77
|
-
expect(def.description).toBeTruthy();
|
|
78
|
-
expect(def.inputSchema).toBeTruthy();
|
|
79
|
-
expect(def.inputSchema.type).toBe('object');
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('isAnalyticsTool', () => {
|
|
85
|
-
it('should return true for analytics tools', () => {
|
|
86
|
-
expect(isAnalyticsTool('massu_quality_score')).toBe(true);
|
|
87
|
-
expect(isAnalyticsTool('massu_quality_trend')).toBe(true);
|
|
88
|
-
expect(isAnalyticsTool('massu_quality_report')).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should return false for non-analytics tools', () => {
|
|
92
|
-
expect(isAnalyticsTool('massu_cost_session')).toBe(false);
|
|
93
|
-
expect(isAnalyticsTool('massu_audit_log')).toBe(false);
|
|
94
|
-
expect(isAnalyticsTool('random_tool')).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should handle base names without prefix', () => {
|
|
98
|
-
expect(isAnalyticsTool('quality_score')).toBe(true);
|
|
99
|
-
expect(isAnalyticsTool('quality_trend')).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('calculateQualityScore', () => {
|
|
104
|
-
it('should calculate quality score from observations', () => {
|
|
105
|
-
const sessionId = 'test-session-1';
|
|
106
|
-
|
|
107
|
-
// Insert session
|
|
108
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
109
|
-
sessionId,
|
|
110
|
-
new Date().toISOString(),
|
|
111
|
-
Math.floor(Date.now() / 1000)
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Insert observations
|
|
115
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
116
|
-
sessionId, 'vr_pass', 'Security check passed', new Date().toISOString()
|
|
117
|
-
);
|
|
118
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
119
|
-
sessionId, 'clean_commit', 'Clean commit with security improvements', new Date().toISOString()
|
|
120
|
-
);
|
|
121
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
122
|
-
sessionId, 'bug_found', 'Bug found in architecture', new Date().toISOString()
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const result = calculateQualityScore(db, sessionId);
|
|
126
|
-
|
|
127
|
-
expect(result.score).toBeGreaterThan(0);
|
|
128
|
-
expect(result.score).toBeLessThanOrEqual(100);
|
|
129
|
-
expect(result.breakdown).toBeDefined();
|
|
130
|
-
expect(result.breakdown.security).toBeDefined();
|
|
131
|
-
expect(result.breakdown.architecture).toBeDefined();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should start with base score of 50', () => {
|
|
135
|
-
const sessionId = 'test-session-2';
|
|
136
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
137
|
-
sessionId,
|
|
138
|
-
new Date().toISOString(),
|
|
139
|
-
Math.floor(Date.now() / 1000)
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const result = calculateQualityScore(db, sessionId);
|
|
143
|
-
expect(result.score).toBe(50); // No observations = base score
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should clamp score between 0 and 100', () => {
|
|
147
|
-
const sessionId = 'test-session-3';
|
|
148
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
149
|
-
sessionId,
|
|
150
|
-
new Date().toISOString(),
|
|
151
|
-
Math.floor(Date.now() / 1000)
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// Add many negative observations
|
|
155
|
-
for (let i = 0; i < 10; i++) {
|
|
156
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
157
|
-
sessionId, 'incident', 'Critical incident', new Date().toISOString()
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const result = calculateQualityScore(db, sessionId);
|
|
162
|
-
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
163
|
-
expect(result.score).toBeLessThanOrEqual(100);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('storeQualityScore', () => {
|
|
168
|
-
it('should store quality score in database', () => {
|
|
169
|
-
const sessionId = 'test-session-4';
|
|
170
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
171
|
-
sessionId,
|
|
172
|
-
new Date().toISOString(),
|
|
173
|
-
Math.floor(Date.now() / 1000)
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const breakdown: QualityBreakdown = {
|
|
177
|
-
security: 5,
|
|
178
|
-
architecture: -3,
|
|
179
|
-
coupling: 0,
|
|
180
|
-
tests: 2,
|
|
181
|
-
rule_compliance: 1,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
storeQualityScore(db, sessionId, 75, breakdown);
|
|
185
|
-
|
|
186
|
-
const stored = db.prepare('SELECT * FROM session_quality_scores WHERE session_id = ?').get(sessionId) as Record<string, unknown>;
|
|
187
|
-
expect(stored).toBeDefined();
|
|
188
|
-
expect(stored.session_id).toBe(sessionId);
|
|
189
|
-
expect(stored.score).toBe(75);
|
|
190
|
-
expect(stored.security_score).toBe(5);
|
|
191
|
-
expect(stored.architecture_score).toBe(-3);
|
|
192
|
-
expect(stored.coupling_score).toBe(0);
|
|
193
|
-
expect(stored.test_score).toBe(2);
|
|
194
|
-
expect(stored.rule_compliance_score).toBe(1);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('backfillQualityScores', () => {
|
|
199
|
-
it('should backfill scores for sessions without them', () => {
|
|
200
|
-
// Create sessions
|
|
201
|
-
const sessions = ['session-1', 'session-2', 'session-3'];
|
|
202
|
-
for (const sid of sessions) {
|
|
203
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
204
|
-
sid,
|
|
205
|
-
new Date().toISOString(),
|
|
206
|
-
Math.floor(Date.now() / 1000)
|
|
207
|
-
);
|
|
208
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
209
|
-
sid, 'vr_pass', 'Test passed', new Date().toISOString()
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const backfilled = backfillQualityScores(db);
|
|
214
|
-
expect(backfilled).toBe(3);
|
|
215
|
-
|
|
216
|
-
// Verify all sessions now have scores
|
|
217
|
-
const scores = db.prepare('SELECT COUNT(*) as count FROM session_quality_scores').get() as { count: number };
|
|
218
|
-
expect(scores.count).toBe(3);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should not backfill sessions that already have scores', () => {
|
|
222
|
-
const sessionId = 'session-with-score';
|
|
223
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
224
|
-
sessionId,
|
|
225
|
-
new Date().toISOString(),
|
|
226
|
-
Math.floor(Date.now() / 1000)
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Already has a score
|
|
230
|
-
const breakdown: QualityBreakdown = {
|
|
231
|
-
security: 0,
|
|
232
|
-
architecture: 0,
|
|
233
|
-
coupling: 0,
|
|
234
|
-
tests: 0,
|
|
235
|
-
rule_compliance: 0,
|
|
236
|
-
};
|
|
237
|
-
storeQualityScore(db, sessionId, 50, breakdown);
|
|
238
|
-
|
|
239
|
-
const backfilled = backfillQualityScores(db);
|
|
240
|
-
expect(backfilled).toBe(0);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('handleAnalyticsToolCall', () => {
|
|
245
|
-
it('should handle quality_score tool call', () => {
|
|
246
|
-
const sessionId = 'test-session-5';
|
|
247
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
248
|
-
sessionId,
|
|
249
|
-
new Date().toISOString(),
|
|
250
|
-
Math.floor(Date.now() / 1000)
|
|
251
|
-
);
|
|
252
|
-
db.prepare('INSERT INTO observations (session_id, type, detail, created_at) VALUES (?, ?, ?, ?)').run(
|
|
253
|
-
sessionId, 'vr_pass', 'Security tests passed', new Date().toISOString()
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
const result = handleAnalyticsToolCall('massu_quality_score', { session_id: sessionId }, db);
|
|
257
|
-
|
|
258
|
-
expect(result.content).toBeDefined();
|
|
259
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
260
|
-
expect(result.content[0].type).toBe('text');
|
|
261
|
-
const text = result.content[0].text;
|
|
262
|
-
expect(text).toContain('Quality Score');
|
|
263
|
-
expect(text).toContain('Breakdown');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should return error for missing session_id', () => {
|
|
267
|
-
const result = handleAnalyticsToolCall('massu_quality_score', {}, db);
|
|
268
|
-
|
|
269
|
-
expect(result.content).toBeDefined();
|
|
270
|
-
expect(result.content[0].type).toBe('text');
|
|
271
|
-
expect(result.content[0].text).toContain('Usage');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should handle unknown tool name', () => {
|
|
275
|
-
const result = handleAnalyticsToolCall('massu_unknown_analytics_tool', {}, db);
|
|
276
|
-
|
|
277
|
-
expect(result.content).toBeDefined();
|
|
278
|
-
expect(result.content[0].type).toBe('text');
|
|
279
|
-
expect(result.content[0].text).toContain('Unknown analytics tool');
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
});
|