@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,348 +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
|
-
getCostToolDefinitions,
|
|
8
|
-
isCostTool,
|
|
9
|
-
extractTokenUsage,
|
|
10
|
-
calculateCost,
|
|
11
|
-
storeSessionCost,
|
|
12
|
-
backfillSessionCosts,
|
|
13
|
-
handleCostToolCall,
|
|
14
|
-
type TokenUsage,
|
|
15
|
-
type CostResult,
|
|
16
|
-
} from '../cost-tracker.ts';
|
|
17
|
-
import type { TranscriptEntry } from '../transcript-parser.ts';
|
|
18
|
-
|
|
19
|
-
function createTestDb(): Database.Database {
|
|
20
|
-
const db = new Database(':memory:');
|
|
21
|
-
db.pragma('journal_mode = WAL');
|
|
22
|
-
|
|
23
|
-
db.exec(`
|
|
24
|
-
CREATE TABLE sessions (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
-
session_id TEXT UNIQUE NOT NULL,
|
|
27
|
-
started_at TEXT NOT NULL,
|
|
28
|
-
started_at_epoch INTEGER NOT NULL
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
CREATE TABLE session_costs (
|
|
32
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
-
session_id TEXT NOT NULL UNIQUE,
|
|
34
|
-
model TEXT,
|
|
35
|
-
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
36
|
-
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
37
|
-
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
38
|
-
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
39
|
-
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
40
|
-
estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
|
|
41
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
CREATE TABLE feature_costs (
|
|
45
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
-
feature_key TEXT NOT NULL,
|
|
47
|
-
session_id TEXT NOT NULL,
|
|
48
|
-
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
49
|
-
estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
|
|
50
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
51
|
-
);
|
|
52
|
-
`);
|
|
53
|
-
|
|
54
|
-
return db;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('cost-tracker', () => {
|
|
58
|
-
let db: Database.Database;
|
|
59
|
-
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
db = createTestDb();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
db.close();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('getCostToolDefinitions', () => {
|
|
69
|
-
it('should return tool definitions for cost tools', () => {
|
|
70
|
-
const defs = getCostToolDefinitions();
|
|
71
|
-
expect(defs.length).toBe(3);
|
|
72
|
-
|
|
73
|
-
const names = defs.map(d => d.name);
|
|
74
|
-
expect(names.some(n => n.includes('cost_session'))).toBe(true);
|
|
75
|
-
expect(names.some(n => n.includes('cost_trend'))).toBe(true);
|
|
76
|
-
expect(names.some(n => n.includes('cost_feature'))).toBe(true);
|
|
77
|
-
|
|
78
|
-
for (const def of defs) {
|
|
79
|
-
expect(def.name).toBeTruthy();
|
|
80
|
-
expect(def.description).toBeTruthy();
|
|
81
|
-
expect(def.inputSchema).toBeTruthy();
|
|
82
|
-
expect(def.inputSchema.type).toBe('object');
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('isCostTool', () => {
|
|
88
|
-
it('should return true for cost tools', () => {
|
|
89
|
-
expect(isCostTool('massu_cost_session')).toBe(true);
|
|
90
|
-
expect(isCostTool('massu_cost_trend')).toBe(true);
|
|
91
|
-
expect(isCostTool('massu_cost_feature')).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should return false for non-cost tools', () => {
|
|
95
|
-
expect(isCostTool('massu_quality_score')).toBe(false);
|
|
96
|
-
expect(isCostTool('massu_audit_log')).toBe(false);
|
|
97
|
-
expect(isCostTool('random_tool')).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should handle base names without prefix', () => {
|
|
101
|
-
expect(isCostTool('cost_session')).toBe(true);
|
|
102
|
-
expect(isCostTool('cost_trend')).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('extractTokenUsage', () => {
|
|
107
|
-
it('should extract token usage from transcript entries', () => {
|
|
108
|
-
const entries: TranscriptEntry[] = [
|
|
109
|
-
{
|
|
110
|
-
type: 'assistant',
|
|
111
|
-
message: {
|
|
112
|
-
usage: {
|
|
113
|
-
input_tokens: 1000,
|
|
114
|
-
output_tokens: 500,
|
|
115
|
-
cache_read_input_tokens: 200,
|
|
116
|
-
cache_creation_input_tokens: 100,
|
|
117
|
-
},
|
|
118
|
-
model: 'claude-sonnet-4-5',
|
|
119
|
-
} as Record<string, unknown>,
|
|
120
|
-
} as TranscriptEntry,
|
|
121
|
-
{
|
|
122
|
-
type: 'assistant',
|
|
123
|
-
message: {
|
|
124
|
-
usage: {
|
|
125
|
-
input_tokens: 800,
|
|
126
|
-
output_tokens: 400,
|
|
127
|
-
cache_read_tokens: 50,
|
|
128
|
-
cache_write_tokens: 25,
|
|
129
|
-
},
|
|
130
|
-
model: 'claude-sonnet-4-5',
|
|
131
|
-
} as Record<string, unknown>,
|
|
132
|
-
} as TranscriptEntry,
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
const usage = extractTokenUsage(entries);
|
|
136
|
-
expect(usage.inputTokens).toBe(1800);
|
|
137
|
-
expect(usage.outputTokens).toBe(900);
|
|
138
|
-
expect(usage.cacheReadTokens).toBe(250);
|
|
139
|
-
expect(usage.cacheWriteTokens).toBe(125);
|
|
140
|
-
expect(usage.model).toBe('claude-sonnet-4-5');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should return zero tokens for empty entries', () => {
|
|
144
|
-
const usage = extractTokenUsage([]);
|
|
145
|
-
expect(usage.inputTokens).toBe(0);
|
|
146
|
-
expect(usage.outputTokens).toBe(0);
|
|
147
|
-
expect(usage.cacheReadTokens).toBe(0);
|
|
148
|
-
expect(usage.cacheWriteTokens).toBe(0);
|
|
149
|
-
expect(usage.model).toBe('unknown');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('calculateCost', () => {
|
|
154
|
-
it('should calculate cost from token usage', () => {
|
|
155
|
-
const usage: TokenUsage = {
|
|
156
|
-
inputTokens: 1_000_000,
|
|
157
|
-
outputTokens: 500_000,
|
|
158
|
-
cacheReadTokens: 200_000,
|
|
159
|
-
cacheWriteTokens: 100_000,
|
|
160
|
-
model: 'claude-sonnet-4-5',
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const cost = calculateCost(usage);
|
|
164
|
-
|
|
165
|
-
expect(cost.totalCost).toBeGreaterThan(0);
|
|
166
|
-
expect(cost.inputCost).toBeGreaterThan(0);
|
|
167
|
-
expect(cost.outputCost).toBeGreaterThan(0);
|
|
168
|
-
// Cache costs may be 0 if pricing doesn't include cache_read/write rates
|
|
169
|
-
expect(cost.cacheReadCost).toBeGreaterThanOrEqual(0);
|
|
170
|
-
expect(cost.cacheWriteCost).toBeGreaterThanOrEqual(0);
|
|
171
|
-
expect(cost.currency).toBe('USD');
|
|
172
|
-
expect(cost.totalCost).toBe(
|
|
173
|
-
cost.inputCost + cost.outputCost + cost.cacheReadCost + cost.cacheWriteCost
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should use default pricing for unknown models', () => {
|
|
178
|
-
const usage: TokenUsage = {
|
|
179
|
-
inputTokens: 1_000_000,
|
|
180
|
-
outputTokens: 500_000,
|
|
181
|
-
cacheReadTokens: 0,
|
|
182
|
-
cacheWriteTokens: 0,
|
|
183
|
-
model: 'unknown-model',
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const cost = calculateCost(usage);
|
|
187
|
-
expect(cost.totalCost).toBeGreaterThan(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should handle zero tokens', () => {
|
|
191
|
-
const usage: TokenUsage = {
|
|
192
|
-
inputTokens: 0,
|
|
193
|
-
outputTokens: 0,
|
|
194
|
-
cacheReadTokens: 0,
|
|
195
|
-
cacheWriteTokens: 0,
|
|
196
|
-
model: 'claude-sonnet-4-5',
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const cost = calculateCost(usage);
|
|
200
|
-
expect(cost.totalCost).toBe(0);
|
|
201
|
-
expect(cost.inputCost).toBe(0);
|
|
202
|
-
expect(cost.outputCost).toBe(0);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('storeSessionCost', () => {
|
|
207
|
-
it('should store session cost in database', () => {
|
|
208
|
-
const sessionId = 'test-session-1';
|
|
209
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
210
|
-
sessionId,
|
|
211
|
-
new Date().toISOString(),
|
|
212
|
-
Math.floor(Date.now() / 1000)
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const usage: TokenUsage = {
|
|
216
|
-
inputTokens: 1000,
|
|
217
|
-
outputTokens: 500,
|
|
218
|
-
cacheReadTokens: 200,
|
|
219
|
-
cacheWriteTokens: 100,
|
|
220
|
-
model: 'claude-sonnet-4-5',
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const cost: CostResult = {
|
|
224
|
-
totalCost: 0.015,
|
|
225
|
-
inputCost: 0.003,
|
|
226
|
-
outputCost: 0.0075,
|
|
227
|
-
cacheReadCost: 0.0006,
|
|
228
|
-
cacheWriteCost: 0.00375,
|
|
229
|
-
currency: 'USD',
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
storeSessionCost(db, sessionId, usage, cost);
|
|
233
|
-
|
|
234
|
-
const stored = db.prepare('SELECT * FROM session_costs WHERE session_id = ?').get(sessionId) as Record<string, unknown>;
|
|
235
|
-
expect(stored).toBeDefined();
|
|
236
|
-
expect(stored.session_id).toBe(sessionId);
|
|
237
|
-
expect(stored.model).toBe('claude-sonnet-4-5');
|
|
238
|
-
expect(stored.input_tokens).toBe(1000);
|
|
239
|
-
expect(stored.output_tokens).toBe(500);
|
|
240
|
-
expect(stored.cache_read_tokens).toBe(200);
|
|
241
|
-
expect(stored.cache_write_tokens).toBe(100);
|
|
242
|
-
expect(stored.total_tokens).toBe(1800);
|
|
243
|
-
expect(stored.estimated_cost_usd).toBe(0.015);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
describe('backfillSessionCosts', () => {
|
|
248
|
-
it('should return count of sessions without cost data', () => {
|
|
249
|
-
const sessions = ['session-1', 'session-2', 'session-3'];
|
|
250
|
-
for (const sid of sessions) {
|
|
251
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
252
|
-
sid,
|
|
253
|
-
new Date().toISOString(),
|
|
254
|
-
Math.floor(Date.now() / 1000)
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const count = backfillSessionCosts(db);
|
|
259
|
-
expect(count).toBe(3);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('should not count sessions that already have cost data', () => {
|
|
263
|
-
const sessionId = 'session-with-cost';
|
|
264
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
265
|
-
sessionId,
|
|
266
|
-
new Date().toISOString(),
|
|
267
|
-
Math.floor(Date.now() / 1000)
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const usage: TokenUsage = {
|
|
271
|
-
inputTokens: 100,
|
|
272
|
-
outputTokens: 50,
|
|
273
|
-
cacheReadTokens: 0,
|
|
274
|
-
cacheWriteTokens: 0,
|
|
275
|
-
model: 'claude-sonnet-4-5',
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const cost: CostResult = {
|
|
279
|
-
totalCost: 0.001,
|
|
280
|
-
inputCost: 0.0003,
|
|
281
|
-
outputCost: 0.00075,
|
|
282
|
-
cacheReadCost: 0,
|
|
283
|
-
cacheWriteCost: 0,
|
|
284
|
-
currency: 'USD',
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
storeSessionCost(db, sessionId, usage, cost);
|
|
288
|
-
|
|
289
|
-
const count = backfillSessionCosts(db);
|
|
290
|
-
expect(count).toBe(0);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('handleCostToolCall', () => {
|
|
295
|
-
it('should handle cost_session tool call', () => {
|
|
296
|
-
const sessionId = 'test-session-2';
|
|
297
|
-
db.prepare('INSERT INTO sessions (session_id, started_at, started_at_epoch) VALUES (?, ?, ?)').run(
|
|
298
|
-
sessionId,
|
|
299
|
-
new Date().toISOString(),
|
|
300
|
-
Math.floor(Date.now() / 1000)
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
const usage: TokenUsage = {
|
|
304
|
-
inputTokens: 1000,
|
|
305
|
-
outputTokens: 500,
|
|
306
|
-
cacheReadTokens: 100,
|
|
307
|
-
cacheWriteTokens: 50,
|
|
308
|
-
model: 'claude-sonnet-4-5',
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const cost: CostResult = {
|
|
312
|
-
totalCost: 0.01,
|
|
313
|
-
inputCost: 0.003,
|
|
314
|
-
outputCost: 0.0075,
|
|
315
|
-
cacheReadCost: 0.0003,
|
|
316
|
-
cacheWriteCost: 0.0001875,
|
|
317
|
-
currency: 'USD',
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
storeSessionCost(db, sessionId, usage, cost);
|
|
321
|
-
|
|
322
|
-
const result = handleCostToolCall('massu_cost_session', { session_id: sessionId }, db);
|
|
323
|
-
|
|
324
|
-
expect(result.content).toBeDefined();
|
|
325
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
326
|
-
expect(result.content[0].type).toBe('text');
|
|
327
|
-
const text = result.content[0].text;
|
|
328
|
-
expect(text).toContain('Session Cost');
|
|
329
|
-
expect(text).toContain('Token Usage');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('should return error for missing session_id', () => {
|
|
333
|
-
const result = handleCostToolCall('massu_cost_session', {}, db);
|
|
334
|
-
|
|
335
|
-
expect(result.content).toBeDefined();
|
|
336
|
-
expect(result.content[0].type).toBe('text');
|
|
337
|
-
expect(result.content[0].text).toContain('Usage');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('should handle unknown tool name', () => {
|
|
341
|
-
const result = handleCostToolCall('massu_unknown_cost_tool', {}, db);
|
|
342
|
-
|
|
343
|
-
expect(result.content).toBeDefined();
|
|
344
|
-
expect(result.content[0].type).toBe('text');
|
|
345
|
-
expect(result.content[0].text).toContain('Unknown cost tool');
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
});
|
package/src/__tests__/db.test.ts
DELETED
|
@@ -1,177 +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, vi } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import { isDataStale, updateBuildTimestamp } from '../db.ts';
|
|
7
|
-
import { unlinkSync, existsSync } from 'fs';
|
|
8
|
-
import { resolve } from 'path';
|
|
9
|
-
|
|
10
|
-
const TEST_DATA_DB_PATH = resolve(__dirname, '../test-data-db.db');
|
|
11
|
-
const TEST_CODEGRAPH_DB_PATH = resolve(__dirname, '../test-codegraph-db.db');
|
|
12
|
-
|
|
13
|
-
function createTestDataDb(): Database.Database {
|
|
14
|
-
if (existsSync(TEST_DATA_DB_PATH)) {
|
|
15
|
-
unlinkSync(TEST_DATA_DB_PATH);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const db = new Database(TEST_DATA_DB_PATH);
|
|
19
|
-
db.pragma('journal_mode = WAL');
|
|
20
|
-
db.pragma('foreign_keys = ON');
|
|
21
|
-
|
|
22
|
-
db.exec(`
|
|
23
|
-
CREATE TABLE IF NOT EXISTS massu_meta (
|
|
24
|
-
key TEXT PRIMARY KEY,
|
|
25
|
-
value TEXT NOT NULL
|
|
26
|
-
);
|
|
27
|
-
`);
|
|
28
|
-
|
|
29
|
-
return db;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function createTestCodeGraphDb(): Database.Database {
|
|
33
|
-
if (existsSync(TEST_CODEGRAPH_DB_PATH)) {
|
|
34
|
-
unlinkSync(TEST_CODEGRAPH_DB_PATH);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const db = new Database(TEST_CODEGRAPH_DB_PATH);
|
|
38
|
-
db.pragma('journal_mode = WAL');
|
|
39
|
-
|
|
40
|
-
db.exec(`
|
|
41
|
-
CREATE TABLE IF NOT EXISTS files (
|
|
42
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
-
path TEXT UNIQUE NOT NULL,
|
|
44
|
-
indexed_at INTEGER NOT NULL
|
|
45
|
-
);
|
|
46
|
-
`);
|
|
47
|
-
|
|
48
|
-
return db;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('Database Module', () => {
|
|
52
|
-
let dataDb: Database.Database;
|
|
53
|
-
let codegraphDb: Database.Database;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
dataDb = createTestDataDb();
|
|
57
|
-
codegraphDb = createTestCodeGraphDb();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
dataDb.close();
|
|
62
|
-
codegraphDb.close();
|
|
63
|
-
if (existsSync(TEST_DATA_DB_PATH)) {
|
|
64
|
-
unlinkSync(TEST_DATA_DB_PATH);
|
|
65
|
-
}
|
|
66
|
-
if (existsSync(TEST_CODEGRAPH_DB_PATH)) {
|
|
67
|
-
unlinkSync(TEST_CODEGRAPH_DB_PATH);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('isDataStale', () => {
|
|
72
|
-
it('returns true when no last_build_time exists', () => {
|
|
73
|
-
codegraphDb.prepare(`INSERT INTO files (path, indexed_at) VALUES ('test.ts', ?)`).run(
|
|
74
|
-
Math.floor(Date.now() / 1000)
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const stale = isDataStale(dataDb, codegraphDb);
|
|
78
|
-
expect(stale).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('returns true when codegraph is newer than last build', () => {
|
|
82
|
-
const oldTime = new Date(Date.now() - 60000); // 1 minute ago
|
|
83
|
-
dataDb.prepare(`INSERT INTO massu_meta (key, value) VALUES ('last_build_time', ?)`).run(
|
|
84
|
-
oldTime.toISOString()
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const newTimestamp = Math.floor(Date.now() / 1000); // Now
|
|
88
|
-
codegraphDb.prepare(`INSERT INTO files (path, indexed_at) VALUES ('test.ts', ?)`).run(newTimestamp);
|
|
89
|
-
|
|
90
|
-
const stale = isDataStale(dataDb, codegraphDb);
|
|
91
|
-
expect(stale).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('returns false when data is up to date', () => {
|
|
95
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
|
96
|
-
const pastTime = currentTime - 60; // 1 minute ago
|
|
97
|
-
|
|
98
|
-
codegraphDb.prepare(`INSERT INTO files (path, indexed_at) VALUES ('test.ts', ?)`).run(pastTime);
|
|
99
|
-
|
|
100
|
-
dataDb.prepare(`INSERT INTO massu_meta (key, value) VALUES ('last_build_time', ?)`).run(
|
|
101
|
-
new Date().toISOString()
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const stale = isDataStale(dataDb, codegraphDb);
|
|
105
|
-
expect(stale).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('returns true when no files in codegraph', () => {
|
|
109
|
-
dataDb.prepare(`INSERT INTO massu_meta (key, value) VALUES ('last_build_time', ?)`).run(
|
|
110
|
-
new Date().toISOString()
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const stale = isDataStale(dataDb, codegraphDb);
|
|
114
|
-
expect(stale).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('updateBuildTimestamp', () => {
|
|
119
|
-
it('inserts last_build_time when not exists', () => {
|
|
120
|
-
updateBuildTimestamp(dataDb);
|
|
121
|
-
|
|
122
|
-
const result = dataDb.prepare(`SELECT value FROM massu_meta WHERE key = 'last_build_time'`).get() as { value: string } | undefined;
|
|
123
|
-
expect(result).toBeTruthy();
|
|
124
|
-
expect(result?.value).toBeTruthy();
|
|
125
|
-
|
|
126
|
-
const timestamp = new Date(result!.value);
|
|
127
|
-
expect(timestamp.getTime()).toBeGreaterThan(Date.now() - 5000); // Within last 5 seconds
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('updates last_build_time when exists', () => {
|
|
131
|
-
const oldTime = new Date(Date.now() - 60000).toISOString(); // 1 minute ago
|
|
132
|
-
dataDb.prepare(`INSERT INTO massu_meta (key, value) VALUES ('last_build_time', ?)`).run(oldTime);
|
|
133
|
-
|
|
134
|
-
updateBuildTimestamp(dataDb);
|
|
135
|
-
|
|
136
|
-
const result = dataDb.prepare(`SELECT value FROM massu_meta WHERE key = 'last_build_time'`).get() as { value: string };
|
|
137
|
-
expect(result.value).not.toBe(oldTime);
|
|
138
|
-
|
|
139
|
-
const timestamp = new Date(result.value);
|
|
140
|
-
expect(timestamp.getTime()).toBeGreaterThan(Date.now() - 5000); // Within last 5 seconds
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('stores timestamp as ISO string', () => {
|
|
144
|
-
updateBuildTimestamp(dataDb);
|
|
145
|
-
|
|
146
|
-
const result = dataDb.prepare(`SELECT value FROM massu_meta WHERE key = 'last_build_time'`).get() as { value: string };
|
|
147
|
-
expect(result.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); // ISO format
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('Data DB schema', () => {
|
|
152
|
-
it('creates massu_meta table', () => {
|
|
153
|
-
const tables = dataDb.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='massu_meta'`).all();
|
|
154
|
-
expect(tables.length).toBe(1);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('massu_meta has correct columns', () => {
|
|
158
|
-
const columns = dataDb.prepare(`PRAGMA table_info(massu_meta)`).all() as { name: string; type: string }[];
|
|
159
|
-
const columnNames = columns.map(c => c.name);
|
|
160
|
-
expect(columnNames).toContain('key');
|
|
161
|
-
expect(columnNames).toContain('value');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('CodeGraph DB schema', () => {
|
|
166
|
-
it('creates files table', () => {
|
|
167
|
-
const tables = codegraphDb.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='files'`).all();
|
|
168
|
-
expect(tables.length).toBe(1);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('files table has indexed_at column', () => {
|
|
172
|
-
const columns = codegraphDb.prepare(`PRAGMA table_info(files)`).all() as { name: string }[];
|
|
173
|
-
const columnNames = columns.map(c => c.name);
|
|
174
|
-
expect(columnNames).toContain('indexed_at');
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
});
|