@ronkovic/aad 0.3.0 → 0.3.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.
@@ -0,0 +1,120 @@
1
+ import { describe, test, expect, mock, afterEach } from "bun:test";
2
+ import { createMemoryMonitor } from "../memory-monitor";
3
+ import type { MemoryStatus } from "../memory-check";
4
+
5
+ function createMockLogger() {
6
+ return {
7
+ warn: mock(() => {}),
8
+ info: mock(() => {}),
9
+ error: mock(() => {}),
10
+ debug: mock(() => {}),
11
+ child: mock(() => createMockLogger()),
12
+ } as any;
13
+ }
14
+
15
+ function makeStatus(freeGB: number): MemoryStatus {
16
+ return {
17
+ totalGB: 16,
18
+ usedGB: 16 - freeGB,
19
+ freeGB,
20
+ usedPercent: Math.round(((16 - freeGB) / 16) * 100),
21
+ isLowMemory: freeGB < 2,
22
+ };
23
+ }
24
+
25
+ describe("memory-monitor", () => {
26
+ let monitors: ReturnType<typeof createMemoryMonitor>[] = [];
27
+
28
+ afterEach(() => {
29
+ monitors.forEach((m) => m.stop());
30
+ monitors = [];
31
+ });
32
+
33
+ test("calls callback when memory is critical", async () => {
34
+ const callback = mock(() => {});
35
+ const getStatus = mock(async () => makeStatus(0.5));
36
+
37
+ const monitor = createMemoryMonitor({
38
+ logger: createMockLogger(),
39
+ checkIntervalMs: 10,
40
+ criticalFreeGB: 0.8,
41
+ _getMemoryStatus: getStatus,
42
+ });
43
+ monitors.push(monitor);
44
+
45
+ monitor.onCritical(callback);
46
+ monitor.start();
47
+
48
+ await new Promise((r) => setTimeout(r, 50));
49
+ monitor.stop();
50
+
51
+ expect(callback).toHaveBeenCalledTimes(1);
52
+ });
53
+
54
+ test("does not fire callback when memory is fine", async () => {
55
+ const callback = mock(() => {});
56
+ const getStatus = mock(async () => makeStatus(4.0));
57
+
58
+ const monitor = createMemoryMonitor({
59
+ logger: createMockLogger(),
60
+ checkIntervalMs: 10,
61
+ criticalFreeGB: 0.8,
62
+ _getMemoryStatus: getStatus,
63
+ });
64
+ monitors.push(monitor);
65
+
66
+ monitor.onCritical(callback);
67
+ monitor.start();
68
+
69
+ await new Promise((r) => setTimeout(r, 50));
70
+ monitor.stop();
71
+
72
+ expect(callback).not.toHaveBeenCalled();
73
+ });
74
+
75
+ test("stop clears the interval", async () => {
76
+ let callCount = 0;
77
+ const getStatus = mock(async () => {
78
+ callCount++;
79
+ return makeStatus(4.0);
80
+ });
81
+
82
+ const monitor = createMemoryMonitor({
83
+ logger: createMockLogger(),
84
+ checkIntervalMs: 10,
85
+ _getMemoryStatus: getStatus,
86
+ });
87
+ monitors.push(monitor);
88
+
89
+ monitor.start();
90
+ await new Promise((r) => setTimeout(r, 50));
91
+ monitor.stop();
92
+
93
+ const countAfterStop = callCount;
94
+ await new Promise((r) => setTimeout(r, 50));
95
+
96
+ // No new calls after stop
97
+ expect(callCount).toBe(countAfterStop);
98
+ });
99
+
100
+ test("fires callback only once", async () => {
101
+ const callback = mock(() => {});
102
+ const getStatus = mock(async () => makeStatus(0.3));
103
+
104
+ const monitor = createMemoryMonitor({
105
+ logger: createMockLogger(),
106
+ checkIntervalMs: 10,
107
+ criticalFreeGB: 0.8,
108
+ _getMemoryStatus: getStatus,
109
+ });
110
+ monitors.push(monitor);
111
+
112
+ monitor.onCritical(callback);
113
+ monitor.start();
114
+
115
+ await new Promise((r) => setTimeout(r, 80));
116
+ monitor.stop();
117
+
118
+ expect(callback).toHaveBeenCalledTimes(1);
119
+ });
120
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, test, expect, beforeEach, mock } from "bun:test";
2
+ import {
3
+ installShutdownHandler,
4
+ isShuttingDown,
5
+ _resetShutdownState,
6
+ } from "../shutdown-handler";
7
+ import type { TaskStore, RunStore } from "../../modules/persistence/stores.port";
8
+ import type { Task, RunState } from "../types";
9
+ import { createRunId, createTaskId } from "../types";
10
+
11
+ function createMockLogger() {
12
+ return {
13
+ warn: mock(() => {}),
14
+ info: mock(() => {}),
15
+ error: mock(() => {}),
16
+ debug: mock(() => {}),
17
+ child: mock(() => createMockLogger()),
18
+ } as any;
19
+ }
20
+
21
+ function createMockStores(tasks: Task[], runState: RunState | null) {
22
+ const savedTasks: Task[] = [];
23
+ let savedRun: RunState | null = null;
24
+
25
+ const taskStore: TaskStore = {
26
+ get: mock(async (id) => tasks.find((t) => t.taskId === id) ?? null),
27
+ getAll: mock(async () => tasks),
28
+ getByStatus: mock(async (status) => tasks.filter((t) => t.status === status)),
29
+ save: mock(async (task) => {
30
+ savedTasks.push(task);
31
+ // Update in-place for subsequent getAll calls
32
+ const idx = tasks.findIndex((t) => t.taskId === task.taskId);
33
+ if (idx >= 0) tasks[idx] = task;
34
+ }),
35
+ delete: mock(async () => {}),
36
+ };
37
+
38
+ const runStore: RunStore = {
39
+ get: mock(async () => runState),
40
+ getLatest: mock(async () => runState),
41
+ save: mock(async (state) => { savedRun = state; }),
42
+ };
43
+
44
+ return { taskStore, runStore, savedTasks, getSavedRun: () => savedRun };
45
+ }
46
+
47
+ const runId = createRunId("test-run-1");
48
+
49
+ function makeTask(id: string, status: "pending" | "running" | "completed" | "failed"): Task {
50
+ return {
51
+ taskId: createTaskId(id),
52
+ title: id,
53
+ description: "",
54
+ filesToModify: [],
55
+ dependsOn: [],
56
+ priority: 1,
57
+ status,
58
+ retryCount: 0,
59
+ };
60
+ }
61
+
62
+ describe("shutdown-handler", () => {
63
+ beforeEach(() => {
64
+ _resetShutdownState();
65
+ });
66
+
67
+ test("isShuttingDown returns false initially", () => {
68
+ expect(isShuttingDown()).toBe(false);
69
+ });
70
+
71
+ test("saves state on signal and sets shuttingDown", async () => {
72
+ const tasks = [
73
+ makeTask("t1", "completed"),
74
+ makeTask("t2", "running"),
75
+ makeTask("t3", "pending"),
76
+ ];
77
+ const runState: RunState = {
78
+ runId,
79
+ parentBranch: "main",
80
+ totalTasks: 3,
81
+ pending: 1,
82
+ running: 1,
83
+ completed: 1,
84
+ failed: 0,
85
+ startTime: new Date().toISOString(),
86
+ };
87
+
88
+ const exitFn = mock(() => {});
89
+ const stores = createMockStores(tasks, runState);
90
+ const logger = createMockLogger();
91
+
92
+ installShutdownHandler({
93
+ runId,
94
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
95
+ logger,
96
+ exitFn,
97
+ });
98
+
99
+ // Simulate SIGINT
100
+ process.emit("SIGINT");
101
+
102
+ // Wait for async handler
103
+ await new Promise((r) => setTimeout(r, 50));
104
+
105
+ expect(isShuttingDown()).toBe(true);
106
+ expect(exitFn).toHaveBeenCalledWith(1);
107
+ // Running task should be saved as pending
108
+ expect(stores.taskStore.save).toHaveBeenCalled();
109
+ expect(stores.runStore.save).toHaveBeenCalled();
110
+
111
+ const savedRun = stores.getSavedRun();
112
+ expect(savedRun).not.toBeNull();
113
+ expect(savedRun!.running).toBe(0);
114
+ });
115
+
116
+ test("debounces duplicate signals", async () => {
117
+ const tasks: Task[] = [];
118
+ const exitFn = mock(() => {});
119
+ const stores = createMockStores(tasks, null);
120
+ const logger = createMockLogger();
121
+
122
+ installShutdownHandler({
123
+ runId,
124
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
125
+ logger,
126
+ exitFn,
127
+ });
128
+
129
+ process.emit("SIGINT");
130
+ process.emit("SIGINT");
131
+ process.emit("SIGINT");
132
+
133
+ await new Promise((r) => setTimeout(r, 50));
134
+
135
+ // exitFn called only once due to debounce
136
+ expect(exitFn).toHaveBeenCalledTimes(1);
137
+ });
138
+ });
@@ -0,0 +1,294 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { capitalize, toLowerCase, trim, reverse, toUpperCase } from "../utils";
3
+
4
+ describe("capitalize", () => {
5
+ test.each([
6
+ ["hello", "Hello"],
7
+ ["world", "World"],
8
+ ["a", "A"],
9
+ ["test", "Test"],
10
+ ["capitalize", "Capitalize"],
11
+ ])("capitalizes '%s' to '%s'", (input, expected) => {
12
+ expect(capitalize(input)).toBe(expected);
13
+ });
14
+
15
+ test.each([
16
+ ["Hello", "Hello"],
17
+ ["WORLD", "WORLD"],
18
+ ["Already", "Already"],
19
+ ])("does not change '%s' when already capitalized", (input, expected) => {
20
+ expect(capitalize(input)).toBe(expected);
21
+ });
22
+
23
+ test("handles empty string", () => {
24
+ expect(capitalize("")).toBe("");
25
+ });
26
+
27
+ test.each([
28
+ ["123test", "123test"],
29
+ ["!hello", "!hello"],
30
+ [" space", " space"],
31
+ ])("handles string starting with non-letter: '%s'", (input, expected) => {
32
+ expect(capitalize(input)).toBe(expected);
33
+ });
34
+
35
+ test.each([
36
+ ["hello world", "Hello world"],
37
+ ["test string", "Test string"],
38
+ ])("only capitalizes first character: '%s' to '%s'", (input, expected) => {
39
+ expect(capitalize(input)).toBe(expected);
40
+ });
41
+ });
42
+
43
+ describe("toLowerCase", () => {
44
+ test.each([
45
+ ["HELLO", "hello"],
46
+ ["WORLD", "world"],
47
+ ["TEST", "test"],
48
+ ["UPPERCASE", "uppercase"],
49
+ ])("converts '%s' to '%s'", (input, expected) => {
50
+ expect(toLowerCase(input)).toBe(expected);
51
+ });
52
+
53
+ test("handles empty string", () => {
54
+ expect(toLowerCase("")).toBe("");
55
+ });
56
+
57
+ test.each([
58
+ ["hello", "hello"],
59
+ ["world", "world"],
60
+ ["lowercase", "lowercase"],
61
+ ])("does not change '%s' when already lowercase", (input, expected) => {
62
+ expect(toLowerCase(input)).toBe(expected);
63
+ });
64
+
65
+ test.each([
66
+ ["Hello World", "hello world"],
67
+ ["TeSt StRiNg", "test string"],
68
+ ["MiXeD CaSe", "mixed case"],
69
+ ])("converts mixed case '%s' to '%s'", (input, expected) => {
70
+ expect(toLowerCase(input)).toBe(expected);
71
+ });
72
+ });
73
+
74
+ describe("trim", () => {
75
+ test.each([
76
+ [" hello ", "hello"],
77
+ [" world ", "world"],
78
+ [" test", "test"],
79
+ ["test ", "test"],
80
+ [" spaces ", "spaces"],
81
+ ])("removes leading and trailing whitespace from '%s'", (input, expected) => {
82
+ expect(trim(input)).toBe(expected);
83
+ });
84
+
85
+ test.each([
86
+ ["\thello\t", "hello"],
87
+ ["\nworld\n", "world"],
88
+ [" \t\ntest\n\t ", "test"],
89
+ ])("removes various whitespace characters from '%s'", (input, expected) => {
90
+ expect(trim(input)).toBe(expected);
91
+ });
92
+
93
+ test.each([
94
+ ["hello world", "hello world"],
95
+ ["test string", "test string"],
96
+ ["multiple spaces", "multiple spaces"],
97
+ ])("preserves internal whitespace in '%s'", (input, expected) => {
98
+ expect(trim(input)).toBe(expected);
99
+ });
100
+
101
+ test("handles empty string", () => {
102
+ expect(trim("")).toBe("");
103
+ });
104
+
105
+ test.each([
106
+ ["nowhitespace", "nowhitespace"],
107
+ ["alreadytrimmed", "alreadytrimmed"],
108
+ ])("does not change '%s' when already trimmed", (input, expected) => {
109
+ expect(trim(input)).toBe(expected);
110
+ });
111
+ });
112
+
113
+ describe("reverse", () => {
114
+ test.each([
115
+ ["hello", "olleh"],
116
+ ["world", "dlrow"],
117
+ ["test", "tset"],
118
+ ["typescript", "tpircsepyt"],
119
+ ["reverse", "esrever"],
120
+ ])("reverses normal string '%s' to '%s'", (input, expected) => {
121
+ expect(reverse(input)).toBe(expected);
122
+ });
123
+
124
+ test("handles empty string", () => {
125
+ expect(reverse("")).toBe("");
126
+ });
127
+
128
+ test.each([
129
+ ["a", "a"],
130
+ ["b", "b"],
131
+ ["z", "z"],
132
+ ])("handles single character string '%s'", (input, expected) => {
133
+ expect(reverse(input)).toBe(expected);
134
+ });
135
+
136
+ test.each([
137
+ ["123", "321"],
138
+ ["456789", "987654"],
139
+ ["test123", "321tset"],
140
+ ["123test", "tset321"],
141
+ ["a1b2c3", "3c2b1a"],
142
+ ])("reverses string with numbers '%s' to '%s'", (input, expected) => {
143
+ expect(reverse(input)).toBe(expected);
144
+ });
145
+
146
+ test.each([
147
+ ["hello world", "dlrow olleh"],
148
+ ["test string", "gnirts tset"],
149
+ ["multiple spaces", "secaps elpitlum"],
150
+ [" leading", "gnidael "],
151
+ ["trailing ", " gniliart"],
152
+ ])("reverses string with spaces '%s' to '%s'", (input, expected) => {
153
+ expect(reverse(input)).toBe(expected);
154
+ });
155
+
156
+ test.each([
157
+ ["!hello", "olleh!"],
158
+ ["world?", "?dlrow"],
159
+ ["test@email.com", "moc.liame@tset"],
160
+ ["special#$%chars", "srahc%$#laiceps"],
161
+ ])("reverses string with special characters '%s' to '%s'", (input, expected) => {
162
+ expect(reverse(input)).toBe(expected);
163
+ });
164
+
165
+ test.each([
166
+ ["Hello", "olleH"],
167
+ ["WORLD", "DLROW"],
168
+ ["MiXeD", "DeXiM"],
169
+ ["CamelCase", "esaClemaC"],
170
+ ])("reverses string with mixed case '%s' to '%s'", (input, expected) => {
171
+ expect(reverse(input)).toBe(expected);
172
+ });
173
+
174
+ test.each([
175
+ ["こんにちは", "はちにんこ"],
176
+ ["你好", "好你"],
177
+ ["안녕", "녕안"],
178
+ ])("reverses string with multibyte characters '%s' to '%s'", (input, expected) => {
179
+ expect(reverse(input)).toBe(expected);
180
+ });
181
+
182
+ test.each([
183
+ ["\thello\t", "\tolleh\t"],
184
+ ["\nworld\n", "\ndlrow\n"],
185
+ ["test\r\n", "\n\rtset"],
186
+ ])("reverses string with whitespace characters '%s'", (input, expected) => {
187
+ expect(reverse(input)).toBe(expected);
188
+ });
189
+
190
+ test.each([
191
+ ["aba", "aba"],
192
+ ["racecar", "racecar"],
193
+ ["noon", "noon"],
194
+ ])("reverses palindrome '%s' to '%s'", (input, expected) => {
195
+ expect(reverse(input)).toBe(expected);
196
+ });
197
+ });
198
+
199
+ describe("toUpperCase", () => {
200
+ // Test case 1: Lowercase-only strings
201
+ test.each([
202
+ ["hello", "HELLO"],
203
+ ["world", "WORLD"],
204
+ ["test", "TEST"],
205
+ ["lowercase", "LOWERCASE"],
206
+ ["typescript", "TYPESCRIPT"],
207
+ ["function", "FUNCTION"],
208
+ ])("converts lowercase-only string '%s' to '%s'", (input, expected) => {
209
+ expect(toUpperCase(input)).toBe(expected);
210
+ });
211
+
212
+ // Test case 2: Strings with mixed uppercase and lowercase
213
+ test.each([
214
+ ["Hello World", "HELLO WORLD"],
215
+ ["TeSt StRiNg", "TEST STRING"],
216
+ ["MiXeD CaSe", "MIXED CASE"],
217
+ ["CamelCase", "CAMELCASE"],
218
+ ["snake_Case", "SNAKE_CASE"],
219
+ ])("converts mixed case string '%s' to '%s'", (input, expected) => {
220
+ expect(toUpperCase(input)).toBe(expected);
221
+ });
222
+
223
+ // Test case 3: Strings containing numbers
224
+ test.each([
225
+ ["123", "123"],
226
+ ["456789", "456789"],
227
+ ["test123", "TEST123"],
228
+ ["123test", "123TEST"],
229
+ ["a1b2c3", "A1B2C3"],
230
+ ["hello123world", "HELLO123WORLD"],
231
+ ])("converts string with numbers '%s' to '%s'", (input, expected) => {
232
+ expect(toUpperCase(input)).toBe(expected);
233
+ });
234
+
235
+ // Test case 4: Strings containing symbols
236
+ test.each([
237
+ ["!@#$%", "!@#$%"],
238
+ ["hello!", "HELLO!"],
239
+ ["test@email.com", "TEST@EMAIL.COM"],
240
+ ["special#$%chars", "SPECIAL#$%CHARS"],
241
+ ["under_score", "UNDER_SCORE"],
242
+ ["dash-case", "DASH-CASE"],
243
+ ])("converts string with symbols '%s' to '%s'", (input, expected) => {
244
+ expect(toUpperCase(input)).toBe(expected);
245
+ });
246
+
247
+ // Test case 5: Empty strings
248
+ test("handles empty string", () => {
249
+ expect(toUpperCase("")).toBe("");
250
+ });
251
+
252
+ // Test case 6: Already uppercase strings
253
+ test.each([
254
+ ["HELLO", "HELLO"],
255
+ ["WORLD", "WORLD"],
256
+ ["UPPERCASE", "UPPERCASE"],
257
+ ["ALREADY", "ALREADY"],
258
+ ["TEST", "TEST"],
259
+ ])("does not change '%s' when already uppercase", (input, expected) => {
260
+ expect(toUpperCase(input)).toBe(expected);
261
+ });
262
+
263
+ // Test case 7: Single character strings
264
+ test.each([
265
+ ["a", "A"],
266
+ ["z", "Z"],
267
+ ["m", "M"],
268
+ ["A", "A"],
269
+ ["Z", "Z"],
270
+ ])("converts single character '%s' to '%s'", (input, expected) => {
271
+ expect(toUpperCase(input)).toBe(expected);
272
+ });
273
+
274
+ // Test case 8: Strings with whitespace
275
+ test.each([
276
+ [" hello ", " HELLO "],
277
+ ["hello world", "HELLO WORLD"],
278
+ [" multiple spaces ", " MULTIPLE SPACES "],
279
+ ["\thello\t", "\tHELLO\t"],
280
+ ["\nworld\n", "\nWORLD\n"],
281
+ ])("converts string with whitespace '%s' to '%s'", (input, expected) => {
282
+ expect(toUpperCase(input)).toBe(expected);
283
+ });
284
+
285
+ // Test case 9: Complex mixed content
286
+ test.each([
287
+ ["Test123!@#", "TEST123!@#"],
288
+ ["hello_world_123", "HELLO_WORLD_123"],
289
+ ["email@test.com", "EMAIL@TEST.COM"],
290
+ ["complex MiXeD 123 !@#", "COMPLEX MIXED 123 !@#"],
291
+ ])("converts complex string '%s' to '%s'", (input, expected) => {
292
+ expect(toUpperCase(input)).toBe(expected);
293
+ });
294
+ });
@@ -120,6 +120,13 @@ export class TestRunnerError extends AadError {
120
120
  }
121
121
  }
122
122
 
123
+ export class MemoryError extends AadError {
124
+ constructor(message: string, context: Record<string, unknown> = {}) {
125
+ super("MEMORY_ERROR", message, context);
126
+ this.name = "MemoryError";
127
+ }
128
+ }
129
+
123
130
  export class MergeConflictError extends AadError {
124
131
  constructor(message: string, context: Record<string, unknown> = {}) {
125
132
  super("MERGE_CONFLICT_ERROR", message, context);