@lobu/core 3.0.13 → 3.0.19
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/dist/__tests__/fixtures/mock-redis.d.ts +3 -0
- package/dist/__tests__/fixtures/mock-redis.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-redis.js +12 -0
- package/dist/__tests__/fixtures/mock-redis.js.map +1 -1
- package/dist/__tests__/secret-refs.test.d.ts +2 -0
- package/dist/__tests__/secret-refs.test.d.ts.map +1 -0
- package/dist/__tests__/secret-refs.test.js +29 -0
- package/dist/__tests__/secret-refs.test.js.map +1 -0
- package/dist/agent-store.d.ts +33 -6
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js.map +1 -1
- package/dist/api-types.d.ts +1 -5
- package/dist/api-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/integration-types.d.ts +9 -1
- package/dist/integration-types.d.ts.map +1 -1
- package/dist/lobu-toml-schema.d.ts +168 -0
- package/dist/lobu-toml-schema.d.ts.map +1 -0
- package/dist/lobu-toml-schema.js +108 -0
- package/dist/lobu-toml-schema.js.map +1 -0
- package/dist/secret-refs.d.ts +11 -0
- package/dist/secret-refs.d.ts.map +1 -0
- package/dist/secret-refs.js +31 -0
- package/dist/secret-refs.js.map +1 -0
- package/dist/types.d.ts +29 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -3
- package/dist/types.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/encryption.test.ts +0 -103
- package/src/__tests__/fixtures/factories.ts +0 -76
- package/src/__tests__/fixtures/index.ts +0 -9
- package/src/__tests__/fixtures/mock-fetch.ts +0 -32
- package/src/__tests__/fixtures/mock-queue.ts +0 -50
- package/src/__tests__/fixtures/mock-redis.ts +0 -300
- package/src/__tests__/retry.test.ts +0 -134
- package/src/__tests__/sanitize.test.ts +0 -158
- package/src/agent-policy.ts +0 -207
- package/src/agent-store.ts +0 -220
- package/src/api-types.ts +0 -256
- package/src/command-registry.ts +0 -73
- package/src/constants.ts +0 -60
- package/src/errors.ts +0 -220
- package/src/index.ts +0 -131
- package/src/integration-types.ts +0 -26
- package/src/logger.ts +0 -248
- package/src/modules.ts +0 -184
- package/src/otel.ts +0 -307
- package/src/plugin-types.ts +0 -46
- package/src/provider-config-types.ts +0 -54
- package/src/redis/base-store.ts +0 -200
- package/src/sentry.ts +0 -56
- package/src/trace.ts +0 -32
- package/src/types.ts +0 -440
- package/src/utils/encryption.ts +0 -78
- package/src/utils/env.ts +0 -50
- package/src/utils/json.ts +0 -37
- package/src/utils/lock.ts +0 -75
- package/src/utils/mcp-tool-instructions.ts +0 -5
- package/src/utils/retry.ts +0 -91
- package/src/utils/sanitize.ts +0 -127
- package/src/worker/auth.ts +0 -100
- package/src/worker/transport.ts +0 -107
- package/tsconfig.json +0 -20
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified in-memory Redis mock for testing.
|
|
3
|
-
*
|
|
4
|
-
* Replaces three duplicated implementations:
|
|
5
|
-
* - MockRedisClient in gateway setup.ts
|
|
6
|
-
* - FakeRedis in system-message-limiter.test.ts
|
|
7
|
-
* - queue mock getRedisClient
|
|
8
|
-
*
|
|
9
|
-
* Supports string, set, list, and hash operations with TTL tracking.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
type SetMode = "NX" | undefined;
|
|
13
|
-
|
|
14
|
-
export class MockRedisClient {
|
|
15
|
-
private store = new Map<string, { value: string; ttl?: number }>();
|
|
16
|
-
private sets = new Map<string, Set<string>>();
|
|
17
|
-
private lists = new Map<string, string[]>();
|
|
18
|
-
private currentTime = Date.now();
|
|
19
|
-
|
|
20
|
-
// --- String operations ---
|
|
21
|
-
|
|
22
|
-
async exists(key: string): Promise<number> {
|
|
23
|
-
const entry = this.store.get(key);
|
|
24
|
-
if (!entry) return 0;
|
|
25
|
-
if (entry.ttl && entry.ttl < this.currentTime) {
|
|
26
|
-
this.store.delete(key);
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
return 1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async get(key: string): Promise<string | null> {
|
|
33
|
-
const entry = this.store.get(key);
|
|
34
|
-
if (!entry) return null;
|
|
35
|
-
if (entry.ttl && entry.ttl < this.currentTime) {
|
|
36
|
-
this.store.delete(key);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
return entry.value;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* ioredis-compatible set with optional EX / NX flags.
|
|
44
|
-
* Supports: set(key, value), set(key, value, "EX", sec), set(key, value, "EX", sec, "NX")
|
|
45
|
-
*/
|
|
46
|
-
async set(
|
|
47
|
-
key: string,
|
|
48
|
-
value: string,
|
|
49
|
-
exTokenOrTtl?: "EX" | number,
|
|
50
|
-
exSeconds?: number,
|
|
51
|
-
mode?: SetMode
|
|
52
|
-
): Promise<"OK" | null> {
|
|
53
|
-
// NX check must be synchronous to avoid TOCTOU race
|
|
54
|
-
if (mode === "NX") {
|
|
55
|
-
const entry = this.store.get(key);
|
|
56
|
-
const alive = entry && (!entry.ttl || entry.ttl >= this.currentTime);
|
|
57
|
-
if (alive) return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let ttl: number | undefined;
|
|
61
|
-
if (exTokenOrTtl === "EX" && typeof exSeconds === "number") {
|
|
62
|
-
ttl = this.currentTime + exSeconds * 1000;
|
|
63
|
-
} else if (typeof exTokenOrTtl === "number") {
|
|
64
|
-
ttl = this.currentTime + exTokenOrTtl * 1000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.store.set(key, { value, ttl });
|
|
68
|
-
return "OK";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async setex(key: string, ttlSeconds: number, value: string): Promise<void> {
|
|
72
|
-
const ttl = this.currentTime + ttlSeconds * 1000;
|
|
73
|
-
this.store.set(key, { value, ttl });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async incr(key: string): Promise<number> {
|
|
77
|
-
const current = await this.get(key);
|
|
78
|
-
const nextValue = (current ? Number.parseInt(current, 10) : 0) + 1;
|
|
79
|
-
const entry = this.store.get(key);
|
|
80
|
-
this.store.set(key, {
|
|
81
|
-
value: String(nextValue),
|
|
82
|
-
ttl: entry?.ttl,
|
|
83
|
-
});
|
|
84
|
-
return nextValue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async del(...keys: string[]): Promise<number> {
|
|
88
|
-
let deleted = 0;
|
|
89
|
-
for (const key of keys) {
|
|
90
|
-
const existed =
|
|
91
|
-
this.store.has(key) || this.sets.has(key) || this.lists.has(key);
|
|
92
|
-
this.store.delete(key);
|
|
93
|
-
this.sets.delete(key);
|
|
94
|
-
this.lists.delete(key);
|
|
95
|
-
if (existed) deleted++;
|
|
96
|
-
}
|
|
97
|
-
return deleted;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async getdel(key: string): Promise<string | null> {
|
|
101
|
-
const value = await this.get(key);
|
|
102
|
-
if (value !== null) {
|
|
103
|
-
await this.del(key);
|
|
104
|
-
}
|
|
105
|
-
return value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async expire(key: string, seconds: number): Promise<number> {
|
|
109
|
-
if (this.store.has(key)) {
|
|
110
|
-
const entry = this.store.get(key)!;
|
|
111
|
-
entry.ttl = this.currentTime + seconds * 1000;
|
|
112
|
-
return 1;
|
|
113
|
-
}
|
|
114
|
-
return 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async ttl(key: string): Promise<number> {
|
|
118
|
-
const entry = this.store.get(key);
|
|
119
|
-
if (!entry) return -2;
|
|
120
|
-
if (!entry.ttl) return -1;
|
|
121
|
-
if (entry.ttl < this.currentTime) {
|
|
122
|
-
this.store.delete(key);
|
|
123
|
-
return -2;
|
|
124
|
-
}
|
|
125
|
-
return Math.max(0, Math.ceil((entry.ttl - this.currentTime) / 1000));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --- Set operations ---
|
|
129
|
-
|
|
130
|
-
async sadd(key: string, ...members: string[]): Promise<number> {
|
|
131
|
-
if (!this.sets.has(key)) this.sets.set(key, new Set());
|
|
132
|
-
const set = this.sets.get(key)!;
|
|
133
|
-
let added = 0;
|
|
134
|
-
for (const m of members) {
|
|
135
|
-
if (!set.has(m)) {
|
|
136
|
-
set.add(m);
|
|
137
|
-
added++;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return added;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async srem(key: string, ...members: string[]): Promise<number> {
|
|
144
|
-
const set = this.sets.get(key);
|
|
145
|
-
if (!set) return 0;
|
|
146
|
-
let removed = 0;
|
|
147
|
-
for (const m of members) {
|
|
148
|
-
if (set.delete(m)) removed++;
|
|
149
|
-
}
|
|
150
|
-
return removed;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async smembers(key: string): Promise<string[]> {
|
|
154
|
-
const set = this.sets.get(key);
|
|
155
|
-
return set ? Array.from(set) : [];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async sismember(key: string, member: string): Promise<number> {
|
|
159
|
-
const set = this.sets.get(key);
|
|
160
|
-
return set?.has(member) ? 1 : 0;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// --- List operations ---
|
|
164
|
-
|
|
165
|
-
async rpush(key: string, ...values: string[]): Promise<number> {
|
|
166
|
-
if (!this.lists.has(key)) this.lists.set(key, []);
|
|
167
|
-
const list = this.lists.get(key)!;
|
|
168
|
-
list.push(...values);
|
|
169
|
-
return list.length;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async lrange(key: string, start: number, stop: number): Promise<string[]> {
|
|
173
|
-
const list = this.lists.get(key);
|
|
174
|
-
if (!list) return [];
|
|
175
|
-
const end = stop === -1 ? list.length : stop + 1;
|
|
176
|
-
return list.slice(start, end);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// --- Scan ---
|
|
180
|
-
|
|
181
|
-
async scan(
|
|
182
|
-
_cursor: string,
|
|
183
|
-
...args: (string | number)[]
|
|
184
|
-
): Promise<[string, string[]]> {
|
|
185
|
-
// Extract pattern from args: "MATCH", pattern, "COUNT", count
|
|
186
|
-
let pattern = "*";
|
|
187
|
-
for (let i = 0; i < args.length - 1; i++) {
|
|
188
|
-
if (String(args[i]).toUpperCase() === "MATCH") {
|
|
189
|
-
pattern = String(args[i + 1]);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const allKeys = new Set<string>();
|
|
195
|
-
for (const key of this.store.keys()) allKeys.add(key);
|
|
196
|
-
for (const key of this.sets.keys()) allKeys.add(key);
|
|
197
|
-
for (const key of this.lists.keys()) allKeys.add(key);
|
|
198
|
-
|
|
199
|
-
const matching: string[] = [];
|
|
200
|
-
for (const key of allKeys) {
|
|
201
|
-
if (pattern === "*") {
|
|
202
|
-
matching.push(key);
|
|
203
|
-
} else if (pattern.endsWith("*")) {
|
|
204
|
-
const prefix = pattern.slice(0, -1);
|
|
205
|
-
if (key.startsWith(prefix)) matching.push(key);
|
|
206
|
-
} else if (key === pattern) {
|
|
207
|
-
matching.push(key);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return ["0", matching];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// --- Batch get ---
|
|
215
|
-
|
|
216
|
-
async mget(...keys: string[]): Promise<(string | null)[]> {
|
|
217
|
-
return Promise.all(keys.map((key) => this.get(key)));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// --- Watch / Unwatch (no-ops) ---
|
|
221
|
-
|
|
222
|
-
async watch(..._keys: string[]): Promise<"OK"> {
|
|
223
|
-
return "OK";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async unwatch(): Promise<"OK"> {
|
|
227
|
-
return "OK";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// --- Multi ---
|
|
231
|
-
|
|
232
|
-
multi(): {
|
|
233
|
-
set(key: string, value: string): any;
|
|
234
|
-
exec(): Promise<[null, string][]>;
|
|
235
|
-
} {
|
|
236
|
-
const pending: Array<{ key: string; value: string }> = [];
|
|
237
|
-
const self = this;
|
|
238
|
-
const chain = {
|
|
239
|
-
set(key: string, value: string) {
|
|
240
|
-
pending.push({ key, value });
|
|
241
|
-
return chain;
|
|
242
|
-
},
|
|
243
|
-
async exec(): Promise<[null, string][]> {
|
|
244
|
-
const results: [null, string][] = [];
|
|
245
|
-
for (const op of pending) {
|
|
246
|
-
self.store.set(op.key, { value: op.value });
|
|
247
|
-
results.push([null, "OK"]);
|
|
248
|
-
}
|
|
249
|
-
return results;
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
return chain;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- Pipeline ---
|
|
256
|
-
|
|
257
|
-
pipeline(): {
|
|
258
|
-
setex(key: string, ttl: number, value: string): any;
|
|
259
|
-
del(...keys: string[]): any;
|
|
260
|
-
exec(): Promise<Array<[null, any]>>;
|
|
261
|
-
} {
|
|
262
|
-
const ops: Array<() => Promise<any>> = [];
|
|
263
|
-
const self = this;
|
|
264
|
-
const chain = {
|
|
265
|
-
setex(key: string, ttl: number, value: string) {
|
|
266
|
-
ops.push(() => self.setex(key, ttl, value));
|
|
267
|
-
return chain;
|
|
268
|
-
},
|
|
269
|
-
del(...keys: string[]) {
|
|
270
|
-
ops.push(() => self.del(...keys));
|
|
271
|
-
return chain;
|
|
272
|
-
},
|
|
273
|
-
async exec(): Promise<Array<[null, any]>> {
|
|
274
|
-
const results: Array<[null, any]> = [];
|
|
275
|
-
for (const op of ops) {
|
|
276
|
-
const result = await op();
|
|
277
|
-
results.push([null, result ?? "OK"]);
|
|
278
|
-
}
|
|
279
|
-
return results;
|
|
280
|
-
},
|
|
281
|
-
};
|
|
282
|
-
return chain;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// --- Test helpers ---
|
|
286
|
-
|
|
287
|
-
advanceTime(ms: number): void {
|
|
288
|
-
this.currentTime += ms;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
has(key: string): boolean {
|
|
292
|
-
return this.store.has(key);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
clear(): void {
|
|
296
|
-
this.store.clear();
|
|
297
|
-
this.sets.clear();
|
|
298
|
-
this.lists.clear();
|
|
299
|
-
}
|
|
300
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { retryWithBackoff } from "../utils/retry";
|
|
3
|
-
|
|
4
|
-
describe("retryWithBackoff", () => {
|
|
5
|
-
test("returns result on first success", async () => {
|
|
6
|
-
const fn = mock(() => Promise.resolve("ok"));
|
|
7
|
-
const result = await retryWithBackoff(fn);
|
|
8
|
-
expect(result).toBe("ok");
|
|
9
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("retries on failure and returns on eventual success", async () => {
|
|
13
|
-
let attempt = 0;
|
|
14
|
-
const fn = mock(async () => {
|
|
15
|
-
attempt++;
|
|
16
|
-
if (attempt < 3) throw new Error(`fail ${attempt}`);
|
|
17
|
-
return "success";
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const result = await retryWithBackoff(fn, {
|
|
21
|
-
maxRetries: 3,
|
|
22
|
-
baseDelay: 0,
|
|
23
|
-
});
|
|
24
|
-
expect(result).toBe("success");
|
|
25
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("throws last error when all retries exhausted", async () => {
|
|
29
|
-
const fn = mock(async () => {
|
|
30
|
-
throw new Error("always fails");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await expect(
|
|
34
|
-
retryWithBackoff(fn, { maxRetries: 2, baseDelay: 0 })
|
|
35
|
-
).rejects.toThrow("always fails");
|
|
36
|
-
// 1 initial + 2 retries = 3 calls
|
|
37
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("maxRetries=0 means single attempt, no retries", async () => {
|
|
41
|
-
const fn = mock(async () => {
|
|
42
|
-
throw new Error("fail");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
await expect(
|
|
46
|
-
retryWithBackoff(fn, { maxRetries: 0, baseDelay: 0 })
|
|
47
|
-
).rejects.toThrow("fail");
|
|
48
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("calls onRetry callback with attempt number and error", async () => {
|
|
52
|
-
let attempt = 0;
|
|
53
|
-
const fn = async () => {
|
|
54
|
-
attempt++;
|
|
55
|
-
if (attempt < 3) throw new Error(`err-${attempt}`);
|
|
56
|
-
return "done";
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const retries: { attempt: number; message: string }[] = [];
|
|
60
|
-
await retryWithBackoff(fn, {
|
|
61
|
-
maxRetries: 3,
|
|
62
|
-
baseDelay: 0,
|
|
63
|
-
onRetry: (attempt, error) => {
|
|
64
|
-
retries.push({ attempt, message: error.message });
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
expect(retries).toEqual([
|
|
69
|
-
{ attempt: 1, message: "err-1" },
|
|
70
|
-
{ attempt: 2, message: "err-2" },
|
|
71
|
-
]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("uses defaults when no options provided", async () => {
|
|
75
|
-
const fn = mock(() => Promise.resolve(42));
|
|
76
|
-
const result = await retryWithBackoff(fn);
|
|
77
|
-
expect(result).toBe(42);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("linear strategy increases delay linearly", async () => {
|
|
81
|
-
const delays: number[] = [];
|
|
82
|
-
const originalSetTimeout = globalThis.setTimeout;
|
|
83
|
-
|
|
84
|
-
// Patch setTimeout to capture delays (but resolve immediately)
|
|
85
|
-
globalThis.setTimeout = ((fn: () => void, ms: number) => {
|
|
86
|
-
delays.push(ms);
|
|
87
|
-
return originalSetTimeout(fn, 0);
|
|
88
|
-
}) as any;
|
|
89
|
-
|
|
90
|
-
let attempt = 0;
|
|
91
|
-
try {
|
|
92
|
-
await retryWithBackoff(
|
|
93
|
-
async () => {
|
|
94
|
-
attempt++;
|
|
95
|
-
if (attempt <= 3) throw new Error("fail");
|
|
96
|
-
return "ok";
|
|
97
|
-
},
|
|
98
|
-
{ maxRetries: 3, baseDelay: 100, strategy: "linear" }
|
|
99
|
-
);
|
|
100
|
-
} finally {
|
|
101
|
-
globalThis.setTimeout = originalSetTimeout;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Linear: 100*(0+1)=100, 100*(1+1)=200, 100*(2+1)=300
|
|
105
|
-
expect(delays).toEqual([100, 200, 300]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("exponential strategy doubles delay", async () => {
|
|
109
|
-
const delays: number[] = [];
|
|
110
|
-
const originalSetTimeout = globalThis.setTimeout;
|
|
111
|
-
|
|
112
|
-
globalThis.setTimeout = ((fn: () => void, ms: number) => {
|
|
113
|
-
delays.push(ms);
|
|
114
|
-
return originalSetTimeout(fn, 0);
|
|
115
|
-
}) as any;
|
|
116
|
-
|
|
117
|
-
let attempt = 0;
|
|
118
|
-
try {
|
|
119
|
-
await retryWithBackoff(
|
|
120
|
-
async () => {
|
|
121
|
-
attempt++;
|
|
122
|
-
if (attempt <= 3) throw new Error("fail");
|
|
123
|
-
return "ok";
|
|
124
|
-
},
|
|
125
|
-
{ maxRetries: 3, baseDelay: 100, strategy: "exponential" }
|
|
126
|
-
);
|
|
127
|
-
} finally {
|
|
128
|
-
globalThis.setTimeout = originalSetTimeout;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Exponential: 100*2^0=100, 100*2^1=200, 100*2^2=400
|
|
132
|
-
expect(delays).toEqual([100, 200, 400]);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
sanitizeConversationId,
|
|
4
|
-
sanitizeFilename,
|
|
5
|
-
sanitizeForLogging,
|
|
6
|
-
} from "../utils/sanitize";
|
|
7
|
-
|
|
8
|
-
describe("sanitizeFilename", () => {
|
|
9
|
-
test("removes path traversal (strips to basename)", () => {
|
|
10
|
-
// The regex strips everything up to and including the last / or \
|
|
11
|
-
expect(sanitizeFilename("../../etc/passwd")).toBe("passwd");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("removes windows path traversal (strips to basename)", () => {
|
|
15
|
-
expect(sanitizeFilename("..\\..\\windows\\system32")).toBe("system32");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("removes special characters", () => {
|
|
19
|
-
expect(sanitizeFilename('file<>|*?:"name.txt')).toBe("file_______name.txt");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("removes leading dots (hidden files)", () => {
|
|
23
|
-
expect(sanitizeFilename(".hidden")).toBe("hidden");
|
|
24
|
-
expect(sanitizeFilename("...secret")).toBe("secret");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("collapses consecutive dots", () => {
|
|
28
|
-
expect(sanitizeFilename("file..name..txt")).toBe("file.name.txt");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("returns unnamed_file for empty result", () => {
|
|
32
|
-
expect(sanitizeFilename("")).toBe("unnamed_file");
|
|
33
|
-
expect(sanitizeFilename("...")).toBe("unnamed_file");
|
|
34
|
-
expect(sanitizeFilename("///")).toBe("unnamed_file");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("preserves safe filenames", () => {
|
|
38
|
-
expect(sanitizeFilename("document.pdf")).toBe("document.pdf");
|
|
39
|
-
expect(sanitizeFilename("my-file_v2.tar.gz")).toBe("my-file_v2.tar.gz");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("truncates to maxLength", () => {
|
|
43
|
-
const long = "a".repeat(300);
|
|
44
|
-
expect(sanitizeFilename(long).length).toBe(255);
|
|
45
|
-
expect(sanitizeFilename(long, 10).length).toBe(10);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("preserves spaces", () => {
|
|
49
|
-
expect(sanitizeFilename("my file name.txt")).toBe("my file name.txt");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("strips directory path components", () => {
|
|
53
|
-
expect(sanitizeFilename("/path/to/file.txt")).toBe("file.txt");
|
|
54
|
-
expect(sanitizeFilename("C:\\Users\\doc.pdf")).toBe("doc.pdf");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("sanitizeConversationId", () => {
|
|
59
|
-
test("preserves valid conversation IDs", () => {
|
|
60
|
-
expect(sanitizeConversationId("1756766056.836119")).toBe(
|
|
61
|
-
"1756766056.836119"
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("replaces slashes and special chars", () => {
|
|
66
|
-
// Only non-alphanumeric (except . and -) are replaced
|
|
67
|
-
expect(sanitizeConversationId("thread/123/../456")).toBe(
|
|
68
|
-
"thread_123_.._456"
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("preserves hyphens and dots", () => {
|
|
73
|
-
expect(sanitizeConversationId("abc-def.123")).toBe("abc-def.123");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("replaces colons and spaces", () => {
|
|
77
|
-
expect(sanitizeConversationId("a:b c")).toBe("a_b_c");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("sanitizeForLogging", () => {
|
|
82
|
-
test("redacts default sensitive keys (lowercase match)", () => {
|
|
83
|
-
const obj = {
|
|
84
|
-
// "token" is in the sensitive list and matches case-insensitively
|
|
85
|
-
token: "bearer-xyz",
|
|
86
|
-
// "password" matches
|
|
87
|
-
password: "secret123",
|
|
88
|
-
timeout: 5000,
|
|
89
|
-
};
|
|
90
|
-
const result = sanitizeForLogging(obj);
|
|
91
|
-
expect(result.token).toBe("[REDACTED:10]");
|
|
92
|
-
expect(result.password).toBe("[REDACTED:9]");
|
|
93
|
-
expect(result.timeout).toBe(5000);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("matches via includes (key containing sensitive substring)", () => {
|
|
97
|
-
// Object key "my_api_key_field" lowercased includes "api_key"
|
|
98
|
-
const obj = { my_api_key_field: "secret-value" };
|
|
99
|
-
const result = sanitizeForLogging(obj);
|
|
100
|
-
expect(result.my_api_key_field).toBe("[REDACTED:12]");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("redacts authorization header (case-insensitive key)", () => {
|
|
104
|
-
// "Authorization" lowered → "authorization" which includes "authorization"
|
|
105
|
-
const obj = { Authorization: "Bearer tok12" };
|
|
106
|
-
const result = sanitizeForLogging(obj);
|
|
107
|
-
expect(result.Authorization).toBe("[REDACTED:12]");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("recursively sanitizes nested objects", () => {
|
|
111
|
-
const obj = {
|
|
112
|
-
config: { password: "secret", port: 3000 },
|
|
113
|
-
};
|
|
114
|
-
const result = sanitizeForLogging(obj);
|
|
115
|
-
expect(result.config.password).toBe("[REDACTED:6]");
|
|
116
|
-
expect(result.config.port).toBe(3000);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("recursively sanitizes env key", () => {
|
|
120
|
-
const obj = { env: { TOKEN: "abc123" } };
|
|
121
|
-
const result = sanitizeForLogging(obj);
|
|
122
|
-
expect(result.env.TOKEN).toBe("[REDACTED:6]");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("handles arrays (recurses into elements)", () => {
|
|
126
|
-
const arr = [{ token: "secret" }, { name: "safe" }];
|
|
127
|
-
const result = sanitizeForLogging(arr);
|
|
128
|
-
expect(result[0].token).toBe("[REDACTED:6]");
|
|
129
|
-
expect(result[1].name).toBe("safe");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("handles additional sensitive keys", () => {
|
|
133
|
-
const obj = { customSecret: "hidden", name: "visible" };
|
|
134
|
-
const result = sanitizeForLogging(obj, ["customsecret"]);
|
|
135
|
-
expect(result.customSecret).toBe("[REDACTED:6]");
|
|
136
|
-
expect(result.name).toBe("visible");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("returns primitives unchanged", () => {
|
|
140
|
-
expect(sanitizeForLogging("string")).toBe("string");
|
|
141
|
-
expect(sanitizeForLogging(42)).toBe(42);
|
|
142
|
-
expect(sanitizeForLogging(null)).toBe(null);
|
|
143
|
-
expect(sanitizeForLogging(undefined)).toBe(undefined);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("does not mutate original object", () => {
|
|
147
|
-
const obj = { apiKey: "secret" };
|
|
148
|
-
sanitizeForLogging(obj);
|
|
149
|
-
expect(obj.apiKey).toBe("secret");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("only redacts string values of sensitive keys", () => {
|
|
153
|
-
const obj = { token: 12345 };
|
|
154
|
-
const result = sanitizeForLogging(obj);
|
|
155
|
-
// Non-string sensitive values are not redacted
|
|
156
|
-
expect(result.token).toBe(12345);
|
|
157
|
-
});
|
|
158
|
-
});
|