@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,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
- });