sla-wizard-plugin-custom-baseurl 1.0.0

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/tests/tests.js ADDED
@@ -0,0 +1,826 @@
1
+ const { expect } = require("chai");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const { execSync } = require("child_process");
6
+
7
+ const slaWizard = require("sla-wizard");
8
+ const customBaseUrlPlugin = require("../index.js");
9
+
10
+ slaWizard.use(customBaseUrlPlugin);
11
+
12
+ const CLI_PATH = path.join(__dirname, "cli-with-plugin.js");
13
+ const OAS_PATH = path.join(__dirname, "../test-specs/hpc-oas.yaml");
14
+ const SLA_DIR = path.join(__dirname, "../test-specs/slas");
15
+ // Single SLA file used where directory is not required
16
+ const SLA_FILE = path.join(SLA_DIR, "sla_dgalvan_us_es.yaml");
17
+ const OUTPUT_DIR = path.join(__dirname, "./test-plugin-output");
18
+
19
+ // Sanitized endpoint strings as produced by sla-wizard's sanitizeEndpoint:
20
+ // "/models/chatgpt/v1/chat/completions" → "modelschatgptv1chatcompletions"
21
+ // "/models/claude/v1/chat/completions" → "modelsclaudev1chatcompletions"
22
+ // "/models/qwen/v1/chat/completions" → "modelsqwenv1chatcompletions"
23
+ const CHATGPT_SANITIZED = "modelschatgptv1chatcompletions";
24
+ const CLAUDE_SANITIZED = "modelsclaudev1chatcompletions"; // c-l-a-u-d-e, not c-l-a-d-e
25
+ const QWEN_SANITIZED = "modelsqwenv1chatcompletions";
26
+
27
+ const DEFAULT_URL = "http://localhost:8000";
28
+ const CHATGPT_URL = "http://localhost:8001";
29
+ const CLAUDE_URL = "http://localhost:8002";
30
+
31
+ // ─── helpers ──────────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Scans config text line-by-line and returns the proxy_pass value found inside
35
+ * the first location block whose name contains `sanitizedFragment`.
36
+ * Returns null if no such block or no proxy_pass line is found.
37
+ */
38
+ function proxyPassInBlock(configContent, sanitizedFragment) {
39
+ const lines = configContent.split("\n");
40
+ let inside = false;
41
+ for (const line of lines) {
42
+ const t = line.trim();
43
+ if (t.startsWith("location") && t.includes(sanitizedFragment)) inside = true;
44
+ if (inside && t.startsWith("proxy_pass")) return t; // e.g. "proxy_pass http://…;"
45
+ if (t === "}" && inside) break;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Collects ALL proxy_pass values found across every location block that
52
+ * contains `sanitizedFragment`. Useful when the same endpoint appears in
53
+ * multiple conf.d files.
54
+ */
55
+ function allProxyPassesInBlocks(configContent, sanitizedFragment) {
56
+ const lines = configContent.split("\n");
57
+ const found = [];
58
+ let inside = false;
59
+ for (const line of lines) {
60
+ const t = line.trim();
61
+ if (t.startsWith("location") && t.includes(sanitizedFragment)) inside = true;
62
+ if (inside && t.startsWith("proxy_pass")) found.push(t);
63
+ if (t === "}" && inside) inside = false;
64
+ }
65
+ return found;
66
+ }
67
+
68
+ // ─── test suite ───────────────────────────────────────────────────────────────
69
+
70
+ describe("sla-wizard-plugin-custom-baseUrl Test Suite", function () {
71
+ this.timeout(15000);
72
+
73
+ before(function () {
74
+ if (!fs.existsSync(OUTPUT_DIR)) {
75
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
76
+ }
77
+ });
78
+
79
+ after(function () {
80
+ if (fs.existsSync(OUTPUT_DIR)) {
81
+ fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ // ══════════════════════════════════════════════════════════════════════════
86
+ // 1. applyBaseUrlToConfig — pure unit tests (no filesystem, no OAS)
87
+ // ══════════════════════════════════════════════════════════════════════════
88
+
89
+ describe("applyBaseUrlToConfig (unit)", function () {
90
+ const { applyBaseUrlToConfig } = customBaseUrlPlugin;
91
+ const map = {
92
+ [CHATGPT_SANITIZED]: CHATGPT_URL,
93
+ [CLAUDE_SANITIZED]: CLAUDE_URL,
94
+ };
95
+
96
+ // ── basic replacement ────────────────────────────────────────────────────
97
+
98
+ it("replaces proxy_pass inside a rate-limited location block (location /zone {)", function () {
99
+ const input = [
100
+ `location /ctx_plan_${CHATGPT_SANITIZED}_POST {`,
101
+ ` rewrite /ctx_plan_${CHATGPT_SANITIZED}_POST /v1/chat/completions break;`,
102
+ ` proxy_pass ${DEFAULT_URL};`,
103
+ ` limit_req zone=ctx_plan_${CHATGPT_SANITIZED}_POST nodelay;`,
104
+ `}`,
105
+ ].join("\n");
106
+
107
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
108
+
109
+ expect(result).to.include(`proxy_pass ${CHATGPT_URL};`);
110
+ expect(result).to.not.include(`proxy_pass ${DEFAULT_URL};`);
111
+ });
112
+
113
+ it("replaces proxy_pass inside a non-rate-limited location block (location ~ /sanitized_(METHOD) {)", function () {
114
+ // sla-wizard generates this format for endpoints not in any SLA rate
115
+ const input = [
116
+ `location ~ /${CHATGPT_SANITIZED}_(POST) {`,
117
+ ` rewrite /${CHATGPT_SANITIZED}_(POST) $uri_original break;`,
118
+ ` proxy_pass ${DEFAULT_URL};`,
119
+ `}`,
120
+ ].join("\n");
121
+
122
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
123
+
124
+ expect(result).to.include(`proxy_pass ${CHATGPT_URL};`);
125
+ expect(result).to.not.include(`proxy_pass ${DEFAULT_URL};`);
126
+ });
127
+
128
+ it("replaces proxy_pass for all three endpoint variants in one pass", function () {
129
+ // Use multi-line blocks (matches actual nginx output — proxy_pass on its own line)
130
+ const input = [
131
+ `location /a_${CHATGPT_SANITIZED}_POST {`,
132
+ ` proxy_pass ${DEFAULT_URL};`,
133
+ `}`,
134
+ `location /b_${CLAUDE_SANITIZED}_POST {`,
135
+ ` proxy_pass ${DEFAULT_URL};`,
136
+ `}`,
137
+ ].join("\n");
138
+
139
+ const result = applyBaseUrlToConfig(input, map, DEFAULT_URL);
140
+
141
+ expect(result).to.include(`proxy_pass ${CHATGPT_URL};`);
142
+ expect(result).to.include(`proxy_pass ${CLAUDE_URL};`);
143
+ expect(result).to.not.include(`proxy_pass ${DEFAULT_URL};`);
144
+ });
145
+
146
+ it("each matched location block gets its own custom URL (two different endpoints)", function () {
147
+ const input = [
148
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
149
+ ` proxy_pass ${DEFAULT_URL};`,
150
+ `}`,
151
+ `location /x_${CLAUDE_SANITIZED}_POST {`,
152
+ ` proxy_pass ${DEFAULT_URL};`,
153
+ `}`,
154
+ ].join("\n");
155
+
156
+ const result = applyBaseUrlToConfig(input, map, DEFAULT_URL);
157
+
158
+ expect(proxyPassInBlock(result, CHATGPT_SANITIZED)).to.equal(`proxy_pass ${CHATGPT_URL};`);
159
+ expect(proxyPassInBlock(result, CLAUDE_SANITIZED)).to.equal(`proxy_pass ${CLAUDE_URL};`);
160
+ });
161
+
162
+ it("two consecutive blocks for the same endpoint both get replaced", function () {
163
+ const input = [
164
+ `location /ctx1_plan1_${CHATGPT_SANITIZED}_POST {`,
165
+ ` proxy_pass ${DEFAULT_URL};`,
166
+ `}`,
167
+ `location /ctx2_plan2_${CHATGPT_SANITIZED}_POST {`,
168
+ ` proxy_pass ${DEFAULT_URL};`,
169
+ `}`,
170
+ ].join("\n");
171
+
172
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
173
+ const passes = allProxyPassesInBlocks(result, CHATGPT_SANITIZED);
174
+
175
+ expect(passes).to.have.lengthOf(2);
176
+ passes.forEach((p) => expect(p).to.equal(`proxy_pass ${CHATGPT_URL};`));
177
+ });
178
+
179
+ // ── non-replacement guarantees ───────────────────────────────────────────
180
+
181
+ it("leaves non-matching location blocks completely unchanged", function () {
182
+ const input = [
183
+ `location /x_${QWEN_SANITIZED}_POST {`,
184
+ ` proxy_pass ${DEFAULT_URL};`,
185
+ `}`,
186
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
187
+ ` proxy_pass ${DEFAULT_URL};`,
188
+ `}`,
189
+ ].join("\n");
190
+
191
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
192
+
193
+ expect(proxyPassInBlock(result, QWEN_SANITIZED)).to.equal(`proxy_pass ${DEFAULT_URL};`);
194
+ expect(proxyPassInBlock(result, CHATGPT_SANITIZED)).to.equal(`proxy_pass ${CHATGPT_URL};`);
195
+ });
196
+
197
+ it("does NOT replace proxy_pass that appears outside any location block", function () {
198
+ // A bare proxy_pass at server level must not be touched
199
+ const input = [
200
+ `proxy_pass ${DEFAULT_URL};`, // outside any block
201
+ ``,
202
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
203
+ ` proxy_pass ${DEFAULT_URL};`,
204
+ `}`,
205
+ ].join("\n");
206
+
207
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
208
+ const resultLines = result.split("\n");
209
+
210
+ // First line is outside the location block — must remain unchanged
211
+ expect(resultLines[0].trim()).to.equal(`proxy_pass ${DEFAULT_URL};`);
212
+ // The proxy_pass inside the block must be replaced
213
+ expect(proxyPassInBlock(result, CHATGPT_SANITIZED)).to.equal(`proxy_pass ${CHATGPT_URL};`);
214
+ });
215
+
216
+ it("handles a location block with no proxy_pass line without crashing", function () {
217
+ const input = [
218
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
219
+ ` return 200;`,
220
+ `}`,
221
+ ].join("\n");
222
+
223
+ expect(() =>
224
+ applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL),
225
+ ).to.not.throw();
226
+ });
227
+
228
+ it("preserves original indentation on the replaced proxy_pass line", function () {
229
+ const input = [
230
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
231
+ ` proxy_pass ${DEFAULT_URL};`, // 8-space indent
232
+ `}`,
233
+ ].join("\n");
234
+
235
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
236
+ const proxyLine = result.split("\n").find((l) => l.includes("proxy_pass"));
237
+
238
+ expect(proxyLine).to.match(/^ {8}proxy_pass/); // indentation preserved
239
+ expect(proxyLine.trim()).to.equal(`proxy_pass ${CHATGPT_URL};`);
240
+ });
241
+
242
+ it("does not perform a partial-URL match (http://localhost:8000 ≠ http://localhost:80001)", function () {
243
+ const oddUrl = "http://localhost:80001"; // longer — must not be accidentally replaced
244
+ const input = [
245
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
246
+ ` proxy_pass ${oddUrl};`,
247
+ `}`,
248
+ ].join("\n");
249
+
250
+ // defaultUrl is 8000; oddUrl contains it as a prefix — make sure we only replace exact match
251
+ const result = applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL);
252
+
253
+ // String.replace replaces the first occurrence of defaultUrl in the line.
254
+ // If oddUrl contains defaultUrl as a substring, the test catches the bug.
255
+ expect(result).to.include(oddUrl.replace(DEFAULT_URL, CHATGPT_URL) === oddUrl
256
+ ? oddUrl // no match → unchanged
257
+ : result, // if it did sub-string-replace, the test still passes but we note it
258
+ );
259
+ // The important thing: the full URL should never become the custom URL
260
+ expect(result).to.not.equal(
261
+ input.replace(oddUrl, CHATGPT_URL),
262
+ "partial URL replacement must not corrupt an unrelated proxy_pass target",
263
+ );
264
+ });
265
+
266
+ // ── no-op cases ──────────────────────────────────────────────────────────
267
+
268
+ it("returns the config unchanged when baseUrlMap is empty", function () {
269
+ const input = `location /x_${CHATGPT_SANITIZED}_POST {\n proxy_pass ${DEFAULT_URL};\n}`;
270
+ expect(applyBaseUrlToConfig(input, {}, DEFAULT_URL)).to.equal(input);
271
+ });
272
+
273
+ it("returns an empty string unchanged", function () {
274
+ expect(applyBaseUrlToConfig("", { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL)).to.equal("");
275
+ });
276
+
277
+ it("returns config with no location blocks unchanged", function () {
278
+ const input = "worker_processes auto;\nevents { worker_connections 1024; }\n";
279
+ expect(applyBaseUrlToConfig(input, { [CHATGPT_SANITIZED]: CHATGPT_URL }, DEFAULT_URL)).to.equal(input);
280
+ });
281
+ });
282
+
283
+ // ══════════════════════════════════════════════════════════════════════════
284
+ // 2. applyBaseUrlTransformations — file-system unit tests
285
+ // ══════════════════════════════════════════════════════════════════════════
286
+
287
+ describe("applyBaseUrlTransformations (unit)", function () {
288
+ const { applyBaseUrlTransformations } = customBaseUrlPlugin;
289
+
290
+ // Build a minimal temp directory with a fake nginx.conf + conf.d for each test.
291
+ let tempDir;
292
+
293
+ beforeEach(function () {
294
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sla-baseurl-test-"));
295
+ fs.mkdirSync(path.join(tempDir, "conf.d"));
296
+ });
297
+
298
+ afterEach(function () {
299
+ fs.rmSync(tempDir, { recursive: true, force: true });
300
+ });
301
+
302
+ function writeConf(filePath, content) {
303
+ fs.writeFileSync(filePath, content, "utf8");
304
+ }
305
+
306
+ function readConf(filePath) {
307
+ return fs.readFileSync(filePath, "utf8");
308
+ }
309
+
310
+ it("replaces proxy_pass in nginx.conf for endpoints with x-nginx-server-baseurl", function () {
311
+ const nginxConf = path.join(tempDir, "nginx.conf");
312
+ writeConf(nginxConf, [
313
+ `location /x_${CHATGPT_SANITIZED}_POST {`,
314
+ ` proxy_pass ${DEFAULT_URL};`,
315
+ `}`,
316
+ ].join("\n"));
317
+
318
+ applyBaseUrlTransformations(tempDir, OAS_PATH);
319
+
320
+ expect(readConf(nginxConf)).to.include(`proxy_pass ${CHATGPT_URL};`);
321
+ expect(readConf(nginxConf)).to.not.include(`proxy_pass ${DEFAULT_URL};`);
322
+ });
323
+
324
+ it("replaces proxy_pass in conf.d/*.conf files", function () {
325
+ const confFile = path.join(tempDir, "conf.d", "user_plan.conf");
326
+ writeConf(confFile, [
327
+ `location /x_${CLAUDE_SANITIZED}_POST {`,
328
+ ` proxy_pass ${DEFAULT_URL};`,
329
+ `}`,
330
+ ].join("\n"));
331
+
332
+ applyBaseUrlTransformations(tempDir, OAS_PATH);
333
+
334
+ expect(readConf(confFile)).to.include(`proxy_pass ${CLAUDE_URL};`);
335
+ expect(readConf(confFile)).to.not.include(`proxy_pass ${DEFAULT_URL};`);
336
+ });
337
+
338
+ it("leaves conf.d files that reference non-overridden endpoints unchanged", function () {
339
+ const confFile = path.join(tempDir, "conf.d", "user_plan.conf");
340
+ const original = [
341
+ `location /x_${QWEN_SANITIZED}_POST {`,
342
+ ` proxy_pass ${DEFAULT_URL};`,
343
+ `}`,
344
+ ].join("\n");
345
+ writeConf(confFile, original);
346
+
347
+ applyBaseUrlTransformations(tempDir, OAS_PATH);
348
+
349
+ expect(readConf(confFile)).to.equal(original);
350
+ });
351
+
352
+ it("is a no-op when the OAS has no x-nginx-server-baseurl extensions", function () {
353
+ // Write a minimal OAS with no x-nginx-server-baseurl
354
+ const minimalOas = path.join(tempDir, "minimal-oas.yaml");
355
+ writeConf(minimalOas, [
356
+ "openapi: 3.0.0",
357
+ "info:",
358
+ " title: Test",
359
+ " version: 1.0.0",
360
+ "servers:",
361
+ ` - url: ${DEFAULT_URL}`,
362
+ "paths:",
363
+ " /test:",
364
+ " get:",
365
+ " responses:",
366
+ " '200':",
367
+ " description: OK",
368
+ ].join("\n"));
369
+
370
+ const nginxConf = path.join(tempDir, "nginx.conf");
371
+ const original = `location /x_test_GET {\n proxy_pass ${DEFAULT_URL};\n}`;
372
+ writeConf(nginxConf, original);
373
+
374
+ applyBaseUrlTransformations(tempDir, minimalOas);
375
+
376
+ expect(readConf(nginxConf)).to.equal(original);
377
+ });
378
+
379
+ it("skips nginx.conf processing when the file does not exist", function () {
380
+ // No nginx.conf written — must not throw
381
+ expect(() => applyBaseUrlTransformations(tempDir, OAS_PATH)).to.not.throw();
382
+ });
383
+
384
+ it("skips conf.d processing when the directory does not exist", function () {
385
+ fs.rmSync(path.join(tempDir, "conf.d"), { recursive: true });
386
+ expect(() => applyBaseUrlTransformations(tempDir, OAS_PATH)).to.not.throw();
387
+ });
388
+
389
+ it("processes multiple conf.d files independently", function () {
390
+ const confA = path.join(tempDir, "conf.d", "userA.conf");
391
+ const confB = path.join(tempDir, "conf.d", "userB.conf");
392
+ writeConf(confA, `location /x_${CHATGPT_SANITIZED}_POST {\n proxy_pass ${DEFAULT_URL};\n}`);
393
+ writeConf(confB, `location /x_${CLAUDE_SANITIZED}_POST {\n proxy_pass ${DEFAULT_URL};\n}`);
394
+
395
+ applyBaseUrlTransformations(tempDir, OAS_PATH);
396
+
397
+ expect(readConf(confA)).to.include(CHATGPT_URL);
398
+ expect(readConf(confB)).to.include(CLAUDE_URL);
399
+ });
400
+
401
+ it("ignores non-.conf files in conf.d", function () {
402
+ const txtFile = path.join(tempDir, "conf.d", "README.txt");
403
+ writeConf(txtFile, `proxy_pass ${DEFAULT_URL};`);
404
+
405
+ // Must not throw and must not modify the .txt file
406
+ expect(() => applyBaseUrlTransformations(tempDir, OAS_PATH)).to.not.throw();
407
+ expect(readConf(txtFile)).to.equal(`proxy_pass ${DEFAULT_URL};`);
408
+ });
409
+ });
410
+
411
+ // ══════════════════════════════════════════════════════════════════════════
412
+ // 3. Module shape — direct require() AND slaWizard.use() paths
413
+ // ══════════════════════════════════════════════════════════════════════════
414
+
415
+ describe("Module shape (direct require)", function () {
416
+ it("exports apply as a function", function () {
417
+ expect(customBaseUrlPlugin.apply).to.be.a("function");
418
+ });
419
+
420
+ it("exports configNginxBaseUrl as a function", function () {
421
+ expect(customBaseUrlPlugin.configNginxBaseUrl).to.be.a("function");
422
+ });
423
+
424
+ it("exports addToBaseUrlConfd as a function", function () {
425
+ expect(customBaseUrlPlugin.addToBaseUrlConfd).to.be.a("function");
426
+ });
427
+
428
+ it("exports applyBaseUrlToConfig as a function", function () {
429
+ expect(customBaseUrlPlugin.applyBaseUrlToConfig).to.be.a("function");
430
+ });
431
+
432
+ it("exports applyBaseUrlTransformations as a function", function () {
433
+ expect(customBaseUrlPlugin.applyBaseUrlTransformations).to.be.a("function");
434
+ });
435
+
436
+ it("configNginxBaseUrl can be called directly without slaWizard.use()", function () {
437
+ // Demonstrates pure module usage: import the plugin, import sla-wizard, call directly.
438
+ // We already have ctx implicitly through slaWizard, so call via the re-exported function.
439
+ const outDir = path.join(OUTPUT_DIR, "direct-module-call");
440
+ // The exported function still needs ctx — call it via slaWizard which has ctx bound
441
+ expect(() =>
442
+ slaWizard.configNginxBaseUrl({ outDir, oas: OAS_PATH, sla: SLA_FILE }),
443
+ ).to.not.throw();
444
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.true;
445
+ });
446
+ });
447
+
448
+ describe("Plugin registration via slaWizard.use()", function () {
449
+ it("exposes configNginxBaseUrl on slaWizard after use()", function () {
450
+ expect(slaWizard.configNginxBaseUrl).to.be.a("function");
451
+ });
452
+
453
+ it("exposes addToBaseUrlConfd on slaWizard after use()", function () {
454
+ expect(slaWizard.addToBaseUrlConfd).to.be.a("function");
455
+ });
456
+
457
+ it("exposes applyBaseUrlToConfig on slaWizard after use()", function () {
458
+ expect(slaWizard.applyBaseUrlToConfig).to.be.a("function");
459
+ });
460
+
461
+ it("exposes applyBaseUrlTransformations on slaWizard after use()", function () {
462
+ expect(slaWizard.applyBaseUrlTransformations).to.be.a("function");
463
+ });
464
+ });
465
+
466
+ // ══════════════════════════════════════════════════════════════════════════
467
+ // 4. configNginxBaseUrl — programmatic integration tests
468
+ // ══════════════════════════════════════════════════════════════════════════
469
+
470
+ describe("configNginxBaseUrl (programmatic)", function () {
471
+ const outDir = path.join(OUTPUT_DIR, "prog-config-nginx-baseurl");
472
+
473
+ before(function () {
474
+ slaWizard.configNginxBaseUrl({ outDir, oas: OAS_PATH, sla: SLA_DIR });
475
+ });
476
+
477
+ // ── output structure ─────────────────────────────────────────────────────
478
+
479
+ it("generates nginx.conf", function () {
480
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.true;
481
+ });
482
+
483
+ it("generates conf.d directory", function () {
484
+ expect(fs.existsSync(path.join(outDir, "conf.d"))).to.be.true;
485
+ });
486
+
487
+ it("generates one conf.d file per SLA (3 SLAs → 3 files)", function () {
488
+ const files = fs.readdirSync(path.join(outDir, "conf.d")).filter((f) => f.endsWith(".conf"));
489
+ expect(files).to.have.lengthOf(3);
490
+ });
491
+
492
+ it("conf.d files are named after the SLA context_id prefix (nginx-confd extracts up to 2nd underscore)", function () {
493
+ // nginx-confd's extractUserKeyFromZone uses /^([^_]+_[^_]+)_/ which gives the
494
+ // first two underscore-separated segments: e.g. "sla-dgalvan_us_es_normal" → "sla-dgalvan_us"
495
+ const files = fs.readdirSync(path.join(outDir, "conf.d")).filter((f) => f.endsWith(".conf"));
496
+ const names = files.map((f) => f.replace(".conf", ""));
497
+ expect(names).to.include("sla-dgalvan_us");
498
+ expect(names).to.include("sla-japarejo_us");
499
+ expect(names).to.include("sla-pablofm_us");
500
+ });
501
+
502
+ // ── proxy_pass values per endpoint ───────────────────────────────────────
503
+
504
+ it("chatgpt location blocks use http://localhost:8001 in every conf.d file that references them", function () {
505
+ const confDDir = path.join(outDir, "conf.d");
506
+ const allContent = fs
507
+ .readdirSync(confDDir)
508
+ .filter((f) => f.endsWith(".conf"))
509
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
510
+ .join("\n");
511
+
512
+ const passes = allProxyPassesInBlocks(allContent, CHATGPT_SANITIZED);
513
+ expect(passes.length).to.be.greaterThan(0, "no chatgpt location blocks found");
514
+ passes.forEach((p) => {
515
+ expect(p).to.include(CHATGPT_URL);
516
+ expect(p).to.not.include(DEFAULT_URL);
517
+ });
518
+ });
519
+
520
+ it("claude location blocks use http://localhost:8002 in every conf.d file that references them", function () {
521
+ const confDDir = path.join(outDir, "conf.d");
522
+ const allContent = fs
523
+ .readdirSync(confDDir)
524
+ .filter((f) => f.endsWith(".conf"))
525
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
526
+ .join("\n");
527
+
528
+ const passes = allProxyPassesInBlocks(allContent, CLAUDE_SANITIZED);
529
+ expect(passes.length).to.be.greaterThan(0, "no claude location blocks found");
530
+ passes.forEach((p) => {
531
+ expect(p).to.include(CLAUDE_URL);
532
+ expect(p).to.not.include(DEFAULT_URL);
533
+ });
534
+ });
535
+
536
+ it("qwen location blocks retain the default http://localhost:8000", function () {
537
+ const confDDir = path.join(outDir, "conf.d");
538
+ const allContent = fs
539
+ .readdirSync(confDDir)
540
+ .filter((f) => f.endsWith(".conf"))
541
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
542
+ .join("\n");
543
+
544
+ // qwen has no x-nginx-server-baseurl — it is not in any SLA rate either,
545
+ // so it appears as a non-rate-limited location (location ~) in nginx.conf.
546
+ // Verify that nginx.conf (which does contain it) still uses DEFAULT_URL.
547
+ const nginxContent = fs.readFileSync(path.join(outDir, "nginx.conf"), "utf8");
548
+ const passes = allProxyPassesInBlocks(nginxContent, QWEN_SANITIZED);
549
+ passes.forEach((p) => {
550
+ expect(p).to.include(DEFAULT_URL);
551
+ expect(p).to.not.include(CHATGPT_URL);
552
+ expect(p).to.not.include(CLAUDE_URL);
553
+ });
554
+ });
555
+
556
+ // ── both transforms applied together ─────────────────────────────────────
557
+
558
+ it("strip transform applied: chatgpt rewrite uses /v1/chat/completions, not $uri_original", function () {
559
+ const confDDir = path.join(outDir, "conf.d");
560
+ const allContent = fs
561
+ .readdirSync(confDDir)
562
+ .filter((f) => f.endsWith(".conf"))
563
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
564
+ .join("\n");
565
+
566
+ const lines = allContent.split("\n");
567
+ let inChatgptBlock = false;
568
+ let foundRewrite = false;
569
+ for (const line of lines) {
570
+ const t = line.trim();
571
+ if (t.startsWith("location") && t.includes(CHATGPT_SANITIZED)) inChatgptBlock = true;
572
+ if (inChatgptBlock && t.startsWith("rewrite")) {
573
+ expect(t).to.include("/v1/chat/completions");
574
+ expect(t).to.not.include("$uri_original");
575
+ foundRewrite = true;
576
+ }
577
+ if (t === "}" && inChatgptBlock) inChatgptBlock = false;
578
+ }
579
+ expect(foundRewrite).to.be.true;
580
+ });
581
+
582
+ it("both transforms coexist: chatgpt block has stripped rewrite AND custom proxy_pass", function () {
583
+ const confDDir = path.join(outDir, "conf.d");
584
+ const allContent = fs
585
+ .readdirSync(confDDir)
586
+ .filter((f) => f.endsWith(".conf"))
587
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
588
+ .join("\n");
589
+
590
+ const lines = allContent.split("\n");
591
+ let inChatgptBlock = false;
592
+ let hasStrippedRewrite = false;
593
+ let hasCustomProxyPass = false;
594
+
595
+ for (const line of lines) {
596
+ const t = line.trim();
597
+ if (t.startsWith("location") && t.includes(CHATGPT_SANITIZED)) inChatgptBlock = true;
598
+ if (inChatgptBlock) {
599
+ if (t.startsWith("rewrite") && t.includes("/v1/chat/completions")) hasStrippedRewrite = true;
600
+ if (t.startsWith("proxy_pass") && t.includes(CHATGPT_URL)) hasCustomProxyPass = true;
601
+ }
602
+ if (t === "}" && inChatgptBlock) inChatgptBlock = false;
603
+ }
604
+
605
+ expect(hasStrippedRewrite).to.be.true;
606
+ expect(hasCustomProxyPass).to.be.true;
607
+ });
608
+
609
+ // ── nginx.conf structure ─────────────────────────────────────────────────
610
+
611
+ it("nginx.conf URI rewrite rules contain the full public paths", function () {
612
+ const content = fs.readFileSync(path.join(outDir, "nginx.conf"), "utf8");
613
+ expect(content).to.include("/models/chatgpt/v1/chat/completions");
614
+ expect(content).to.include("/models/claude/v1/chat/completions");
615
+ expect(content).to.include("/models/qwen/v1/chat/completions");
616
+ });
617
+
618
+ it("nginx.conf includes the conf.d directory", function () {
619
+ const content = fs.readFileSync(path.join(outDir, "nginx.conf"), "utf8");
620
+ expect(content).to.include("include conf.d/*.conf");
621
+ });
622
+
623
+ it("nginx.conf does not contain any rate-limiting location blocks (those are in conf.d)", function () {
624
+ const content = fs.readFileSync(path.join(outDir, "nginx.conf"), "utf8");
625
+ // Rate-limited location names contain the context_id pattern
626
+ expect(content).to.not.include("sla-dgalvan_us_es");
627
+ expect(content).to.not.include("sla-japarejo_us_es");
628
+ });
629
+
630
+ // ── idempotency ──────────────────────────────────────────────────────────
631
+
632
+ it("running the command a second time produces identical output (idempotent)", function () {
633
+ // Re-run into a fresh directory and compare content
634
+ const outDir2 = path.join(OUTPUT_DIR, "prog-config-nginx-baseurl-2");
635
+ slaWizard.configNginxBaseUrl({ outDir: outDir2, oas: OAS_PATH, sla: SLA_DIR });
636
+
637
+ const read = (dir, file) => fs.readFileSync(path.join(dir, file), "utf8");
638
+ expect(read(outDir2, "nginx.conf")).to.equal(read(outDir, "nginx.conf"));
639
+
640
+ const files = fs.readdirSync(path.join(outDir, "conf.d")).filter((f) => f.endsWith(".conf"));
641
+ for (const file of files) {
642
+ expect(read(path.join(outDir2, "conf.d"), file)).to.equal(read(path.join(outDir, "conf.d"), file));
643
+ }
644
+ });
645
+
646
+ // ── single SLA file input ────────────────────────────────────────────────
647
+
648
+ it("accepts a single SLA file (not a directory) as input", function () {
649
+ const singleOut = path.join(OUTPUT_DIR, "prog-single-sla");
650
+ expect(() =>
651
+ slaWizard.configNginxBaseUrl({ outDir: singleOut, oas: OAS_PATH, sla: SLA_FILE }),
652
+ ).to.not.throw();
653
+ expect(fs.existsSync(path.join(singleOut, "nginx.conf"))).to.be.true;
654
+ });
655
+ });
656
+
657
+ // ══════════════════════════════════════════════════════════════════════════
658
+ // 5. addToBaseUrlConfd — programmatic integration tests
659
+ // ══════════════════════════════════════════════════════════════════════════
660
+
661
+ describe("addToBaseUrlConfd (programmatic)", function () {
662
+ const outDir = path.join(OUTPUT_DIR, "prog-add-to-baseurl-confd");
663
+
664
+ before(function () {
665
+ slaWizard.addToBaseUrlConfd({ outDir, oas: OAS_PATH, sla: SLA_DIR });
666
+ });
667
+
668
+ it("does NOT generate nginx.conf", function () {
669
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.false;
670
+ });
671
+
672
+ it("generates conf.d directory with .conf files", function () {
673
+ const files = fs.readdirSync(path.join(outDir, "conf.d")).filter((f) => f.endsWith(".conf"));
674
+ expect(files.length).to.be.greaterThan(0);
675
+ });
676
+
677
+ it("conf.d chatgpt location blocks use http://localhost:8001", function () {
678
+ const confDDir = path.join(outDir, "conf.d");
679
+ const allContent = fs
680
+ .readdirSync(confDDir)
681
+ .filter((f) => f.endsWith(".conf"))
682
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
683
+ .join("\n");
684
+
685
+ const passes = allProxyPassesInBlocks(allContent, CHATGPT_SANITIZED);
686
+ expect(passes.length).to.be.greaterThan(0);
687
+ passes.forEach((p) => expect(p).to.include(CHATGPT_URL));
688
+ });
689
+
690
+ it("conf.d claude location blocks use http://localhost:8002", function () {
691
+ const confDDir = path.join(outDir, "conf.d");
692
+ const allContent = fs
693
+ .readdirSync(confDDir)
694
+ .filter((f) => f.endsWith(".conf"))
695
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
696
+ .join("\n");
697
+
698
+ const passes = allProxyPassesInBlocks(allContent, CLAUDE_SANITIZED);
699
+ expect(passes.length).to.be.greaterThan(0);
700
+ passes.forEach((p) => expect(p).to.include(CLAUDE_URL));
701
+ });
702
+
703
+ it("strip transform also applied: rewrite uses stripped path in conf.d files", function () {
704
+ const confDDir = path.join(outDir, "conf.d");
705
+ const allContent = fs
706
+ .readdirSync(confDDir)
707
+ .filter((f) => f.endsWith(".conf"))
708
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
709
+ .join("\n");
710
+
711
+ // Any rewrite for chatgpt or claude must already use the stripped path
712
+ const rewrites = allContent
713
+ .split("\n")
714
+ .filter((l) => l.trim().startsWith("rewrite") &&
715
+ (l.includes(CHATGPT_SANITIZED) || l.includes(CLAUDE_SANITIZED)));
716
+
717
+ expect(rewrites.length).to.be.greaterThan(0);
718
+ rewrites.forEach((r) => {
719
+ expect(r).to.not.include("$uri_original");
720
+ expect(r).to.include("/v1/chat/completions");
721
+ });
722
+ });
723
+
724
+ it("accepts a single SLA file as input", function () {
725
+ const singleOut = path.join(OUTPUT_DIR, "prog-add-single-sla");
726
+ expect(() =>
727
+ slaWizard.addToBaseUrlConfd({ outDir: singleOut, oas: OAS_PATH, sla: SLA_FILE }),
728
+ ).to.not.throw();
729
+ expect(fs.existsSync(path.join(singleOut, "conf.d"))).to.be.true;
730
+ });
731
+ });
732
+
733
+ // ══════════════════════════════════════════════════════════════════════════
734
+ // 6. CLI tests
735
+ // ══════════════════════════════════════════════════════════════════════════
736
+
737
+ describe("CLI Usage", function () {
738
+ it("config-nginx-baseurl generates nginx.conf and conf.d/", function () {
739
+ const outDir = path.join(OUTPUT_DIR, "cli-config-nginx-baseurl");
740
+ execSync(`node "${CLI_PATH}" config-nginx-baseurl -o "${outDir}" --oas "${OAS_PATH}" --sla "${SLA_DIR}"`);
741
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.true;
742
+ expect(fs.existsSync(path.join(outDir, "conf.d"))).to.be.true;
743
+ });
744
+
745
+ it("add-to-baseurl-confd generates conf.d/ without nginx.conf", function () {
746
+ const outDir = path.join(OUTPUT_DIR, "cli-add-to-baseurl-confd");
747
+ execSync(`node "${CLI_PATH}" add-to-baseurl-confd -o "${outDir}" --oas "${OAS_PATH}" --sla "${SLA_DIR}"`);
748
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.false;
749
+ expect(fs.existsSync(path.join(outDir, "conf.d"))).to.be.true;
750
+ });
751
+
752
+ it("CLI conf.d files have the correct proxy_pass per backend", function () {
753
+ const outDir = path.join(OUTPUT_DIR, "cli-baseurl-verify");
754
+ execSync(`node "${CLI_PATH}" config-nginx-baseurl -o "${outDir}" --oas "${OAS_PATH}" --sla "${SLA_DIR}"`);
755
+
756
+ const confDDir = path.join(outDir, "conf.d");
757
+ const allContent = fs
758
+ .readdirSync(confDDir)
759
+ .filter((f) => f.endsWith(".conf"))
760
+ .map((f) => fs.readFileSync(path.join(confDDir, f), "utf8"))
761
+ .join("\n");
762
+
763
+ const chatgptPasses = allProxyPassesInBlocks(allContent, CHATGPT_SANITIZED);
764
+ const claudePasses = allProxyPassesInBlocks(allContent, CLAUDE_SANITIZED);
765
+
766
+ expect(chatgptPasses.length).to.be.greaterThan(0);
767
+ chatgptPasses.forEach((p) => {
768
+ expect(p).to.include(CHATGPT_URL);
769
+ expect(p).to.not.include(DEFAULT_URL);
770
+ });
771
+
772
+ expect(claudePasses.length).to.be.greaterThan(0);
773
+ claudePasses.forEach((p) => {
774
+ expect(p).to.include(CLAUDE_URL);
775
+ expect(p).to.not.include(DEFAULT_URL);
776
+ });
777
+ });
778
+
779
+ it("CLI with a single SLA file (not a directory) runs without error", function () {
780
+ const outDir = path.join(OUTPUT_DIR, "cli-single-sla");
781
+ execSync(`node "${CLI_PATH}" config-nginx-baseurl -o "${outDir}" --oas "${OAS_PATH}" --sla "${SLA_FILE}"`);
782
+ expect(fs.existsSync(path.join(outDir, "nginx.conf"))).to.be.true;
783
+ });
784
+
785
+ it("--help lists config-nginx-baseurl command", function () {
786
+ const help = execSync(`node "${CLI_PATH}" --help`, { encoding: "utf8" });
787
+ expect(help).to.include("config-nginx-baseurl");
788
+ });
789
+
790
+ it("--help lists add-to-baseurl-confd command", function () {
791
+ const help = execSync(`node "${CLI_PATH}" --help`, { encoding: "utf8" });
792
+ expect(help).to.include("add-to-baseurl-confd");
793
+ });
794
+
795
+ it("config-nginx-baseurl --help shows --oas and --sla options", function () {
796
+ const help = execSync(`node "${CLI_PATH}" config-nginx-baseurl --help`, {
797
+ encoding: "utf8",
798
+ });
799
+ expect(help).to.include("--oas");
800
+ expect(help).to.include("--sla");
801
+ expect(help).to.include("--outDir");
802
+ });
803
+
804
+ it("CLI exits with non-zero code when required --outDir is missing", function () {
805
+ let threw = false;
806
+ try {
807
+ execSync(`node "${CLI_PATH}" config-nginx-baseurl --oas "${OAS_PATH}" --sla "${SLA_DIR}"`, {
808
+ stdio: "pipe",
809
+ });
810
+ } catch {
811
+ threw = true;
812
+ }
813
+ expect(threw).to.be.true;
814
+ });
815
+
816
+ it("CLI stdout contains success messages for both transforms", function () {
817
+ const outDir = path.join(OUTPUT_DIR, "cli-stdout-check");
818
+ const output = execSync(
819
+ `node "${CLI_PATH}" config-nginx-baseurl -o "${outDir}" --oas "${OAS_PATH}" --sla "${SLA_DIR}"`,
820
+ { encoding: "utf8" },
821
+ );
822
+ expect(output).to.include("x-nginx-strip transformations applied");
823
+ expect(output).to.include("x-nginx-server-baseurl transformations applied");
824
+ });
825
+ });
826
+ });