@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- 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
|
+
});
|