@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.
- package/README.md +16 -0
- package/package.json +2 -2
- package/src/main.ts +1 -1
- package/src/modules/claude-provider/claude-provider.port.ts +1 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +61 -4
- package/src/modules/cli/__tests__/run.test.ts +4 -0
- package/src/modules/cli/commands/run.ts +112 -6
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +103 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +83 -0
- package/src/modules/git-workspace/branch-manager.ts +8 -8
- package/src/modules/git-workspace/index.ts +1 -1
- package/src/modules/git-workspace/worktree-manager.ts +113 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +156 -3
- package/src/modules/planning/planning.service.ts +103 -51
- package/src/shared/__tests__/memory-check.test.ts +91 -0
- package/src/shared/__tests__/memory-monitor.test.ts +120 -0
- package/src/shared/__tests__/shutdown-handler.test.ts +138 -0
- package/src/shared/__tests__/utils.test.ts +294 -0
- package/src/shared/errors.ts +7 -0
- package/src/shared/memory-check.ts +155 -0
- package/src/shared/memory-monitor.ts +69 -0
- package/src/shared/shutdown-handler.ts +115 -0
- package/src/shared/utils.ts +49 -0
|
@@ -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
|
+
});
|
package/src/shared/errors.ts
CHANGED
|
@@ -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);
|