@rerouter/helper 0.0.0-stage → 0.1.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.
@@ -0,0 +1,215 @@
1
+ import { afterEach, beforeEach, describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { promises as fs } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { parseJsonc } from "./lib/files.js";
7
+ import { applyPlan, guide, planTarget, snippet } from "./lib/targets.js";
8
+
9
+ let sandbox;
10
+ let savedHome;
11
+ let savedUserProfile;
12
+
13
+ beforeEach(async () => {
14
+ sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "rerouter-helper-"));
15
+ savedHome = process.env.HOME;
16
+ savedUserProfile = process.env.USERPROFILE;
17
+ process.env.HOME = sandbox;
18
+ process.env.USERPROFILE = sandbox;
19
+ });
20
+
21
+ afterEach(async () => {
22
+ process.env.HOME = savedHome;
23
+ process.env.USERPROFILE = savedUserProfile;
24
+ await fs.rm(sandbox, { recursive: true, force: true });
25
+ });
26
+
27
+ const ctx = { baseUrl: "https://api.test.local", model: "acme/pro-1", smallModel: "acme/mini-1" };
28
+
29
+ describe("parseJsonc", () => {
30
+ it("handles comments and trailing commas", () => {
31
+ const parsed = parseJsonc(`{
32
+ // line comment
33
+ "a": 1,
34
+ /* block */
35
+ "url": "https://x/y", // not a comment inside a string
36
+ }`);
37
+ assert.deepEqual(parsed, { a: 1, url: "https://x/y" });
38
+ });
39
+
40
+ it("rejects broken JSON", () => {
41
+ assert.throws(() => parseJsonc("{ not json"));
42
+ });
43
+ });
44
+
45
+ describe("planTarget claude", () => {
46
+ it("merges settings.json, preserves other keys, does not write the key", async () => {
47
+ const file = path.join(sandbox, ".claude", "settings.json");
48
+ await fs.mkdir(path.dirname(file), { recursive: true });
49
+ await fs.writeFile(file, JSON.stringify({ theme: "dark", env: { FOO: "bar", ANTHROPIC_API_KEY: "old" } }));
50
+
51
+ const plan = planTarget("claude", ctx);
52
+ const content = await plan.build();
53
+ const next = JSON.parse(content);
54
+
55
+ assert.equal(next.theme, "dark");
56
+ assert.equal(next.env.FOO, "bar");
57
+ assert.equal(next.env.ANTHROPIC_BASE_URL, "https://api.test.local");
58
+ assert.equal(next.env.ANTHROPIC_MODEL, "acme/pro-1");
59
+ assert.equal(next.env.ANTHROPIC_SMALL_FAST_MODEL, "acme/mini-1");
60
+ assert.equal("ANTHROPIC_API_KEY" in next.env, false);
61
+ assert.equal("ANTHROPIC_AUTH_TOKEN" in next.env, false);
62
+ assert.ok(!content.includes("sk-rr"), "output must not contain the key");
63
+ });
64
+
65
+ it("rejects broken settings.json without modifying the file", async () => {
66
+ const file = path.join(sandbox, ".claude", "settings.json");
67
+ await fs.mkdir(path.dirname(file), { recursive: true });
68
+ await fs.writeFile(file, "{ totally broken");
69
+ const plan = planTarget("claude", ctx);
70
+ await assert.rejects(plan.build(), /could not be parsed/);
71
+ assert.equal(await fs.readFile(file, "utf8"), "{ totally broken");
72
+ });
73
+ });
74
+
75
+ describe("applyPlan", () => {
76
+ it("writes a new file and creates a backup on overwrite", async () => {
77
+ const plan = planTarget("claude", ctx);
78
+ const first = await plan.build();
79
+ const results1 = await applyPlan(plan, first);
80
+ assert.equal(results1[0].backupPath, null);
81
+
82
+ const second = await plan.build();
83
+ const results2 = await applyPlan(plan, second);
84
+ assert.match(results2[0].backupPath, /\.rerouter-backup-\d{14}$/);
85
+ const backup = await fs.readFile(results2[0].backupPath, "utf8");
86
+ assert.equal(backup, first);
87
+ });
88
+ });
89
+
90
+ describe("planTarget continue", () => {
91
+ it("writes config.yaml + .env placeholder without the key", async () => {
92
+ const plan = planTarget("continue", ctx);
93
+ const content = await plan.build();
94
+ assert.match(content, /apiBase: https:\/\/api\.test\.local\/v1/);
95
+ assert.match(content, /apiKey: \$\{\{ secrets\.REROUTER_API_KEY \}\}/);
96
+ assert.ok(!content.includes("sk-rr"));
97
+
98
+ const results = await applyPlan(plan, content);
99
+ assert.equal(results.length, 2);
100
+ const dotenv = await fs.readFile(path.join(sandbox, ".continue", ".env"), "utf8");
101
+ assert.match(dotenv, /^REROUTER_API_KEY=$/m);
102
+ // No real key assignment (value after '=' must be empty).
103
+ assert.ok(!/^REROUTER_API_KEY=\S/m.test(dotenv));
104
+
105
+ // second apply does not duplicate the .env line
106
+ const results2 = await applyPlan(plan, content);
107
+ assert.equal(results2.length, 1);
108
+ });
109
+ });
110
+
111
+ describe("planTarget opencode", () => {
112
+ it("merges rerouter provider with env reference", async () => {
113
+ const plan = planTarget("opencode", ctx);
114
+ const content = await plan.build();
115
+ const next = JSON.parse(content);
116
+ assert.equal(next.provider.rerouter.options.baseURL, "https://api.test.local/v1");
117
+ assert.equal(next.provider.rerouter.options.apiKey, "{env:REROUTER_API_KEY}");
118
+ assert.equal(next.provider.rerouter.models["acme/pro-1"].name, "acme/pro-1");
119
+ assert.equal(next.model, "rerouter/acme/pro-1");
120
+ });
121
+ });
122
+
123
+ describe("planTarget codex", () => {
124
+ it("writes a new config.toml with local bridge provider", async () => {
125
+ const plan = planTarget("codex", ctx);
126
+ const content = await plan.build();
127
+
128
+ assert.match(content, /^model_provider = "rerouter"$/m);
129
+ assert.match(content, /^model = "acme\/pro-1"$/m);
130
+ assert.match(content, /^\[model_providers\.rerouter\]$/m);
131
+ assert.match(content, /base_url = "http:\/\/127\.0\.0\.1:8787\/v1"/);
132
+ assert.match(content, /env_key = "REROUTER_API_KEY"/);
133
+ assert.match(content, /wire_api = "responses"/);
134
+ assert.ok(!content.includes("sk-rr"), "output must not contain the key");
135
+ // Notes mention the bridge
136
+ assert.ok(plan.notes.some((n) => n.includes("bridge")));
137
+ });
138
+
139
+ it("honors custom --port", async () => {
140
+ const plan = planTarget("codex", { ...ctx, port: 9911 });
141
+ const content = await plan.build();
142
+ assert.match(content, /base_url = "http:\/\/127\.0\.0\.1:9911\/v1"/);
143
+ assert.ok(plan.notes.some((n) => n.includes("--port 9911")));
144
+ });
145
+
146
+ it("merges into an existing config.toml, preserves other sections, idempotent", async () => {
147
+ const file = path.join(sandbox, ".codex", "config.toml");
148
+ await fs.mkdir(path.dirname(file), { recursive: true });
149
+ await fs.writeFile(
150
+ file,
151
+ [
152
+ "# personal config",
153
+ 'theme = "dark"',
154
+ "",
155
+ "[profiles.work]",
156
+ 'model = "other"',
157
+ "",
158
+ ].join("\n"),
159
+ );
160
+
161
+ const plan = planTarget("codex", ctx);
162
+ const first = await plan.build();
163
+ assert.match(first, /^theme = "dark"$/m);
164
+ assert.match(first, /^\[profiles\.work\]$/m);
165
+ assert.match(first, /^model_provider = "rerouter"$/m);
166
+
167
+ // apply twice — the provider block is not duplicated
168
+ await applyPlan(plan, first);
169
+ const second = await plan.build();
170
+ const occurrences = second.match(/^\[model_providers\.rerouter\]$/gm) ?? [];
171
+ assert.equal(occurrences.length, 1);
172
+ });
173
+
174
+ it("updates existing model_provider/model without duplicating keys", async () => {
175
+ const file = path.join(sandbox, ".codex", "config.toml");
176
+ await fs.mkdir(path.dirname(file), { recursive: true });
177
+ await fs.writeFile(file, 'model_provider = "openai"\nmodel = "old-gpt"\n');
178
+
179
+ const plan = planTarget("codex", ctx);
180
+ const content = await plan.build();
181
+ assert.equal((content.match(/^model_provider\s*=/gm) ?? []).length, 1);
182
+ assert.equal((content.match(/^model\s*=/gm) ?? []).length, 1);
183
+ assert.match(content, /^model_provider = "rerouter"$/m);
184
+ assert.match(content, /^model = "acme\/pro-1"$/m);
185
+ });
186
+
187
+ it("rejects ambiguous top-scope without modifying the file", async () => {
188
+ const file = path.join(sandbox, ".codex", "config.toml");
189
+ await fs.mkdir(path.dirname(file), { recursive: true });
190
+ const original = "this is not valid toml {{{\n";
191
+ await fs.writeFile(file, original);
192
+
193
+ const plan = planTarget("codex", ctx);
194
+ await assert.rejects(plan.build(), /merge manually/);
195
+ assert.equal(await fs.readFile(file, "utf8"), original);
196
+ });
197
+ });
198
+
199
+ describe("snippet & guide", () => {
200
+ it("snippet never contains the key, only env references", () => {
201
+ const sh = snippet("anthropic", ctx);
202
+ assert.match(sh, /ANTHROPIC_BASE_URL="https:\/\/api\.test\.local"/);
203
+ assert.match(sh, /\$REROUTER_API_KEY/);
204
+ const oi = snippet("openai", ctx);
205
+ assert.match(oi, /OPENAI_BASE_URL="https:\/\/api\.test\.local\/v1"/);
206
+ });
207
+
208
+ it("guide contains the base URL and model", () => {
209
+ for (const t of ["cursor", "vscode", "cline", "roo"]) {
210
+ const text = guide(t, ctx);
211
+ assert.ok(text.includes("https://api.test.local/v1"), t);
212
+ assert.ok(text.includes("acme/pro-1"), t);
213
+ }
214
+ });
215
+ });
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.