Skip to content

Commit 29d232a

Browse files
chore: handoff checkpoint on fix/STA-186-error-handling
1 parent aa550be commit 29d232a

11 files changed

Lines changed: 3622 additions & 0 deletions

File tree

src/core/merge/__tests__/unified-merge-resolver.test.ts

Lines changed: 550 additions & 0 deletions
Large diffs are not rendered by default.

src/core/merge/unified-merge-resolver.ts

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* Tests for Logger utility
3+
*/
4+
5+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6+
7+
// Reset singleton between tests
8+
let Logger: typeof import('../logger.js').Logger;
9+
let LogLevel: typeof import('../logger.js').LogLevel;
10+
11+
describe('Logger', () => {
12+
beforeEach(async () => {
13+
vi.resetModules();
14+
// Clear any environment variables that affect logger
15+
delete process.env['STACKMEMORY_LOG_LEVEL'];
16+
delete process.env['STACKMEMORY_LOG_FILE'];
17+
18+
// Re-import to get fresh singleton
19+
const module = await import('../logger.js');
20+
Logger = module.Logger;
21+
LogLevel = module.LogLevel;
22+
});
23+
24+
afterEach(() => {
25+
vi.restoreAllMocks();
26+
});
27+
28+
describe('LogLevel', () => {
29+
it('should have correct log level values', () => {
30+
expect(LogLevel.ERROR).toBe(0);
31+
expect(LogLevel.WARN).toBe(1);
32+
expect(LogLevel.INFO).toBe(2);
33+
expect(LogLevel.DEBUG).toBe(3);
34+
});
35+
});
36+
37+
describe('getInstance', () => {
38+
it('should return singleton instance', () => {
39+
const instance1 = Logger.getInstance();
40+
const instance2 = Logger.getInstance();
41+
expect(instance1).toBe(instance2);
42+
});
43+
});
44+
45+
describe('logging methods', () => {
46+
it('should log error messages', () => {
47+
const consoleSpy = vi
48+
.spyOn(console, 'error')
49+
.mockImplementation(() => {});
50+
const logger = Logger.getInstance();
51+
52+
logger.error('Test error message');
53+
54+
expect(consoleSpy).toHaveBeenCalled();
55+
const logOutput = consoleSpy.mock.calls[0][0];
56+
expect(logOutput).toContain('ERROR');
57+
expect(logOutput).toContain('Test error message');
58+
});
59+
60+
it('should log error with Error object', () => {
61+
const consoleSpy = vi
62+
.spyOn(console, 'error')
63+
.mockImplementation(() => {});
64+
const logger = Logger.getInstance();
65+
const testError = new Error('Test error object');
66+
67+
logger.error('Error occurred', testError);
68+
69+
expect(consoleSpy).toHaveBeenCalled();
70+
});
71+
72+
it('should log error with context', () => {
73+
const consoleSpy = vi
74+
.spyOn(console, 'error')
75+
.mockImplementation(() => {});
76+
const logger = Logger.getInstance();
77+
78+
logger.error('Error with context', { detail: 'some info' });
79+
80+
expect(consoleSpy).toHaveBeenCalled();
81+
});
82+
83+
it('should log warn messages', () => {
84+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
85+
const logger = Logger.getInstance();
86+
87+
logger.warn('Test warning message');
88+
89+
expect(consoleSpy).toHaveBeenCalled();
90+
const logOutput = consoleSpy.mock.calls[0][0];
91+
expect(logOutput).toContain('WARN');
92+
expect(logOutput).toContain('Test warning message');
93+
});
94+
95+
it('should log info messages', () => {
96+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
97+
const logger = Logger.getInstance();
98+
99+
logger.info('Test info message');
100+
101+
expect(consoleSpy).toHaveBeenCalled();
102+
const logOutput = consoleSpy.mock.calls[0][0];
103+
expect(logOutput).toContain('INFO');
104+
expect(logOutput).toContain('Test info message');
105+
});
106+
107+
it('should log info with context', () => {
108+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
109+
const logger = Logger.getInstance();
110+
111+
logger.info('Info with context', { key: 'value' });
112+
113+
expect(consoleSpy).toHaveBeenCalled();
114+
});
115+
116+
it('should not log debug messages at INFO level', () => {
117+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
118+
const logger = Logger.getInstance();
119+
120+
logger.debug('Debug message');
121+
122+
// Debug should not be logged at INFO level (default)
123+
const debugCalls = consoleSpy.mock.calls.filter((call) =>
124+
call[0]?.includes?.('DEBUG')
125+
);
126+
expect(debugCalls.length).toBe(0);
127+
});
128+
});
129+
});
130+
131+
describe('Logger with DEBUG level', () => {
132+
beforeEach(async () => {
133+
vi.resetModules();
134+
process.env['STACKMEMORY_LOG_LEVEL'] = 'DEBUG';
135+
136+
const module = await import('../logger.js');
137+
Logger = module.Logger;
138+
LogLevel = module.LogLevel;
139+
});
140+
141+
afterEach(() => {
142+
delete process.env['STACKMEMORY_LOG_LEVEL'];
143+
vi.restoreAllMocks();
144+
});
145+
146+
it('should log debug messages when level is DEBUG', () => {
147+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
148+
const logger = Logger.getInstance();
149+
150+
logger.debug('Debug message');
151+
152+
expect(consoleSpy).toHaveBeenCalled();
153+
const logOutput = consoleSpy.mock.calls[0][0];
154+
expect(logOutput).toContain('DEBUG');
155+
expect(logOutput).toContain('Debug message');
156+
});
157+
});
158+
159+
describe('Logger with ERROR level', () => {
160+
beforeEach(async () => {
161+
vi.resetModules();
162+
process.env['STACKMEMORY_LOG_LEVEL'] = 'ERROR';
163+
164+
const module = await import('../logger.js');
165+
Logger = module.Logger;
166+
LogLevel = module.LogLevel;
167+
});
168+
169+
afterEach(() => {
170+
delete process.env['STACKMEMORY_LOG_LEVEL'];
171+
vi.restoreAllMocks();
172+
});
173+
174+
it('should not log info messages at ERROR level', () => {
175+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
176+
const logger = Logger.getInstance();
177+
178+
logger.info('Info message');
179+
180+
const infoCalls = consoleSpy.mock.calls.filter((call) =>
181+
call[0]?.includes?.('INFO')
182+
);
183+
expect(infoCalls.length).toBe(0);
184+
});
185+
186+
it('should not log warn messages at ERROR level', () => {
187+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
188+
const logger = Logger.getInstance();
189+
190+
logger.warn('Warn message');
191+
192+
const warnCalls = consoleSpy.mock.calls.filter((call) =>
193+
call[0]?.includes?.('WARN')
194+
);
195+
expect(warnCalls.length).toBe(0);
196+
});
197+
});
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Tests for Metrics collector
3+
*/
4+
5+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6+
import { Metrics, metrics } from '../metrics.js';
7+
8+
describe('Metrics', () => {
9+
beforeEach(() => {
10+
Metrics.reset();
11+
});
12+
13+
afterEach(() => {
14+
Metrics.reset();
15+
});
16+
17+
describe('record', () => {
18+
it('should record a metric value', async () => {
19+
await Metrics.record('test.metric', 42);
20+
21+
const stats = Metrics.getStats('test.metric');
22+
expect(stats['test.metric']).toBeDefined();
23+
expect(stats['test.metric'].sum).toBe(42);
24+
expect(stats['test.metric'].count).toBe(1);
25+
});
26+
27+
it('should record multiple metric values', async () => {
28+
await Metrics.record('test.metric', 10);
29+
await Metrics.record('test.metric', 20);
30+
await Metrics.record('test.metric', 30);
31+
32+
const stats = Metrics.getStats('test.metric');
33+
expect(stats['test.metric'].sum).toBe(60);
34+
expect(stats['test.metric'].count).toBe(3);
35+
expect(stats['test.metric'].avg).toBe(20);
36+
expect(stats['test.metric'].min).toBe(10);
37+
expect(stats['test.metric'].max).toBe(30);
38+
});
39+
40+
it('should record metric with tags', async () => {
41+
await Metrics.record('tagged.metric', 100, { env: 'test' });
42+
43+
const stats = Metrics.getStats('tagged.metric');
44+
expect(stats['tagged.metric']).toBeDefined();
45+
});
46+
});
47+
48+
describe('increment', () => {
49+
it('should increment a counter', async () => {
50+
await Metrics.increment('test.counter');
51+
await Metrics.increment('test.counter');
52+
await Metrics.increment('test.counter');
53+
54+
const stats = Metrics.getStats('test.counter');
55+
expect(stats['test.counter'].sum).toBe(3);
56+
expect(stats['test.counter'].count).toBe(3);
57+
});
58+
59+
it('should increment with custom value', async () => {
60+
// Note: increment doesn't take a custom value parameter in the current API
61+
// Each call increments by 1
62+
await Metrics.increment('test.counter');
63+
64+
const stats = Metrics.getStats('test.counter');
65+
expect(stats['test.counter'].sum).toBe(1);
66+
});
67+
68+
it('should increment with tags', async () => {
69+
await Metrics.increment('tagged.counter', { operation: 'insert' });
70+
71+
const stats = Metrics.getStats('tagged.counter');
72+
expect(stats['tagged.counter']).toBeDefined();
73+
});
74+
});
75+
76+
describe('timing', () => {
77+
it('should record timing metric', async () => {
78+
await Metrics.timing('operation.duration', 150);
79+
await Metrics.timing('operation.duration', 200);
80+
81+
const stats = Metrics.getStats('operation.duration');
82+
expect(stats['operation.duration'].sum).toBe(350);
83+
expect(stats['operation.duration'].avg).toBe(175);
84+
expect(stats['operation.duration'].min).toBe(150);
85+
expect(stats['operation.duration'].max).toBe(200);
86+
});
87+
88+
it('should record timing with tags', async () => {
89+
await Metrics.timing('api.latency', 50, { endpoint: '/test' });
90+
91+
const stats = Metrics.getStats('api.latency');
92+
expect(stats['api.latency']).toBeDefined();
93+
});
94+
});
95+
96+
describe('getStats', () => {
97+
it('should return empty object for non-existent metric', () => {
98+
const stats = Metrics.getStats('nonexistent');
99+
expect(stats).toEqual({});
100+
});
101+
102+
it('should return all stats when no metric specified', async () => {
103+
await Metrics.record('metric1', 10);
104+
await Metrics.record('metric2', 20);
105+
106+
const stats = Metrics.getStats();
107+
expect(stats['metric1']).toBeDefined();
108+
expect(stats['metric2']).toBeDefined();
109+
});
110+
111+
it('should calculate average correctly', async () => {
112+
await Metrics.record('avg.test', 10);
113+
await Metrics.record('avg.test', 30);
114+
115+
const stats = Metrics.getStats('avg.test');
116+
expect(stats['avg.test'].avg).toBe(20);
117+
});
118+
});
119+
120+
describe('reset', () => {
121+
it('should clear all metrics', async () => {
122+
await Metrics.record('test.metric', 42);
123+
await Metrics.increment('test.counter');
124+
125+
Metrics.reset();
126+
127+
const stats = Metrics.getStats();
128+
expect(Object.keys(stats).length).toBe(0);
129+
});
130+
});
131+
132+
describe('event emitter', () => {
133+
it('should emit metric events', async () => {
134+
const handler = vi.fn();
135+
Metrics.on('metric', handler);
136+
137+
await Metrics.record('event.test', 100);
138+
139+
expect(handler).toHaveBeenCalledWith(
140+
expect.objectContaining({
141+
metric: 'event.test',
142+
value: 100,
143+
type: 'gauge',
144+
})
145+
);
146+
});
147+
});
148+
149+
describe('metrics export', () => {
150+
it('should export same API via default export', () => {
151+
expect(metrics).toBe(Metrics);
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)