cursor-route 0.1.6 → 0.1.8
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/CHANGELOG.md +40 -0
- package/CONTRIBUTING.md +14 -5
- package/README.md +60 -10
- package/dist/adapters/claude-ds.js +43 -29
- package/dist/adapters/deepseek.js +127 -8
- package/dist/cli.js +37 -13
- package/dist/config.js +48 -12
- package/dist/jobs.js +28 -3
- package/docs/DEMO_GIF.md +19 -2
- package/docs/briefs/WORKING.md +18 -7
- package/docs/demo-notes.md +5 -3
- package/docs/fixtures/generate-hero-demo.sh +109 -0
- package/docs/fixtures/hero-demo.log +52 -0
- package/llms.txt +9 -2
- package/package.json +2 -1
- package/skills/route-orch/SKILL.md +15 -2
- package/src/adapters/claude-ds.ts +45 -27
- package/src/adapters/deepseek.ts +145 -11
- package/src/adapters/types.ts +5 -1
- package/src/cli.test.ts +506 -19
- package/src/cli.ts +41 -13
- package/src/config.ts +59 -10
- package/src/jobs.ts +30 -5
package/src/cli.test.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
chmodSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { resolveWorker, startJob } from "./jobs.ts";
|
|
14
|
+
import { config, resolveDsModel, resolveDsModelAlias } from "./config.ts";
|
|
4
15
|
import { shellQuote, newJobId } from "./util.ts";
|
|
5
16
|
import { runHealth } from "./health.ts";
|
|
6
17
|
import { looksLikeSecretMaterial, redactSecrets } from "./secrets.ts";
|
|
7
18
|
import { isDeepSeekRouted, isDeepSeekBaseUrl, claudeDsAdapter } from "./adapters/claude-ds.ts";
|
|
8
|
-
import { deepseekAdapter } from "./adapters/deepseek.ts";
|
|
19
|
+
import { deepseekAdapter, patchPathForPrompt } from "./adapters/deepseek.ts";
|
|
20
|
+
import { grokAdapter } from "./adapters/grok.ts";
|
|
9
21
|
|
|
10
22
|
describe("resolveWorker", () => {
|
|
11
23
|
test("lane easy → openrouter", () => {
|
|
@@ -30,27 +42,32 @@ describe("resolveWorker", () => {
|
|
|
30
42
|
});
|
|
31
43
|
});
|
|
32
44
|
|
|
33
|
-
describe("
|
|
45
|
+
describe("resolveDsModel", () => {
|
|
34
46
|
test("defaults to flash", () => {
|
|
35
|
-
expect(
|
|
47
|
+
expect(resolveDsModel()).toEqual({ alias: "flash", id: "deepseek-v4-flash" });
|
|
36
48
|
expect(resolveDsModelAlias("")).toBe("flash");
|
|
37
49
|
});
|
|
38
50
|
test("accepts aliases and full ids", () => {
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
expect(resolveDsModel("flash").id).toBe("deepseek-v4-flash");
|
|
52
|
+
expect(resolveDsModel("pro").id).toBe("deepseek-v4-pro");
|
|
53
|
+
expect(resolveDsModel("deepseek-v4-pro[1m]")).toEqual({
|
|
54
|
+
alias: "pro",
|
|
55
|
+
id: "deepseek-v4-pro[1m]",
|
|
56
|
+
});
|
|
44
57
|
});
|
|
45
58
|
test("rejects unknown", () => {
|
|
46
|
-
expect(() =>
|
|
59
|
+
expect(() => resolveDsModel("opus")).toThrow(/flash\|pro/);
|
|
47
60
|
});
|
|
48
61
|
});
|
|
49
62
|
|
|
50
63
|
describe("claude-ds -Model", () => {
|
|
51
64
|
test("shim path passes -Model deepseek-v4-flash by default", () => {
|
|
52
65
|
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
66
|
+
const prevDs = process.env.CURSOR_ROUTE_DS_MODEL;
|
|
67
|
+
const prevAm = process.env.ANTHROPIC_MODEL;
|
|
53
68
|
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
69
|
+
delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
70
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
54
71
|
try {
|
|
55
72
|
const plan = claudeDsAdapter.buildLaunch({
|
|
56
73
|
promptFile: "/tmp/p.prompt",
|
|
@@ -62,6 +79,10 @@ describe("claude-ds -Model", () => {
|
|
|
62
79
|
} finally {
|
|
63
80
|
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
64
81
|
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
82
|
+
if (prevDs === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
83
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prevDs;
|
|
84
|
+
if (prevAm === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
85
|
+
else process.env.ANTHROPIC_MODEL = prevAm;
|
|
65
86
|
}
|
|
66
87
|
});
|
|
67
88
|
test("pro alias maps to deepseek-v4-pro", () => {
|
|
@@ -81,18 +102,454 @@ describe("claude-ds -Model", () => {
|
|
|
81
102
|
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
82
103
|
}
|
|
83
104
|
});
|
|
105
|
+
test("preserves deepseek-v4-pro[1m] via modelId", () => {
|
|
106
|
+
const prev = process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
107
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
108
|
+
try {
|
|
109
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
110
|
+
promptFile: "/tmp/p.prompt",
|
|
111
|
+
cwd: "/tmp",
|
|
112
|
+
alwaysApprove: true,
|
|
113
|
+
model: "pro",
|
|
114
|
+
modelId: "deepseek-v4-pro[1m]",
|
|
115
|
+
});
|
|
116
|
+
expect(plan.command).toContain("deepseek-v4-pro[1m]");
|
|
117
|
+
} finally {
|
|
118
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
119
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
test("Anthropic escape hatch omits DeepSeek --model", () => {
|
|
123
|
+
const shimDir = join(tmpdir(), `cr-anth-escape-${process.pid}`);
|
|
124
|
+
mkdirSync(shimDir, { recursive: true });
|
|
125
|
+
const claude = join(shimDir, "claude");
|
|
126
|
+
writeFileSync(claude, "#!/bin/sh\necho ok\n");
|
|
127
|
+
chmodSync(claude, 0o755);
|
|
128
|
+
|
|
129
|
+
const prev = {
|
|
130
|
+
bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
|
|
131
|
+
allow: process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC,
|
|
132
|
+
base: process.env.ANTHROPIC_BASE_URL,
|
|
133
|
+
path: process.env.PATH,
|
|
134
|
+
};
|
|
135
|
+
delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
136
|
+
// Explicit non-DeepSeek base so ~/.claude/settings.json cannot force DeepSeek routing
|
|
137
|
+
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
138
|
+
process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = "1";
|
|
139
|
+
process.env.PATH = `${shimDir}:/usr/bin:/bin`;
|
|
140
|
+
try {
|
|
141
|
+
const plan = claudeDsAdapter.buildLaunch({
|
|
142
|
+
promptFile: "/tmp/p.prompt",
|
|
143
|
+
cwd: "/tmp",
|
|
144
|
+
alwaysApprove: true,
|
|
145
|
+
model: "pro",
|
|
146
|
+
});
|
|
147
|
+
expect(plan.command).toContain(claude);
|
|
148
|
+
expect(plan.command).not.toContain("deepseek-v4");
|
|
149
|
+
expect(plan.command).not.toContain("--model");
|
|
150
|
+
} finally {
|
|
151
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
152
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
|
|
153
|
+
if (prev.allow === undefined) delete process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC;
|
|
154
|
+
else process.env.CURSOR_ROUTE_ALLOW_ANTHROPIC = prev.allow;
|
|
155
|
+
if (prev.base === undefined) delete process.env.ANTHROPIC_BASE_URL;
|
|
156
|
+
else process.env.ANTHROPIC_BASE_URL = prev.base;
|
|
157
|
+
process.env.PATH = prev.path || "";
|
|
158
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
84
161
|
});
|
|
85
162
|
|
|
86
|
-
describe("
|
|
87
|
-
test("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
163
|
+
describe("startJob product path", () => {
|
|
164
|
+
test("CURSOR_ROUTE_DS_MODEL=pro applies when --model unset", () => {
|
|
165
|
+
const prev = {
|
|
166
|
+
bin: process.env.CURSOR_ROUTE_CLAUDE_DS_BIN,
|
|
167
|
+
ds: process.env.CURSOR_ROUTE_DS_MODEL,
|
|
168
|
+
am: process.env.ANTHROPIC_MODEL,
|
|
169
|
+
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
170
|
+
};
|
|
171
|
+
const jobsDir = join(tmpdir(), `cr-jobs-ds-${process.pid}`);
|
|
172
|
+
mkdirSync(jobsDir, { recursive: true });
|
|
173
|
+
process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = "/tmp/fake-claude-ds";
|
|
174
|
+
process.env.CURSOR_ROUTE_DS_MODEL = "pro";
|
|
175
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
176
|
+
process.env.CURSOR_ROUTE_JOBS_DIR = jobsDir;
|
|
177
|
+
try {
|
|
178
|
+
const result = startJob({
|
|
179
|
+
prompt: "ping",
|
|
180
|
+
lane: "mid",
|
|
181
|
+
dryRun: true,
|
|
182
|
+
});
|
|
183
|
+
expect(result.ok).toBe(true);
|
|
184
|
+
if (!result.ok) return;
|
|
185
|
+
expect(result.job.model).toBe("pro");
|
|
186
|
+
expect(result.command).toContain("deepseek-v4-pro");
|
|
187
|
+
expect(result.command).not.toContain("deepseek-v4-flash");
|
|
188
|
+
} finally {
|
|
189
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_CLAUDE_DS_BIN;
|
|
190
|
+
else process.env.CURSOR_ROUTE_CLAUDE_DS_BIN = prev.bin;
|
|
191
|
+
if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
192
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
|
|
193
|
+
if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
194
|
+
else process.env.ANTHROPIC_MODEL = prev.am;
|
|
195
|
+
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
196
|
+
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
197
|
+
rmSync(jobsDir, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("--worker deepseek dry-run succeeds with fake dsh + key", () => {
|
|
202
|
+
const dir = join(tmpdir(), `cr-dsh-start-${process.pid}`);
|
|
203
|
+
mkdirSync(dir, { recursive: true });
|
|
204
|
+
const bin = join(dir, "dsh");
|
|
205
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-dsh\n");
|
|
206
|
+
chmodSync(bin, 0o755);
|
|
207
|
+
const prev = {
|
|
208
|
+
bin: process.env.CURSOR_ROUTE_DSH_BIN,
|
|
209
|
+
key: process.env.DEEPSEEK_API_KEY,
|
|
210
|
+
ds: process.env.CURSOR_ROUTE_DS_MODEL,
|
|
211
|
+
am: process.env.ANTHROPIC_MODEL,
|
|
212
|
+
jobs: process.env.CURSOR_ROUTE_JOBS_DIR,
|
|
213
|
+
};
|
|
214
|
+
process.env.CURSOR_ROUTE_DSH_BIN = bin;
|
|
215
|
+
// Not a real key: short and dash-separated so the CLI refuse gate would not trip.
|
|
216
|
+
process.env.DEEPSEEK_API_KEY = "sk-test-not-a-real-key";
|
|
217
|
+
delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
218
|
+
delete process.env.ANTHROPIC_MODEL;
|
|
219
|
+
process.env.CURSOR_ROUTE_JOBS_DIR = join(dir, "jobs");
|
|
220
|
+
try {
|
|
221
|
+
const result = startJob({
|
|
222
|
+
prompt: "ping",
|
|
223
|
+
worker: "deepseek",
|
|
224
|
+
dryRun: true,
|
|
225
|
+
});
|
|
226
|
+
expect(result.ok).toBe(true);
|
|
227
|
+
if (!result.ok) return;
|
|
228
|
+
expect(result.job.worker).toBe("deepseek");
|
|
229
|
+
expect(result.job.model).toBe("flash");
|
|
230
|
+
expect(result.command).toContain("--profile headless");
|
|
231
|
+
expect(result.command).toContain("--patch");
|
|
232
|
+
expect(result.command).toContain("$(cat");
|
|
233
|
+
expect(result.command).toContain("DSH_PERMISSION_MODE");
|
|
234
|
+
expect(result.command).not.toContain("sk-test");
|
|
235
|
+
expect(result.command).not.toContain("npx");
|
|
236
|
+
// Dry-run keeps no durable artifacts (prompt + dsh patch both removed)
|
|
237
|
+
expect(readdirSync(join(dir, "jobs")).length).toBe(0);
|
|
238
|
+
} finally {
|
|
239
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_DSH_BIN;
|
|
240
|
+
else process.env.CURSOR_ROUTE_DSH_BIN = prev.bin;
|
|
241
|
+
if (prev.key === undefined) delete process.env.DEEPSEEK_API_KEY;
|
|
242
|
+
else process.env.DEEPSEEK_API_KEY = prev.key;
|
|
243
|
+
if (prev.ds === undefined) delete process.env.CURSOR_ROUTE_DS_MODEL;
|
|
244
|
+
else process.env.CURSOR_ROUTE_DS_MODEL = prev.ds;
|
|
245
|
+
if (prev.am === undefined) delete process.env.ANTHROPIC_MODEL;
|
|
246
|
+
else process.env.ANTHROPIC_MODEL = prev.am;
|
|
247
|
+
if (prev.jobs === undefined) delete process.env.CURSOR_ROUTE_JOBS_DIR;
|
|
248
|
+
else process.env.CURSOR_ROUTE_JOBS_DIR = prev.jobs;
|
|
249
|
+
rmSync(dir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("grok command never includes deepseek-v4", () => {
|
|
254
|
+
const prev = process.env.CURSOR_ROUTE_GROK_BIN;
|
|
255
|
+
process.env.CURSOR_ROUTE_GROK_BIN = "/tmp/fake-grok";
|
|
256
|
+
try {
|
|
257
|
+
const plan = grokAdapter.buildLaunch({
|
|
258
|
+
promptFile: "/tmp/p.prompt",
|
|
92
259
|
cwd: "/tmp",
|
|
93
260
|
alwaysApprove: true,
|
|
94
|
-
|
|
95
|
-
|
|
261
|
+
model: "pro",
|
|
262
|
+
});
|
|
263
|
+
expect(plan.command).not.toContain("deepseek-v4");
|
|
264
|
+
expect(plan.command).not.toMatch(/(^|[\s'-])Model\b/);
|
|
265
|
+
} finally {
|
|
266
|
+
if (prev === undefined) delete process.env.CURSOR_ROUTE_GROK_BIN;
|
|
267
|
+
else process.env.CURSOR_ROUTE_GROK_BIN = prev;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("deepseek adapter (dsh)", () => {
|
|
273
|
+
/** Executable fake `dsh` in a fresh tmpdir (existsSync passes). */
|
|
274
|
+
const makeFakeDsh = (suffix: string) => {
|
|
275
|
+
const dir = join(tmpdir(), `cr-dsh-${process.pid}-${suffix}`);
|
|
276
|
+
mkdirSync(dir, { recursive: true });
|
|
277
|
+
const bin = join(dir, "dsh");
|
|
278
|
+
writeFileSync(bin, "#!/bin/sh\necho fake-dsh\n");
|
|
279
|
+
chmodSync(bin, 0o755);
|
|
280
|
+
return { dir, bin };
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** Set/delete env for the duration of fn; restore afterwards. */
|
|
284
|
+
const withEnv = (
|
|
285
|
+
patch: Record<string, string | undefined>,
|
|
286
|
+
fn: () => void,
|
|
287
|
+
) => {
|
|
288
|
+
const prev = new Map<string, string | undefined>();
|
|
289
|
+
for (const k of Object.keys(patch)) {
|
|
290
|
+
prev.set(k, process.env[k]);
|
|
291
|
+
const v = patch[k];
|
|
292
|
+
if (v === undefined) delete process.env[k];
|
|
293
|
+
else process.env[k] = v;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
fn();
|
|
297
|
+
} finally {
|
|
298
|
+
for (const [k, v] of prev) {
|
|
299
|
+
if (v === undefined) delete process.env[k];
|
|
300
|
+
else process.env[k] = v;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
test("health: no binary and no key → not ok, install hint names the npm package", () => {
|
|
306
|
+
withEnv(
|
|
307
|
+
{
|
|
308
|
+
CURSOR_ROUTE_DSH_BIN: join(tmpdir(), `missing-dsh-${process.pid}`),
|
|
309
|
+
DEEPSEEK_API_KEY: undefined,
|
|
310
|
+
},
|
|
311
|
+
() => {
|
|
312
|
+
const h = deepseekAdapter.health();
|
|
313
|
+
expect(h.ok).toBe(false);
|
|
314
|
+
expect(h.detail).toMatch(/npm i -g @deepseek-ai\/dsh/);
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("health: binary but no key → not ok, tells operator to export DEEPSEEK_API_KEY", () => {
|
|
320
|
+
const { bin } = makeFakeDsh("nokey");
|
|
321
|
+
withEnv({ CURSOR_ROUTE_DSH_BIN: bin, DEEPSEEK_API_KEY: undefined }, () => {
|
|
322
|
+
const h = deepseekAdapter.health();
|
|
323
|
+
expect(h.ok).toBe(false);
|
|
324
|
+
expect(h.detail).toMatch(/DEEPSEEK_API_KEY/);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("health: binary + key → ok", () => {
|
|
329
|
+
const { bin } = makeFakeDsh("ok");
|
|
330
|
+
withEnv({ CURSOR_ROUTE_DSH_BIN: bin, DEEPSEEK_API_KEY: "test-key" }, () => {
|
|
331
|
+
const h = deepseekAdapter.health();
|
|
332
|
+
expect(h.ok).toBe(true);
|
|
333
|
+
expect(h.binary).toBe(bin);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("buildLaunch: flash default patch next to prompt; key only in env", () => {
|
|
338
|
+
const { dir, bin } = makeFakeDsh("flash");
|
|
339
|
+
const promptFile = join(dir, "job.prompt");
|
|
340
|
+
writeFileSync(promptFile, "ping");
|
|
341
|
+
withEnv(
|
|
342
|
+
{
|
|
343
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
344
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
345
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
346
|
+
ANTHROPIC_MODEL: undefined,
|
|
347
|
+
},
|
|
348
|
+
() => {
|
|
349
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
350
|
+
promptFile,
|
|
351
|
+
cwd: dir,
|
|
352
|
+
alwaysApprove: true,
|
|
353
|
+
});
|
|
354
|
+
expect(plan.command).toContain("--profile headless");
|
|
355
|
+
expect(plan.command).toContain("--patch");
|
|
356
|
+
expect(plan.command).toContain("$(cat");
|
|
357
|
+
expect(plan.command).not.toContain("npx");
|
|
358
|
+
expect(plan.command).not.toContain("test-key");
|
|
359
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("danger-full-access");
|
|
360
|
+
expect(plan.env?.DEEPSEEK_API_KEY).toBe("test-key");
|
|
361
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
362
|
+
expect(patch).toContain("agent-default-model");
|
|
363
|
+
expect(patch).toContain("provider: deepseek-official");
|
|
364
|
+
expect(patch).toContain("deepseek-v4-flash");
|
|
365
|
+
expect(patch).not.toContain("deepseek-v4-pro");
|
|
366
|
+
expect(patch).not.toContain("test-key");
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
rmSync(dir, { recursive: true, force: true });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("buildLaunch: pro model → deepseek-v4-pro in patch", () => {
|
|
373
|
+
const { dir, bin } = makeFakeDsh("pro");
|
|
374
|
+
const promptFile = join(dir, "job.prompt");
|
|
375
|
+
writeFileSync(promptFile, "ping");
|
|
376
|
+
withEnv(
|
|
377
|
+
{
|
|
378
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
379
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
380
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
381
|
+
ANTHROPIC_MODEL: undefined,
|
|
382
|
+
},
|
|
383
|
+
() => {
|
|
384
|
+
deepseekAdapter.buildLaunch({
|
|
385
|
+
promptFile,
|
|
386
|
+
cwd: dir,
|
|
387
|
+
alwaysApprove: true,
|
|
388
|
+
model: "pro",
|
|
389
|
+
});
|
|
390
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
391
|
+
expect(patch).toContain("deepseek-v4-pro");
|
|
392
|
+
expect(patch).not.toContain("deepseek-v4-flash");
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
rmSync(dir, { recursive: true, force: true });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("buildLaunch: preserves deepseek-v4-pro[1m] in patch via modelId", () => {
|
|
399
|
+
const { dir, bin } = makeFakeDsh("pro1m");
|
|
400
|
+
const promptFile = join(dir, "job.prompt");
|
|
401
|
+
writeFileSync(promptFile, "ping");
|
|
402
|
+
withEnv(
|
|
403
|
+
{
|
|
404
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
405
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
406
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
407
|
+
ANTHROPIC_MODEL: undefined,
|
|
408
|
+
},
|
|
409
|
+
() => {
|
|
410
|
+
deepseekAdapter.buildLaunch({
|
|
411
|
+
promptFile,
|
|
412
|
+
cwd: dir,
|
|
413
|
+
alwaysApprove: true,
|
|
414
|
+
model: "pro",
|
|
415
|
+
modelId: "deepseek-v4-pro[1m]",
|
|
416
|
+
});
|
|
417
|
+
const patch = readFileSync(join(dir, "job.dsh-patch.yml"), "utf8");
|
|
418
|
+
expect(patch).toContain("deepseek-v4-pro[1m]");
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
rmSync(dir, { recursive: true, force: true });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("buildLaunch: alwaysApprove false (--ask) → workspace-write", () => {
|
|
425
|
+
const { dir, bin } = makeFakeDsh("ask");
|
|
426
|
+
const promptFile = join(dir, "job.prompt");
|
|
427
|
+
writeFileSync(promptFile, "ping");
|
|
428
|
+
withEnv(
|
|
429
|
+
{
|
|
430
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
431
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
432
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
433
|
+
ANTHROPIC_MODEL: undefined,
|
|
434
|
+
},
|
|
435
|
+
() => {
|
|
436
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
437
|
+
promptFile,
|
|
438
|
+
cwd: dir,
|
|
439
|
+
alwaysApprove: false,
|
|
440
|
+
});
|
|
441
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("workspace-write");
|
|
442
|
+
expect(plan.env?.DSH_PERMISSION_MODE).not.toBe("danger-full-access");
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
rmSync(dir, { recursive: true, force: true });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("buildLaunch: CURSOR_ROUTE_ASK=1 opts out even when alwaysApprove true", () => {
|
|
449
|
+
const { dir, bin } = makeFakeDsh("ask-env");
|
|
450
|
+
const promptFile = join(dir, "job.prompt");
|
|
451
|
+
writeFileSync(promptFile, "ping");
|
|
452
|
+
withEnv(
|
|
453
|
+
{
|
|
454
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
455
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
456
|
+
CURSOR_ROUTE_ASK: "1",
|
|
457
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
458
|
+
ANTHROPIC_MODEL: undefined,
|
|
459
|
+
},
|
|
460
|
+
() => {
|
|
461
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
462
|
+
promptFile,
|
|
463
|
+
cwd: dir,
|
|
464
|
+
alwaysApprove: true,
|
|
465
|
+
});
|
|
466
|
+
expect(plan.env?.DSH_PERMISSION_MODE).toBe("workspace-write");
|
|
467
|
+
expect(plan.alwaysApprove).toBe(false);
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
rmSync(dir, { recursive: true, force: true });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("buildLaunch: missing binary falls back to plain `dsh` for dry-run", () => {
|
|
474
|
+
const { dir } = makeFakeDsh("nobin");
|
|
475
|
+
const promptFile = join(dir, "job.prompt");
|
|
476
|
+
writeFileSync(promptFile, "ping");
|
|
477
|
+
// Point PATH at an empty dir so `command -v dsh` finds nothing,
|
|
478
|
+
// regardless of what the dev machine has installed.
|
|
479
|
+
const empty = join(dir, "empty");
|
|
480
|
+
mkdirSync(empty, { recursive: true });
|
|
481
|
+
withEnv(
|
|
482
|
+
{
|
|
483
|
+
CURSOR_ROUTE_DSH_BIN: undefined,
|
|
484
|
+
DEEPSEEK_API_KEY: undefined,
|
|
485
|
+
PATH: empty,
|
|
486
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
487
|
+
ANTHROPIC_MODEL: undefined,
|
|
488
|
+
},
|
|
489
|
+
() => {
|
|
490
|
+
const plan = deepseekAdapter.buildLaunch({
|
|
491
|
+
promptFile,
|
|
492
|
+
cwd: dir,
|
|
493
|
+
alwaysApprove: true,
|
|
494
|
+
dryRun: true,
|
|
495
|
+
});
|
|
496
|
+
expect(plan.command).toContain("'dsh' --profile headless");
|
|
497
|
+
expect(plan.command).toContain("--patch");
|
|
498
|
+
expect(plan.env?.DEEPSEEK_API_KEY).toBeUndefined();
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
rmSync(dir, { recursive: true, force: true });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("buildLaunch: non-.prompt promptFile does not overwrite the prompt", () => {
|
|
505
|
+
const { dir, bin } = makeFakeDsh("noprompt-ext");
|
|
506
|
+
const promptFile = join(dir, "job");
|
|
507
|
+
writeFileSync(promptFile, "keep-me");
|
|
508
|
+
withEnv(
|
|
509
|
+
{
|
|
510
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
511
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
512
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
513
|
+
ANTHROPIC_MODEL: undefined,
|
|
514
|
+
},
|
|
515
|
+
() => {
|
|
516
|
+
expect(patchPathForPrompt(promptFile)).toBe(`${promptFile}.dsh-patch.yml`);
|
|
517
|
+
deepseekAdapter.buildLaunch({
|
|
518
|
+
promptFile,
|
|
519
|
+
cwd: dir,
|
|
520
|
+
alwaysApprove: true,
|
|
521
|
+
});
|
|
522
|
+
expect(readFileSync(promptFile, "utf8")).toBe("keep-me");
|
|
523
|
+
expect(existsSync(`${promptFile}.dsh-patch.yml`)).toBe(true);
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
rmSync(dir, { recursive: true, force: true });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("buildLaunch: rejects modelId that would break YAML", () => {
|
|
530
|
+
const { dir, bin } = makeFakeDsh("badid");
|
|
531
|
+
const promptFile = join(dir, "job.prompt");
|
|
532
|
+
writeFileSync(promptFile, "ping");
|
|
533
|
+
withEnv(
|
|
534
|
+
{
|
|
535
|
+
CURSOR_ROUTE_DSH_BIN: bin,
|
|
536
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
537
|
+
CURSOR_ROUTE_DS_MODEL: undefined,
|
|
538
|
+
ANTHROPIC_MODEL: undefined,
|
|
539
|
+
},
|
|
540
|
+
() => {
|
|
541
|
+
expect(() =>
|
|
542
|
+
deepseekAdapter.buildLaunch({
|
|
543
|
+
promptFile,
|
|
544
|
+
cwd: dir,
|
|
545
|
+
alwaysApprove: true,
|
|
546
|
+
model: "flash",
|
|
547
|
+
modelId: "x\n injected: true",
|
|
548
|
+
}),
|
|
549
|
+
).toThrow(/Invalid DeepSeek model id/);
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
rmSync(dir, { recursive: true, force: true });
|
|
96
553
|
});
|
|
97
554
|
});
|
|
98
555
|
|
|
@@ -153,11 +610,41 @@ describe("health", () => {
|
|
|
153
610
|
test("returns structured report", () => {
|
|
154
611
|
const r = runHealth();
|
|
155
612
|
expect(r.product).toBe("cursor-route");
|
|
613
|
+
expect(r.version).toBe("0.1.8");
|
|
156
614
|
expect(r.checks.length).toBeGreaterThan(3);
|
|
157
615
|
expect(r.checks.some((c) => c.name === "tmux")).toBe(true);
|
|
158
616
|
expect(r.checks.some((c) => c.name === "cursor_cli")).toBe(true);
|
|
159
617
|
});
|
|
160
618
|
|
|
619
|
+
test("config version is 0.1.8", () => {
|
|
620
|
+
expect(config.version).toBe("0.1.8");
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("OR-gate: ok can be true while worker:deepseek is false", () => {
|
|
624
|
+
const prev = {
|
|
625
|
+
relaxed: process.env.CURSOR_ROUTE_RELAXED,
|
|
626
|
+
bin: process.env.CURSOR_ROUTE_DSH_BIN,
|
|
627
|
+
key: process.env.DEEPSEEK_API_KEY,
|
|
628
|
+
};
|
|
629
|
+
process.env.CURSOR_ROUTE_RELAXED = "1";
|
|
630
|
+
// Force deepseek unhealthy even on machines that have a real dsh + key.
|
|
631
|
+
process.env.CURSOR_ROUTE_DSH_BIN = join(tmpdir(), `missing-dsh-${process.pid}`);
|
|
632
|
+
delete process.env.DEEPSEEK_API_KEY;
|
|
633
|
+
try {
|
|
634
|
+
const r = runHealth();
|
|
635
|
+
const ds = r.checks.find((c) => c.name === "worker:deepseek");
|
|
636
|
+
expect(ds?.ok).toBe(false);
|
|
637
|
+
expect(r.ok).toBe(true);
|
|
638
|
+
} finally {
|
|
639
|
+
if (prev.relaxed === undefined) delete process.env.CURSOR_ROUTE_RELAXED;
|
|
640
|
+
else process.env.CURSOR_ROUTE_RELAXED = prev.relaxed;
|
|
641
|
+
if (prev.bin === undefined) delete process.env.CURSOR_ROUTE_DSH_BIN;
|
|
642
|
+
else process.env.CURSOR_ROUTE_DSH_BIN = prev.bin;
|
|
643
|
+
if (prev.key === undefined) delete process.env.DEEPSEEK_API_KEY;
|
|
644
|
+
else process.env.DEEPSEEK_API_KEY = prev.key;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
161
648
|
test("relaxed env can pass without workers", () => {
|
|
162
649
|
const prev = process.env.CURSOR_ROUTE_RELAXED;
|
|
163
650
|
process.env.CURSOR_ROUTE_RELAXED = "1";
|