@wenathlan/getry 2.0.154 → 2.0.158
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/cli.ts +943 -0
- package/docs/features.md +104 -0
- package/docs/gateway.md +116 -0
- package/package.json +199 -16
- package/tests/cli.test.ts +300 -0
- package/tests/debonairlane.test.ts +66 -21
- package/tests/design.test.ts +9 -9
- package/tests/gateway.test.ts +962 -0
- package/tests/gatewayserver.ts +99 -0
- package/tests/intelligence.test.ts +62 -0
- package/tests/jsonpolicy.test.ts +289 -0
- package/tests/llm.test.ts +696 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/providercli.test.ts +84 -0
- package/tests/providerconstants.test.ts +134 -0
- package/tests/providerfingerprint.test.ts +149 -0
- package/tests/providerfixtures.ts +224 -0
- package/tests/providermodels.test.ts +250 -0
- package/tests/provideroauth.test.ts +150 -0
- package/tests/researchfixtures.ts +4572 -0
- package/tests/sandboxmodality.test.ts +4 -4
- package/tests/serverauth.test.ts +394 -0
- package/tests/serverengine.test.ts +1074 -0
- package/tests/serverproperty.test.ts +220 -0
- package/tests/serverutils.test.ts +546 -0
- package/tests/streamfallback.test.ts +5 -1
- package/tests/torturellm.test.ts +807 -0
- package/docs/.gitkeep +0 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils — aggressive edge case tests
|
|
3
|
+
* every helper is pushed to its bounds: malformed inputs, extreme numbers,
|
|
4
|
+
* adversarial strings, circular structures, unicode planes, concurrency
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
autofrequencypenalty,
|
|
11
|
+
automaxtokens,
|
|
12
|
+
auton,
|
|
13
|
+
autopresencepenalty,
|
|
14
|
+
autoseed,
|
|
15
|
+
autotemp,
|
|
16
|
+
autothinking,
|
|
17
|
+
autotopp,
|
|
18
|
+
clamp,
|
|
19
|
+
corsheaders,
|
|
20
|
+
ctjson,
|
|
21
|
+
ctndjson,
|
|
22
|
+
ctoctet,
|
|
23
|
+
ctplain,
|
|
24
|
+
defaultmaxtokens,
|
|
25
|
+
detectcontenttype,
|
|
26
|
+
esttokens,
|
|
27
|
+
genid,
|
|
28
|
+
getip,
|
|
29
|
+
handlecors,
|
|
30
|
+
jsonheaders,
|
|
31
|
+
levenshtein,
|
|
32
|
+
makethinker,
|
|
33
|
+
maxdurationconst,
|
|
34
|
+
randombase36,
|
|
35
|
+
safejsonparse,
|
|
36
|
+
safestringify,
|
|
37
|
+
securerandom,
|
|
38
|
+
thinkingbudget,
|
|
39
|
+
thinkingbudgets,
|
|
40
|
+
truncatemessages,
|
|
41
|
+
} from "../utils.js";
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function req(headers: Record<string, string>, method = "GET"): Request {
|
|
48
|
+
return new Request("http://x/api", { method, headers });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// clamp — the foundation of every sampling param guard
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe("clamp — extreme inputs", () => {
|
|
56
|
+
it("bounds normal numbers", () => {
|
|
57
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
58
|
+
expect(clamp(-5, 0, 10)).toBe(0);
|
|
59
|
+
expect(clamp(15, 0, 10)).toBe(10);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("rejects nan with the fallback", () => {
|
|
63
|
+
expect(clamp(Number.NaN, 0, 10)).toBe(0);
|
|
64
|
+
expect(clamp(Number.NaN, 0, 10, 7)).toBe(7);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("parses numeric strings", () => {
|
|
68
|
+
expect(clamp("0.5", 0, 10)).toBe(0.5);
|
|
69
|
+
expect(clamp("-3", 0, 10)).toBe(0);
|
|
70
|
+
expect(clamp("abc", 0, 10)).toBe(0);
|
|
71
|
+
expect(clamp("abc", 0, 10, 4)).toBe(4);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles infinity and beyond", () => {
|
|
75
|
+
expect(clamp(Number.POSITIVE_INFINITY, 0, 10)).toBe(10);
|
|
76
|
+
expect(clamp(Number.NEGATIVE_INFINITY, 0, 10)).toBe(0);
|
|
77
|
+
expect(clamp(1e308, 0, 10)).toBe(10);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles non numeric types via fallback", () => {
|
|
81
|
+
expect(clamp(null, 0, 10)).toBe(0);
|
|
82
|
+
expect(clamp(undefined, 0, 10)).toBe(0);
|
|
83
|
+
expect(clamp([], 0, 10, 3)).toBe(3);
|
|
84
|
+
expect(clamp({}, 0, 10, 3)).toBe(3);
|
|
85
|
+
expect(clamp(true, 0, 10, 3)).toBe(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("inverted bounds still clamp inside min max math", () => {
|
|
89
|
+
// max(min(10, 5), 0) — inverted bounds produce max bound
|
|
90
|
+
expect(clamp(5, 10, 0)).toBe(10);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("float precision at the boundaries", () => {
|
|
94
|
+
expect(clamp(0.1 + 0.2, 0, 0.3)).toBe(0.3);
|
|
95
|
+
expect(clamp(2 - 2, 0, 2)).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// sampling param guards — every auto* wrapper
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
describe("autotemp — temperature 0 to 2", () => {
|
|
104
|
+
it("clamps the valid range", () => {
|
|
105
|
+
expect(autotemp(0)).toBe(0);
|
|
106
|
+
expect(autotemp(1.5)).toBe(1.5);
|
|
107
|
+
expect(autotemp(2)).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
it("clamps out of range", () => {
|
|
110
|
+
expect(autotemp(3)).toBe(2);
|
|
111
|
+
expect(autotemp(-1)).toBe(0);
|
|
112
|
+
expect(autotemp(Number.NaN)).toBe(0.7);
|
|
113
|
+
expect(autotemp("hot")).toBe(0.7);
|
|
114
|
+
});
|
|
115
|
+
it("accepts numeric strings", () => {
|
|
116
|
+
expect(autotemp("0.5")).toBe(0.5);
|
|
117
|
+
expect(autotemp("2.5")).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("autotopp — top p 0 to 1", () => {
|
|
122
|
+
it("clamps the valid range", () => {
|
|
123
|
+
expect(autotopp(0)).toBe(0);
|
|
124
|
+
expect(autotopp(0.9)).toBe(0.9);
|
|
125
|
+
expect(autotopp(1)).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
it("clamps out of range", () => {
|
|
128
|
+
expect(autotopp(1.5)).toBe(1);
|
|
129
|
+
expect(autotopp(-0.5)).toBe(0);
|
|
130
|
+
expect(autotopp(Number.NaN)).toBe(0.9);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("auton — n 1 to 10", () => {
|
|
135
|
+
it("clamps the valid range", () => {
|
|
136
|
+
expect(auton(1)).toBe(1);
|
|
137
|
+
expect(auton(5)).toBe(5);
|
|
138
|
+
expect(auton(10)).toBe(10);
|
|
139
|
+
});
|
|
140
|
+
it("zero and below fall to the bound or fallback", () => {
|
|
141
|
+
expect(auton(0)).toBe(1);
|
|
142
|
+
expect(auton(-3)).toBe(1);
|
|
143
|
+
expect(auton(Number.NaN)).toBe(1);
|
|
144
|
+
expect(auton(11)).toBe(10);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("autofrequencypenalty autopresencepenalty — -2 to 2", () => {
|
|
149
|
+
it("clamp both directions", () => {
|
|
150
|
+
expect(autofrequencypenalty(3)).toBe(2);
|
|
151
|
+
expect(autofrequencypenalty(-3)).toBe(-2);
|
|
152
|
+
expect(autopresencepenalty(3)).toBe(2);
|
|
153
|
+
expect(autopresencepenalty(-3)).toBe(-2);
|
|
154
|
+
expect(autofrequencypenalty(Number.NaN)).toBe(0);
|
|
155
|
+
expect(autopresencepenalty(Number.NaN)).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("autoseed — nonnegative int bounded", () => {
|
|
160
|
+
it("normalizes negatives and floats", () => {
|
|
161
|
+
expect(autoseed(-100)).toBe(100);
|
|
162
|
+
expect(autoseed(12.9)).toBe(12);
|
|
163
|
+
expect(autoseed(-12.9)).toBe(12);
|
|
164
|
+
});
|
|
165
|
+
it("bounds at the 32 bit int ceiling", () => {
|
|
166
|
+
expect(autoseed(2 ** 31)).toBe(2147483647);
|
|
167
|
+
expect(autoseed(Number.MAX_SAFE_INTEGER)).toBe(2147483647);
|
|
168
|
+
});
|
|
169
|
+
it("parses numeric strings", () => {
|
|
170
|
+
expect(autoseed("42")).toBe(42);
|
|
171
|
+
expect(autoseed("42abc")).toBe(42);
|
|
172
|
+
expect(autoseed("abc")).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("automaxtokens — 1 to 98304", () => {
|
|
177
|
+
it("zero falls back to the default", () => {
|
|
178
|
+
expect(automaxtokens(0)).toBe(defaultmaxtokens);
|
|
179
|
+
expect(automaxtokens(0, 512)).toBe(512);
|
|
180
|
+
});
|
|
181
|
+
it("clamps the range", () => {
|
|
182
|
+
expect(automaxtokens(200000)).toBe(98304);
|
|
183
|
+
expect(automaxtokens(-5)).toBe(1);
|
|
184
|
+
expect(automaxtokens("2048")).toBe(2048);
|
|
185
|
+
expect(automaxtokens(Number.NaN, 99)).toBe(99);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// thinking level normalization — the 7 level canonical map
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
describe("autothinking — level normalization", () => {
|
|
194
|
+
it("canonical levels pass through", () => {
|
|
195
|
+
for (const level of ["none", "minimal", "low", "medium", "high", "xhigh", "max"]) {
|
|
196
|
+
expect(autothinking(level)).toBe(level);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
it("aliases map to canonical levels", () => {
|
|
200
|
+
expect(autothinking("disabled")).toBe("none");
|
|
201
|
+
expect(autothinking("off")).toBe("none");
|
|
202
|
+
expect(autothinking("false")).toBe("none");
|
|
203
|
+
expect(autothinking("minimum")).toBe("minimal");
|
|
204
|
+
expect(autothinking("med")).toBe("medium");
|
|
205
|
+
expect(autothinking("mid")).toBe("medium");
|
|
206
|
+
expect(autothinking("extra")).toBe("xhigh");
|
|
207
|
+
expect(autothinking("ultra")).toBe("xhigh");
|
|
208
|
+
expect(autothinking("maximum")).toBe("max");
|
|
209
|
+
expect(autothinking("full")).toBe("max");
|
|
210
|
+
expect(autothinking("all")).toBe("max");
|
|
211
|
+
});
|
|
212
|
+
it("case and whitespace are normalized", () => {
|
|
213
|
+
expect(autothinking("MAX")).toBe("max");
|
|
214
|
+
expect(autothinking(" High ")).toBe("high");
|
|
215
|
+
expect(autothinking("MEDIUM")).toBe("medium");
|
|
216
|
+
});
|
|
217
|
+
it("garbage and non strings fall back to high", () => {
|
|
218
|
+
expect(autothinking("bogus")).toBe("high");
|
|
219
|
+
expect(autothinking("")).toBe("high");
|
|
220
|
+
expect(autothinking(undefined)).toBe("high");
|
|
221
|
+
expect(autothinking(42)).toBe("high");
|
|
222
|
+
expect(autothinking({ level: "low" })).toBe("high");
|
|
223
|
+
expect(autothinking(null)).toBe("high");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("thinkingbudget — budgets for levels", () => {
|
|
228
|
+
it("returns the canonical budget map", () => {
|
|
229
|
+
expect(thinkingbudget("none")).toBe(0);
|
|
230
|
+
expect(thinkingbudget("minimal")).toBe(1400);
|
|
231
|
+
expect(thinkingbudget("low")).toBe(5500);
|
|
232
|
+
expect(thinkingbudget("medium")).toBe(17000);
|
|
233
|
+
expect(thinkingbudget("high")).toBe(68000);
|
|
234
|
+
expect(thinkingbudget("max")).toBe(68000);
|
|
235
|
+
});
|
|
236
|
+
it("budgets are monotonic nondecreasing", () => {
|
|
237
|
+
const levels = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
238
|
+
for (let i = 1; i < levels.length; i += 1) {
|
|
239
|
+
expect(thinkingbudgets[levels[i]]).toBeGreaterThanOrEqual(thinkingbudgets[levels[i - 1]]);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
it("garbage falls back to the high budget", () => {
|
|
243
|
+
expect(thinkingbudget("garbage")).toBe(68000);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("makethinker — thinking budget objects", () => {
|
|
248
|
+
it("none produces the disabled shape", () => {
|
|
249
|
+
expect(makethinker("none")).toEqual({ type: "disabled" });
|
|
250
|
+
expect(makethinker("off")).toEqual({ type: "disabled" });
|
|
251
|
+
});
|
|
252
|
+
it("levels produce enabled with budget", () => {
|
|
253
|
+
expect(makethinker("low")).toEqual({ type: "enabled", budget: 5500 });
|
|
254
|
+
expect(makethinker(undefined)).toEqual({ type: "enabled", budget: 68000 });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// json safety — never throw invariants
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
describe("safestringify — never throws", () => {
|
|
263
|
+
it("serializes normal values", () => {
|
|
264
|
+
expect(safestringify({ a: 1 })).toBe('{"a":1}');
|
|
265
|
+
expect(safestringify("x")).toBe('"x"');
|
|
266
|
+
expect(safestringify(null)).toBe("null");
|
|
267
|
+
expect(safestringify([1, 2])).toBe("[1,2]");
|
|
268
|
+
});
|
|
269
|
+
it("survives circular structures", () => {
|
|
270
|
+
const a: Record<string, unknown> = {};
|
|
271
|
+
a["self"] = a;
|
|
272
|
+
const out = safestringify(a);
|
|
273
|
+
expect(typeof out).toBe("string");
|
|
274
|
+
expect(out.length).toBeGreaterThan(0);
|
|
275
|
+
});
|
|
276
|
+
it("survives bigint which json rejects", () => {
|
|
277
|
+
const out = safestringify({ big: 10n });
|
|
278
|
+
expect(typeof out).toBe("string");
|
|
279
|
+
expect(out.length).toBeGreaterThan(0);
|
|
280
|
+
});
|
|
281
|
+
it("survives symbols and functions inside objects", () => {
|
|
282
|
+
const out = safestringify({ fn: () => 1, sym: Symbol("x") });
|
|
283
|
+
expect(typeof out).toBe("string");
|
|
284
|
+
});
|
|
285
|
+
it("handles deeply nested structures", () => {
|
|
286
|
+
let deep: unknown = { v: 1 };
|
|
287
|
+
for (let i = 0; i < 2000; i += 1) deep = { child: deep };
|
|
288
|
+
const out = safestringify(deep);
|
|
289
|
+
expect(out.length).toBeGreaterThan(0);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("safejsonparse — never throws", () => {
|
|
294
|
+
it("parses valid json", () => {
|
|
295
|
+
expect(safejsonparse('{"a":1}')).toEqual({ a: 1 });
|
|
296
|
+
expect(safejsonparse("[1,2]")).toEqual([1, 2]);
|
|
297
|
+
expect(safejsonparse('"s"')).toBe("s");
|
|
298
|
+
expect(safejsonparse("null")).toBeNull();
|
|
299
|
+
expect(safejsonparse("42")).toBe(42);
|
|
300
|
+
});
|
|
301
|
+
it("returns null on garbage", () => {
|
|
302
|
+
expect(safejsonparse("{")).toBeNull();
|
|
303
|
+
expect(safejsonparse("")).toBeNull();
|
|
304
|
+
expect(safejsonparse("not json")).toBeNull();
|
|
305
|
+
expect(safejsonparse('{"a":')).toBeNull();
|
|
306
|
+
expect(safejsonparse("undefined")).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
it("round trips through safestringify", () => {
|
|
309
|
+
const obj = { nested: { list: [1, "two", false, null] } };
|
|
310
|
+
expect(safejsonparse(safestringify(obj))).toEqual(obj);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// token estimation and truncation
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
describe("esttokens — cjk aware estimation", () => {
|
|
319
|
+
it("empty and nullish are zero", () => {
|
|
320
|
+
expect(esttokens("")).toBe(0);
|
|
321
|
+
});
|
|
322
|
+
it("ascii counts 4 chars per token", () => {
|
|
323
|
+
expect(esttokens("abcd")).toBe(1);
|
|
324
|
+
expect(esttokens("ab")).toBe(1); // ceil(2/4) = 1
|
|
325
|
+
expect(esttokens("abcdefgh")).toBe(2);
|
|
326
|
+
});
|
|
327
|
+
it("cjk counts 2 chars per token", () => {
|
|
328
|
+
expect(esttokens("中文")).toBe(1);
|
|
329
|
+
expect(esttokens("中")).toBe(1); // ceil(1/2) = 1
|
|
330
|
+
expect(esttokens("中文中文")).toBe(2);
|
|
331
|
+
});
|
|
332
|
+
it("korean hangul counts as cjk", () => {
|
|
333
|
+
expect(esttokens("한국")).toBe(1);
|
|
334
|
+
});
|
|
335
|
+
it("mixed scripts split correctly", () => {
|
|
336
|
+
const mixed = "abcd中文";
|
|
337
|
+
const tokens = esttokens(mixed);
|
|
338
|
+
expect(tokens).toBe(2); // 1 ascii + 1 cjk
|
|
339
|
+
});
|
|
340
|
+
it("emoji pass through the ascii bucket", () => {
|
|
341
|
+
expect(esttokens("😀😀😀😀")).toBe(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("truncatemessages — context window fitting", () => {
|
|
346
|
+
it("empty list stays empty", () => {
|
|
347
|
+
expect(truncatemessages([], 100)).toEqual([]);
|
|
348
|
+
});
|
|
349
|
+
it("keeps everything when it fits", () => {
|
|
350
|
+
const msgs = [
|
|
351
|
+
{ role: "user", content: "hello" },
|
|
352
|
+
{ role: "assistant", content: "hi" },
|
|
353
|
+
];
|
|
354
|
+
expect(truncatemessages(msgs, 1000)).toEqual(msgs);
|
|
355
|
+
});
|
|
356
|
+
it("drops the oldest messages first — newest survive", () => {
|
|
357
|
+
const msgs = [
|
|
358
|
+
{ role: "user", content: "a".repeat(100) },
|
|
359
|
+
{ role: "assistant", content: "b".repeat(100) },
|
|
360
|
+
{ role: "user", content: "latest" },
|
|
361
|
+
];
|
|
362
|
+
const out = truncatemessages(msgs, 30) as Array<{ content: string }>;
|
|
363
|
+
expect(out.length).toBe(1);
|
|
364
|
+
expect(out[0].content).toBe("latest");
|
|
365
|
+
});
|
|
366
|
+
it("returns empty when nothing fits", () => {
|
|
367
|
+
const msgs = [{ role: "user", content: "a".repeat(1000) }];
|
|
368
|
+
expect(truncatemessages(msgs, 10)).toEqual([]);
|
|
369
|
+
});
|
|
370
|
+
it("serializes non string content to estimate", () => {
|
|
371
|
+
const msgs = [{ role: "user", content: { nested: true } }];
|
|
372
|
+
const out = truncatemessages(msgs, 1000);
|
|
373
|
+
expect(out.length).toBe(1);
|
|
374
|
+
});
|
|
375
|
+
it("zero budget drops everything", () => {
|
|
376
|
+
const msgs = [{ role: "user", content: "x" }];
|
|
377
|
+
expect(truncatemessages(msgs, 0)).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
it("exact boundary fit includes the message", () => {
|
|
380
|
+
// content "x" = 1 token + 4 overhead = 5
|
|
381
|
+
const msgs = [{ role: "user", content: "x" }];
|
|
382
|
+
expect(truncatemessages(msgs, 5).length).toBe(1);
|
|
383
|
+
expect(truncatemessages(msgs, 4).length).toBe(0);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// id generation — uniqueness under load
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
describe("genid — unique ids", () => {
|
|
392
|
+
it("default prefix is chatcmpl", () => {
|
|
393
|
+
expect(genid().startsWith("chatcmpl-")).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
it("custom prefix is preserved", () => {
|
|
396
|
+
expect(genid("req").startsWith("req-")).toBe(true);
|
|
397
|
+
expect(genid("").startsWith("-")).toBe(true);
|
|
398
|
+
});
|
|
399
|
+
it("stays unique across 5000 rapid calls", () => {
|
|
400
|
+
const ids = new Set<string>();
|
|
401
|
+
for (let i = 0; i < 5000; i += 1) ids.add(genid());
|
|
402
|
+
expect(ids.size).toBe(5000);
|
|
403
|
+
});
|
|
404
|
+
it("concurrent generation stays unique", async () => {
|
|
405
|
+
const batches = await Promise.all(
|
|
406
|
+
Array.from({ length: 20 }, () => Promise.resolve(Array.from({ length: 100 }, () => genid()))),
|
|
407
|
+
);
|
|
408
|
+
const all = batches.flat();
|
|
409
|
+
expect(new Set(all).size).toBe(all.length);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("randombase36 — secure base36", () => {
|
|
414
|
+
it("zero length returns empty", () => {
|
|
415
|
+
expect(randombase36(0)).toBe("");
|
|
416
|
+
});
|
|
417
|
+
it("respects the requested length", () => {
|
|
418
|
+
expect(randombase36(1)).toHaveLength(1);
|
|
419
|
+
expect(randombase36(64)).toHaveLength(64);
|
|
420
|
+
});
|
|
421
|
+
it("alphabet is strictly 0-9a-z", () => {
|
|
422
|
+
expect(randombase36(500)).toMatch(/^[0-9a-z]+$/);
|
|
423
|
+
});
|
|
424
|
+
it("distribution covers the alphabet broadly", () => {
|
|
425
|
+
const out = randombase36(2000);
|
|
426
|
+
const distinct = new Set(out.split(""));
|
|
427
|
+
expect(distinct.size).toBeGreaterThanOrEqual(20);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("securerandom — uniform float", () => {
|
|
432
|
+
it("stays in 0 1", () => {
|
|
433
|
+
for (let i = 0; i < 1000; i += 1) {
|
|
434
|
+
const v = securerandom();
|
|
435
|
+
expect(v).toBeGreaterThanOrEqual(0);
|
|
436
|
+
expect(v).toBeLessThan(1);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
it("produces many distinct values", () => {
|
|
440
|
+
const values = new Set<number>();
|
|
441
|
+
for (let i = 0; i < 500; i += 1) values.add(securerandom());
|
|
442
|
+
expect(values.size).toBeGreaterThan(400);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// request helpers — ip cors and content type detection
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
describe("getip — client ip extraction", () => {
|
|
451
|
+
it("reads the first hop of x-forwarded-for", () => {
|
|
452
|
+
expect(getip(req({ "x-forwarded-for": "1.1.1.1, 2.2.2.2" }))).toBe("1.1.1.1");
|
|
453
|
+
});
|
|
454
|
+
it("falls through the header chain", () => {
|
|
455
|
+
expect(getip(req({ "x-real-ip": "3.3.3.3" }))).toBe("3.3.3.3");
|
|
456
|
+
expect(getip(req({ "cf-connecting-ip": "4.4.4.4" }))).toBe("4.4.4.4");
|
|
457
|
+
expect(getip(req({ "x-client-ip": "5.5.5.5" }))).toBe("5.5.5.5");
|
|
458
|
+
});
|
|
459
|
+
it("unknown when nothing matches", () => {
|
|
460
|
+
expect(getip(req({}))).toBe("unknown");
|
|
461
|
+
expect(getip(req({ accept: "text/html" }))).toBe("unknown");
|
|
462
|
+
});
|
|
463
|
+
it("priority order — forwarded wins over real ip", () => {
|
|
464
|
+
expect(getip(req({ "x-forwarded-for": "9.9.9.9", "x-real-ip": "8.8.8.8" }))).toBe("9.9.9.9");
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("handlecors — preflight handling", () => {
|
|
469
|
+
it("answers options with 204 and cors headers", () => {
|
|
470
|
+
const res = handlecors(req({}, "OPTIONS"));
|
|
471
|
+
expect(res).not.toBeNull();
|
|
472
|
+
expect(res?.status).toBe(204);
|
|
473
|
+
expect(res?.headers.get("access-control-allow-origin")).toBe("*");
|
|
474
|
+
});
|
|
475
|
+
it("returns null for non options methods", () => {
|
|
476
|
+
expect(handlecors(req({}))).toBeNull();
|
|
477
|
+
expect(handlecors(req({}, "POST"))).toBeNull();
|
|
478
|
+
expect(handlecors(req({ origin: "https://x.com" }))).toBeNull();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("detectcontenttype — accept header detection", () => {
|
|
483
|
+
it("detects each format", () => {
|
|
484
|
+
expect(detectcontenttype(req({ accept: ctndjson }))).toBe("ndjson");
|
|
485
|
+
expect(detectcontenttype(req({ accept: "text/event-stream" }))).toBe("sse");
|
|
486
|
+
expect(detectcontenttype(req({ accept: ctplain }))).toBe("plain");
|
|
487
|
+
expect(detectcontenttype(req({ accept: ctoctet }))).toBe("octet");
|
|
488
|
+
expect(detectcontenttype(req({ accept: ctjson }))).toBe("json");
|
|
489
|
+
});
|
|
490
|
+
it("defaults to json when absent or unknown", () => {
|
|
491
|
+
expect(detectcontenttype(req({}))).toBe("json");
|
|
492
|
+
expect(detectcontenttype(req({ accept: "text/html" }))).toBe("json");
|
|
493
|
+
expect(detectcontenttype(req({ accept: "*/*" }))).toBe("json");
|
|
494
|
+
});
|
|
495
|
+
it("ndjson wins over sse when both present", () => {
|
|
496
|
+
expect(detectcontenttype(req({ accept: `${ctndjson}, text/event-stream` }))).toBe("ndjson");
|
|
497
|
+
});
|
|
498
|
+
it("case insensitive", () => {
|
|
499
|
+
expect(detectcontenttype(req({ accept: "TEXT/EVENT-STREAM" }))).toBe("sse");
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe("header constants", () => {
|
|
504
|
+
it("jsonheaders carry cors plus json content type", () => {
|
|
505
|
+
const h = jsonheaders();
|
|
506
|
+
expect(h["content-type"]).toBe(ctjson);
|
|
507
|
+
expect(h["access-control-allow-origin"]).toBe("*");
|
|
508
|
+
});
|
|
509
|
+
it("corsheaders expose request and session ids", () => {
|
|
510
|
+
expect(corsheaders["access-control-expose-headers"]).toContain("x-request-id");
|
|
511
|
+
expect(corsheaders["access-control-expose-headers"]).toContain("x-session-id");
|
|
512
|
+
});
|
|
513
|
+
it("maxdurationconst is the 31 bit ceiling", () => {
|
|
514
|
+
expect(maxdurationconst).toBe(2 ** 31 - 1);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// levenshtein — fuzzy distance
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
describe("levenshtein — distance properties", () => {
|
|
523
|
+
it("empty versus non empty is the length", () => {
|
|
524
|
+
expect(levenshtein("", "abc")).toBe(3);
|
|
525
|
+
expect(levenshtein("abc", "")).toBe(3);
|
|
526
|
+
expect(levenshtein("", "")).toBe(0);
|
|
527
|
+
});
|
|
528
|
+
it("identical strings are zero", () => {
|
|
529
|
+
expect(levenshtein("gateway", "gateway")).toBe(0);
|
|
530
|
+
});
|
|
531
|
+
it("single edits cost one", () => {
|
|
532
|
+
expect(levenshtein("cat", "cut")).toBe(1); // substitution
|
|
533
|
+
expect(levenshtein("cat", "cart")).toBe(1); // insertion
|
|
534
|
+
expect(levenshtein("cart", "cat")).toBe(1); // deletion
|
|
535
|
+
});
|
|
536
|
+
it("symmetry", () => {
|
|
537
|
+
expect(levenshtein("kitten", "sitting")).toBe(levenshtein("sitting", "kitten"));
|
|
538
|
+
expect(levenshtein("kitten", "sitting")).toBe(3);
|
|
539
|
+
});
|
|
540
|
+
it("case sensitive", () => {
|
|
541
|
+
expect(levenshtein("ABC", "abc")).toBe(3);
|
|
542
|
+
});
|
|
543
|
+
it("unicode handled by code units", () => {
|
|
544
|
+
expect(levenshtein("中", "文")).toBe(1);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* streamfallback.test.ts — honest unit tests for the stream fallback,
|
|
3
3
|
* runnable with the vitest runner:
|
|
4
4
|
* pnpm test
|
|
5
|
+
*
|
|
6
|
+
* the edgelandes fold (wave/streamfold) retired streamfallback.ts into
|
|
7
|
+
* streaming.ts as its trailing fallback arm — the battery imports the arm
|
|
8
|
+
* from the one stream keyword file now.
|
|
5
9
|
*/
|
|
6
10
|
import assert from "node:assert/strict";
|
|
7
11
|
import { describe, it } from "vitest";
|
|
@@ -16,7 +20,7 @@ import {
|
|
|
16
20
|
runwithfallback,
|
|
17
21
|
StreamError,
|
|
18
22
|
verdictfor,
|
|
19
|
-
} from "../
|
|
23
|
+
} from "../streaming.ts";
|
|
20
24
|
|
|
21
25
|
/** the two-route chain the tests walk. */
|
|
22
26
|
const CHAIN: FallbackRoute[] = [
|