@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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/dist/hooks/cost-tracker.js +23 -35
  3. package/dist/hooks/post-edit-context.js +2 -2
  4. package/dist/hooks/post-tool-use.js +43 -58
  5. package/dist/hooks/pre-compact.js +23 -38
  6. package/dist/hooks/pre-delete-check.js +18 -31
  7. package/dist/hooks/quality-event.js +23 -35
  8. package/dist/hooks/session-end.js +62 -78
  9. package/dist/hooks/session-start.js +33 -42
  10. package/dist/hooks/user-prompt.js +23 -38
  11. package/package.json +8 -14
  12. package/src/adr-generator.ts +9 -2
  13. package/src/analytics.ts +9 -3
  14. package/src/audit-trail.ts +10 -3
  15. package/src/cloud-sync.ts +14 -18
  16. package/src/commands/init.ts +1 -5
  17. package/src/cost-tracker.ts +11 -6
  18. package/src/dependency-scorer.ts +9 -2
  19. package/src/docs-tools.ts +13 -10
  20. package/src/hooks/post-edit-context.ts +3 -3
  21. package/src/hooks/session-end.ts +3 -3
  22. package/src/hooks/session-start.ts +2 -2
  23. package/src/memory-db.ts +1351 -23
  24. package/src/memory-tools.ts +14 -15
  25. package/src/observability-tools.ts +13 -2
  26. package/src/prompt-analyzer.ts +9 -2
  27. package/src/regression-detector.ts +9 -3
  28. package/src/security-scorer.ts +9 -2
  29. package/src/sentinel-db.ts +43 -88
  30. package/src/sentinel-tools.ts +8 -11
  31. package/src/server.ts +1 -2
  32. package/src/team-knowledge.ts +9 -2
  33. package/src/tools.ts +771 -35
  34. package/src/validate-features-runner.ts +0 -1
  35. package/src/validation-engine.ts +9 -2
  36. package/dist/cli.js +0 -7890
  37. package/dist/server.js +0 -7008
  38. package/src/__tests__/adr-generator.test.ts +0 -260
  39. package/src/__tests__/analytics.test.ts +0 -282
  40. package/src/__tests__/audit-trail.test.ts +0 -382
  41. package/src/__tests__/backfill-sessions.test.ts +0 -690
  42. package/src/__tests__/cli.test.ts +0 -290
  43. package/src/__tests__/cloud-sync.test.ts +0 -261
  44. package/src/__tests__/config-sections.test.ts +0 -359
  45. package/src/__tests__/config.test.ts +0 -732
  46. package/src/__tests__/cost-tracker.test.ts +0 -348
  47. package/src/__tests__/db.test.ts +0 -177
  48. package/src/__tests__/dependency-scorer.test.ts +0 -325
  49. package/src/__tests__/docs-integration.test.ts +0 -178
  50. package/src/__tests__/docs-tools.test.ts +0 -199
  51. package/src/__tests__/domains.test.ts +0 -236
  52. package/src/__tests__/hooks.test.ts +0 -221
  53. package/src/__tests__/import-resolver.test.ts +0 -95
  54. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  55. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  56. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  57. package/src/__tests__/memory-db.test.ts +0 -404
  58. package/src/__tests__/memory-enhancements.test.ts +0 -316
  59. package/src/__tests__/memory-tools.test.ts +0 -199
  60. package/src/__tests__/middleware-tree.test.ts +0 -177
  61. package/src/__tests__/observability-tools.test.ts +0 -595
  62. package/src/__tests__/observability.test.ts +0 -437
  63. package/src/__tests__/observation-extractor.test.ts +0 -167
  64. package/src/__tests__/page-deps.test.ts +0 -60
  65. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  66. package/src/__tests__/regression-detector.test.ts +0 -295
  67. package/src/__tests__/rules.test.ts +0 -87
  68. package/src/__tests__/schema-mapper.test.ts +0 -29
  69. package/src/__tests__/security-scorer.test.ts +0 -238
  70. package/src/__tests__/security-utils.test.ts +0 -175
  71. package/src/__tests__/sentinel-db.test.ts +0 -491
  72. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  73. package/src/__tests__/sentinel-tools.test.ts +0 -324
  74. package/src/__tests__/sentinel-types.test.ts +0 -750
  75. package/src/__tests__/server.test.ts +0 -452
  76. package/src/__tests__/session-archiver.test.ts +0 -524
  77. package/src/__tests__/session-state-generator.test.ts +0 -900
  78. package/src/__tests__/team-knowledge.test.ts +0 -327
  79. package/src/__tests__/tools.test.ts +0 -340
  80. package/src/__tests__/transcript-parser.test.ts +0 -195
  81. package/src/__tests__/trpc-index.test.ts +0 -25
  82. package/src/__tests__/validate-features-runner.test.ts +0 -517
  83. package/src/__tests__/validation-engine.test.ts +0 -300
  84. package/src/core-tools.ts +0 -685
  85. package/src/memory-queries.ts +0 -804
  86. package/src/memory-schema.ts +0 -546
  87. package/src/tool-helpers.ts +0 -41
