@hellcoder/companion 0.96.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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Mocks
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const mockExecSync = vi.hoisted(() => vi.fn());
|
|
11
|
+
const mockExecFileSync = vi.hoisted(() => vi.fn());
|
|
12
|
+
|
|
13
|
+
vi.mock("node:child_process", () => ({
|
|
14
|
+
execSync: mockExecSync,
|
|
15
|
+
execFileSync: mockExecFileSync,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock global fetch at the module level (persists across tests)
|
|
19
|
+
const mockFetch = vi.fn();
|
|
20
|
+
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Module under test - re-imported each time to reset module-level cache
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
let mod: typeof import("./usage-limits.js");
|
|
26
|
+
const originalPlatform = process.platform;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
mockExecSync.mockReset();
|
|
31
|
+
mockExecFileSync.mockReset();
|
|
32
|
+
mockFetch.mockReset();
|
|
33
|
+
mod = await import("./usage-limits.js");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const SAMPLE_TOKEN = "sk-ant-fake-token-123";
|
|
45
|
+
|
|
46
|
+
function makeCredentialsJson(token: string, opts?: { expired?: boolean }): string {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
claudeAiOauth: {
|
|
49
|
+
accessToken: token,
|
|
50
|
+
refreshToken: "sk-ant-ort01-fake-refresh-token",
|
|
51
|
+
expiresAt: opts?.expired ? Date.now() - 1000 : Date.now() + 3_600_000,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeCredentialsHex(token: string): string {
|
|
57
|
+
return Buffer.from(makeCredentialsJson(token), "utf-8").toString("hex");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeFetchResponse(body: object, ok = true) {
|
|
61
|
+
return Promise.resolve({
|
|
62
|
+
ok,
|
|
63
|
+
json: () => Promise.resolve(body),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SAMPLE_LIMITS = {
|
|
68
|
+
five_hour: { utilization: 42, resets_at: "2025-01-01T12:00:00Z" },
|
|
69
|
+
seven_day: { utilization: 15, resets_at: null },
|
|
70
|
+
extra_usage: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
// getCredentials (macOS - Keychain)
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
describe("getCredentials (macOS)", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
// Force macOS platform so the Keychain (execSync) path is used
|
|
79
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("extracts token from plain JSON output", async () => {
|
|
83
|
+
vi.resetModules();
|
|
84
|
+
mockFetch.mockReset();
|
|
85
|
+
const darwinMod = await import("./usage-limits.js");
|
|
86
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN));
|
|
87
|
+
expect(darwinMod.getCredentials()).toBe(SAMPLE_TOKEN);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("extracts token from hex-encoded output", async () => {
|
|
91
|
+
vi.resetModules();
|
|
92
|
+
mockFetch.mockReset();
|
|
93
|
+
const darwinMod = await import("./usage-limits.js");
|
|
94
|
+
mockExecSync.mockReturnValue(makeCredentialsHex(SAMPLE_TOKEN));
|
|
95
|
+
expect(darwinMod.getCredentials()).toBe(SAMPLE_TOKEN);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns null when execSync throws (e.g. no keychain entry)", async () => {
|
|
99
|
+
vi.resetModules();
|
|
100
|
+
mockFetch.mockReset();
|
|
101
|
+
const darwinMod = await import("./usage-limits.js");
|
|
102
|
+
mockExecSync.mockImplementation(() => {
|
|
103
|
+
throw new Error("security: command not found");
|
|
104
|
+
});
|
|
105
|
+
expect(darwinMod.getCredentials()).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns null when JSON has no claudeAiOauth field", async () => {
|
|
109
|
+
vi.resetModules();
|
|
110
|
+
mockFetch.mockReset();
|
|
111
|
+
const darwinMod = await import("./usage-limits.js");
|
|
112
|
+
mockExecSync.mockReturnValue(JSON.stringify({ other: "data" }));
|
|
113
|
+
expect(darwinMod.getCredentials()).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns token even when format does not match sk-ant-*", async () => {
|
|
117
|
+
vi.resetModules();
|
|
118
|
+
mockFetch.mockReset();
|
|
119
|
+
const darwinMod = await import("./usage-limits.js");
|
|
120
|
+
mockExecSync.mockReturnValue(
|
|
121
|
+
JSON.stringify({ claudeAiOauth: { accessToken: "not-a-valid-token" } }),
|
|
122
|
+
);
|
|
123
|
+
expect(darwinMod.getCredentials()).toBe("not-a-valid-token");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ===========================================================================
|
|
128
|
+
// getCredentials - Windows path
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
describe("getCredentials (Windows)", () => {
|
|
131
|
+
let tempDir: string;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
135
|
+
tempDir = mkdtempSync(join(tmpdir(), "usage-limits-test-"));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("reads token from credentials file on Windows", async () => {
|
|
143
|
+
const claudeDir = join(tempDir, ".claude");
|
|
144
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
145
|
+
writeFileSync(
|
|
146
|
+
join(claudeDir, ".credentials.json"),
|
|
147
|
+
makeCredentialsJson(SAMPLE_TOKEN),
|
|
148
|
+
);
|
|
149
|
+
process.env.USERPROFILE = tempDir;
|
|
150
|
+
|
|
151
|
+
// Re-import to pick up the mocked platform
|
|
152
|
+
vi.resetModules();
|
|
153
|
+
mockFetch.mockReset();
|
|
154
|
+
const winMod = await import("./usage-limits.js");
|
|
155
|
+
expect(winMod.getCredentials()).toBe(SAMPLE_TOKEN);
|
|
156
|
+
|
|
157
|
+
delete process.env.USERPROFILE;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns null when credentials file does not exist on Windows", async () => {
|
|
161
|
+
process.env.USERPROFILE = tempDir;
|
|
162
|
+
|
|
163
|
+
vi.resetModules();
|
|
164
|
+
const winMod = await import("./usage-limits.js");
|
|
165
|
+
expect(winMod.getCredentials()).toBeNull();
|
|
166
|
+
|
|
167
|
+
delete process.env.USERPROFILE;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns null when credentials file has invalid JSON on Windows", async () => {
|
|
171
|
+
const claudeDir = join(tempDir, ".claude");
|
|
172
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
173
|
+
writeFileSync(join(claudeDir, ".credentials.json"), "NOT VALID JSON{{{");
|
|
174
|
+
process.env.USERPROFILE = tempDir;
|
|
175
|
+
|
|
176
|
+
vi.resetModules();
|
|
177
|
+
const winMod = await import("./usage-limits.js");
|
|
178
|
+
expect(winMod.getCredentials()).toBeNull();
|
|
179
|
+
|
|
180
|
+
delete process.env.USERPROFILE;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ===========================================================================
|
|
185
|
+
// getCredentials - Linux / Docker path
|
|
186
|
+
// ===========================================================================
|
|
187
|
+
describe("getCredentials (Linux / Docker)", () => {
|
|
188
|
+
let tempDir: string;
|
|
189
|
+
const originalHome = process.env.HOME;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
// Force Linux platform so the file-based credential path is used
|
|
193
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
194
|
+
tempDir = mkdtempSync(join(tmpdir(), "usage-limits-linux-test-"));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
// Restore original HOME to avoid cross-test environment pollution
|
|
199
|
+
if (originalHome !== undefined) {
|
|
200
|
+
process.env.HOME = originalHome;
|
|
201
|
+
} else {
|
|
202
|
+
delete process.env.HOME;
|
|
203
|
+
}
|
|
204
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("reads token from .credentials.json on Linux", async () => {
|
|
208
|
+
const claudeDir = join(tempDir, ".claude");
|
|
209
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
210
|
+
writeFileSync(
|
|
211
|
+
join(claudeDir, ".credentials.json"),
|
|
212
|
+
makeCredentialsJson(SAMPLE_TOKEN),
|
|
213
|
+
);
|
|
214
|
+
process.env.HOME = tempDir;
|
|
215
|
+
|
|
216
|
+
vi.resetModules();
|
|
217
|
+
mockFetch.mockReset();
|
|
218
|
+
const linuxMod = await import("./usage-limits.js");
|
|
219
|
+
expect(linuxMod.getCredentials()).toBe(SAMPLE_TOKEN);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("falls back to alternative credential file names", async () => {
|
|
223
|
+
// Only auth.json exists (not .credentials.json)
|
|
224
|
+
const claudeDir = join(tempDir, ".claude");
|
|
225
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
226
|
+
writeFileSync(
|
|
227
|
+
join(claudeDir, "auth.json"),
|
|
228
|
+
makeCredentialsJson(SAMPLE_TOKEN),
|
|
229
|
+
);
|
|
230
|
+
process.env.HOME = tempDir;
|
|
231
|
+
|
|
232
|
+
vi.resetModules();
|
|
233
|
+
mockFetch.mockReset();
|
|
234
|
+
const linuxMod = await import("./usage-limits.js");
|
|
235
|
+
expect(linuxMod.getCredentials()).toBe(SAMPLE_TOKEN);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns null when no credential files exist on Linux", async () => {
|
|
239
|
+
process.env.HOME = tempDir;
|
|
240
|
+
|
|
241
|
+
vi.resetModules();
|
|
242
|
+
mockFetch.mockReset();
|
|
243
|
+
const linuxMod = await import("./usage-limits.js");
|
|
244
|
+
expect(linuxMod.getCredentials()).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("returns null when credentials file has invalid JSON on Linux", async () => {
|
|
248
|
+
const claudeDir = join(tempDir, ".claude");
|
|
249
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
250
|
+
writeFileSync(join(claudeDir, ".credentials.json"), "NOT VALID JSON{{{");
|
|
251
|
+
process.env.HOME = tempDir;
|
|
252
|
+
|
|
253
|
+
vi.resetModules();
|
|
254
|
+
mockFetch.mockReset();
|
|
255
|
+
const linuxMod = await import("./usage-limits.js");
|
|
256
|
+
expect(linuxMod.getCredentials()).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("does not call macOS security command on Linux", async () => {
|
|
260
|
+
const claudeDir = join(tempDir, ".claude");
|
|
261
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
262
|
+
writeFileSync(
|
|
263
|
+
join(claudeDir, ".credentials.json"),
|
|
264
|
+
makeCredentialsJson(SAMPLE_TOKEN),
|
|
265
|
+
);
|
|
266
|
+
process.env.HOME = tempDir;
|
|
267
|
+
|
|
268
|
+
vi.resetModules();
|
|
269
|
+
mockFetch.mockReset();
|
|
270
|
+
const linuxMod = await import("./usage-limits.js");
|
|
271
|
+
linuxMod.getCredentials();
|
|
272
|
+
// security command should never be invoked on Linux
|
|
273
|
+
expect(mockExecSync).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("writes refreshed credentials back to the same source file", async () => {
|
|
277
|
+
// Credentials are stored in auth.json (not the default .credentials.json)
|
|
278
|
+
const claudeDir = join(tempDir, ".claude");
|
|
279
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
280
|
+
writeFileSync(
|
|
281
|
+
join(claudeDir, "auth.json"),
|
|
282
|
+
makeCredentialsJson(SAMPLE_TOKEN, { expired: true }),
|
|
283
|
+
);
|
|
284
|
+
process.env.HOME = tempDir;
|
|
285
|
+
|
|
286
|
+
vi.resetModules();
|
|
287
|
+
mockFetch.mockReset();
|
|
288
|
+
const linuxMod = await import("./usage-limits.js");
|
|
289
|
+
|
|
290
|
+
// First call = token refresh, second call = usage API
|
|
291
|
+
mockFetch
|
|
292
|
+
.mockReturnValueOnce(
|
|
293
|
+
makeFetchResponse({
|
|
294
|
+
access_token: "sk-ant-new-token",
|
|
295
|
+
refresh_token: "sk-ant-new-refresh",
|
|
296
|
+
expires_in: 3600,
|
|
297
|
+
}),
|
|
298
|
+
)
|
|
299
|
+
.mockReturnValueOnce(makeFetchResponse(SAMPLE_LIMITS));
|
|
300
|
+
|
|
301
|
+
const result = await linuxMod.getUsageLimits();
|
|
302
|
+
expect(result).toEqual(SAMPLE_LIMITS);
|
|
303
|
+
|
|
304
|
+
// Credentials should have been written to file (not via execFileSync/security)
|
|
305
|
+
expect(mockExecFileSync).not.toHaveBeenCalled();
|
|
306
|
+
|
|
307
|
+
// Verify the refreshed token was written back to auth.json (the source file)
|
|
308
|
+
const updatedCreds = JSON.parse(
|
|
309
|
+
readFileSync(join(claudeDir, "auth.json"), "utf-8"),
|
|
310
|
+
);
|
|
311
|
+
expect(updatedCreds.claudeAiOauth.accessToken).toBe("sk-ant-new-token");
|
|
312
|
+
expect(updatedCreds.claudeAiOauth.refreshToken).toBe("sk-ant-new-refresh");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ===========================================================================
|
|
317
|
+
// fetchUsageLimits
|
|
318
|
+
// ===========================================================================
|
|
319
|
+
describe("fetchUsageLimits", () => {
|
|
320
|
+
it("returns parsed limits on success", async () => {
|
|
321
|
+
mockFetch.mockReturnValue(makeFetchResponse(SAMPLE_LIMITS));
|
|
322
|
+
|
|
323
|
+
const result = await mod.fetchUsageLimits(SAMPLE_TOKEN);
|
|
324
|
+
expect(result).toEqual(SAMPLE_LIMITS);
|
|
325
|
+
|
|
326
|
+
// Verify correct headers
|
|
327
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
328
|
+
"https://api.anthropic.com/api/oauth/usage",
|
|
329
|
+
expect.objectContaining({
|
|
330
|
+
method: "GET",
|
|
331
|
+
headers: expect.objectContaining({
|
|
332
|
+
Authorization: `Bearer ${SAMPLE_TOKEN}`,
|
|
333
|
+
}),
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("returns null on non-ok response", async () => {
|
|
339
|
+
mockFetch.mockReturnValue(makeFetchResponse({}, false));
|
|
340
|
+
const result = await mod.fetchUsageLimits(SAMPLE_TOKEN);
|
|
341
|
+
expect(result).toBeNull();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("returns null on network error", async () => {
|
|
345
|
+
mockFetch.mockRejectedValue(new Error("network error"));
|
|
346
|
+
const result = await mod.fetchUsageLimits(SAMPLE_TOKEN);
|
|
347
|
+
expect(result).toBeNull();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("normalizes missing fields to null", async () => {
|
|
351
|
+
mockFetch.mockReturnValue(
|
|
352
|
+
makeFetchResponse({ five_hour: { utilization: 10, resets_at: null } }),
|
|
353
|
+
);
|
|
354
|
+
const result = await mod.fetchUsageLimits(SAMPLE_TOKEN);
|
|
355
|
+
expect(result).toEqual({
|
|
356
|
+
five_hour: { utilization: 10, resets_at: null },
|
|
357
|
+
seven_day: null,
|
|
358
|
+
extra_usage: null,
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
// getUsageLimits (orchestrator with cache)
|
|
365
|
+
// These tests use macOS platform to exercise the execSync/keychain path
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
describe("getUsageLimits", () => {
|
|
368
|
+
const EMPTY = { five_hour: null, seven_day: null, extra_usage: null };
|
|
369
|
+
|
|
370
|
+
beforeEach(() => {
|
|
371
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("returns empty when no credentials are available", async () => {
|
|
375
|
+
vi.resetModules();
|
|
376
|
+
mockFetch.mockReset();
|
|
377
|
+
const darwinMod = await import("./usage-limits.js");
|
|
378
|
+
mockExecSync.mockImplementation(() => {
|
|
379
|
+
throw new Error("no keychain");
|
|
380
|
+
});
|
|
381
|
+
const result = await darwinMod.getUsageLimits();
|
|
382
|
+
expect(result).toEqual(EMPTY);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("returns limits and caches the result", async () => {
|
|
386
|
+
vi.resetModules();
|
|
387
|
+
mockFetch.mockReset();
|
|
388
|
+
const darwinMod = await import("./usage-limits.js");
|
|
389
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN));
|
|
390
|
+
mockFetch.mockReturnValue(makeFetchResponse(SAMPLE_LIMITS));
|
|
391
|
+
|
|
392
|
+
const first = await darwinMod.getUsageLimits();
|
|
393
|
+
expect(first).toEqual(SAMPLE_LIMITS);
|
|
394
|
+
|
|
395
|
+
// Second call should use cache - fetch should not be called again
|
|
396
|
+
const second = await darwinMod.getUsageLimits();
|
|
397
|
+
expect(second).toEqual(SAMPLE_LIMITS);
|
|
398
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("refreshes cache after TTL expires", async () => {
|
|
402
|
+
vi.resetModules();
|
|
403
|
+
mockFetch.mockReset();
|
|
404
|
+
const darwinMod = await import("./usage-limits.js");
|
|
405
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN));
|
|
406
|
+
mockFetch.mockReturnValue(makeFetchResponse(SAMPLE_LIMITS));
|
|
407
|
+
|
|
408
|
+
await darwinMod.getUsageLimits();
|
|
409
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
410
|
+
|
|
411
|
+
// Manually expire the cache by advancing Date.now via spy
|
|
412
|
+
const realNow = Date.now();
|
|
413
|
+
vi.spyOn(Date, "now").mockReturnValue(realNow + 61_000);
|
|
414
|
+
|
|
415
|
+
const updated = {
|
|
416
|
+
...SAMPLE_LIMITS,
|
|
417
|
+
five_hour: { utilization: 99, resets_at: null },
|
|
418
|
+
};
|
|
419
|
+
mockFetch.mockReturnValue(makeFetchResponse(updated));
|
|
420
|
+
|
|
421
|
+
const result = await darwinMod.getUsageLimits();
|
|
422
|
+
expect(result).toEqual(updated);
|
|
423
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
424
|
+
|
|
425
|
+
vi.spyOn(Date, "now").mockRestore();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("returns empty when fetch fails", async () => {
|
|
429
|
+
vi.resetModules();
|
|
430
|
+
mockFetch.mockReset();
|
|
431
|
+
const darwinMod = await import("./usage-limits.js");
|
|
432
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN));
|
|
433
|
+
mockFetch.mockReturnValue(makeFetchResponse({}, false));
|
|
434
|
+
|
|
435
|
+
const result = await darwinMod.getUsageLimits();
|
|
436
|
+
expect(result).toEqual(EMPTY);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ===========================================================================
|
|
441
|
+
// Token refresh flow (via getUsageLimits -> getValidAccessToken)
|
|
442
|
+
// These tests use macOS platform to exercise the execSync/keychain path
|
|
443
|
+
// ===========================================================================
|
|
444
|
+
describe("token refresh", () => {
|
|
445
|
+
const EMPTY = { five_hour: null, seven_day: null, extra_usage: null };
|
|
446
|
+
|
|
447
|
+
beforeEach(() => {
|
|
448
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("refreshes an expired token and uses the new one", async () => {
|
|
452
|
+
vi.resetModules();
|
|
453
|
+
mockFetch.mockReset();
|
|
454
|
+
mockExecSync.mockReset();
|
|
455
|
+
mockExecFileSync.mockReset();
|
|
456
|
+
const darwinMod = await import("./usage-limits.js");
|
|
457
|
+
|
|
458
|
+
// Provide an expired token
|
|
459
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN, { expired: true }));
|
|
460
|
+
|
|
461
|
+
// First call = token refresh, second call = usage API
|
|
462
|
+
mockFetch
|
|
463
|
+
.mockReturnValueOnce(
|
|
464
|
+
makeFetchResponse({
|
|
465
|
+
access_token: "sk-ant-new-token",
|
|
466
|
+
refresh_token: "sk-ant-new-refresh",
|
|
467
|
+
expires_in: 3600,
|
|
468
|
+
}),
|
|
469
|
+
)
|
|
470
|
+
.mockReturnValueOnce(makeFetchResponse(SAMPLE_LIMITS));
|
|
471
|
+
|
|
472
|
+
const result = await darwinMod.getUsageLimits();
|
|
473
|
+
expect(result).toEqual(SAMPLE_LIMITS);
|
|
474
|
+
|
|
475
|
+
// First fetch = refresh call to platform.claude.com
|
|
476
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
477
|
+
const [refreshUrl, refreshOpts] = mockFetch.mock.calls[0];
|
|
478
|
+
expect(refreshUrl).toContain("oauth/token");
|
|
479
|
+
expect(refreshOpts.method).toBe("POST");
|
|
480
|
+
|
|
481
|
+
// Second fetch = usage API with refreshed token
|
|
482
|
+
const [usageUrl, usageOpts] = mockFetch.mock.calls[1];
|
|
483
|
+
expect(usageUrl).toContain("oauth/usage");
|
|
484
|
+
expect(usageOpts.headers.Authorization).toBe("Bearer sk-ant-new-token");
|
|
485
|
+
|
|
486
|
+
// Credentials should have been written back via execFileSync (macOS keychain)
|
|
487
|
+
expect(mockExecFileSync).toHaveBeenCalled();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("returns empty when token is expired and refresh fails", async () => {
|
|
491
|
+
vi.resetModules();
|
|
492
|
+
mockFetch.mockReset();
|
|
493
|
+
mockExecSync.mockReset();
|
|
494
|
+
const darwinMod = await import("./usage-limits.js");
|
|
495
|
+
|
|
496
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN, { expired: true }));
|
|
497
|
+
// Refresh call fails
|
|
498
|
+
mockFetch.mockReturnValueOnce(makeFetchResponse({}, false));
|
|
499
|
+
|
|
500
|
+
const result = await darwinMod.getUsageLimits();
|
|
501
|
+
expect(result).toEqual(EMPTY);
|
|
502
|
+
// Only the refresh call, no usage call
|
|
503
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("returns empty when token is expired and refresh throws", async () => {
|
|
507
|
+
vi.resetModules();
|
|
508
|
+
mockFetch.mockReset();
|
|
509
|
+
mockExecSync.mockReset();
|
|
510
|
+
const darwinMod = await import("./usage-limits.js");
|
|
511
|
+
|
|
512
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN, { expired: true }));
|
|
513
|
+
mockFetch.mockRejectedValueOnce(new Error("network error"));
|
|
514
|
+
|
|
515
|
+
const result = await darwinMod.getUsageLimits();
|
|
516
|
+
expect(result).toEqual(EMPTY);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("uses valid (non-expired) token without refreshing", async () => {
|
|
520
|
+
vi.resetModules();
|
|
521
|
+
mockFetch.mockReset();
|
|
522
|
+
mockExecSync.mockReset();
|
|
523
|
+
const darwinMod = await import("./usage-limits.js");
|
|
524
|
+
|
|
525
|
+
// Token has a future expiry (default in makeCredentialsJson)
|
|
526
|
+
mockExecSync.mockReturnValue(makeCredentialsJson(SAMPLE_TOKEN));
|
|
527
|
+
mockFetch.mockReturnValueOnce(makeFetchResponse(SAMPLE_LIMITS));
|
|
528
|
+
|
|
529
|
+
const result = await darwinMod.getUsageLimits();
|
|
530
|
+
expect(result).toEqual(SAMPLE_LIMITS);
|
|
531
|
+
// Only one fetch call (usage API), no refresh call
|
|
532
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
533
|
+
const [url] = mockFetch.mock.calls[0];
|
|
534
|
+
expect(url).toContain("oauth/usage");
|
|
535
|
+
});
|
|
536
|
+
});
|