dsh-commandcode-quota 0.0.0-stage → 0.2.1

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,259 @@
1
+ // 模拟宿主、凭据与 API,不读取真实凭据,也不访问网络。
2
+ const fs = require("node:fs");
3
+ const vm = require("node:vm");
4
+ const assert = require("node:assert/strict");
5
+ const { createHash } = require("node:crypto");
6
+ const { join, dirname } = require("node:path");
7
+ const { tmpdir } = require("node:os");
8
+ const plugin = join(__dirname, "..");
9
+ const source = fs.readFileSync(join(plugin, "src/index.js"), "utf8");
10
+
11
+ // 完全隔离的 DSH_HOME:保存凭据选择会写配置,绝不能落到真实的 ~/.dsh。
12
+ const fakeHome = fs.mkdtempSync(join(tmpdir(), "dsh-quota-test-"));
13
+ fs.writeFileSync(join(fakeHome, ".credentials.yaml"), [
14
+ "version: 1",
15
+ "refs:",
16
+ " PRIMARY_TEST_KEY: fixture-primary-value",
17
+ " FALLBACK_TEST_KEY: fixture-fallback-value",
18
+ "records: {}",
19
+ "",
20
+ ].join("\n"));
21
+ process.on("exit", () => { try { fs.rmSync(fakeHome, { recursive: true, force: true }); } catch { /* 清理失败不影响测试结论 */ } });
22
+
23
+ const responses = {
24
+ "/alpha/billing/credits": { credits: { monthlyCredits: 70 }, windowLimits: { fiveHour: { used: 10, cap: 100 } } },
25
+ "/alpha/usage/summary": { totalMonthlyCredits: 30, totalTokens: 100, totalCost: 5, successRate: 98 },
26
+ "/alpha/billing/subscriptions": { data: { status: "active", currentPeriodEnd: "2026-11-01T00:00:00Z" } },
27
+ "/alpha/whoami": { user: { name: "测试账号" } },
28
+ };
29
+ function load() {
30
+ const requests = [];
31
+ let reply = async (url) => ({ ok: true, status: 200, json: async () => responses[new URL(url).pathname] });
32
+ const context = vm.createContext({
33
+ Buffer, URL, AbortController, setTimeout, clearTimeout, createHash,
34
+ // 源码的 import 会被剥掉,所以这里用到的 node 内置必须显式注入
35
+ readFileSync: fs.readFileSync,
36
+ writeFileSync: fs.writeFileSync,
37
+ mkdirSync: fs.mkdirSync,
38
+ join, dirname,
39
+ homedir: () => fakeHome,
40
+ process: { env: { DSH_HOME: fakeHome } },
41
+ credentialRef: (ref) => ref,
42
+ fetch: async (url, options) => { requests.push({ url, options }); return reply(url, options); },
43
+ });
44
+ vm.runInContext(source.replace(/^import .*;\r?$/gm, "").replace(/^export .*;\r?$/gm, "") +
45
+ "\nglobalThis.host = { apply, fetchDashboard, inject, name };", context);
46
+ return { host: context.host, requests, respond(fn) { reply = fn; } };
47
+ }
48
+ const DASHBOARD = "/__dsh-commandcode-quota/dashboard";
49
+ const CREDS = "/__dsh-commandcode-quota/credentials";
50
+
51
+ function mount(runtime, config = {}, keys = {}, lazy = false, settings = undefined) {
52
+ const routes = new Map();
53
+ const refs = [];
54
+ const web = { register(row) { routes.set(row.path, row); return () => {}; } };
55
+ const credentials = { async resolve(ref) { refs.push(ref); return { value: keys[ref] ?? "" }; } };
56
+ const ctx = {
57
+ get(name) {
58
+ if (name === "credentials") return credentials;
59
+ if (name === "settings") return settings;
60
+ if (name === "webServer") return lazy ? undefined : web;
61
+ return undefined;
62
+ },
63
+ // 真实 cordis 里访问未注入的服务属性会【抛异常】而不是返回 undefined。
64
+ // 这条 getter 守住它:代码必须走 ctx.get 并降级,
65
+ // 否则凭据端点会整个 400(曾经真的这么挂过一次)。
66
+ get settings() {
67
+ throw new Error("service settings is not injected");
68
+ },
69
+ effect(fn) { return fn(); },
70
+ inject(names, fn) { assert.deepEqual(Array.from(names), ["webServer"]); fn({ webServer: web, effect: ctx.effect }); },
71
+ logger: { info() {} },
72
+ };
73
+ runtime.host.apply(ctx, config);
74
+ assert.equal(runtime.host.name, "dsh-commandcode-quota");
75
+ assert.deepEqual(Array.from(runtime.host.inject), ["credentials"]);
76
+
77
+ const dashboard = routes.get(DASHBOARD);
78
+ assert.ok(dashboard, "必须注册 dashboard 端点");
79
+ assert.equal(dashboard.kind, "exact");
80
+ const creds = routes.get(CREDS);
81
+ assert.ok(creds, "必须注册 credentials 端点");
82
+ assert.equal(creds.kind, "exact");
83
+
84
+ async function invoke(handler, method, url, payload) {
85
+ let status, headers, raw;
86
+ const listeners = {};
87
+ const req = {
88
+ method,
89
+ url,
90
+ on(event, fn) { (listeners[event] = listeners[event] || []).push(fn); return req; },
91
+ destroy() {},
92
+ };
93
+ const pending = handler(req, {
94
+ writeHead(code, values) { status = code; headers = values; },
95
+ end(body) { raw = body; },
96
+ });
97
+ if (payload !== undefined) {
98
+ for (const fn of listeners.data || []) fn(JSON.stringify(payload));
99
+ for (const fn of listeners.end || []) fn();
100
+ }
101
+ await pending;
102
+ assert.equal(headers["cache-control"], "no-store");
103
+ assert.equal(headers["content-length"], Buffer.byteLength(raw));
104
+ return { status, body: JSON.parse(raw), raw };
105
+ }
106
+
107
+ return {
108
+ refs,
109
+ request(url = DASHBOARD, method = "GET", payload) {
110
+ return invoke(url.includes("/credentials") ? creds.handler : dashboard.handler, method, url, payload);
111
+ },
112
+ };
113
+ }
114
+ const config = { apiKeyEnv: "PRIMARY_TEST_KEY", fallbackEnv: "FALLBACK_TEST_KEY", baseURL: "https://mock.invalid" };
115
+ async function main() {
116
+ const runtime = load();
117
+ const app = mount(runtime, config, { PRIMARY_TEST_KEY: "fixture-primary" });
118
+ const full = await app.request();
119
+ assert.equal(full.status, 200);
120
+ assert.equal(full.body.ok, true);
121
+ assert.equal(full.body.balance.monthlyCredits, 70);
122
+ assert.equal(full.body.usage.totalMonthlyCredits, 30);
123
+ assert.equal(full.body.subscription.status, "active");
124
+ assert.equal(full.body.user.name, "测试账号");
125
+ for (const field of ["providers", "providerId", "providerLabel", "keyInfo", "warnings"]) {
126
+ assert.ok(!(field in full.body), `单账号响应不应包含 ${field}`);
127
+ }
128
+ assert.ok(!full.raw.includes("fixture-primary"));
129
+ assert.equal(runtime.requests.length, 4);
130
+ assert.ok(runtime.requests.every(req => req.options.headers.authorization === "Bearer fixture-primary"));
131
+ assert.deepEqual(app.refs, ["PRIMARY_TEST_KEY"]);
132
+ runtime.requests.length = 0;
133
+ const quick = await app.request(DASHBOARD + "?scope=quick");
134
+ assert.equal(runtime.requests.length, 1);
135
+ assert.equal(new URL(runtime.requests[0].url).pathname, "/alpha/billing/credits");
136
+ assert.equal(quick.body.usage.totalMonthlyCredits, 30);
137
+ assert.deepEqual(quick.body.cached, ["usage", "subscriptions", "whoami"]);
138
+ runtime.respond(async () => ({ status: 503, json: async () => ({}) }));
139
+ const stale = await app.request();
140
+ assert.equal(stale.body.balance.monthlyCredits, 70);
141
+ assert.equal(stale.body.stale.length, 4);
142
+ runtime.respond(async () => ({ status: 401, json: async () => ({}) }));
143
+ const unauthorized = await app.request();
144
+ assert.equal(unauthorized.body.ok, false);
145
+ assert.equal(unauthorized.body.balance, undefined);
146
+ assert.equal(unauthorized.body.errors.length, 4);
147
+ runtime.requests.length = 0;
148
+ assert.equal((await app.request(undefined, "POST")).status, 405);
149
+ assert.equal(runtime.requests.length, 0);
150
+ const fallbackRuntime = load();
151
+ const fallback = mount(fallbackRuntime, config, { FALLBACK_TEST_KEY: "fixture-fallback" }, true);
152
+ assert.equal((await fallback.request(DASHBOARD + "?scope=quick")).body.keyConfigured, true);
153
+ assert.deepEqual(fallback.refs, ["PRIMARY_TEST_KEY", "FALLBACK_TEST_KEY"]);
154
+ assert.equal(fallbackRuntime.requests[0].options.headers.authorization, "Bearer fixture-fallback");
155
+ const emptyRuntime = load();
156
+ const missing = await mount(emptyRuntime, config).request();
157
+ assert.equal(missing.body.keyConfigured, false);
158
+ assert.equal(missing.body.ok, false);
159
+ assert.equal(emptyRuntime.requests.length, 0);
160
+ assert.ok(missing.body.error.includes("凭据条目"), "缺 key 的提示应指向面板里的选择器");
161
+ // 缺 key 时,凭据端点必须照常可用 —— 这正是用户最需要它的时候
162
+ const missingApp = mount(load(), config);
163
+ const listedWhenEmpty = await missingApp.request(CREDS);
164
+ assert.equal(listedWhenEmpty.status, 200, "没配 key 时凭据端点也必须可用");
165
+ assert.equal(listedWhenEmpty.body.apiKeyEnv, "PRIMARY_TEST_KEY");
166
+ const defaultRuntime = load();
167
+ assert.equal((await mount(defaultRuntime, {}, { COMMANDCODE_API_KEY: "fixture-default" }).request()).body.ok, true);
168
+ let attempts = 0;
169
+ const retryRuntime = load();
170
+ retryRuntime.respond(async (url) => {
171
+ if (++attempts === 1) throw new TypeError("模拟连接失败");
172
+ return { status: 200, json: async () => responses[new URL(url).pathname] };
173
+ });
174
+ assert.equal((await mount(retryRuntime, config, { PRIMARY_TEST_KEY: "fixture-primary" }).request(DASHBOARD + "?scope=quick")).body.ok, true);
175
+ assert.equal(attempts, 2);
176
+ const timeoutRuntime = load();
177
+ timeoutRuntime.respond((url, options) => new Promise((resolve, reject) => {
178
+ options.signal.addEventListener("abort", () => reject(new Error("模拟超时")), { once: true });
179
+ }));
180
+ assert.equal((await mount(timeoutRuntime, { ...config, timeoutMs: 5 }, { PRIMARY_TEST_KEY: "fixture-primary" }).request(DASHBOARD + "?scope=quick")).body.ok, false);
181
+ assert.equal(timeoutRuntime.requests.length, 1, "主动超时不重试");
182
+
183
+ // 凭据条目端点:GET 只回条目名 + 当前选择,POST 保存选择。放在最后跑,因为保存会改变后续读取。
184
+ const credsApp = mount(load(), config, { PRIMARY_TEST_KEY: "fixture-primary" });
185
+ const listed = await credsApp.request(CREDS);
186
+ assert.equal(listed.status, 200);
187
+ assert.deepEqual(listed.body.entries, [
188
+ { ref: "PRIMARY_TEST_KEY", provider: null },
189
+ { ref: "FALLBACK_TEST_KEY", provider: null },
190
+ ], "一个都对不上供应商时回退显示全部,免得多拉是空的");
191
+ assert.equal(listed.body.apiKeyEnv, "PRIMARY_TEST_KEY");
192
+ for (const secret of ["fixture-primary-value", "fixture-fallback-value"]) {
193
+ assert.ok(!listed.raw.includes(secret), `凭据端点绝不能回值:${secret}`);
194
+ }
195
+ const saved = await credsApp.request(CREDS, "POST", { apiKeyEnv: "FALLBACK_TEST_KEY" });
196
+ assert.equal(saved.status, 200);
197
+ assert.equal(saved.body.apiKeyEnv, "FALLBACK_TEST_KEY");
198
+ assert.equal(
199
+ (await credsApp.request(CREDS, "POST", { apiKeyEnv: "not a valid ref" })).status,
200
+ 400,
201
+ "非法条目名必须被拒",
202
+ );
203
+ assert.equal((await credsApp.request(CREDS, "DELETE")).status, 405);
204
+ // 保存过的选择要覆盖 profile patch 里的 apiKeyEnv
205
+ const chosenRuntime = load();
206
+ const chosen = mount(chosenRuntime, config, { FALLBACK_TEST_KEY: "fixture-fallback" });
207
+ assert.equal((await chosen.request(DASHBOARD + "?scope=quick")).body.keyConfigured, true);
208
+ assert.deepEqual(chosen.refs, ["FALLBACK_TEST_KEY"], "保存的选择应优先于 patch 配置");
209
+
210
+ // 供应商映射:provider.apiKeyEnv 指向条目名时,下拉显示供应商名而不是裸条目名
211
+ const settingsStub = {
212
+ describe: () => ([{
213
+ ns: "llm-pi-ai",
214
+ user: {
215
+ providers: {
216
+ commandcode2: { apiKeyEnv: "PRIMARY_TEST_KEY" },
217
+ cerebras: { displayName: "bailian", apiKeyEnv: "FALLBACK_TEST_KEY" },
218
+ noKey: { baseURL: "https://example.invalid" },
219
+ },
220
+ },
221
+ }]),
222
+ };
223
+ const labeled = await mount(load(), config, { PRIMARY_TEST_KEY: "x" }, false, settingsStub).request(CREDS);
224
+ assert.deepEqual(labeled.body.entries, [
225
+ { ref: "PRIMARY_TEST_KEY", provider: "commandcode2" },
226
+ { ref: "FALLBACK_TEST_KEY", provider: "bailian" },
227
+ ], "应把条目名翻译成模型设置里显示的供应商名");
228
+ // describe() 抛错时必须降级,而不是让整个端点失败
229
+ const broken = await mount(load(), config, {}, false, { describe() { throw new Error("boom"); } }).request(CREDS);
230
+ assert.equal(broken.status, 200, "settings 读不到时应降级而不是报错");
231
+ assert.deepEqual(broken.body.entries.map((e) => e.provider), [null, null]);
232
+ // 只有部分条目对得上时,只列出对得上的 —— 对不上的说明用户没在用它
233
+ const partial = await mount(load(), config, {}, false, {
234
+ describe: () => ([{
235
+ ns: "llm-pi-ai",
236
+ user: { providers: { cerebras: { displayName: "bailian", apiKeyEnv: "FALLBACK_TEST_KEY" } } },
237
+ }]),
238
+ }).request(CREDS);
239
+ assert.deepEqual(partial.body.entries, [
240
+ { ref: "FALLBACK_TEST_KEY", provider: "bailian" },
241
+ ], "只应列出对得上模型供应商的条目");
242
+
243
+ // 包自检:声明、注册 id 与 bundle 补丁层三者必须一致。
244
+ const manifest = JSON.parse(fs.readFileSync(join(plugin, "package.json"), "utf8"));
245
+ assert.equal(manifest.name, "dsh-commandcode-quota", "package.json 的 name 必须等于宿主端导出 name");
246
+ assert.equal(manifest.dsh.bundle.patch, "./cordis.patch.yml", "必须声明 bundle 补丁层");
247
+ const patch = fs.readFileSync(join(plugin, "cordis.patch.yml"), "utf8");
248
+ assert.ok(
249
+ patch.includes(`id: ${manifest.name}`) && patch.includes(`name: '${manifest.name}'`),
250
+ "bundle 补丁的注册 id 与包名必须一致",
251
+ );
252
+ // 发版时最容易漏的一处:user-agent 里的版本号要与 package.json 同步。
253
+ const uaVersion = source.match(/"user-agent":\s*"[^"]*\/([^"]+)"/)?.[1];
254
+ assert.equal(uaVersion, manifest.version, "user-agent 版本必须与 package.json 的 version 一致");
255
+ console.log("通过:单账号响应、主/回退凭据、完整/快速查询、缓存、401、405、重试和超时。");
256
+ console.log("通过:凭据端点只回条目名、翻译供应商名、保存后覆盖 patch 配置、非法名被拒、无 key 时依然可用。");
257
+ console.log("通过:宿主端导出、路由、包名与 bundle 补丁层一致。");
258
+ }
259
+ main().catch(error => { console.error(error.message); process.exitCode = 1; });