@@ -1,88 +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, vi } from 'vitest';
5
- import { readFileSync, existsSync } from 'fs';
6
- import { resolve } from 'path';
7
- import { parse as parseYaml } from 'yaml';
8
-
9
- // Mock config to return real project root
10
- const PROJECT_ROOT = resolve(__dirname, '../../../../..');
11
-
12
- vi.mock('../../config.ts', () => ({
13
- getConfig: () => {
14
- // Read actual massu.config.yaml for this test
15
- const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
16
- if (existsSync(configPath)) {
17
- const raw = readFileSync(configPath, 'utf-8');
18
- const parsed = parseYaml(raw);
19
- return {
20
- toolPrefix: parsed.toolPrefix || 'massu',
21
- framework: parsed.framework || { type: 'typescript' },
22
- paths: parsed.paths || { source: 'src' },
23
- domains: parsed.domains || [],
24
- analytics: parsed.analytics || {},
25
- };
26
- }
27
- return {
28
- toolPrefix: 'massu',
29
- framework: { type: 'typescript' },
30
- paths: { source: 'src' },
31
- domains: [],
32
- analytics: {},
33
- };
34
- },
35
- getProjectRoot: () => PROJECT_ROOT,
36
- getResolvedPaths: () => ({
37
- codegraphDbPath: resolve(PROJECT_ROOT, 'codegraph.db'),
38
- dataDbPath: resolve(PROJECT_ROOT, 'data.db'),
39
- }),
40
- }));
41
-
42
- describe('Integration: Pricing Consistency', () => {
43
- it('massu.config.yaml exists and parses without errors', () => {
44
- const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
45
- expect(existsSync(configPath)).toBe(true);
46
-
47
- const raw = readFileSync(configPath, 'utf-8');
48
- const parsed = parseYaml(raw);
49
- expect(parsed).toBeTruthy();
50
- expect(parsed.toolPrefix).toBeTruthy();
51
- });
52
-
53
- it('toolPrefix in config matches expected format', () => {
54
- const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
55
- const raw = readFileSync(configPath, 'utf-8');
56
- const parsed = parseYaml(raw);
57
-
58
- // toolPrefix should be a string without spaces or special chars
59
- expect(typeof parsed.toolPrefix).toBe('string');
60
- expect(parsed.toolPrefix).toMatch(/^[a-z][a-z0-9_]*$/);
61
- });
62
-
63
- it('DEFAULT_MODEL_PRICING in cost-tracker has required models', () => {
64
- // Read cost-tracker.ts to extract pricing data
65
- const costTrackerPath = resolve(PROJECT_ROOT, 'packages/core/src/cost-tracker.ts');
66
- expect(existsSync(costTrackerPath)).toBe(true);
67
-
68
- const content = readFileSync(costTrackerPath, 'utf-8');
69
-
70
- // Must have a default model
71
- expect(content).toContain("'default'");
72
-
73
- // Must have pricing structure with input_per_million and output_per_million
74
- expect(content).toContain('input_per_million');
75
- expect(content).toContain('output_per_million');
76
- });
77
-
78
- it('config file has required top-level sections', () => {
79
- const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
80
- const raw = readFileSync(configPath, 'utf-8');
81
- const parsed = parseYaml(raw);
82
-
83
- // Required sections
84
- expect(parsed.toolPrefix).toBeTruthy();
85
- expect(parsed.framework).toBeTruthy();
86
- expect(parsed.paths).toBeTruthy();
87
- });
88
- });
@@ -1,146 +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, vi } from 'vitest';
5
- import Database from 'better-sqlite3';
6
- import { getToolDefinitions, handleToolCall } from '../../tools.ts';
7
-
8
- // Mock all external dependencies
9
- vi.mock('../../config.ts', () => ({
10
- getConfig: () => ({
11
- toolPrefix: 'massu',
12
- framework: { type: 'typescript', router: 'trpc', orm: 'prisma' },
13
- paths: {
14
- source: 'src',
15
- routers: 'src/server/api/routers',
16
- middleware: 'src/middleware.ts',
17
- },
18
- domains: [],
19
- }),
20
- getProjectRoot: () => '/test/project',
21
- getResolvedPaths: () => ({
22
- codegraphDbPath: '/test/codegraph.db',
23
- dataDbPath: '/test/data.db',
24
- }),
25
- }));
26
-
27
- function createMockDb(): Database.Database {
28
- const db = new Database(':memory:');
29
- // Create memory DB tables
30
- db.exec(`
31
- CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, created_at TEXT, updated_at TEXT, context TEXT);
32
- CREATE TABLE IF NOT EXISTS observations (id INTEGER PRIMARY KEY, session_id TEXT, content TEXT, category TEXT, confidence REAL, created_at TEXT);
33
- CREATE TABLE IF NOT EXISTS analytics_events (id INTEGER PRIMARY KEY, session_id TEXT, event_type TEXT, data TEXT, created_at TEXT);
34
- CREATE TABLE IF NOT EXISTS cost_records (id INTEGER PRIMARY KEY, session_id TEXT, model TEXT, input_tokens INTEGER, output_tokens INTEGER, cache_read_tokens INTEGER DEFAULT 0, cache_write_tokens INTEGER DEFAULT 0, cost REAL, created_at TEXT);
35
- CREATE TABLE IF NOT EXISTS prompt_analyses (id INTEGER PRIMARY KEY, session_id TEXT, tool_name TEXT, prompt_text TEXT, analysis TEXT, created_at TEXT);
36
- CREATE TABLE IF NOT EXISTS audit_trail (id INTEGER PRIMARY KEY, session_id TEXT, action TEXT, details TEXT, created_at TEXT);
37
- CREATE TABLE IF NOT EXISTS validation_results (id INTEGER PRIMARY KEY, session_id TEXT, rule_id TEXT, result TEXT, details TEXT, created_at TEXT);
38
- CREATE TABLE IF NOT EXISTS adrs (id INTEGER PRIMARY KEY, session_id TEXT, title TEXT, status TEXT, context TEXT, decision TEXT, consequences TEXT, created_at TEXT);
39
- CREATE TABLE IF NOT EXISTS security_scores (id INTEGER PRIMARY KEY, session_id TEXT, file_path TEXT, score REAL, findings TEXT, created_at TEXT);
40
- CREATE TABLE IF NOT EXISTS dependency_scores (id INTEGER PRIMARY KEY, session_id TEXT, package_name TEXT, score REAL, details TEXT, created_at TEXT);
41
- CREATE TABLE IF NOT EXISTS team_knowledge (id INTEGER PRIMARY KEY, session_id TEXT, topic TEXT, content TEXT, author TEXT, created_at TEXT);
42
- CREATE TABLE IF NOT EXISTS regression_baselines (id INTEGER PRIMARY KEY, session_id TEXT, metric TEXT, value REAL, created_at TEXT);
43
- CREATE TABLE IF NOT EXISTS observability_spans (id INTEGER PRIMARY KEY, session_id TEXT, trace_id TEXT, span_id TEXT, parent_span_id TEXT, name TEXT, start_time TEXT, end_time TEXT, duration_ms REAL, status TEXT, attributes TEXT);
44
- CREATE TABLE IF NOT EXISTS observability_metrics (id INTEGER PRIMARY KEY, session_id TEXT, name TEXT, value REAL, unit TEXT, dimensions TEXT, timestamp TEXT);
45
- CREATE TABLE IF NOT EXISTS observability_logs (id INTEGER PRIMARY KEY, session_id TEXT, level TEXT, message TEXT, context TEXT, timestamp TEXT);
46
- `);
47
- return db;
48
- }
49
-
50
- vi.mock('../../memory-db.ts', () => ({
51
- getMemoryDb: () => createMockDb(),
52
- }));
53
-
54
- vi.mock('../../db.ts', async (importOriginal) => {
55
- const actual = await importOriginal() as Record<string, unknown>;
56
- return {
57
- ...actual,
58
- isDataStale: () => false,
59
- updateBuildTimestamp: vi.fn(),
60
- };
61
- });
62
-
63
- vi.mock('../../import-resolver.ts', () => ({
64
- buildImportIndex: () => 0,
65
- }));
66
-
67
- vi.mock('../../trpc-index.ts', () => ({
68
- buildTrpcIndex: () => ({ totalProcedures: 0, withCallers: 0, withoutCallers: 0 }),
69
- }));
70
-
71
- vi.mock('../../page-deps.ts', () => ({
72
- buildPageDeps: () => 0,
73
- findAffectedPages: () => [],
74
- }));
75
-
76
- vi.mock('../../middleware-tree.ts', () => ({
77
- buildMiddlewareTree: () => 0,
78
- isInMiddlewareTree: () => false,
79
- getMiddlewareTree: () => [],
80
- }));
81
-
82
- vi.mock('../../domains.ts', () => ({
83
- classifyFile: () => ({ domain: 'unknown', layer: 'unknown' }),
84
- classifyRouter: () => 'unknown',
85
- findCrossDomainImports: () => [],
86
- getFilesInDomain: () => [],
87
- }));
88
-
89
- vi.mock('../../schema-mapper.ts', () => ({
90
- parsePrismaSchema: () => [],
91
- detectMismatches: () => [],
92
- findColumnUsageInRouters: () => [],
93
- }));
94
-
95
- vi.mock('../../sentinel-scanner.ts', () => ({
96
- runFeatureScan: () => ({ newFeatures: 0, updatedFeatures: 0 }),
97
- }));
98
-
99
- describe('Integration: Tool Registration Completeness', () => {
100
- it('every tool in getToolDefinitions() has a matching handler', () => {
101
- const definitions = getToolDefinitions();
102
-
103
- // Every tool should have a name and inputSchema
104
- for (const tool of definitions) {
105
- expect(tool.name).toBeTruthy();
106
- expect(tool.inputSchema).toBeTruthy();
107
- }
108
-
109
- // There should be a meaningful number of tools
110
- expect(definitions.length).toBeGreaterThan(10);
111
- });
112
-
113
- it('no tool returns "Unknown tool" when called with valid name', () => {
114
- const definitions = getToolDefinitions();
115
- const unknownResponses: string[] = [];
116
-
117
- for (const tool of definitions) {
118
- try {
119
- const result = handleToolCall(tool.name, {});
120
- const text = result.content[0]?.text || '';
121
- if (text.includes('Unknown tool')) {
122
- unknownResponses.push(tool.name);
123
- }
124
- } catch {
125
- // Some tools may throw on missing args - that's fine,
126
- // it means the handler IS registered and tried to execute
127
- }
128
- }
129
-
130
- expect(unknownResponses).toEqual([]);
131
- });
132
-
133
- it('tool names all start with configured prefix', () => {
134
- const definitions = getToolDefinitions();
135
- for (const tool of definitions) {
136
- expect(tool.name).toMatch(/^massu_/);
137
- }
138
- });
139
-
140
- it('no duplicate tool names exist', () => {
141
- const definitions = getToolDefinitions();
142
- const names = definitions.map(d => d.name);
143
- const uniqueNames = new Set(names);
144
- expect(uniqueNames.size).toBe(names.length);
145
- });
146
- });
@@ -1,404 +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
- getMemoryDb,
8
- createSession,
9
- endSession,
10
- addObservation,
11
- addSummary,
12
- addUserPrompt,
13
- searchObservations,
14
- getRecentObservations,
15
- getSessionSummaries,
16
- getSessionTimeline,
17
- getFailedAttempts,
18
- getDecisionsAbout,
19
- pruneOldObservations,
20
- deduplicateFailedAttempt,
21
- getSessionsByTask,
22
- getCrossTaskProgress,
23
- assignImportance,
24
- linkSessionToTask,
25
- autoDetectTaskId,
26
- } from '../memory-db.ts';
27
- import { resolve } from 'path';
28
- import { unlinkSync, existsSync } from 'fs';
29
-
30
- // P7-001: Memory Database Tests
31
-
32
- const TEST_DB_PATH = resolve(__dirname, '../test-memory.db');
33
-
34
- function createTestDb(): Database.Database {
35
- // Remove existing test DB
36
- if (existsSync(TEST_DB_PATH)) {
37
- unlinkSync(TEST_DB_PATH);
38
- }
39
-
40
- // Temporarily override getMemoryDb behavior by directly creating a db
41
- const db = new Database(TEST_DB_PATH);
42
- db.pragma('journal_mode = WAL');
43
- db.pragma('foreign_keys = ON');
44
-
45
- // Init schema manually (same as getMemoryDb)
46
- db.exec(`
47
- CREATE TABLE IF NOT EXISTS sessions (
48
- id INTEGER PRIMARY KEY AUTOINCREMENT,
49
- session_id TEXT UNIQUE NOT NULL,
50
- project TEXT NOT NULL DEFAULT 'my-project',
51
- git_branch TEXT,
52
- started_at TEXT NOT NULL,
53
- started_at_epoch INTEGER NOT NULL,
54
- ended_at TEXT,
55
- ended_at_epoch INTEGER,
56
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
57
- plan_file TEXT,
58
- plan_phase TEXT,
59
- task_id TEXT
60
- );
61
- CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
62
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
63
- CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
64
-
65
- CREATE TABLE IF NOT EXISTS observations (
66
- id INTEGER PRIMARY KEY AUTOINCREMENT,
67
- session_id TEXT NOT NULL,
68
- type TEXT NOT NULL CHECK(type IN (
69
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
70
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
71
- 'file_change', 'incident_near_miss'
72
- )),
73
- title TEXT NOT NULL,
74
- detail TEXT,
75
- files_involved TEXT DEFAULT '[]',
76
- plan_item TEXT,
77
- cr_rule TEXT,
78
- vr_type TEXT,
79
- evidence TEXT,
80
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
81
- recurrence_count INTEGER NOT NULL DEFAULT 1,
82
- original_tokens INTEGER DEFAULT 0,
83
- created_at TEXT NOT NULL,
84
- created_at_epoch INTEGER NOT NULL,
85
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
86
- );
87
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
88
- CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
89
- CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
90
- CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
91
-
92
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
93
- title, detail, evidence,
94
- content='observations',
95
- content_rowid='id'
96
- );
97
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
98
- INSERT INTO observations_fts(rowid, title, detail, evidence)
99
- VALUES (new.id, new.title, new.detail, new.evidence);
100
- END;
101
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
102
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
103
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
104
- END;
105
- CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
106
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
107
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
108
- INSERT INTO observations_fts(rowid, title, detail, evidence)
109
- VALUES (new.id, new.title, new.detail, new.evidence);
110
- END;
111
-
112
- CREATE TABLE IF NOT EXISTS session_summaries (
113
- id INTEGER PRIMARY KEY AUTOINCREMENT,
114
- session_id TEXT NOT NULL,
115
- request TEXT,
116
- investigated TEXT,
117
- decisions TEXT,
118
- completed TEXT,
119
- failed_attempts TEXT,
120
- next_steps TEXT,
121
- files_created TEXT DEFAULT '[]',
122
- files_modified TEXT DEFAULT '[]',
123
- verification_results TEXT DEFAULT '{}',
124
- plan_progress TEXT DEFAULT '{}',
125
- created_at TEXT NOT NULL,
126
- created_at_epoch INTEGER NOT NULL,
127
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
128
- );
129
- CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
130
-
131
- CREATE TABLE IF NOT EXISTS user_prompts (
132
- id INTEGER PRIMARY KEY AUTOINCREMENT,
133
- session_id TEXT NOT NULL,
134
- prompt_text TEXT NOT NULL,
135
- prompt_number INTEGER NOT NULL DEFAULT 1,
136
- created_at TEXT NOT NULL,
137
- created_at_epoch INTEGER NOT NULL,
138
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
139
- );
140
-
141
- CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
142
- prompt_text,
143
- content='user_prompts',
144
- content_rowid='id'
145
- );
146
- CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
147
- INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
148
- END;
149
-
150
- CREATE TABLE IF NOT EXISTS memory_meta (
151
- key TEXT PRIMARY KEY,
152
- value TEXT NOT NULL
153
- );
154
- `);
155
-
156
- return db;
157
- }
158
-
159
- describe('Memory Database', () => {
160
- let db: Database.Database;
161
-
162
- beforeEach(() => {
163
- db = createTestDb();
164
- });
165
-
166
- afterEach(() => {
167
- db.close();
168
- if (existsSync(TEST_DB_PATH)) {
169
- unlinkSync(TEST_DB_PATH);
170
- }
171
- });
172
-
173
- describe('Schema', () => {
174
- it('creates all tables', () => {
175
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as { name: string }[];
176
- const tableNames = tables.map(t => t.name);
177
- expect(tableNames).toContain('sessions');
178
- expect(tableNames).toContain('observations');
179
- expect(tableNames).toContain('session_summaries');
180
- expect(tableNames).toContain('user_prompts');
181
- expect(tableNames).toContain('memory_meta');
182
- });
183
-
184
- it('creates FTS5 tables', () => {
185
- const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%fts%'").all() as { name: string }[];
186
- const tableNames = tables.map(t => t.name);
187
- expect(tableNames.some(n => n.includes('observations_fts'))).toBe(true);
188
- expect(tableNames.some(n => n.includes('user_prompts_fts'))).toBe(true);
189
- });
190
- });
191
-
192
- describe('Session CRUD', () => {
193
- it('creates a session with INSERT OR IGNORE', () => {
194
- createSession(db, 'test-session-1', { branch: 'main' });
195
- const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get('test-session-1') as Record<string, unknown>;
196
- expect(session).toBeTruthy();
197
- expect(session.git_branch).toBe('main');
198
- expect(session.status).toBe('active');
199
- });
200
-
201
- it('is idempotent (INSERT OR IGNORE)', () => {
202
- createSession(db, 'test-session-1', { branch: 'main' });
203
- createSession(db, 'test-session-1', { branch: 'feature' }); // Should not throw or update
204
- const count = db.prepare('SELECT COUNT(*) as c FROM sessions WHERE session_id = ?').get('test-session-1') as { c: number };
205
- expect(count.c).toBe(1);
206
- });
207
-
208
- it('ends a session', () => {
209
- createSession(db, 'test-session-1');
210
- endSession(db, 'test-session-1', 'completed');
211
- const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get('test-session-1') as Record<string, unknown>;
212
- expect(session.status).toBe('completed');
213
- expect(session.ended_at).toBeTruthy();
214
- });
215
- });
216
-
217
- describe('Observations', () => {
218
- beforeEach(() => {
219
- createSession(db, 'test-session-1');
220
- });
221
-
222
- it('adds an observation', () => {
223
- const id = addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Full-text search is faster');
224
- expect(id).toBeGreaterThan(0);
225
- const obs = db.prepare('SELECT * FROM observations WHERE id = ?').get(id) as Record<string, unknown>;
226
- expect(obs.title).toBe('Use FTS5 for search');
227
- expect(obs.type).toBe('decision');
228
- });
229
-
230
- it('auto-assigns importance', () => {
231
- const id1 = addObservation(db, 'test-session-1', 'decision', 'Important decision', null);
232
- const id2 = addObservation(db, 'test-session-1', 'file_change', 'Changed a file', null);
233
- const obs1 = db.prepare('SELECT importance FROM observations WHERE id = ?').get(id1) as { importance: number };
234
- const obs2 = db.prepare('SELECT importance FROM observations WHERE id = ?').get(id2) as { importance: number };
235
- expect(obs1.importance).toBe(5); // decision
236
- expect(obs2.importance).toBe(1); // file_change
237
- });
238
-
239
- it('searches observations with FTS5', () => {
240
- addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Better performance');
241
- addObservation(db, 'test-session-1', 'feature', 'Add login page', 'New auth flow');
242
- const results = searchObservations(db, 'FTS5');
243
- expect(results.length).toBe(1);
244
- expect(results[0].title).toContain('FTS5');
245
- });
246
-
247
- it('gets recent observations', () => {
248
- addObservation(db, 'test-session-1', 'decision', 'Decision 1', null);
249
- addObservation(db, 'test-session-1', 'feature', 'Feature 1', null);
250
- const recent = getRecentObservations(db, 10, 'test-session-1');
251
- expect(recent.length).toBe(2);
252
- });
253
- });
254
-
255
- describe('Session Summaries', () => {
256
- beforeEach(() => {
257
- createSession(db, 'test-session-1');
258
- });
259
-
260
- it('adds and retrieves a summary', () => {
261
- addSummary(db, 'test-session-1', {
262
- request: 'Fix the login bug',
263
- completed: 'Fixed auth flow',
264
- planProgress: { 'P1-001': 'complete' },
265
- });
266
- const summaries = getSessionSummaries(db, 5);
267
- expect(summaries.length).toBe(1);
268
- expect(summaries[0].request).toBe('Fix the login bug');
269
- });
270
- });
271
-
272
- describe('Session Timeline', () => {
273
- it('returns full timeline', () => {
274
- createSession(db, 'test-session-1');
275
- addObservation(db, 'test-session-1', 'decision', 'Decision 1', null);
276
- addUserPrompt(db, 'test-session-1', 'Fix the bug', 1);
277
- addSummary(db, 'test-session-1', { request: 'Fix the bug' });
278
-
279
- const timeline = getSessionTimeline(db, 'test-session-1');
280
- expect(timeline.session).toBeTruthy();
281
- expect(timeline.observations.length).toBe(1);
282
- expect(timeline.summary).toBeTruthy();
283
- expect(timeline.prompts.length).toBe(1);
284
- });
285
- });
286
-
287
- describe('Failed Attempts', () => {
288
- beforeEach(() => {
289
- createSession(db, 'test-session-1');
290
- });
291
-
292
- it('retrieves failed attempts', () => {
293
- addObservation(db, 'test-session-1', 'failed_attempt', 'Regex parser fails on nested braces', 'Stopped at first }');
294
- const failures = getFailedAttempts(db);
295
- expect(failures.length).toBe(1);
296
- expect(failures[0].title).toContain('Regex parser');
297
- });
298
-
299
- it('searches failed attempts with FTS5', () => {
300
- addObservation(db, 'test-session-1', 'failed_attempt', 'Regex parser fails on nested braces', 'Stopped at first }');
301
- addObservation(db, 'test-session-1', 'failed_attempt', 'process.cwd() wrong in tests', 'Returns test runner dir');
302
- const results = getFailedAttempts(db, 'regex');
303
- expect(results.length).toBe(1);
304
- expect(results[0].title).toContain('Regex');
305
- });
306
- });
307
-
308
- describe('Decisions', () => {
309
- it('searches decisions with FTS5', () => {
310
- createSession(db, 'test-session-1');
311
- addObservation(db, 'test-session-1', 'decision', 'Use esbuild instead of tsc', 'Faster bundling');
312
- addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Better performance');
313
- const results = getDecisionsAbout(db, 'esbuild');
314
- expect(results.length).toBe(1);
315
- expect(results[0].title).toContain('esbuild');
316
- });
317
- });
318
-
319
- describe('Deduplication', () => {
320
- it('increments recurrence_count for duplicate failed attempts', () => {
321
- createSession(db, 'test-session-1');
322
- createSession(db, 'test-session-2');
323
- deduplicateFailedAttempt(db, 'test-session-1', 'process.cwd() wrong in tests', 'Returns runner dir');
324
- deduplicateFailedAttempt(db, 'test-session-2', 'process.cwd() wrong in tests', 'Same issue again');
325
-
326
- const failures = getFailedAttempts(db);
327
- expect(failures.length).toBe(1);
328
- expect(failures[0].recurrence_count).toBe(2);
329
- });
330
- });
331
-
332
- describe('Task Linking', () => {
333
- it('links sessions to tasks and gets cross-task progress', () => {
334
- createSession(db, 'session-1', { planFile: '/path/2026-01-30-memory-system.md' });
335
- createSession(db, 'session-2', { planFile: '/path/2026-01-30-memory-system.md' });
336
-
337
- // Verify auto-detected task_id
338
- const s1 = db.prepare('SELECT task_id FROM sessions WHERE session_id = ?').get('session-1') as { task_id: string };
339
- expect(s1.task_id).toBe('2026-01-30-memory-system');
340
-
341
- // Add summaries with plan progress
342
- addSummary(db, 'session-1', { planProgress: { 'P1-001': 'complete', 'P1-002': 'in_progress' } });
343
- addSummary(db, 'session-2', { planProgress: { 'P1-002': 'complete', 'P2-001': 'complete' } });
344
-
345
- const progress = getCrossTaskProgress(db, '2026-01-30-memory-system');
346
- expect(progress['P1-001']).toBe('complete');
347
- expect(progress['P1-002']).toBe('complete'); // Later status wins
348
- expect(progress['P2-001']).toBe('complete');
349
- });
350
-
351
- it('gets sessions by task', () => {
352
- createSession(db, 'session-1');
353
- createSession(db, 'session-2');
354
- linkSessionToTask(db, 'session-1', 'task-1');
355
- linkSessionToTask(db, 'session-2', 'task-1');
356
-
357
- const sessions = getSessionsByTask(db, 'task-1');
358
- expect(sessions.length).toBe(2);
359
- });
360
- });
361
-
362
- describe('Pruning', () => {
363
- it('prunes old observations', () => {
364
- createSession(db, 'test-session-1');
365
- // Insert observation with old epoch
366
- const oldEpoch = Math.floor(Date.now() / 1000) - (100 * 86400); // 100 days ago
367
- db.prepare(`
368
- INSERT INTO observations (session_id, type, title, importance, created_at, created_at_epoch)
369
- VALUES (?, 'discovery', 'Old observation', 1, ?, ?)
370
- `).run('test-session-1', new Date(oldEpoch * 1000).toISOString(), oldEpoch);
371
-
372
- addObservation(db, 'test-session-1', 'decision', 'Recent decision', null);
373
-
374
- const pruned = pruneOldObservations(db, 90);
375
- expect(pruned).toBe(1);
376
-
377
- const remaining = db.prepare('SELECT COUNT(*) as c FROM observations').get() as { c: number };
378
- expect(remaining.c).toBe(1);
379
- });
380
- });
381
-
382
- describe('Importance', () => {
383
- it('assigns correct importance by type', () => {
384
- expect(assignImportance('decision')).toBe(5);
385
- expect(assignImportance('failed_attempt')).toBe(5);
386
- expect(assignImportance('cr_violation')).toBe(4);
387
- expect(assignImportance('vr_check', 'FAIL')).toBe(4);
388
- expect(assignImportance('vr_check', 'PASS')).toBe(2);
389
- expect(assignImportance('feature')).toBe(3);
390
- expect(assignImportance('bugfix')).toBe(3);
391
- expect(assignImportance('refactor')).toBe(2);
392
- expect(assignImportance('file_change')).toBe(1);
393
- expect(assignImportance('discovery')).toBe(1);
394
- });
395
- });
396
-
397
- describe('autoDetectTaskId', () => {
398
- it('derives task_id from plan file path', () => {
399
- expect(autoDetectTaskId('/path/to/2026-01-30-massu-memory.md')).toBe('2026-01-30-massu-memory');
400
- expect(autoDetectTaskId(null)).toBeNull();
401
- expect(autoDetectTaskId(undefined)).toBeNull();
402
- });
403
- });
404
- });