fix: gate login result text on error state (#1103)

## Summary
- Fixes #1092 by treating `Please run /login` in a `success` result as
auth-required only when the result is actually error-shaped.
- Applies the same error-state guard to goal-command failure detection
so quoted login text in a normal answer does not roll back the active
goal.
- Adds a regression test for a successful model answer that mentions
`/login` without publishing a session failure.

## Test Plan
- [x] `npm run test:run -- src/tests/acp-agent.test.ts -t
'login|auth_required'`
- [x] `npm run build`
- [x] `npm run check`
- [ ] `npm run test:run` (blocked locally by pre-existing root
permission-mode assertions: `resolvePermissionMode("bypassPermissions")`
and `resolvePermissionMode("bypass")` resolve to `default` when running
as root)

Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>
This commit is contained in:
Emre K.
2026-09-23 12:39:03 +03:00
committed by GitHub
parent 2b0201a801
commit 6e97e071c5
2 changed files with 33 additions and 2 deletions

View File

@@ -5042,7 +5042,9 @@ export class ClaudeAcpAgent {
const goalCommandFailed =
message.is_error ||
message.stop_reason === "refusal" ||
("result" in message && message.result.includes("Please run /login"));
("result" in message &&
message.is_error &&
message.result.includes("Please run /login"));
if (goalCommandFailed) {
await this.publishGoal(params.sessionId, pendingGoalUpdate.previous ?? null);
}
@@ -5406,7 +5408,7 @@ export class ClaudeAcpAgent {
switch (message.subtype) {
case "success": {
if (message.result.includes("Please run /login")) {
if (message.is_error && message.result.includes("Please run /login")) {
await failActiveWithSessionFailure(
"auth_required",
RequestError.authRequired(),

View File

@@ -7439,6 +7439,35 @@ describe("stop reason propagation", () => {
expect(logged.join("\n")).not.toContain("cannot fail active turn");
});
it("does not treat a successful answer mentioning /login as auth_required", async () => {
const updates: SessionNotification[] = [];
const agent = new ClaudeAcpAgent(
{
sessionUpdate: async (update: SessionNotification) => {
updates.push(update);
},
} as unknown as AcpClient,
{ log: () => {}, error: () => {} },
);
(agent as any).clientCapabilities = airSessionFailureCapabilities;
const answer = "A normal answer can quote: Please run /login.";
injectSession(agent, [
createAssistantError(undefined, answer),
createResultMessage({
subtype: "success",
stop_reason: "end_turn",
is_error: false,
result: answer,
}),
{ type: "system", subtype: "session_state_changed", state: "idle" },
]);
await expect(
agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] }),
).resolves.toMatchObject({ stopReason: "end_turn" });
expect(sessionFailuresFromUpdates(updates)).toEqual([]);
});
it("republishes the signed-out state after a real model answer cleared the last one", async () => {
const updates: SessionNotification[] = [];
const agent = new ClaudeAcpAgent(