@massu/core 0.1.1 → 0.4.0

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.
Files changed (151) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +7772 -3140
  34. package/dist/hooks/cost-tracker.js +103 -40
  35. package/dist/hooks/post-edit-context.js +74 -8
  36. package/dist/hooks/post-tool-use.js +268 -106
  37. package/dist/hooks/pre-compact.js +167 -43
  38. package/dist/hooks/pre-delete-check.js +159 -42
  39. package/dist/hooks/quality-event.js +103 -40
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +143 -84
  42. package/dist/hooks/session-start.js +186 -49
  43. package/dist/hooks/user-prompt.js +189 -43
  44. package/package.json +10 -15
  45. package/src/adr-generator.ts +9 -2
  46. package/src/analytics.ts +9 -3
  47. package/src/audit-trail.ts +10 -3
  48. package/src/backfill-sessions.ts +5 -4
  49. package/src/cli.ts +6 -0
  50. package/src/cloud-sync.ts +14 -18
  51. package/src/commands/doctor.ts +193 -6
  52. package/src/commands/init.ts +230 -5
  53. package/src/commands/install-commands.ts +137 -0
  54. package/src/config.ts +68 -2
  55. package/src/cost-tracker.ts +11 -6
  56. package/src/db.ts +115 -2
  57. package/src/dependency-scorer.ts +9 -2
  58. package/src/docs-tools.ts +21 -16
  59. package/src/hooks/post-edit-context.ts +4 -4
  60. package/src/hooks/post-tool-use.ts +130 -0
  61. package/src/hooks/pre-compact.ts +23 -1
  62. package/src/hooks/pre-delete-check.ts +92 -4
  63. package/src/hooks/security-gate.ts +32 -0
  64. package/src/hooks/session-end.ts +3 -3
  65. package/src/hooks/session-start.ts +99 -6
  66. package/src/hooks/user-prompt.ts +46 -1
  67. package/src/import-resolver.ts +2 -1
  68. package/src/knowledge-db.ts +169 -0
  69. package/src/knowledge-indexer.ts +704 -0
  70. package/src/knowledge-tools.ts +1413 -0
  71. package/src/license.ts +482 -0
  72. package/src/memory-db.ts +1364 -23
  73. package/src/memory-tools.ts +14 -15
  74. package/src/observability-tools.ts +13 -2
  75. package/src/observation-extractor.ts +11 -4
  76. package/src/page-deps.ts +3 -2
  77. package/src/prompt-analyzer.ts +9 -2
  78. package/src/python/coupling-detector.ts +124 -0
  79. package/src/python/domain-enforcer.ts +83 -0
  80. package/src/python/impact-analyzer.ts +95 -0
  81. package/src/python/import-parser.ts +244 -0
  82. package/src/python/import-resolver.ts +135 -0
  83. package/src/python/migration-indexer.ts +115 -0
  84. package/src/python/migration-parser.ts +332 -0
  85. package/src/python/model-indexer.ts +70 -0
  86. package/src/python/model-parser.ts +279 -0
  87. package/src/python/route-indexer.ts +58 -0
  88. package/src/python/route-parser.ts +317 -0
  89. package/src/python-tools.ts +629 -0
  90. package/src/regression-detector.ts +9 -3
  91. package/src/security-scorer.ts +9 -2
  92. package/src/sentinel-db.ts +45 -89
  93. package/src/sentinel-tools.ts +8 -11
  94. package/src/server.ts +29 -7
  95. package/src/session-archiver.ts +4 -5
  96. package/src/team-knowledge.ts +9 -2
  97. package/src/tools.ts +1032 -44
  98. package/src/validate-features-runner.ts +0 -1
  99. package/src/validation-engine.ts +9 -2
  100. package/README.md +0 -40
  101. package/dist/server.js +0 -7008
  102. package/src/__tests__/adr-generator.test.ts +0 -260
  103. package/src/__tests__/analytics.test.ts +0 -282
  104. package/src/__tests__/audit-trail.test.ts +0 -382
  105. package/src/__tests__/backfill-sessions.test.ts +0 -690
  106. package/src/__tests__/cli.test.ts +0 -290
  107. package/src/__tests__/cloud-sync.test.ts +0 -261
  108. package/src/__tests__/config-sections.test.ts +0 -359
  109. package/src/__tests__/config.test.ts +0 -732
  110. package/src/__tests__/cost-tracker.test.ts +0 -348
  111. package/src/__tests__/db.test.ts +0 -177
  112. package/src/__tests__/dependency-scorer.test.ts +0 -325
  113. package/src/__tests__/docs-integration.test.ts +0 -178
  114. package/src/__tests__/docs-tools.test.ts +0 -199
  115. package/src/__tests__/domains.test.ts +0 -236
  116. package/src/__tests__/hooks.test.ts +0 -221
  117. package/src/__tests__/import-resolver.test.ts +0 -95
  118. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  119. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  120. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  121. package/src/__tests__/memory-db.test.ts +0 -404
  122. package/src/__tests__/memory-enhancements.test.ts +0 -316
  123. package/src/__tests__/memory-tools.test.ts +0 -199
  124. package/src/__tests__/middleware-tree.test.ts +0 -177
  125. package/src/__tests__/observability-tools.test.ts +0 -595
  126. package/src/__tests__/observability.test.ts +0 -437
  127. package/src/__tests__/observation-extractor.test.ts +0 -167
  128. package/src/__tests__/page-deps.test.ts +0 -60
  129. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  130. package/src/__tests__/regression-detector.test.ts +0 -295
  131. package/src/__tests__/rules.test.ts +0 -87
  132. package/src/__tests__/schema-mapper.test.ts +0 -29
  133. package/src/__tests__/security-scorer.test.ts +0 -238
  134. package/src/__tests__/security-utils.test.ts +0 -175
  135. package/src/__tests__/sentinel-db.test.ts +0 -491
  136. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  137. package/src/__tests__/sentinel-tools.test.ts +0 -324
  138. package/src/__tests__/sentinel-types.test.ts +0 -750
  139. package/src/__tests__/server.test.ts +0 -452
  140. package/src/__tests__/session-archiver.test.ts +0 -524
  141. package/src/__tests__/session-state-generator.test.ts +0 -900
  142. package/src/__tests__/team-knowledge.test.ts +0 -327
  143. package/src/__tests__/tools.test.ts +0 -340
  144. package/src/__tests__/transcript-parser.test.ts +0 -195
  145. package/src/__tests__/trpc-index.test.ts +0 -25
  146. package/src/__tests__/validate-features-runner.test.ts +0 -517
  147. package/src/__tests__/validation-engine.test.ts +0 -300
  148. package/src/core-tools.ts +0 -685
  149. package/src/memory-queries.ts +0 -804
  150. package/src/memory-schema.ts +0 -546
  151. 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
- });
@@ -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
- });