@massu/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -1,290 +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 { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
6
|
-
import { resolve } from 'path';
|
|
7
|
-
import {
|
|
8
|
-
detectFramework,
|
|
9
|
-
generateConfig,
|
|
10
|
-
registerMcpServer,
|
|
11
|
-
installHooks,
|
|
12
|
-
buildHooksConfig,
|
|
13
|
-
} from '../commands/init.ts';
|
|
14
|
-
|
|
15
|
-
const TEST_DIR = resolve(__dirname, '../../.test-cli');
|
|
16
|
-
|
|
17
|
-
function setupTestDir(): void {
|
|
18
|
-
if (existsSync(TEST_DIR)) {
|
|
19
|
-
rmSync(TEST_DIR, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function cleanupTestDir(): void {
|
|
25
|
-
if (existsSync(TEST_DIR)) {
|
|
26
|
-
rmSync(TEST_DIR, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('CLI: Framework Detection', () => {
|
|
31
|
-
beforeEach(setupTestDir);
|
|
32
|
-
afterEach(cleanupTestDir);
|
|
33
|
-
|
|
34
|
-
it('detects TypeScript', () => {
|
|
35
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
36
|
-
devDependencies: { typescript: '^5.0.0' },
|
|
37
|
-
}));
|
|
38
|
-
const result = detectFramework(TEST_DIR);
|
|
39
|
-
expect(result.type).toBe('typescript');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('detects Next.js', () => {
|
|
43
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
44
|
-
dependencies: { next: '^14.0.0', react: '^18.0.0' },
|
|
45
|
-
devDependencies: { typescript: '^5.0.0' },
|
|
46
|
-
}));
|
|
47
|
-
const result = detectFramework(TEST_DIR);
|
|
48
|
-
expect(result.ui).toBe('nextjs');
|
|
49
|
-
expect(result.type).toBe('typescript');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('detects Prisma ORM', () => {
|
|
53
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
54
|
-
dependencies: { '@prisma/client': '^5.0.0' },
|
|
55
|
-
}));
|
|
56
|
-
const result = detectFramework(TEST_DIR);
|
|
57
|
-
expect(result.orm).toBe('prisma');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('detects tRPC router', () => {
|
|
61
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
62
|
-
dependencies: { '@trpc/server': '^10.0.0' },
|
|
63
|
-
}));
|
|
64
|
-
const result = detectFramework(TEST_DIR);
|
|
65
|
-
expect(result.router).toBe('trpc');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('detects SvelteKit', () => {
|
|
69
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
70
|
-
devDependencies: { '@sveltejs/kit': '^2.0.0' },
|
|
71
|
-
}));
|
|
72
|
-
const result = detectFramework(TEST_DIR);
|
|
73
|
-
expect(result.ui).toBe('sveltekit');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('detects drizzle ORM', () => {
|
|
77
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
78
|
-
dependencies: { 'drizzle-orm': '^0.30.0' },
|
|
79
|
-
}));
|
|
80
|
-
const result = detectFramework(TEST_DIR);
|
|
81
|
-
expect(result.orm).toBe('drizzle');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('detects GraphQL router', () => {
|
|
85
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
86
|
-
dependencies: { graphql: '^16.0.0' },
|
|
87
|
-
}));
|
|
88
|
-
const result = detectFramework(TEST_DIR);
|
|
89
|
-
expect(result.router).toBe('graphql');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('detects Express REST', () => {
|
|
93
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
94
|
-
dependencies: { express: '^4.0.0' },
|
|
95
|
-
}));
|
|
96
|
-
const result = detectFramework(TEST_DIR);
|
|
97
|
-
expect(result.router).toBe('rest');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('returns defaults when no package.json', () => {
|
|
101
|
-
const result = detectFramework(TEST_DIR);
|
|
102
|
-
expect(result.type).toBe('javascript');
|
|
103
|
-
expect(result.router).toBe('none');
|
|
104
|
-
expect(result.orm).toBe('none');
|
|
105
|
-
expect(result.ui).toBe('none');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('detects full stack: TS + Next.js + Prisma + tRPC', () => {
|
|
109
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({
|
|
110
|
-
dependencies: {
|
|
111
|
-
next: '^14.0.0',
|
|
112
|
-
'@prisma/client': '^5.0.0',
|
|
113
|
-
'@trpc/server': '^10.0.0',
|
|
114
|
-
},
|
|
115
|
-
devDependencies: {
|
|
116
|
-
typescript: '^5.0.0',
|
|
117
|
-
},
|
|
118
|
-
}));
|
|
119
|
-
const result = detectFramework(TEST_DIR);
|
|
120
|
-
expect(result.type).toBe('typescript');
|
|
121
|
-
expect(result.ui).toBe('nextjs');
|
|
122
|
-
expect(result.orm).toBe('prisma');
|
|
123
|
-
expect(result.router).toBe('trpc');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('CLI: Config Generation', () => {
|
|
128
|
-
beforeEach(setupTestDir);
|
|
129
|
-
afterEach(cleanupTestDir);
|
|
130
|
-
|
|
131
|
-
it('creates massu.config.yaml', () => {
|
|
132
|
-
const framework = { type: 'typescript', router: 'trpc', orm: 'prisma', ui: 'nextjs' };
|
|
133
|
-
const created = generateConfig(TEST_DIR, framework);
|
|
134
|
-
expect(created).toBe(true);
|
|
135
|
-
expect(existsSync(resolve(TEST_DIR, 'massu.config.yaml'))).toBe(true);
|
|
136
|
-
|
|
137
|
-
const content = readFileSync(resolve(TEST_DIR, 'massu.config.yaml'), 'utf-8');
|
|
138
|
-
expect(content).toContain('toolPrefix: massu');
|
|
139
|
-
expect(content).toContain('type: typescript');
|
|
140
|
-
expect(content).toContain('router: trpc');
|
|
141
|
-
expect(content).toContain('orm: prisma');
|
|
142
|
-
expect(content).toContain('ui: nextjs');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('skips if config already exists', () => {
|
|
146
|
-
writeFileSync(resolve(TEST_DIR, 'massu.config.yaml'), 'existing: true\n');
|
|
147
|
-
const framework = { type: 'typescript', router: 'none', orm: 'none', ui: 'none' };
|
|
148
|
-
const created = generateConfig(TEST_DIR, framework);
|
|
149
|
-
expect(created).toBe(false);
|
|
150
|
-
|
|
151
|
-
const content = readFileSync(resolve(TEST_DIR, 'massu.config.yaml'), 'utf-8');
|
|
152
|
-
expect(content).toBe('existing: true\n');
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('CLI: MCP Registration', () => {
|
|
157
|
-
beforeEach(setupTestDir);
|
|
158
|
-
afterEach(cleanupTestDir);
|
|
159
|
-
|
|
160
|
-
it('creates .mcp.json when it does not exist', () => {
|
|
161
|
-
const registered = registerMcpServer(TEST_DIR);
|
|
162
|
-
expect(registered).toBe(true);
|
|
163
|
-
expect(existsSync(resolve(TEST_DIR, '.mcp.json'))).toBe(true);
|
|
164
|
-
|
|
165
|
-
const content = JSON.parse(readFileSync(resolve(TEST_DIR, '.mcp.json'), 'utf-8'));
|
|
166
|
-
expect(content.mcpServers.massu).toBeDefined();
|
|
167
|
-
expect(content.mcpServers.massu.type).toBe('stdio');
|
|
168
|
-
expect(content.mcpServers.massu.command).toBe('npx');
|
|
169
|
-
expect(content.mcpServers.massu.args).toEqual(['-y', '@massu/core']);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('merges into existing .mcp.json without overwriting other servers', () => {
|
|
173
|
-
writeFileSync(resolve(TEST_DIR, '.mcp.json'), JSON.stringify({
|
|
174
|
-
mcpServers: {
|
|
175
|
-
other: { type: 'stdio', command: 'other-server' },
|
|
176
|
-
},
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
const registered = registerMcpServer(TEST_DIR);
|
|
180
|
-
expect(registered).toBe(true);
|
|
181
|
-
|
|
182
|
-
const content = JSON.parse(readFileSync(resolve(TEST_DIR, '.mcp.json'), 'utf-8'));
|
|
183
|
-
expect(content.mcpServers.massu).toBeDefined();
|
|
184
|
-
expect(content.mcpServers.other).toBeDefined();
|
|
185
|
-
expect(content.mcpServers.other.command).toBe('other-server');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('skips if massu already registered', () => {
|
|
189
|
-
writeFileSync(resolve(TEST_DIR, '.mcp.json'), JSON.stringify({
|
|
190
|
-
mcpServers: {
|
|
191
|
-
massu: { type: 'stdio', command: 'npx', args: ['-y', '@massu/core'] },
|
|
192
|
-
},
|
|
193
|
-
}));
|
|
194
|
-
|
|
195
|
-
const registered = registerMcpServer(TEST_DIR);
|
|
196
|
-
expect(registered).toBe(false);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('is idempotent (running twice does not duplicate)', () => {
|
|
200
|
-
registerMcpServer(TEST_DIR);
|
|
201
|
-
const registered = registerMcpServer(TEST_DIR);
|
|
202
|
-
expect(registered).toBe(false);
|
|
203
|
-
|
|
204
|
-
const content = JSON.parse(readFileSync(resolve(TEST_DIR, '.mcp.json'), 'utf-8'));
|
|
205
|
-
expect(Object.keys(content.mcpServers)).toHaveLength(1);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe('CLI: Hooks Installation', () => {
|
|
210
|
-
beforeEach(setupTestDir);
|
|
211
|
-
afterEach(cleanupTestDir);
|
|
212
|
-
|
|
213
|
-
it('creates .claude/settings.local.json with hooks', () => {
|
|
214
|
-
const { installed, count } = installHooks(TEST_DIR);
|
|
215
|
-
expect(installed).toBe(true);
|
|
216
|
-
expect(count).toBe(11);
|
|
217
|
-
|
|
218
|
-
const settingsPath = resolve(TEST_DIR, '.claude/settings.local.json');
|
|
219
|
-
expect(existsSync(settingsPath)).toBe(true);
|
|
220
|
-
|
|
221
|
-
const content = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
222
|
-
expect(content.hooks).toBeDefined();
|
|
223
|
-
expect(content.hooks.SessionStart).toBeDefined();
|
|
224
|
-
expect(content.hooks.PreToolUse).toBeDefined();
|
|
225
|
-
expect(content.hooks.PostToolUse).toBeDefined();
|
|
226
|
-
expect(content.hooks.Stop).toBeDefined();
|
|
227
|
-
expect(content.hooks.PreCompact).toBeDefined();
|
|
228
|
-
expect(content.hooks.UserPromptSubmit).toBeDefined();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('preserves existing settings when installing hooks', () => {
|
|
232
|
-
mkdirSync(resolve(TEST_DIR, '.claude'), { recursive: true });
|
|
233
|
-
writeFileSync(resolve(TEST_DIR, '.claude/settings.local.json'), JSON.stringify({
|
|
234
|
-
permissions: { allow: ['Bash'] },
|
|
235
|
-
customSetting: 'preserved',
|
|
236
|
-
}));
|
|
237
|
-
|
|
238
|
-
installHooks(TEST_DIR);
|
|
239
|
-
|
|
240
|
-
const content = JSON.parse(readFileSync(resolve(TEST_DIR, '.claude/settings.local.json'), 'utf-8'));
|
|
241
|
-
expect(content.permissions).toEqual({ allow: ['Bash'] });
|
|
242
|
-
expect(content.customSetting).toBe('preserved');
|
|
243
|
-
expect(content.hooks).toBeDefined();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('generates correct hook commands', () => {
|
|
247
|
-
const hooksConfig = buildHooksConfig('node_modules/@massu/core/dist/hooks');
|
|
248
|
-
|
|
249
|
-
// Check PreToolUse has security-gate and pre-delete-check
|
|
250
|
-
const preToolUse = hooksConfig.PreToolUse;
|
|
251
|
-
expect(preToolUse).toHaveLength(2);
|
|
252
|
-
expect(preToolUse[0].matcher).toBe('Bash');
|
|
253
|
-
expect(preToolUse[0].hooks[0].command).toContain('security-gate.js');
|
|
254
|
-
expect(preToolUse[1].matcher).toBe('Bash|Write');
|
|
255
|
-
expect(preToolUse[1].hooks[0].command).toContain('pre-delete-check.js');
|
|
256
|
-
|
|
257
|
-
// Check PostToolUse has all 4 hooks
|
|
258
|
-
const postToolUse = hooksConfig.PostToolUse;
|
|
259
|
-
expect(postToolUse).toHaveLength(2);
|
|
260
|
-
expect(postToolUse[0].hooks).toHaveLength(3);
|
|
261
|
-
expect(postToolUse[0].hooks[0].command).toContain('post-tool-use.js');
|
|
262
|
-
expect(postToolUse[0].hooks[1].command).toContain('quality-event.js');
|
|
263
|
-
expect(postToolUse[0].hooks[2].command).toContain('cost-tracker.js');
|
|
264
|
-
expect(postToolUse[1].matcher).toBe('Edit|Write');
|
|
265
|
-
expect(postToolUse[1].hooks[0].command).toContain('post-edit-context.js');
|
|
266
|
-
|
|
267
|
-
// Check Stop has session-end
|
|
268
|
-
expect(hooksConfig.Stop[0].hooks[0].command).toContain('session-end.js');
|
|
269
|
-
|
|
270
|
-
// Check PreCompact
|
|
271
|
-
expect(hooksConfig.PreCompact[0].hooks[0].command).toContain('pre-compact.js');
|
|
272
|
-
|
|
273
|
-
// Check UserPromptSubmit
|
|
274
|
-
const userPrompt = hooksConfig.UserPromptSubmit;
|
|
275
|
-
expect(userPrompt[0].hooks).toHaveLength(2);
|
|
276
|
-
expect(userPrompt[0].hooks[0].command).toContain('user-prompt.js');
|
|
277
|
-
expect(userPrompt[0].hooks[1].command).toContain('intent-suggester.js');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('counts all 11 hooks correctly', () => {
|
|
281
|
-
const hooksConfig = buildHooksConfig('test/path');
|
|
282
|
-
let count = 0;
|
|
283
|
-
for (const groups of Object.values(hooksConfig)) {
|
|
284
|
-
for (const group of groups) {
|
|
285
|
-
count += group.hooks.length;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
expect(count).toBe(11);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
@@ -1,261 +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, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import { syncToCloud, drainSyncQueue } from '../cloud-sync.ts';
|
|
7
|
-
import type { SyncPayload } from '../cloud-sync.ts';
|
|
8
|
-
import {
|
|
9
|
-
enqueueSyncPayload,
|
|
10
|
-
dequeuePendingSync,
|
|
11
|
-
removePendingSync,
|
|
12
|
-
incrementRetryCount,
|
|
13
|
-
} from '../memory-db.ts';
|
|
14
|
-
|
|
15
|
-
// Mock getConfig
|
|
16
|
-
vi.mock('../config.ts', () => ({
|
|
17
|
-
getConfig: vi.fn(() => ({
|
|
18
|
-
cloud: {
|
|
19
|
-
enabled: true,
|
|
20
|
-
apiKey: 'ms_live_test_key_12345',
|
|
21
|
-
endpoint: 'https://test.supabase.co/functions/v1/sync',
|
|
22
|
-
sync: { memory: true, analytics: true, audit: true },
|
|
23
|
-
},
|
|
24
|
-
toolPrefix: 'massu',
|
|
25
|
-
project: { name: 'test', root: '/tmp/test' },
|
|
26
|
-
framework: { type: 'typescript', router: 'none', orm: 'none', ui: 'none' },
|
|
27
|
-
paths: { source: 'src', aliases: {} },
|
|
28
|
-
domains: [],
|
|
29
|
-
rules: [],
|
|
30
|
-
})),
|
|
31
|
-
getProjectRoot: vi.fn(() => '/tmp/test'),
|
|
32
|
-
getResolvedPaths: vi.fn(() => ({
|
|
33
|
-
memoryDbPath: ':memory:',
|
|
34
|
-
codegraphDbPath: ':memory:',
|
|
35
|
-
dataDbPath: ':memory:',
|
|
36
|
-
srcDir: '/tmp/test/src',
|
|
37
|
-
pathAlias: {},
|
|
38
|
-
extensions: ['.ts'],
|
|
39
|
-
indexFiles: ['index.ts'],
|
|
40
|
-
patternsDir: '/tmp/.claude/patterns',
|
|
41
|
-
claudeMdPath: '/tmp/.claude/CLAUDE.md',
|
|
42
|
-
docsMapPath: '/tmp/.massu/docs-map.json',
|
|
43
|
-
helpSitePath: '/tmp/test-help',
|
|
44
|
-
prismaSchemaPath: '/tmp/prisma/schema.prisma',
|
|
45
|
-
rootRouterPath: '/tmp/src/server/api/root.ts',
|
|
46
|
-
routersDir: '/tmp/src/server/api/routers',
|
|
47
|
-
})),
|
|
48
|
-
resetConfig: vi.fn(),
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
// Mock global fetch
|
|
52
|
-
const mockFetch = vi.fn();
|
|
53
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
54
|
-
|
|
55
|
-
function createTestDb(): Database.Database {
|
|
56
|
-
const db = new Database(':memory:');
|
|
57
|
-
db.exec(`
|
|
58
|
-
CREATE TABLE IF NOT EXISTS pending_sync (
|
|
59
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
-
payload TEXT NOT NULL,
|
|
61
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
62
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
63
|
-
last_error TEXT
|
|
64
|
-
);
|
|
65
|
-
`);
|
|
66
|
-
return db;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
describe('cloud-sync', () => {
|
|
70
|
-
let db: Database.Database;
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
db = createTestDb();
|
|
74
|
-
mockFetch.mockReset();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
db.close();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const testPayload: SyncPayload = {
|
|
82
|
-
sessions: [{
|
|
83
|
-
local_session_id: 'test-session-1',
|
|
84
|
-
summary: 'Test session',
|
|
85
|
-
ended_at: new Date().toISOString(),
|
|
86
|
-
}],
|
|
87
|
-
observations: [{
|
|
88
|
-
local_observation_id: 'obs-1',
|
|
89
|
-
type: 'decision',
|
|
90
|
-
content: 'Test observation',
|
|
91
|
-
importance: 3,
|
|
92
|
-
}],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
describe('syncToCloud', () => {
|
|
96
|
-
it('should return no-op when cloud is disabled', async () => {
|
|
97
|
-
const { getConfig } = await import('../config.ts');
|
|
98
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
99
|
-
cloud: { enabled: false },
|
|
100
|
-
toolPrefix: 'massu',
|
|
101
|
-
project: { name: 'test', root: '/tmp' },
|
|
102
|
-
framework: { type: 'typescript', router: 'none', orm: 'none', ui: 'none' },
|
|
103
|
-
paths: { source: 'src', aliases: {} },
|
|
104
|
-
domains: [],
|
|
105
|
-
rules: [],
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const result = await syncToCloud(db, testPayload);
|
|
109
|
-
expect(result.success).toBe(true);
|
|
110
|
-
expect(result.synced.sessions).toBe(0);
|
|
111
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should return error when API key is missing', async () => {
|
|
115
|
-
const { getConfig } = await import('../config.ts');
|
|
116
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
117
|
-
cloud: { enabled: true },
|
|
118
|
-
toolPrefix: 'massu',
|
|
119
|
-
project: { name: 'test', root: '/tmp' },
|
|
120
|
-
framework: { type: 'typescript', router: 'none', orm: 'none', ui: 'none' },
|
|
121
|
-
paths: { source: 'src', aliases: {} },
|
|
122
|
-
domains: [],
|
|
123
|
-
rules: [],
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const result = await syncToCloud(db, testPayload);
|
|
127
|
-
expect(result.success).toBe(false);
|
|
128
|
-
expect(result.error).toBe('No API key configured');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should POST payload to endpoint on success', async () => {
|
|
132
|
-
mockFetch.mockResolvedValueOnce({
|
|
133
|
-
ok: true,
|
|
134
|
-
json: async () => ({ synced: { sessions: 1, observations: 1, analytics: 0 } }),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const result = await syncToCloud(db, testPayload);
|
|
138
|
-
|
|
139
|
-
expect(result.success).toBe(true);
|
|
140
|
-
expect(result.synced.sessions).toBe(1);
|
|
141
|
-
expect(mockFetch).toHaveBeenCalledOnce();
|
|
142
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
143
|
-
'https://test.supabase.co/functions/v1/sync',
|
|
144
|
-
expect.objectContaining({
|
|
145
|
-
method: 'POST',
|
|
146
|
-
headers: expect.objectContaining({
|
|
147
|
-
'Authorization': 'Bearer ms_live_test_key_12345',
|
|
148
|
-
}),
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should enqueue payload after retry failures', async () => {
|
|
154
|
-
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
155
|
-
|
|
156
|
-
const result = await syncToCloud(db, testPayload);
|
|
157
|
-
|
|
158
|
-
expect(result.success).toBe(false);
|
|
159
|
-
expect(result.error).toBe('Network error');
|
|
160
|
-
|
|
161
|
-
// Verify payload was enqueued
|
|
162
|
-
const pending = dequeuePendingSync(db, 10);
|
|
163
|
-
expect(pending.length).toBe(1);
|
|
164
|
-
const enqueuedPayload = JSON.parse(pending[0].payload);
|
|
165
|
-
expect(enqueuedPayload.sessions).toHaveLength(1);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should not retry on 4xx client errors', async () => {
|
|
169
|
-
mockFetch.mockResolvedValue({
|
|
170
|
-
ok: false,
|
|
171
|
-
status: 401,
|
|
172
|
-
statusText: 'Unauthorized',
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const result = await syncToCloud(db, testPayload);
|
|
176
|
-
|
|
177
|
-
expect(result.success).toBe(false);
|
|
178
|
-
// Should only call once (no retry on client errors)
|
|
179
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should filter payload based on sync config', async () => {
|
|
183
|
-
const { getConfig } = await import('../config.ts');
|
|
184
|
-
vi.mocked(getConfig).mockReturnValueOnce({
|
|
185
|
-
cloud: {
|
|
186
|
-
enabled: true,
|
|
187
|
-
apiKey: 'ms_live_key',
|
|
188
|
-
endpoint: 'https://test.supabase.co/functions/v1/sync',
|
|
189
|
-
sync: { memory: true, analytics: false, audit: false },
|
|
190
|
-
},
|
|
191
|
-
toolPrefix: 'massu',
|
|
192
|
-
project: { name: 'test', root: '/tmp' },
|
|
193
|
-
framework: { type: 'typescript', router: 'none', orm: 'none', ui: 'none' },
|
|
194
|
-
paths: { source: 'src', aliases: {} },
|
|
195
|
-
domains: [],
|
|
196
|
-
rules: [],
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
mockFetch.mockResolvedValueOnce({
|
|
200
|
-
ok: true,
|
|
201
|
-
json: async () => ({ synced: { sessions: 1, observations: 0, analytics: 0 } }),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const payloadWithAll: SyncPayload = {
|
|
205
|
-
...testPayload,
|
|
206
|
-
analytics: [{ event_type: 'test', event_data: {} }],
|
|
207
|
-
audit: [{ action: 'test', details: {} }],
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
await syncToCloud(db, payloadWithAll);
|
|
211
|
-
|
|
212
|
-
const sentBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
213
|
-
expect(sentBody.sessions).toBeDefined();
|
|
214
|
-
expect(sentBody.analytics).toBeUndefined();
|
|
215
|
-
expect(sentBody.audit).toBeUndefined();
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('pending_sync queue functions', () => {
|
|
220
|
-
it('should enqueue and dequeue payloads', () => {
|
|
221
|
-
enqueueSyncPayload(db, JSON.stringify(testPayload));
|
|
222
|
-
enqueueSyncPayload(db, JSON.stringify({ sessions: [] }));
|
|
223
|
-
|
|
224
|
-
const items = dequeuePendingSync(db, 10);
|
|
225
|
-
expect(items).toHaveLength(2);
|
|
226
|
-
expect(JSON.parse(items[0].payload).sessions).toHaveLength(1);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should remove successfully synced items', () => {
|
|
230
|
-
enqueueSyncPayload(db, JSON.stringify(testPayload));
|
|
231
|
-
const items = dequeuePendingSync(db, 10);
|
|
232
|
-
expect(items).toHaveLength(1);
|
|
233
|
-
|
|
234
|
-
removePendingSync(db, items[0].id);
|
|
235
|
-
const remaining = dequeuePendingSync(db, 10);
|
|
236
|
-
expect(remaining).toHaveLength(0);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should increment retry count on failure', () => {
|
|
240
|
-
enqueueSyncPayload(db, JSON.stringify(testPayload));
|
|
241
|
-
const items = dequeuePendingSync(db, 10);
|
|
242
|
-
|
|
243
|
-
incrementRetryCount(db, items[0].id, 'Network timeout');
|
|
244
|
-
|
|
245
|
-
const updated = dequeuePendingSync(db, 10);
|
|
246
|
-
expect(updated[0].retry_count).toBe(1);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('should discard items with retry_count >= 10', () => {
|
|
250
|
-
enqueueSyncPayload(db, JSON.stringify(testPayload));
|
|
251
|
-
const items = dequeuePendingSync(db, 10);
|
|
252
|
-
|
|
253
|
-
// Set retry count to 10 manually
|
|
254
|
-
db.prepare('UPDATE pending_sync SET retry_count = 10 WHERE id = ?').run(items[0].id);
|
|
255
|
-
|
|
256
|
-
// Next dequeue should discard the stale item
|
|
257
|
-
const remaining = dequeuePendingSync(db, 10);
|
|
258
|
-
expect(remaining).toHaveLength(0);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
});
|