@mmmbuto/nexuscli 0.5.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Integration Test - Session Sync Flow (Phase 0-2)
3
+ *
4
+ * Tests the complete flow:
5
+ * 1. history.jsonl → HistorySync → database
6
+ * 2. Chat route with optional conversationId
7
+ * 3. No ghost sessions created
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const HistorySync = require('../services/history-sync');
13
+ const { initDb, getDb, prepare } = require('../db');
14
+
15
+ describe('Session Sync Integration', () => {
16
+ let historySync;
17
+ let testHistoryPath;
18
+
19
+ beforeAll(async () => {
20
+ // Setup in-memory test database
21
+ await initDb({ skipMigrationCheck: true });
22
+ });
23
+
24
+ beforeEach(() => {
25
+ // Create test history.jsonl
26
+ testHistoryPath = path.join(__dirname, 'fixtures', 'test-history-integration.jsonl');
27
+
28
+ const testData = [
29
+ {
30
+ sessionId: 'session-001',
31
+ display: 'Fix the database bug',
32
+ timestamp: Date.now() - 10000,
33
+ project: '/test/workspace1'
34
+ },
35
+ {
36
+ sessionId: 'session-001',
37
+ display: 'Add unit tests',
38
+ timestamp: Date.now() - 5000,
39
+ project: '/test/workspace1'
40
+ },
41
+ {
42
+ sessionId: 'session-002',
43
+ display: 'Create new feature',
44
+ timestamp: Date.now() - 3000,
45
+ project: '/test/workspace2'
46
+ }
47
+ ];
48
+
49
+ fs.mkdirSync(path.dirname(testHistoryPath), { recursive: true });
50
+ fs.writeFileSync(
51
+ testHistoryPath,
52
+ testData.map(e => JSON.stringify(e)).join('\n')
53
+ );
54
+
55
+ historySync = new HistorySync({ historyPath: testHistoryPath });
56
+ });
57
+
58
+ afterEach(() => {
59
+ // Cleanup
60
+ if (fs.existsSync(testHistoryPath)) {
61
+ fs.unlinkSync(testHistoryPath);
62
+ }
63
+
64
+ // Clear database
65
+ const db = getDb();
66
+ if (db) {
67
+ db.exec('DELETE FROM messages');
68
+ db.exec('DELETE FROM conversations');
69
+ if (db.prepare('SELECT name FROM sqlite_master WHERE name="sessions"').get()) {
70
+ db.exec('DELETE FROM sessions');
71
+ }
72
+ }
73
+ });
74
+
75
+ test('parseHistory correctly groups messages by sessionId', async () => {
76
+ const sessions = await historySync.parseHistory();
77
+
78
+ expect(sessions.size).toBe(2);
79
+ expect(sessions.get('session-001').messages).toHaveLength(2);
80
+ expect(sessions.get('session-002').messages).toHaveLength(1);
81
+ });
82
+
83
+ test('syncToDatabase creates conversations without duplicates', async () => {
84
+ // First sync
85
+ await historySync.sync(true);
86
+
87
+ const db = getDb();
88
+ const convStmt = prepare('SELECT COUNT(*) as count FROM conversations');
89
+ const msgStmt = prepare('SELECT COUNT(*) as count FROM messages');
90
+
91
+ const conversations = convStmt.get();
92
+ const messages = msgStmt.get();
93
+
94
+ expect(conversations.count).toBe(2);
95
+ expect(messages.count).toBe(3);
96
+
97
+ // Second sync (should not create duplicates)
98
+ await historySync.sync(true);
99
+
100
+ const conversationsAfter = convStmt.get();
101
+ const messagesAfter = msgStmt.get();
102
+
103
+ expect(conversationsAfter.count).toBe(2);
104
+ expect(messagesAfter.count).toBe(3);
105
+ });
106
+
107
+ test('workspace filtering returns correct sessions', async () => {
108
+ await historySync.sync(true);
109
+
110
+ const workspace1Sessions = await historySync.getWorkspaceSessions('/test/workspace1');
111
+ const workspace2Sessions = await historySync.getWorkspaceSessions('/test/workspace2');
112
+
113
+ expect(workspace1Sessions).toHaveLength(1);
114
+ expect(workspace1Sessions[0].id).toBe('session-001');
115
+
116
+ expect(workspace2Sessions).toHaveLength(1);
117
+ expect(workspace2Sessions[0].id).toBe('session-002');
118
+ });
119
+
120
+ test('conversation titles are generated from first message', async () => {
121
+ await historySync.sync(true);
122
+
123
+ const db = getDb();
124
+ const stmt = prepare('SELECT id, title FROM conversations WHERE id = ?');
125
+ const conv = stmt.get('session-001');
126
+
127
+ expect(conv).toBeDefined();
128
+ expect(conv.title).toContain('Fix');
129
+ expect(conv.title.length).toBeLessThanOrEqual(83); // Max 80 + "..."
130
+ });
131
+
132
+ test('sessions table populated with workspace_path', async () => {
133
+ await historySync.sync(true);
134
+
135
+ const db = getDb();
136
+
137
+ // Check if sessions table exists
138
+ const hasSessionsTable = prepare(
139
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'"
140
+ ).get();
141
+
142
+ if (hasSessionsTable) {
143
+ const stmt = prepare('SELECT COUNT(*) as count FROM sessions');
144
+ const result = stmt.get();
145
+
146
+ expect(result.count).toBeGreaterThan(0);
147
+ } else {
148
+ console.warn('sessions table not found - Ultra-Light migration may not have run');
149
+ }
150
+ });
151
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Integration Tests - Ultra-Light Architecture
3
+ * Phase 7 - End-to-end flow validation
4
+ */
5
+
6
+ const { prepare } = require('../db');
7
+
8
+ describe('Database Integration', () => {
9
+ test('should initialize database successfully', async () => {
10
+ const db = await prepare();
11
+ expect(db).toBeDefined();
12
+ expect(typeof db.exec).toBe('function');
13
+ });
14
+
15
+ test('should have sessions table', async () => {
16
+ const db = await prepare();
17
+ const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'");
18
+ expect(result.length).toBeGreaterThan(0);
19
+ });
20
+
21
+ test('should have session_summaries table', async () => {
22
+ const db = await prepare();
23
+ const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='session_summaries'");
24
+ expect(result.length).toBeGreaterThan(0);
25
+ });
26
+
27
+ test('should have workspace_memory table', async () => {
28
+ const db = await prepare();
29
+ const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='workspace_memory'");
30
+ expect(result.length).toBeGreaterThan(0);
31
+ });
32
+
33
+ test('should query sessions successfully', async () => {
34
+ const db = await prepare();
35
+ const result = db.exec("SELECT COUNT(*) as count FROM sessions");
36
+ expect(result.length).toBeGreaterThan(0);
37
+ expect(result[0].columns).toContain('count');
38
+ });
39
+ });
40
+
41
+ describe('Service Integration', () => {
42
+ test('WorkspaceManager + CliLoader should share path configuration', () => {
43
+ const WorkspaceManager = require('../services/workspace-manager');
44
+ const CliLoader = require('../services/cli-loader');
45
+
46
+ const manager = new WorkspaceManager();
47
+ const loader = new CliLoader();
48
+
49
+ expect(manager.claudePath).toBe(loader.claudePath);
50
+ expect(manager.historyPath).toBe(loader.historyPath);
51
+ });
52
+
53
+ test('All services should initialize without errors', () => {
54
+ const WorkspaceManager = require('../services/workspace-manager');
55
+ const CliLoader = require('../services/cli-loader');
56
+ const SummaryGenerator = require('../services/summary-generator');
57
+
58
+ expect(() => new WorkspaceManager()).not.toThrow();
59
+ expect(() => new CliLoader()).not.toThrow();
60
+ expect(() => new SummaryGenerator({ apiKey: 'test' })).not.toThrow();
61
+ });
62
+ });
63
+
64
+ describe('API Routes Availability', () => {
65
+ test('should have workspaces router module', () => {
66
+ expect(() => require('../routes/workspaces')).not.toThrow();
67
+ });
68
+
69
+ test('should have sessions router module', () => {
70
+ expect(() => require('../routes/sessions')).not.toThrow();
71
+ });
72
+
73
+ test('should have chat router module', () => {
74
+ expect(() => require('../routes/chat')).not.toThrow();
75
+ });
76
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Performance Tests - Ultra-Light Architecture
3
+ * Phase 7 - Performance benchmarks
4
+ */
5
+
6
+ const WorkspaceManager = require('../services/workspace-manager');
7
+ const SummaryGenerator = require('../services/summary-generator');
8
+
9
+ describe('Performance Benchmarks', () => {
10
+ test('WorkspaceManager cache should improve load time', () => {
11
+ const manager = new WorkspaceManager();
12
+
13
+ // First load (cache miss)
14
+ const start1 = Date.now();
15
+ const cache1 = manager.historyCache;
16
+ const time1 = Date.now() - start1;
17
+
18
+ // Second load (should use cache)
19
+ const start2 = Date.now();
20
+ const cache2 = manager.historyCache;
21
+ const time2 = Date.now() - start2;
22
+
23
+ // Cache access should be faster
24
+ expect(time2).toBeLessThanOrEqual(time1 + 1);
25
+ });
26
+
27
+ test('SummaryGenerator should handle large transcripts', () => {
28
+ const generator = new SummaryGenerator({ apiKey: 'test' });
29
+
30
+ // Generate 100 messages
31
+ const messages = Array(100).fill(null).map((_, i) => ({
32
+ role: i % 2 === 0 ? 'user' : 'assistant',
33
+ content: 'A'.repeat(50),
34
+ created_at: Date.now()
35
+ }));
36
+
37
+ const start = Date.now();
38
+ const transcript = generator.buildTranscript(messages);
39
+ const duration = Date.now() - start;
40
+
41
+ // Should complete quickly
42
+ expect(duration).toBeLessThan(100); // 100ms
43
+ expect(transcript.length).toBeLessThanOrEqual(6000);
44
+ });
45
+
46
+ test('Workspace validation should be fast', async () => {
47
+ const manager = new WorkspaceManager();
48
+ const testPath = '/home/user/myproject';
49
+
50
+ const start = Date.now();
51
+ const validated = await manager.validateWorkspace(testPath);
52
+ const duration = Date.now() - start;
53
+
54
+ // Should complete in <10ms
55
+ expect(duration).toBeLessThan(10);
56
+ expect(validated).toBe(testPath);
57
+ });
58
+
59
+ test('Title extraction should be fast for large messages', () => {
60
+ const manager = new WorkspaceManager();
61
+
62
+ const messages = [{
63
+ display: 'This is a very long message that contains multiple sentences and should be truncated to a reasonable length for display purposes in the UI. The title should only show the first few words.'
64
+ }];
65
+
66
+ const start = Date.now();
67
+ const title = manager.extractTitle(messages);
68
+ const duration = Date.now() - start;
69
+
70
+ // Should complete in <5ms
71
+ expect(duration).toBeLessThan(5);
72
+ expect(title.length).toBeLessThanOrEqual(50);
73
+ });
74
+ });
75
+
76
+ describe('Memory Efficiency', () => {
77
+ test('Cache should have TTL mechanism', () => {
78
+ const manager = new WorkspaceManager();
79
+ expect(manager.cacheTtlMs).toBe(5 * 60 * 1000); // 5 minutes
80
+ });
81
+
82
+ test('Transcript should be limited to prevent memory bloat', () => {
83
+ const generator = new SummaryGenerator({ apiKey: 'test' });
84
+
85
+ // Create very long messages
86
+ const messages = Array(200).fill(null).map(() => ({
87
+ role: 'user',
88
+ content: 'X'.repeat(1000),
89
+ created_at: Date.now()
90
+ }));
91
+
92
+ const transcript = generator.buildTranscript(messages);
93
+
94
+ // Should be limited to 6000 chars
95
+ expect(transcript.length).toBeLessThanOrEqual(6000);
96
+ });
97
+ });
98
+
99
+ describe('Ultra-Light Compliance', () => {
100
+ test('should not store assistant messages in DB (code verification)', () => {
101
+ const fs = require('fs');
102
+ const chatCode = fs.readFileSync('routes/chat.js', 'utf8');
103
+
104
+ // Verify comment exists about assistant messages
105
+ expect(chatCode).toContain('assistant replies stay in CLI files');
106
+ expect(chatCode).toContain('User message saved');
107
+ });
108
+
109
+ test('should use cache for history.jsonl reads', () => {
110
+ const fs = require('fs');
111
+ const managerCode = fs.readFileSync('services/workspace-manager.js', 'utf8');
112
+
113
+ // Verify cache implementation exists
114
+ expect(managerCode).toContain('historyCache');
115
+ expect(managerCode).toContain('cacheTtlMs');
116
+ expect(managerCode).toContain('fs.watch');
117
+ });
118
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Unit Tests for Ultra-Light Services
3
+ * Phase 7 - Testing & Deployment
4
+ */
5
+
6
+ const WorkspaceManager = require('../services/workspace-manager');
7
+ const CliLoader = require('../services/cli-loader');
8
+ const SummaryGenerator = require('../services/summary-generator');
9
+
10
+ describe('WorkspaceManager', () => {
11
+ let manager;
12
+
13
+ beforeEach(() => {
14
+ manager = new WorkspaceManager();
15
+ });
16
+
17
+ test('should validate workspace path', async () => {
18
+ // Test with allowed path
19
+ const validPath = '/home/user/myproject';
20
+ const result = await manager.validateWorkspace(validPath);
21
+ expect(result).toBe(validPath);
22
+ });
23
+
24
+ test('should reject invalid workspace paths', async () => {
25
+ // Test with disallowed root
26
+ const invalidPath = '/etc/passwd';
27
+ await expect(manager.validateWorkspace(invalidPath)).rejects.toThrow('not in allowed directories');
28
+ });
29
+
30
+ test('should detect non-existent workspace', async () => {
31
+ const nonExistent = '/var/nonexistent-workspace-12345';
32
+ await expect(manager.validateWorkspace(nonExistent)).rejects.toThrow('does not exist');
33
+ });
34
+
35
+ test('should have cache configuration', () => {
36
+ expect(manager.cacheTtlMs).toBe(5 * 60 * 1000); // 5 minutes
37
+ expect(manager.historyCache).toBeDefined();
38
+ expect(manager.historyCache.entries).toBeNull();
39
+ expect(manager.historyCache.timestamp).toBe(0);
40
+ });
41
+
42
+ test('should extract title from messages', () => {
43
+ const messages = [
44
+ { display: 'Implement user authentication feature' },
45
+ { display: 'Follow up on previous discussion' }
46
+ ];
47
+ const title = manager.extractTitle(messages);
48
+ expect(title).toContain('Implement');
49
+ expect(title.length).toBeLessThanOrEqual(50);
50
+ });
51
+ });
52
+
53
+ describe('CliLoader', () => {
54
+ let loader;
55
+
56
+ beforeEach(() => {
57
+ loader = new CliLoader();
58
+ });
59
+
60
+ test('should initialize with correct claudePath', () => {
61
+ const expectedPath = require('path').join(process.env.HOME, '.claude');
62
+ expect(loader.claudePath).toBe(expectedPath);
63
+ });
64
+
65
+ test('should initialize with correct historyPath', () => {
66
+ const expectedPath = require('path').join(process.env.HOME, '.claude', 'history.jsonl');
67
+ expect(loader.historyPath).toBe(expectedPath);
68
+ });
69
+
70
+ test('should have loadMessagesFromCLI method', () => {
71
+ expect(typeof loader.loadMessagesFromCLI).toBe('function');
72
+ });
73
+ });
74
+
75
+ describe('SummaryGenerator', () => {
76
+ let generator;
77
+
78
+ beforeEach(() => {
79
+ generator = new SummaryGenerator({ apiKey: 'test-key' });
80
+ });
81
+
82
+ test('should build transcript from messages', () => {
83
+ const messages = [
84
+ { role: 'user', content: 'Hello', created_at: Date.now() },
85
+ { role: 'assistant', content: 'Hi there!', created_at: Date.now() }
86
+ ];
87
+ const transcript = generator.buildTranscript(messages);
88
+ expect(transcript).toContain('USER:');
89
+ expect(transcript).toContain('ASSISTANT:');
90
+ expect(transcript).toContain('Hello');
91
+ expect(transcript).toContain('Hi there');
92
+ });
93
+
94
+ test('should limit transcript size', () => {
95
+ const longMessages = Array(100).fill(null).map((_, i) => ({
96
+ role: 'user',
97
+ content: 'A'.repeat(100),
98
+ created_at: Date.now()
99
+ }));
100
+ const transcript = generator.buildTranscript(longMessages);
101
+ expect(transcript.length).toBeLessThanOrEqual(6000);
102
+ });
103
+
104
+ test('should build prompt with session info', () => {
105
+ const sessionId = 'test-session-123';
106
+ const transcript = 'USER: Test message';
107
+ const prompt = generator.buildPrompt({ sessionId, transcript, existingSummary: null });
108
+ expect(prompt).toContain(sessionId);
109
+ expect(prompt).toContain('JSON');
110
+ expect(prompt).toContain('summary_short');
111
+ expect(prompt).toContain('summary_long');
112
+ });
113
+
114
+ test('should parse valid JSON response', () => {
115
+ const validJson = JSON.stringify({
116
+ summary_short: 'Short summary',
117
+ summary_long: 'Long summary with more details',
118
+ key_decisions: ['Decision 1', 'Decision 2'],
119
+ tools_used: ['Bash', 'Edit'],
120
+ files_modified: ['file1.js', 'file2.js']
121
+ });
122
+ const result = generator.safeParseJson(validJson);
123
+ expect(result.summary_short).toBe('Short summary');
124
+ expect(result.key_decisions).toHaveLength(2);
125
+ expect(result.tools_used).toContain('Bash');
126
+ });
127
+
128
+ test('should handle invalid JSON', () => {
129
+ const invalidJson = 'This is not JSON';
130
+ expect(() => generator.safeParseJson(invalidJson)).toThrow('Failed to parse summary JSON');
131
+ });
132
+
133
+ test('should normalize array fields', () => {
134
+ const jsonWithStrings = JSON.stringify({
135
+ summary_short: 'Test',
136
+ summary_long: 'Test long',
137
+ key_decisions: 'single decision',
138
+ tools_used: ['tool1'],
139
+ files_modified: 'single_file.js'
140
+ });
141
+ const result = generator.safeParseJson(jsonWithStrings);
142
+ expect(Array.isArray(result.key_decisions)).toBe(true);
143
+ expect(Array.isArray(result.files_modified)).toBe(true);
144
+ });
145
+ });
146
+
147
+ describe('Integration - Service Interactions', () => {
148
+ test('WorkspaceManager should use consistent path resolution', async () => {
149
+ const manager = new WorkspaceManager();
150
+ const testPath = '/home/user/myproject';
151
+ const validated = await manager.validateWorkspace(testPath);
152
+ expect(validated).toBe(testPath);
153
+ });
154
+
155
+ test('CliLoader and WorkspaceManager should use same Claude path', () => {
156
+ const manager = new WorkspaceManager();
157
+ const loader = new CliLoader();
158
+ expect(manager.claudePath).toBe(loader.claudePath);
159
+ });
160
+ });