Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
335 lines
11 KiB
JavaScript
335 lines
11 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
probeClaudeAuth, probeCopilotAuth, probeCodexAuth, probeCodebuddyAuth, probeCursorCliAuth, probeGrokAuth,
|
|
parseCursorStatusJson, resolveCursorCliSpawnSpec,
|
|
} = require("./agentAuthProbes.cjs");
|
|
const { prepareCommandForSpawn } = require("../ai/shellUtils.cjs");
|
|
const { resolveCursorCliSpawnSpec: resolveSharedCursorCliSpawnSpec } = require("./cursorCliSpawn.cjs");
|
|
|
|
test("probeClaudeAuth: env ANTHROPIC_API_KEY -> authenticated env", () => {
|
|
const r = probeClaudeAuth({
|
|
env: { ANTHROPIC_API_KEY: "sk-x" },
|
|
platform: "darwin",
|
|
runSecurity: () => { throw new Error("should not be called"); },
|
|
fileExists: () => false,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "env");
|
|
});
|
|
|
|
test("probeClaudeAuth: macOS keychain hit -> authenticated keychain", () => {
|
|
const r = probeClaudeAuth({
|
|
env: {},
|
|
platform: "darwin",
|
|
runSecurity: () => ({ exitCode: 0, stdout: '{"claudeAiOauth":{}}' }),
|
|
fileExists: () => false,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "keychain");
|
|
});
|
|
|
|
test("probeClaudeAuth: linux credentials file -> authenticated credentials-file", () => {
|
|
const r = probeClaudeAuth({
|
|
env: {},
|
|
platform: "linux",
|
|
runSecurity: () => { throw new Error("no keychain on linux"); },
|
|
fileExists: (p) => p.endsWith(".credentials.json"),
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "credentials-file");
|
|
});
|
|
|
|
test("probeClaudeAuth: nothing -> not authenticated", () => {
|
|
const r = probeClaudeAuth({
|
|
env: {}, platform: "darwin",
|
|
runSecurity: () => ({ exitCode: 44, stdout: "" }),
|
|
fileExists: () => false,
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCopilotAuth: gh auth status exit 0 -> authenticated gh", () => {
|
|
const r = probeCopilotAuth({ runGhAuthStatus: () => ({ exitCode: 0, stderr: "Logged in to github.com" }) });
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "gh");
|
|
});
|
|
|
|
test("probeCopilotAuth: gh auth status non-zero -> not authenticated", () => {
|
|
const r = probeCopilotAuth({ runGhAuthStatus: () => ({ exitCode: 1, stderr: "not logged in" }) });
|
|
assert.equal(r.authenticated, false);
|
|
});
|
|
|
|
test("probeCodexAuth: 'Logged in using ChatGPT' -> authenticated chatgpt", () => {
|
|
const r = probeCodexAuth({
|
|
runLoginStatus: () => ({ exitCode: 0, stdout: "Logged in using ChatGPT" }),
|
|
fileExists: () => false,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "chatgpt");
|
|
});
|
|
|
|
test("probeCodexAuth: auth.json fallback -> authenticated auth-file", () => {
|
|
const r = probeCodexAuth({
|
|
runLoginStatus: () => ({ exitCode: 1, stdout: "not logged in" }),
|
|
fileExists: (p) => p.endsWith("auth.json"),
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "auth-file");
|
|
});
|
|
|
|
// ── CodeBuddy ──
|
|
test("probeCodebuddyAuth: CODEBUDDY_API_KEY env -> authenticated api-key", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: { CODEBUDDY_API_KEY: "cb-key-123" },
|
|
readFile: () => null,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "api-key");
|
|
});
|
|
|
|
test("probeCodebuddyAuth: CODEBUDDY_AUTH_TOKEN env -> authenticated auth-token", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: { CODEBUDDY_AUTH_TOKEN: "oauth-token-xyz" },
|
|
readFile: () => null,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "auth-token");
|
|
});
|
|
|
|
test("probeCodebuddyAuth: settings.json with authToken -> authenticated settings-file", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => '{"authToken":"real-token"}',
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "settings-file");
|
|
});
|
|
|
|
test("probeCodebuddyAuth: settings.json with apiKeyHelper -> authenticated settings-file", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => '{"apiKeyHelper":"/usr/local/bin/helper"}',
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "settings-file");
|
|
});
|
|
|
|
test("probeCodebuddyAuth: empty settings.json -> not authenticated", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => "",
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCodebuddyAuth: malformed JSON in settings.json -> not authenticated", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => "{not valid json",
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCodebuddyAuth: settings.json without auth fields -> not authenticated", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => '{"theme":"dark","language":"en"}',
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCodebuddyAuth: no env, no settings file -> not authenticated", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: {},
|
|
readFile: () => null,
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCodebuddyAuth: CODEBUDDY_API_KEY takes precedence over settings.json", () => {
|
|
const r = probeCodebuddyAuth({
|
|
env: { CODEBUDDY_API_KEY: "cb-key" },
|
|
readFile: () => '{"authToken":"token"}',
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "api-key");
|
|
});
|
|
|
|
// ── Cursor CLI login ──
|
|
test("probeCursorCliAuth: prefers cursor-agent and parses authenticated JSON", () => {
|
|
const calls = [];
|
|
const r = probeCursorCliAuth({
|
|
env: { CURSOR_API_KEY: "should-be-stripped" },
|
|
resolveBinary: (name) => {
|
|
calls.push(name);
|
|
return name === "cursor-agent" ? "/bin/cursor-agent" : null;
|
|
},
|
|
runStatus: (bin) => {
|
|
assert.equal(bin, "/bin/cursor-agent");
|
|
return {
|
|
exitCode: 0,
|
|
stdout: JSON.stringify({
|
|
status: "authenticated",
|
|
isAuthenticated: true,
|
|
userInfo: { email: "user@example.com" },
|
|
}),
|
|
};
|
|
},
|
|
});
|
|
assert.deepEqual(calls, ["cursor-agent"]);
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "cli-login");
|
|
assert.equal(r.email, "user@example.com");
|
|
assert.equal(r.binPath, "/bin/cursor-agent");
|
|
});
|
|
|
|
test("probeCursorCliAuth: does not fall back to bare agent binary", () => {
|
|
const statusCalls = [];
|
|
const resolveCalls = [];
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: (name) => {
|
|
resolveCalls.push(name);
|
|
return name === "agent" ? "/bin/agent" : null;
|
|
},
|
|
runStatus: (bin) => {
|
|
statusCalls.push(bin);
|
|
return {
|
|
exitCode: 0,
|
|
stdout: JSON.stringify({ isAuthenticated: true, userInfo: { email: "a@b.c" } }),
|
|
};
|
|
},
|
|
});
|
|
assert.deepEqual(resolveCalls, ["cursor-agent"]);
|
|
assert.deepEqual(statusCalls, []);
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.binPath, null);
|
|
});
|
|
|
|
// ── Grok Build ──
|
|
test("probeGrokAuth: env XAI_API_KEY -> authenticated env", () => {
|
|
const r = probeGrokAuth({
|
|
env: { XAI_API_KEY: "xai-test" },
|
|
fileExists: () => false,
|
|
homeDir: "/home/user",
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "env");
|
|
});
|
|
|
|
test("probeGrokAuth: auth.json fallback -> authenticated auth-file", () => {
|
|
const path = require("node:path");
|
|
const homeDir = path.join("home", "user");
|
|
const authPath = path.join(homeDir, ".grok", "auth.json");
|
|
const r = probeGrokAuth({
|
|
env: {},
|
|
homeDir,
|
|
fileExists: (p) => p === authPath,
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "auth-file");
|
|
});
|
|
|
|
test("probeGrokAuth: nothing -> not authenticated", () => {
|
|
const r = probeGrokAuth({
|
|
env: {},
|
|
homeDir: "/home/user",
|
|
fileExists: () => false,
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
});
|
|
|
|
test("probeCursorCliAuth: unauthenticated JSON -> not authenticated but keeps binPath", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: (name) => (name === "cursor-agent" ? "/bin/cursor-agent" : null),
|
|
runStatus: () => ({
|
|
exitCode: 0,
|
|
stdout: JSON.stringify({ isAuthenticated: false }),
|
|
}),
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
assert.equal(r.binPath, "/bin/cursor-agent");
|
|
});
|
|
|
|
test("probeCursorCliAuth: missing binary -> not authenticated", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: () => null,
|
|
runStatus: () => { throw new Error("should not run"); },
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.binPath, null);
|
|
});
|
|
|
|
test("probeCursorCliAuth: status command failure -> not authenticated", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: () => "/bin/cursor-agent",
|
|
runStatus: () => ({ exitCode: 1, stdout: "", stderr: "boom" }),
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.binPath, "/bin/cursor-agent");
|
|
});
|
|
|
|
test("probeCursorCliAuth: unrecognized status stdout keeps resolved binPath", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: () => "/bin/cursor-agent",
|
|
runStatus: () => ({ exitCode: 0, stdout: "cursor-agent 1.2.3\nnot json" }),
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
assert.equal(r.binPath, "/bin/cursor-agent");
|
|
});
|
|
|
|
test("probeCursorCliAuth: thrown status probe keeps resolved binPath", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: () => "/bin/cursor-agent",
|
|
runStatus: () => { throw new Error("timeout"); },
|
|
});
|
|
assert.equal(r.authenticated, false);
|
|
assert.equal(r.authSource, null);
|
|
assert.equal(r.binPath, "/bin/cursor-agent");
|
|
});
|
|
|
|
test("probeCursorCliAuth: extracts authenticated JSON wrapped in cmd noise", () => {
|
|
const r = probeCursorCliAuth({
|
|
resolveBinary: () => "C:\\Users\\me\\AppData\\Local\\cursor-agent\\cursor-agent.cmd",
|
|
runStatus: () => ({
|
|
exitCode: 0,
|
|
stdout: "Starting...\r\n{\"status\":\"authenticated\",\"isAuthenticated\":true,\"userInfo\":{\"email\":\"user@example.com\"}}\r\n",
|
|
}),
|
|
});
|
|
assert.equal(r.authenticated, true);
|
|
assert.equal(r.authSource, "cli-login");
|
|
assert.equal(r.email, "user@example.com");
|
|
});
|
|
|
|
test("parseCursorStatusJson accepts BOM and surrounding text", () => {
|
|
const parsed = parseCursorStatusJson(
|
|
"\uFEFFnoise\n{\"isAuthenticated\":true,\"status\":\"authenticated\"}\n",
|
|
);
|
|
assert.equal(parsed.isAuthenticated, true);
|
|
assert.equal(parsed.status, "authenticated");
|
|
});
|
|
|
|
test("resolveCursorCliSpawnSpec re-exports the shared native launch helper", () => {
|
|
const shim = "C:\\Users\\me\\AppData\\Local\\cursor-agent\\cursor-agent.cmd";
|
|
const args = ["status", "--format", "json"];
|
|
assert.deepEqual(
|
|
resolveCursorCliSpawnSpec(shim, args),
|
|
resolveSharedCursorCliSpawnSpec(shim, args),
|
|
);
|
|
assert.deepEqual(
|
|
resolveSharedCursorCliSpawnSpec(shim, args, {
|
|
exists: () => false,
|
|
readFile: () => { throw new Error("missing"); },
|
|
}),
|
|
prepareCommandForSpawn(shim, args, { unwrapNativeExe: false }),
|
|
);
|
|
});
|