@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command router safety tests
|
|
3
|
+
*
|
|
4
|
+
* Locks the observable contract of /anthropic slash commands:
|
|
5
|
+
* - parseCommandArgs() argument parsing with quote support
|
|
6
|
+
* - stripAnsi() ANSI escape code removal
|
|
7
|
+
* - handleAnthropicSlashCommand() routing dispatch to correct handlers
|
|
8
|
+
* - Command response format (heading patterns, message structure)
|
|
9
|
+
*
|
|
10
|
+
* These tests will fail if extraction changes routing or argument behavior.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
parseCommandArgs,
|
|
17
|
+
stripAnsi,
|
|
18
|
+
ANTHROPIC_COMMAND_HANDLED,
|
|
19
|
+
handleAnthropicSlashCommand,
|
|
20
|
+
type CommandDeps,
|
|
21
|
+
} from "./router.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Pure helpers — no mocks needed
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
describe("parseCommandArgs — argument parsing", () => {
|
|
28
|
+
it("returns empty array for empty string", () => {
|
|
29
|
+
expect(parseCommandArgs("")).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns empty array for whitespace-only string", () => {
|
|
33
|
+
expect(parseCommandArgs(" ")).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("splits simple space-separated args", () => {
|
|
37
|
+
expect(parseCommandArgs("list")).toEqual(["list"]);
|
|
38
|
+
expect(parseCommandArgs("switch 2")).toEqual(["switch", "2"]);
|
|
39
|
+
expect(parseCommandArgs("betas add my-beta")).toEqual(["betas", "add", "my-beta"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles double-quoted strings", () => {
|
|
43
|
+
expect(parseCommandArgs('a b "c d"')).toEqual(["a", "b", "c d"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles single-quoted strings", () => {
|
|
47
|
+
expect(parseCommandArgs("a 'c d'")).toEqual(["a", "c d"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("handles escaped quotes inside strings", () => {
|
|
51
|
+
expect(parseCommandArgs('"hello \\"world\\""')).toEqual(['hello "world"']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles mixed quoted and unquoted args", () => {
|
|
55
|
+
expect(parseCommandArgs('files upload "my file.pdf" --account 1')).toEqual([
|
|
56
|
+
"files",
|
|
57
|
+
"upload",
|
|
58
|
+
"my file.pdf",
|
|
59
|
+
"--account",
|
|
60
|
+
"1",
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("stripAnsi — ANSI escape code removal", () => {
|
|
66
|
+
it("returns plain text unchanged", () => {
|
|
67
|
+
expect(stripAnsi("hello world")).toBe("hello world");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("strips color codes", () => {
|
|
71
|
+
expect(stripAnsi("\x1b[32mgreen\x1b[0m")).toBe("green");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("strips bold and dim codes", () => {
|
|
75
|
+
expect(stripAnsi("\x1b[1mbold\x1b[0m \x1b[2mdim\x1b[0m")).toBe("bold dim");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("handles string with no ANSI codes", () => {
|
|
79
|
+
expect(stripAnsi("Account #1 (alice@example.com)")).toBe("Account #1 (alice@example.com)");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles empty string", () => {
|
|
83
|
+
expect(stripAnsi("")).toBe("");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("ANTHROPIC_COMMAND_HANDLED constant", () => {
|
|
88
|
+
it("exports the expected sentinel value", () => {
|
|
89
|
+
expect(ANTHROPIC_COMMAND_HANDLED).toBe("__ANTHROPIC_COMMAND_HANDLED__");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Slash command routing — mock dependencies
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
vi.mock("../storage.js", () => ({
|
|
98
|
+
loadAccounts: vi.fn(),
|
|
99
|
+
saveAccounts: vi.fn().mockResolvedValue(undefined),
|
|
100
|
+
createDefaultStats: vi.fn(() => ({
|
|
101
|
+
requests: 0,
|
|
102
|
+
inputTokens: 0,
|
|
103
|
+
outputTokens: 0,
|
|
104
|
+
cacheReadTokens: 0,
|
|
105
|
+
cacheWriteTokens: 0,
|
|
106
|
+
lastReset: Date.now(),
|
|
107
|
+
})),
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
vi.mock("../config.js", () => ({
|
|
111
|
+
loadConfigFresh: vi.fn(() => ({
|
|
112
|
+
account_selection_strategy: "sticky",
|
|
113
|
+
signature_emulation: { enabled: true, prompt_compaction: "minimal" },
|
|
114
|
+
override_model_limits: { enabled: false },
|
|
115
|
+
idle_refresh: { enabled: false },
|
|
116
|
+
debug: false,
|
|
117
|
+
toasts: { quiet: false },
|
|
118
|
+
custom_betas: [],
|
|
119
|
+
})),
|
|
120
|
+
saveConfig: vi.fn(),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
vi.mock("../env.js", () => ({
|
|
124
|
+
isTruthyEnv: vi.fn(() => false),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
vi.mock("./oauth-flow.js", () => ({
|
|
128
|
+
startSlashOAuth: vi.fn(),
|
|
129
|
+
completeSlashOAuth: vi.fn(),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
import { loadAccounts } from "../storage.js";
|
|
133
|
+
import { saveConfig, loadConfigFresh } from "../config.js";
|
|
134
|
+
import { startSlashOAuth, completeSlashOAuth } from "./oauth-flow.js";
|
|
135
|
+
|
|
136
|
+
const mockLoadAccounts = loadAccounts as Mock;
|
|
137
|
+
const mockSaveConfig = saveConfig as Mock;
|
|
138
|
+
const mockLoadConfigFresh = loadConfigFresh as Mock;
|
|
139
|
+
const mockStartSlashOAuth = startSlashOAuth as Mock;
|
|
140
|
+
const mockCompleteSlashOAuth = completeSlashOAuth as Mock;
|
|
141
|
+
|
|
142
|
+
function createMockDeps(overrides: Partial<CommandDeps> = {}): CommandDeps {
|
|
143
|
+
return {
|
|
144
|
+
sendCommandMessage: vi.fn().mockResolvedValue(undefined),
|
|
145
|
+
accountManager: null,
|
|
146
|
+
runCliCommand: vi.fn().mockResolvedValue({ code: 0, stdout: "OK", stderr: "" }),
|
|
147
|
+
config: {
|
|
148
|
+
account_selection_strategy: "sticky",
|
|
149
|
+
failure_ttl_seconds: 3600,
|
|
150
|
+
debug: false,
|
|
151
|
+
signature_emulation: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
fetch_claude_code_version_on_startup: false,
|
|
154
|
+
prompt_compaction: "minimal",
|
|
155
|
+
sanitize_system_prompt: false,
|
|
156
|
+
},
|
|
157
|
+
override_model_limits: { enabled: false, context: 1_000_000, output: 0 },
|
|
158
|
+
custom_betas: [],
|
|
159
|
+
health_score: {
|
|
160
|
+
initial: 70,
|
|
161
|
+
success_reward: 1,
|
|
162
|
+
rate_limit_penalty: -10,
|
|
163
|
+
failure_penalty: -20,
|
|
164
|
+
recovery_rate_per_hour: 2,
|
|
165
|
+
min_usable: 50,
|
|
166
|
+
max_score: 100,
|
|
167
|
+
},
|
|
168
|
+
token_bucket: { max_tokens: 50, regeneration_rate_per_minute: 6, initial_tokens: 50 },
|
|
169
|
+
toasts: { quiet: false, debounce_seconds: 30 },
|
|
170
|
+
headers: {},
|
|
171
|
+
idle_refresh: { enabled: false, window_minutes: 60, min_interval_minutes: 30 },
|
|
172
|
+
cc_credential_reuse: { enabled: false, auto_detect: false, prefer_over_oauth: false },
|
|
173
|
+
} as any,
|
|
174
|
+
fileAccountMap: new Map(),
|
|
175
|
+
initialAccountPinned: false,
|
|
176
|
+
pendingSlashOAuth: new Map(),
|
|
177
|
+
reloadAccountManagerFromDisk: vi.fn().mockResolvedValue(undefined),
|
|
178
|
+
persistOpenCodeAuth: vi.fn().mockResolvedValue(undefined),
|
|
179
|
+
refreshAccountTokenSingleFlight: vi.fn().mockResolvedValue("access-token"),
|
|
180
|
+
...overrides,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
describe("handleAnthropicSlashCommand — routing", () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
vi.clearAllMocks();
|
|
187
|
+
mockLoadAccounts.mockResolvedValue(null);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("defaults to 'list' when no arguments provided", async () => {
|
|
191
|
+
const deps = createMockDeps();
|
|
192
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "", sessionID: "sess-1" }, deps);
|
|
193
|
+
expect(deps.runCliCommand).toHaveBeenCalledWith(["list"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("routes 'usage' to CLI list command", async () => {
|
|
197
|
+
const deps = createMockDeps();
|
|
198
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "usage", sessionID: "sess-1" }, deps);
|
|
199
|
+
expect(deps.runCliCommand).toHaveBeenCalledWith(["list"]);
|
|
200
|
+
expect(deps.sendCommandMessage).toHaveBeenCalled();
|
|
201
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
202
|
+
expect(msg).toContain("▣ Anthropic");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("routes 'config' to config display", async () => {
|
|
206
|
+
const deps = createMockDeps();
|
|
207
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "config", sessionID: "sess-1" }, deps);
|
|
208
|
+
expect(deps.sendCommandMessage).toHaveBeenCalled();
|
|
209
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
210
|
+
expect(msg).toContain("▣ Anthropic Config");
|
|
211
|
+
expect(msg).toContain("strategy:");
|
|
212
|
+
expect(msg).toContain("emulation:");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("routes 'set' without args to usage message", async () => {
|
|
216
|
+
const deps = createMockDeps();
|
|
217
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "set", sessionID: "sess-1" }, deps);
|
|
218
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
219
|
+
expect(msg).toContain("▣ Anthropic Set");
|
|
220
|
+
expect(msg).toContain("Usage:");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("routes 'set debug on' to saveConfig", async () => {
|
|
224
|
+
const deps = createMockDeps();
|
|
225
|
+
await handleAnthropicSlashCommand(
|
|
226
|
+
{ command: "anthropic", arguments: "set debug on", sessionID: "sess-1" },
|
|
227
|
+
deps,
|
|
228
|
+
);
|
|
229
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({ debug: true });
|
|
230
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
231
|
+
expect(msg).toContain("debug = on");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("routes 'set emulation off' to saveConfig", async () => {
|
|
235
|
+
const deps = createMockDeps();
|
|
236
|
+
await handleAnthropicSlashCommand(
|
|
237
|
+
{ command: "anthropic", arguments: "set emulation off", sessionID: "sess-1" },
|
|
238
|
+
deps,
|
|
239
|
+
);
|
|
240
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({
|
|
241
|
+
signature_emulation: { enabled: false },
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("routes 'set strategy round-robin' to saveConfig", async () => {
|
|
246
|
+
const deps = createMockDeps();
|
|
247
|
+
await handleAnthropicSlashCommand(
|
|
248
|
+
{ command: "anthropic", arguments: "set strategy round-robin", sessionID: "sess-1" },
|
|
249
|
+
deps,
|
|
250
|
+
);
|
|
251
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({ account_selection_strategy: "round-robin" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("rejects invalid strategy in set command by throwing", async () => {
|
|
255
|
+
const deps = createMockDeps();
|
|
256
|
+
await expect(
|
|
257
|
+
handleAnthropicSlashCommand(
|
|
258
|
+
{ command: "anthropic", arguments: "set strategy banana", sessionID: "sess-1" },
|
|
259
|
+
deps,
|
|
260
|
+
),
|
|
261
|
+
).rejects.toThrow("Invalid strategy");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("routes 'betas' to beta list display", async () => {
|
|
265
|
+
const deps = createMockDeps();
|
|
266
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "betas", sessionID: "sess-1" }, deps);
|
|
267
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
268
|
+
expect(msg).toContain("▣ Anthropic Betas");
|
|
269
|
+
expect(msg).toContain("Preset betas");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("routes 'betas add' without beta name to usage", async () => {
|
|
273
|
+
const deps = createMockDeps();
|
|
274
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "betas add", sessionID: "sess-1" }, deps);
|
|
275
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
276
|
+
expect(msg).toContain("Usage:");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("routes 'betas add <beta>' to saveConfig with beta appended", async () => {
|
|
280
|
+
mockLoadConfigFresh.mockReturnValue({
|
|
281
|
+
account_selection_strategy: "sticky",
|
|
282
|
+
custom_betas: [],
|
|
283
|
+
});
|
|
284
|
+
const deps = createMockDeps();
|
|
285
|
+
await handleAnthropicSlashCommand(
|
|
286
|
+
{ command: "anthropic", arguments: "betas add web-search-2025-03-05", sessionID: "sess-1" },
|
|
287
|
+
deps,
|
|
288
|
+
);
|
|
289
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({
|
|
290
|
+
custom_betas: ["web-search-2025-03-05"],
|
|
291
|
+
});
|
|
292
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
293
|
+
expect(msg).toContain("Added: web-search-2025-03-05");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("routes 'betas add' with duplicate beta to already-added message", async () => {
|
|
297
|
+
mockLoadConfigFresh.mockReturnValue({
|
|
298
|
+
account_selection_strategy: "sticky",
|
|
299
|
+
custom_betas: ["web-search-2025-03-05"],
|
|
300
|
+
});
|
|
301
|
+
const deps = createMockDeps();
|
|
302
|
+
await handleAnthropicSlashCommand(
|
|
303
|
+
{ command: "anthropic", arguments: "betas add web-search-2025-03-05", sessionID: "sess-1" },
|
|
304
|
+
deps,
|
|
305
|
+
);
|
|
306
|
+
expect(mockSaveConfig).not.toHaveBeenCalled();
|
|
307
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
308
|
+
expect(msg).toContain("already added");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("routes 'betas remove <beta>' to saveConfig with beta removed", async () => {
|
|
312
|
+
mockLoadConfigFresh.mockReturnValue({
|
|
313
|
+
account_selection_strategy: "sticky",
|
|
314
|
+
custom_betas: ["web-search-2025-03-05", "compact-2026-01-12"],
|
|
315
|
+
});
|
|
316
|
+
const deps = createMockDeps();
|
|
317
|
+
await handleAnthropicSlashCommand(
|
|
318
|
+
{ command: "anthropic", arguments: "betas remove web-search-2025-03-05", sessionID: "sess-1" },
|
|
319
|
+
deps,
|
|
320
|
+
);
|
|
321
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({
|
|
322
|
+
custom_betas: ["compact-2026-01-12"],
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("routes 'manage' to manage-not-available message", async () => {
|
|
327
|
+
const deps = createMockDeps();
|
|
328
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "manage", sessionID: "sess-1" }, deps);
|
|
329
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
330
|
+
expect(msg).toContain("interactive-only");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("routes 'login' to startSlashOAuth", async () => {
|
|
334
|
+
const deps = createMockDeps();
|
|
335
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "login", sessionID: "sess-1" }, deps);
|
|
336
|
+
expect(mockStartSlashOAuth).toHaveBeenCalledWith("sess-1", "login", undefined, expect.any(Object));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("routes 'login complete <code>' to completeSlashOAuth", async () => {
|
|
340
|
+
mockCompleteSlashOAuth.mockResolvedValue({ ok: true, message: "Account added." });
|
|
341
|
+
const deps = createMockDeps();
|
|
342
|
+
await handleAnthropicSlashCommand(
|
|
343
|
+
{ command: "anthropic", arguments: "login complete mycode#mystate", sessionID: "sess-1" },
|
|
344
|
+
deps,
|
|
345
|
+
);
|
|
346
|
+
expect(mockCompleteSlashOAuth).toHaveBeenCalledWith("sess-1", "mycode#mystate", expect.any(Object));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("routes 'login complete' without code to error message", async () => {
|
|
350
|
+
const deps = createMockDeps();
|
|
351
|
+
await handleAnthropicSlashCommand(
|
|
352
|
+
{ command: "anthropic", arguments: "login complete", sessionID: "sess-1" },
|
|
353
|
+
deps,
|
|
354
|
+
);
|
|
355
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
356
|
+
expect(msg).toContain("Missing code");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("routes 'reauth' without number to error message", async () => {
|
|
360
|
+
const deps = createMockDeps();
|
|
361
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "reauth", sessionID: "sess-1" }, deps);
|
|
362
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
363
|
+
expect(msg).toContain("Provide an account number");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("routes 'reauth 1' to startSlashOAuth with index", async () => {
|
|
367
|
+
mockLoadAccounts.mockResolvedValue({
|
|
368
|
+
version: 1,
|
|
369
|
+
accounts: [{ email: "a@test.com", enabled: true, refreshToken: "t1" }],
|
|
370
|
+
activeIndex: 0,
|
|
371
|
+
});
|
|
372
|
+
const deps = createMockDeps();
|
|
373
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "reauth 1", sessionID: "sess-1" }, deps);
|
|
374
|
+
expect(mockStartSlashOAuth).toHaveBeenCalledWith("sess-1", "reauth", 0, expect.any(Object));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("forces --force on destructive commands (remove, logout)", async () => {
|
|
378
|
+
const deps = createMockDeps();
|
|
379
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "remove 1", sessionID: "sess-1" }, deps);
|
|
380
|
+
const cliArgs = (deps.runCliCommand as Mock).mock.calls[0][0] as string[];
|
|
381
|
+
expect(cliArgs).toContain("--force");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("forces --force on logout command", async () => {
|
|
385
|
+
const deps = createMockDeps();
|
|
386
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "logout 1", sessionID: "sess-1" }, deps);
|
|
387
|
+
const cliArgs = (deps.runCliCommand as Mock).mock.calls[0][0] as string[];
|
|
388
|
+
expect(cliArgs).toContain("--force");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("files command without accountManager returns error", async () => {
|
|
392
|
+
const deps = createMockDeps({ accountManager: null });
|
|
393
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "files list", sessionID: "sess-1" }, deps);
|
|
394
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
395
|
+
expect(msg).toContain("No accounts configured");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("passes unknown commands through to CLI", async () => {
|
|
399
|
+
const deps = createMockDeps();
|
|
400
|
+
(deps.runCliCommand as Mock).mockResolvedValue({ code: 0, stdout: "Custom output", stderr: "" });
|
|
401
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "stats", sessionID: "sess-1" }, deps);
|
|
402
|
+
expect(deps.runCliCommand).toHaveBeenCalledWith(["stats"]);
|
|
403
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
404
|
+
expect(msg).toContain("Custom output");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("reports error heading when CLI returns non-zero code", async () => {
|
|
408
|
+
const deps = createMockDeps();
|
|
409
|
+
(deps.runCliCommand as Mock).mockResolvedValue({ code: 1, stdout: "", stderr: "Something failed" });
|
|
410
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "refresh 1", sessionID: "sess-1" }, deps);
|
|
411
|
+
const msg = (deps.sendCommandMessage as Mock).mock.calls[0][1] as string;
|
|
412
|
+
expect(msg).toContain("▣ Anthropic (error)");
|
|
413
|
+
expect(msg).toContain("Something failed");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("calls reloadAccountManagerFromDisk after CLI commands", async () => {
|
|
417
|
+
const deps = createMockDeps();
|
|
418
|
+
await handleAnthropicSlashCommand({ command: "anthropic", arguments: "switch 1", sessionID: "sess-1" }, deps);
|
|
419
|
+
expect(deps.reloadAccountManagerFromDisk).toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
});
|