wraptc 1.0.2 → 1.0.4
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/bin/wraptc +4 -4
- package/package.json +2 -2
- package/src/cli/__tests__/cli.test.ts +337 -0
- package/src/cli/index.ts +149 -0
- package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
- package/src/core/__tests__/integration/integration.test.ts +241 -0
- package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
- package/src/core/__tests__/test-utils.ts +136 -0
- package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
- package/src/core/__tests__/unit/basic-test.test.ts +44 -0
- package/src/core/__tests__/unit/basic.test.ts +12 -0
- package/src/core/__tests__/unit/config.test.ts +244 -0
- package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
- package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
- package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
- package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
- package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
- package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
- package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
- package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
- package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
- package/src/core/__tests__/unit/router.test.ts +967 -0
- package/src/core/__tests__/unit/state.test.ts +1079 -0
- package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
- package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
- package/src/core/adapters/builtin/codex.ts +35 -0
- package/src/core/adapters/builtin/gemini.ts +34 -0
- package/src/core/adapters/builtin/index.ts +31 -0
- package/src/core/adapters/builtin/mock-coder.ts +148 -0
- package/src/core/adapters/builtin/qwen.ts +34 -0
- package/src/core/adapters/define.ts +48 -0
- package/src/core/adapters/index.ts +43 -0
- package/src/core/adapters/loader.ts +143 -0
- package/src/core/adapters/provider-bridge.ts +190 -0
- package/src/core/adapters/runner.ts +437 -0
- package/src/core/adapters/types.ts +172 -0
- package/src/core/config.ts +290 -0
- package/src/core/define-provider.ts +212 -0
- package/src/core/error-patterns.ts +147 -0
- package/src/core/index.ts +130 -0
- package/src/core/memory-monitor.ts +171 -0
- package/src/core/plugin/builtin.ts +87 -0
- package/src/core/plugin/index.ts +34 -0
- package/src/core/plugin/registry.ts +350 -0
- package/src/core/plugin/types.ts +209 -0
- package/src/core/provider-factory.ts +397 -0
- package/src/core/provider-loader.ts +171 -0
- package/src/core/providers/codex.ts +56 -0
- package/src/core/providers/configurable.ts +637 -0
- package/src/core/providers/custom.ts +261 -0
- package/src/core/providers/gemini.ts +41 -0
- package/src/core/providers/index.ts +383 -0
- package/src/core/providers/opencode.ts +168 -0
- package/src/core/providers/qwen-code.ts +41 -0
- package/src/core/router.ts +370 -0
- package/src/core/state.ts +258 -0
- package/src/core/types.ts +206 -0
- package/src/core/unified/capabilities.ts +184 -0
- package/src/core/unified/errors.ts +141 -0
- package/src/core/unified/index.ts +29 -0
- package/src/core/unified/output.ts +189 -0
- package/src/core/wrap-terminalcoder.ts +245 -0
- package/src/mcp/__tests__/server.test.ts +295 -0
- package/src/mcp/server.ts +284 -0
- package/src/test-fixtures/mock-coder.sh +194 -0
- package/dist/cli/index.js +0 -16501
- package/dist/core/index.js +0 -7531
- package/dist/mcp/server.js +0 -14568
- package/dist/wraptc-1.0.2.tgz +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ERROR_PATTERNS,
|
|
4
|
+
classifyErrorDefault,
|
|
5
|
+
createErrorClassifier,
|
|
6
|
+
mergeErrorPatterns,
|
|
7
|
+
} from "../../error-patterns";
|
|
8
|
+
|
|
9
|
+
describe("error-patterns", () => {
|
|
10
|
+
describe("DEFAULT_ERROR_PATTERNS", () => {
|
|
11
|
+
test("should have patterns for all error kinds", () => {
|
|
12
|
+
const expectedKinds = [
|
|
13
|
+
"OUT_OF_CREDITS",
|
|
14
|
+
"RATE_LIMIT",
|
|
15
|
+
"BAD_REQUEST",
|
|
16
|
+
"UNAUTHORIZED",
|
|
17
|
+
"FORBIDDEN",
|
|
18
|
+
"NOT_FOUND",
|
|
19
|
+
"TIMEOUT",
|
|
20
|
+
"CONTEXT_LENGTH",
|
|
21
|
+
"CONTENT_FILTER",
|
|
22
|
+
"INTERNAL",
|
|
23
|
+
"TRANSIENT",
|
|
24
|
+
"UNKNOWN",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const kind of expectedKinds) {
|
|
28
|
+
expect(DEFAULT_ERROR_PATTERNS).toHaveProperty(kind);
|
|
29
|
+
expect(
|
|
30
|
+
Array.isArray(DEFAULT_ERROR_PATTERNS[kind as keyof typeof DEFAULT_ERROR_PATTERNS]),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("should have empty patterns for UNKNOWN", () => {
|
|
36
|
+
expect(DEFAULT_ERROR_PATTERNS.UNKNOWN).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("classifyErrorDefault", () => {
|
|
41
|
+
test("should classify OUT_OF_CREDITS errors", () => {
|
|
42
|
+
expect(classifyErrorDefault("quota exceeded")).toBe("OUT_OF_CREDITS");
|
|
43
|
+
expect(classifyErrorDefault("out of credits")).toBe("OUT_OF_CREDITS");
|
|
44
|
+
expect(classifyErrorDefault("insufficient quota")).toBe("OUT_OF_CREDITS");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("should classify RATE_LIMIT errors", () => {
|
|
48
|
+
expect(classifyErrorDefault("rate limit exceeded")).toBe("RATE_LIMIT");
|
|
49
|
+
expect(classifyErrorDefault("429 too many requests")).toBe("RATE_LIMIT");
|
|
50
|
+
expect(classifyErrorDefault("", "slow down please")).toBe("RATE_LIMIT");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("should classify BAD_REQUEST errors", () => {
|
|
54
|
+
expect(classifyErrorDefault("400 bad request")).toBe("BAD_REQUEST");
|
|
55
|
+
expect(classifyErrorDefault("invalid request format")).toBe("BAD_REQUEST");
|
|
56
|
+
expect(classifyErrorDefault("malformed json")).toBe("BAD_REQUEST");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("should classify UNAUTHORIZED errors", () => {
|
|
60
|
+
expect(classifyErrorDefault("401 unauthorized")).toBe("UNAUTHORIZED");
|
|
61
|
+
expect(classifyErrorDefault("invalid api key")).toBe("UNAUTHORIZED");
|
|
62
|
+
expect(classifyErrorDefault("authentication failed")).toBe("UNAUTHORIZED");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("should classify FORBIDDEN errors", () => {
|
|
66
|
+
expect(classifyErrorDefault("403 forbidden")).toBe("FORBIDDEN");
|
|
67
|
+
expect(classifyErrorDefault("permission denied")).toBe("FORBIDDEN");
|
|
68
|
+
expect(classifyErrorDefault("access denied")).toBe("FORBIDDEN");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("should classify NOT_FOUND errors", () => {
|
|
72
|
+
expect(classifyErrorDefault("404 not found")).toBe("NOT_FOUND");
|
|
73
|
+
expect(classifyErrorDefault("no such file or directory")).toBe("NOT_FOUND");
|
|
74
|
+
expect(classifyErrorDefault("", "", 127)).toBe("NOT_FOUND");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("should classify TIMEOUT errors", () => {
|
|
78
|
+
expect(classifyErrorDefault("request timeout")).toBe("TIMEOUT");
|
|
79
|
+
expect(classifyErrorDefault("operation timed out")).toBe("TIMEOUT");
|
|
80
|
+
expect(classifyErrorDefault("deadline exceeded")).toBe("TIMEOUT");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("should classify CONTEXT_LENGTH errors", () => {
|
|
84
|
+
expect(classifyErrorDefault("context length exceeded")).toBe("CONTEXT_LENGTH");
|
|
85
|
+
expect(classifyErrorDefault("input too long")).toBe("CONTEXT_LENGTH");
|
|
86
|
+
expect(classifyErrorDefault("max tokens exceeded")).toBe("CONTEXT_LENGTH");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("should classify CONTENT_FILTER errors", () => {
|
|
90
|
+
expect(classifyErrorDefault("content filter triggered")).toBe("CONTENT_FILTER");
|
|
91
|
+
expect(classifyErrorDefault("request blocked by safety")).toBe("CONTENT_FILTER");
|
|
92
|
+
expect(classifyErrorDefault("policy violation")).toBe("CONTENT_FILTER");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("should classify INTERNAL errors", () => {
|
|
96
|
+
expect(classifyErrorDefault("500 internal server error")).toBe("INTERNAL");
|
|
97
|
+
expect(classifyErrorDefault("502 bad gateway")).toBe("INTERNAL");
|
|
98
|
+
expect(classifyErrorDefault("internal error occurred")).toBe("INTERNAL");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("should classify TRANSIENT errors", () => {
|
|
102
|
+
expect(classifyErrorDefault("connection refused")).toBe("TRANSIENT");
|
|
103
|
+
expect(classifyErrorDefault("econnrefused")).toBe("TRANSIENT");
|
|
104
|
+
expect(classifyErrorDefault("network error")).toBe("TRANSIENT");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should default to TRANSIENT for unmatched errors", () => {
|
|
108
|
+
expect(classifyErrorDefault("some random error")).toBe("TRANSIENT");
|
|
109
|
+
expect(classifyErrorDefault("")).toBe("TRANSIENT");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should combine stderr and stdout", () => {
|
|
113
|
+
expect(classifyErrorDefault("some stderr", "quota exceeded")).toBe("OUT_OF_CREDITS");
|
|
114
|
+
expect(classifyErrorDefault("rate limit", "some stdout")).toBe("RATE_LIMIT");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("should handle undefined values", () => {
|
|
118
|
+
expect(classifyErrorDefault(undefined as any, undefined as any)).toBe("TRANSIENT");
|
|
119
|
+
expect(classifyErrorDefault("", "")).toBe("TRANSIENT");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("mergeErrorPatterns", () => {
|
|
124
|
+
test("should merge provider patterns with defaults", () => {
|
|
125
|
+
const providerPatterns = {
|
|
126
|
+
OUT_OF_CREDITS: ["plan limit", "subscription required"],
|
|
127
|
+
RATE_LIMIT: ["throttled"],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const merged = mergeErrorPatterns(providerPatterns);
|
|
131
|
+
|
|
132
|
+
// Provider patterns should come first
|
|
133
|
+
expect(merged.OUT_OF_CREDITS[0]).toBe("plan limit");
|
|
134
|
+
expect(merged.OUT_OF_CREDITS[1]).toBe("subscription required");
|
|
135
|
+
// Default patterns should follow
|
|
136
|
+
expect(merged.OUT_OF_CREDITS).toContain("quota exceeded");
|
|
137
|
+
|
|
138
|
+
// RATE_LIMIT should have both
|
|
139
|
+
expect(merged.RATE_LIMIT[0]).toBe("throttled");
|
|
140
|
+
expect(merged.RATE_LIMIT).toContain("rate limit");
|
|
141
|
+
|
|
142
|
+
// Untouched kinds should remain default
|
|
143
|
+
expect(merged.TIMEOUT).toEqual(DEFAULT_ERROR_PATTERNS.TIMEOUT);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("should handle empty provider patterns", () => {
|
|
147
|
+
const merged = mergeErrorPatterns({});
|
|
148
|
+
expect(merged).toEqual(DEFAULT_ERROR_PATTERNS);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("createErrorClassifier", () => {
|
|
153
|
+
test("should create classifier with default patterns", () => {
|
|
154
|
+
const classify = createErrorClassifier();
|
|
155
|
+
|
|
156
|
+
expect(classify("quota exceeded")).toBe("OUT_OF_CREDITS");
|
|
157
|
+
expect(classify("rate limit")).toBe("RATE_LIMIT");
|
|
158
|
+
expect(classify("random error")).toBe("TRANSIENT");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("should create classifier with custom patterns", () => {
|
|
162
|
+
const classify = createErrorClassifier({
|
|
163
|
+
OUT_OF_CREDITS: ["custom credit error"],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(classify("custom credit error")).toBe("OUT_OF_CREDITS");
|
|
167
|
+
// Should still match default patterns
|
|
168
|
+
expect(classify("quota exceeded")).toBe("OUT_OF_CREDITS");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("should handle exit code 127 as NOT_FOUND", () => {
|
|
172
|
+
const classify = createErrorClassifier();
|
|
173
|
+
expect(classify("", "", 127)).toBe("NOT_FOUND");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("should ignore exit code when patterns match", () => {
|
|
177
|
+
const classify = createErrorClassifier();
|
|
178
|
+
expect(classify("rate limit", "", 127)).toBe("RATE_LIMIT");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
MemoryMonitor,
|
|
4
|
+
type MemoryStats,
|
|
5
|
+
type MemoryThresholds,
|
|
6
|
+
formatBytes,
|
|
7
|
+
memoryMonitor,
|
|
8
|
+
} from "../../memory-monitor";
|
|
9
|
+
|
|
10
|
+
// Mock process.memoryUsage
|
|
11
|
+
const mockMemoryUsage = mock(() => ({
|
|
12
|
+
heapUsed: 50 * 1024 * 1024, // 50MB
|
|
13
|
+
heapTotal: 100 * 1024 * 1024, // 100MB
|
|
14
|
+
external: 10 * 1024 * 1024, // 10MB
|
|
15
|
+
rss: 150 * 1024 * 1024, // 150MB
|
|
16
|
+
arrayBuffers: 5 * 1024 * 1024, // 5MB
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Mock console methods
|
|
20
|
+
const mockConsoleLog = mock();
|
|
21
|
+
const mockConsoleWarn = mock();
|
|
22
|
+
const mockConsoleError = mock();
|
|
23
|
+
|
|
24
|
+
describe("MemoryMonitor", () => {
|
|
25
|
+
let monitor: MemoryMonitor;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
// Reset singleton instance
|
|
29
|
+
(MemoryMonitor as any).instance = null;
|
|
30
|
+
monitor = MemoryMonitor.getInstance();
|
|
31
|
+
|
|
32
|
+
// Mock process.memoryUsage
|
|
33
|
+
(process as any).memoryUsage = mockMemoryUsage;
|
|
34
|
+
|
|
35
|
+
// Mock console methods
|
|
36
|
+
spyOn(console, "log").mockImplementation(mockConsoleLog);
|
|
37
|
+
spyOn(console, "warn").mockImplementation(mockConsoleWarn);
|
|
38
|
+
spyOn(console, "error").mockImplementation(mockConsoleError);
|
|
39
|
+
|
|
40
|
+
// Clear all mocks
|
|
41
|
+
mockMemoryUsage.mockClear();
|
|
42
|
+
mockConsoleLog.mockClear();
|
|
43
|
+
mockConsoleWarn.mockClear();
|
|
44
|
+
mockConsoleError.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// Stop monitoring if running
|
|
49
|
+
monitor.stopMonitoring();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("Singleton Pattern", () => {
|
|
53
|
+
test("should return the same instance", () => {
|
|
54
|
+
const instance1 = MemoryMonitor.getInstance();
|
|
55
|
+
const instance2 = MemoryMonitor.getInstance();
|
|
56
|
+
expect(instance1).toBe(instance2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("should export singleton instance", () => {
|
|
60
|
+
// Reset singleton and get fresh instance
|
|
61
|
+
(MemoryMonitor as any).instance = null;
|
|
62
|
+
const freshInstance = MemoryMonitor.getInstance();
|
|
63
|
+
expect(freshInstance).toBeInstanceOf(MemoryMonitor);
|
|
64
|
+
expect(freshInstance).toBe(MemoryMonitor.getInstance());
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("getMemoryStats", () => {
|
|
69
|
+
test("should return formatted memory statistics", () => {
|
|
70
|
+
const stats = monitor.getMemoryStats();
|
|
71
|
+
|
|
72
|
+
expect(stats).toEqual({
|
|
73
|
+
heapUsed: 50, // 50MB
|
|
74
|
+
heapTotal: 100, // 100MB
|
|
75
|
+
external: 10, // 10MB
|
|
76
|
+
rss: 150, // 150MB
|
|
77
|
+
arrayBuffers: 5, // 5MB
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(process.memoryUsage).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("should round values to nearest MB", () => {
|
|
84
|
+
spyOn(process, "memoryUsage").mockReturnValueOnce({
|
|
85
|
+
heapUsed: 50.7 * 1024 * 1024, // 50.7MB
|
|
86
|
+
heapTotal: 100.3 * 1024 * 1024, // 100.3MB
|
|
87
|
+
external: 10.9 * 1024 * 1024, // 10.9MB
|
|
88
|
+
rss: 150.1 * 1024 * 1024, // 150.1MB
|
|
89
|
+
arrayBuffers: 5.5 * 1024 * 1024, // 5.5MB
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const stats = monitor.getMemoryStats();
|
|
93
|
+
|
|
94
|
+
expect(stats).toEqual({
|
|
95
|
+
heapUsed: 51, // Rounded up
|
|
96
|
+
heapTotal: 100, // Rounded down
|
|
97
|
+
external: 11, // Rounded up
|
|
98
|
+
rss: 150, // Rounded down
|
|
99
|
+
arrayBuffers: 6, // Rounded up
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("checkMemoryLimit", () => {
|
|
105
|
+
test("should return true when usage is below threshold", () => {
|
|
106
|
+
// 50MB used / 100MB total = 50% < 85%
|
|
107
|
+
expect(monitor.checkMemoryLimit(0.85)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("should return false when usage exceeds threshold", () => {
|
|
111
|
+
// 50MB used / 100MB total = 50% < 30%
|
|
112
|
+
expect(monitor.checkMemoryLimit(0.3)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("should use default threshold of 0.85", () => {
|
|
116
|
+
expect(monitor.checkMemoryLimit()).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("getMemoryUsagePercent", () => {
|
|
121
|
+
test("should calculate and round memory usage percentage", () => {
|
|
122
|
+
// 50MB / 100MB = 50%
|
|
123
|
+
expect(monitor.getMemoryUsagePercent()).toBe(50);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should handle decimal percentages", () => {
|
|
127
|
+
spyOn(process, "memoryUsage").mockReturnValueOnce({
|
|
128
|
+
heapUsed: 33 * 1024 * 1024, // 33MB
|
|
129
|
+
heapTotal: 100 * 1024 * 1024, // 100MB
|
|
130
|
+
external: 0,
|
|
131
|
+
rss: 0,
|
|
132
|
+
arrayBuffers: 0,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 33MB / 100MB = 33%
|
|
136
|
+
expect(monitor.getMemoryUsagePercent()).toBe(33);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("formatMemoryStats", () => {
|
|
141
|
+
test("should format memory stats with provided stats", () => {
|
|
142
|
+
const customStats: MemoryStats = {
|
|
143
|
+
heapUsed: 25,
|
|
144
|
+
heapTotal: 50,
|
|
145
|
+
external: 5,
|
|
146
|
+
rss: 75,
|
|
147
|
+
arrayBuffers: 2,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
spyOn(monitor, "getMemoryUsagePercent").mockReturnValue(50);
|
|
151
|
+
|
|
152
|
+
const result = monitor.formatMemoryStats(customStats);
|
|
153
|
+
expect(result).toBe("Heap: 25MB/50MB (50%) | RSS: 75MB | Buffers: 2MB");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("should use current stats when none provided", () => {
|
|
157
|
+
spyOn(monitor, "getMemoryUsagePercent").mockReturnValue(50);
|
|
158
|
+
|
|
159
|
+
const result = monitor.formatMemoryStats();
|
|
160
|
+
expect(result).toBe("Heap: 50MB/100MB (50%) | RSS: 150MB | Buffers: 5MB");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("startMonitoring", () => {
|
|
165
|
+
test("should start periodic monitoring", () => {
|
|
166
|
+
monitor.startMonitoring(100);
|
|
167
|
+
|
|
168
|
+
expect(monitor.isMonitoring).toBe(true);
|
|
169
|
+
expect(monitor.intervalId).not.toBeNull();
|
|
170
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
171
|
+
"[MemoryMonitor] Started monitoring memory usage every 100ms",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should not start if already monitoring", () => {
|
|
176
|
+
monitor.startMonitoring(100);
|
|
177
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(
|
|
178
|
+
"[MemoryMonitor] Started monitoring memory usage every 100ms",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Try to start again
|
|
182
|
+
monitor.startMonitoring(100);
|
|
183
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(
|
|
184
|
+
"[MemoryMonitor] Already monitoring memory usage",
|
|
185
|
+
);
|
|
186
|
+
expect(mockConsoleLog).toHaveBeenCalledTimes(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("should log info level by default", async () => {
|
|
190
|
+
monitor.startMonitoring(50); // Short interval for testing
|
|
191
|
+
|
|
192
|
+
// Wait for at least one interval
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
194
|
+
|
|
195
|
+
monitor.stopMonitoring();
|
|
196
|
+
|
|
197
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(
|
|
198
|
+
"[MemoryMonitor] Heap: 50MB/100MB (50%) | RSS: 150MB | Buffers: 5MB",
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("should trigger warning alert when above warning threshold", async () => {
|
|
203
|
+
monitor.setThresholds({ warning: 0.3, critical: 0.9 }); // 30% warning
|
|
204
|
+
|
|
205
|
+
monitor.startMonitoring(50);
|
|
206
|
+
|
|
207
|
+
// Wait for monitoring to trigger
|
|
208
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
209
|
+
|
|
210
|
+
monitor.stopMonitoring();
|
|
211
|
+
|
|
212
|
+
expect(mockConsoleWarn).toHaveBeenCalledWith(
|
|
213
|
+
"[MemoryMonitor] WARNING: Heap: 50MB/100MB (50%) | RSS: 150MB | Buffers: 5MB",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("should trigger critical alert when above critical threshold", async () => {
|
|
218
|
+
monitor.setThresholds({ warning: 0.3, critical: 0.4 }); // 40% critical
|
|
219
|
+
|
|
220
|
+
monitor.startMonitoring(50);
|
|
221
|
+
|
|
222
|
+
// Wait for monitoring to trigger
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
224
|
+
|
|
225
|
+
monitor.stopMonitoring();
|
|
226
|
+
|
|
227
|
+
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
228
|
+
"[MemoryMonitor] CRITICAL: Heap: 50MB/100MB (50%) | RSS: 150MB | Buffers: 5MB",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("stopMonitoring", () => {
|
|
234
|
+
test("should stop monitoring and clear interval", () => {
|
|
235
|
+
monitor.startMonitoring(100);
|
|
236
|
+
expect(monitor.isMonitoring).toBe(true);
|
|
237
|
+
|
|
238
|
+
monitor.stopMonitoring();
|
|
239
|
+
|
|
240
|
+
expect(monitor.isMonitoring).toBe(false);
|
|
241
|
+
expect(monitor.intervalId).toBeNull();
|
|
242
|
+
expect(console.log).toHaveBeenCalledWith("[MemoryMonitor] Stopped monitoring");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("should handle stopping when not monitoring", () => {
|
|
246
|
+
monitor.stopMonitoring();
|
|
247
|
+
|
|
248
|
+
expect(console.log).toHaveBeenCalledWith("[MemoryMonitor] Stopped monitoring");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("setThresholds", () => {
|
|
253
|
+
test("should update memory thresholds", () => {
|
|
254
|
+
const newThresholds: MemoryThresholds = { warning: 0.7, critical: 0.85 };
|
|
255
|
+
|
|
256
|
+
monitor.setThresholds(newThresholds);
|
|
257
|
+
|
|
258
|
+
expect(monitor.thresholds).toEqual(newThresholds);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("forceGC", () => {
|
|
263
|
+
test("should trigger garbage collection when available", () => {
|
|
264
|
+
(global as any).gc = mock();
|
|
265
|
+
|
|
266
|
+
monitor.forceGC();
|
|
267
|
+
|
|
268
|
+
expect((global as any).gc).toHaveBeenCalledTimes(1);
|
|
269
|
+
expect(console.log).toHaveBeenCalledWith("[MemoryMonitor] Garbage collection triggered");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("should log when GC is not available", () => {
|
|
273
|
+
(global as any).gc = undefined;
|
|
274
|
+
|
|
275
|
+
monitor.forceGC();
|
|
276
|
+
|
|
277
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
278
|
+
"[MemoryMonitor] Garbage collection not available (run with --expose-gc flag)",
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("logMemoryReport", () => {
|
|
284
|
+
test("should log detailed memory report", () => {
|
|
285
|
+
spyOn(monitor, "getMemoryUsagePercent").mockReturnValue(50);
|
|
286
|
+
|
|
287
|
+
monitor.logMemoryReport();
|
|
288
|
+
|
|
289
|
+
expect(console.log).toHaveBeenCalledWith("[MemoryMonitor] === MEMORY REPORT ===");
|
|
290
|
+
expect(console.log).toHaveBeenCalledWith("Heap Used: 50MB");
|
|
291
|
+
expect(console.log).toHaveBeenCalledWith("Heap Total: 100MB");
|
|
292
|
+
expect(console.log).toHaveBeenCalledWith("RSS: 150MB");
|
|
293
|
+
expect(console.log).toHaveBeenCalledWith("External: 10MB");
|
|
294
|
+
expect(console.log).toHaveBeenCalledWith("Array Buffers: 5MB");
|
|
295
|
+
expect(console.log).toHaveBeenCalledWith("Usage: 50%");
|
|
296
|
+
expect(console.log).toHaveBeenCalledWith("[MemoryMonitor] ====================");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("triggerMemoryAlert", () => {
|
|
301
|
+
test("should log critical alert message", () => {
|
|
302
|
+
const stats: MemoryStats = {
|
|
303
|
+
heapUsed: 90,
|
|
304
|
+
heapTotal: 100,
|
|
305
|
+
external: 10,
|
|
306
|
+
rss: 150,
|
|
307
|
+
arrayBuffers: 5,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
(monitor as any).triggerMemoryAlert("critical", stats);
|
|
311
|
+
|
|
312
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
313
|
+
"[MemoryMonitor] Memory usage critical! Consider:\n 1. Restarting the application\n 2. Reducing concurrent requests\n 3. Implementing streaming limits\n 4. Adding more memory monitoring",
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("should not log for warning alerts", () => {
|
|
318
|
+
const stats: MemoryStats = {
|
|
319
|
+
heapUsed: 80,
|
|
320
|
+
heapTotal: 100,
|
|
321
|
+
external: 10,
|
|
322
|
+
rss: 150,
|
|
323
|
+
arrayBuffers: 5,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
(monitor as any).triggerMemoryAlert("warning", stats);
|
|
327
|
+
|
|
328
|
+
expect(mockConsoleError).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("formatBytes", () => {
|
|
334
|
+
test("should format bytes correctly", () => {
|
|
335
|
+
expect(formatBytes(0)).toBe("0 Bytes");
|
|
336
|
+
expect(formatBytes(1024)).toBe("1 KB");
|
|
337
|
+
expect(formatBytes(1024 * 1024)).toBe("1 MB");
|
|
338
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe("1 GB");
|
|
339
|
+
expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe("1 TB");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("should handle decimal places", () => {
|
|
343
|
+
expect(formatBytes(1536, 1)).toBe("1.5 KB");
|
|
344
|
+
expect(formatBytes(1536)).toBe("1.5 KB");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("should handle negative decimals", () => {
|
|
348
|
+
expect(formatBytes(1536, -1)).toBe("2 KB");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("should handle large numbers", () => {
|
|
352
|
+
expect(formatBytes(1024 * 1024 * 1024 * 1024 * 2)).toBe("2 TB");
|
|
353
|
+
});
|
|
354
|
+
});
|