From 5590dfe67b40930bee2291f6fd2ce6ecebb5914f Mon Sep 17 00:00:00 2001 From: Hessabi Date: Fri, 23 Oct 2026 11:27:44 -0500 Subject: [PATCH] Fix auth status treating "Not logged in" as logged in `devin auth status` exits 0 when logged out and prints "Not logged in.", a credentials path, and a "Run `devin auth login`" hint. The old /logged in/i check matched that hint text, so setup reported authenticated:true for a logged-out user. Now a session counts as logged in only when a line starts with "logged in" and the output does not contain "not logged in". Adds a loggedOutExitZero fake-devin option and a setup regression test. --- plugins/devin/scripts/lib/devin.mjs | 2 +- tests/companion.test.mjs | 17 +++++++++++++++++ tests/fake-devin-fixture.mjs | 8 +++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/plugins/devin/scripts/lib/devin.mjs b/plugins/devin/scripts/lib/devin.mjs index b85e5d3..a65c5bf 100644 --- a/plugins/devin/scripts/lib/devin.mjs +++ b/plugins/devin/scripts/lib/devin.mjs @@ -136,7 +136,7 @@ export function getDevinAuthStatus(cwd) { return { authenticated: false, detail }; } const output = result.stdout; - const loggedIn = /logged in/i.test(output); + const loggedIn = /^\s*logged in/im.test(output) && !/\bnot logged in\b/i.test(output); const email = output.match(/Email:\s*(\S+)/)?.[1] ?? null; const tier = output.match(/Tier:\s*(.+)/)?.[1]?.trim() ?? null; return { diff --git a/tests/companion.test.mjs b/tests/companion.test.mjs index 633e6a4..99ed541 100644 --- a/tests/companion.test.mjs +++ b/tests/companion.test.mjs @@ -133,6 +133,23 @@ test("setup reports ready=false when devin auth fails", () => { } }); +test("setup reports authenticated=false when devin auth status exits 0 while logged out", () => { + const repo = createTempRepo(); + const fake = createFakeDevin({ loggedOutExitZero: true }); + const dataDir = fs.mkdtempSync("/tmp/devin-plugin-data-"); + try { + const env = { ...fake.env, CLAUDE_PLUGIN_DATA: dataDir }; + const result = runCompanion(["setup", "--json"], { cwd: repo.dir, env }); + const payload = JSON.parse(result.stdout); + assert.equal(payload.devin.installed, true); + assert.equal(payload.devin.authenticated, false); + assert.equal(payload.ready, false); + } finally { + repo.cleanup(); + fake.cleanup(); + } +}); + test("setup --enable-review-gate persists config", () => { const repo = createTempRepo(); const fake = createFakeDevin(); diff --git a/tests/fake-devin-fixture.mjs b/tests/fake-devin-fixture.mjs index 6510670..489a4fb 100644 --- a/tests/fake-devin-fixture.mjs +++ b/tests/fake-devin-fixture.mjs @@ -4,7 +4,7 @@ import path from "node:path"; // Creates a fake `devin` executable that answers the subcommands the companion // uses. The marker file records each -p invocation's prompt file for assertions. -export function createFakeDevin({ responseText = "FAKE_DEVIN_OUTPUT", exitCode = 0, failAuth = false, stderrText = "" } = {}) { +export function createFakeDevin({ responseText = "FAKE_DEVIN_OUTPUT", exitCode = 0, failAuth = false, loggedOutExitZero = false, stderrText = "" } = {}) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fake-devin-")); const marker = path.join(dir, "invocations.jsonl"); const stderrFile = path.join(dir, "stderr.txt"); @@ -23,6 +23,12 @@ case "$cmd" in echo "Not logged in" >&2 exit 1 fi + if [ "${loggedOutExitZero}" = "true" ]; then + echo "Not logged in." + echo " Credentials path: /Users/fake/.local/share/devin/credentials.toml" + echo "Run \\\`devin auth login\\\` to authenticate." + exit 0 + fi echo "Logged in (via Devin)." echo " Email: fake@example.com" echo " Tier: Devin Pro"