fix(agents): retain provider details from prompt rejection

This commit is contained in:
yiliang114 2026-10-01 19:02:54 +08:00
parent 8d7faadd32
commit 8e5cd79cdd
2 changed files with 81 additions and 8 deletions

View file

@ -9,6 +9,7 @@ import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import type { HostRunResult } from '@qwen-code/qwen-code-core';
import type { AcpSessionBridge } from './acp-session-bridge.js';
import { isRevocation, startAgentHostConnection } from './agent-host-client.js';
@ -44,6 +45,83 @@ describe('isRevocation', () => {
});
});
it('returns the provider detail when sendPrompt rejects with a JSON-RPC error', async () => {
const workspaceCwd = await fs.mkdtemp(path.join(os.tmpdir(), 'pr12582-f7-'));
let result: HostRunResult | undefined;
let pickups = 0;
const bridge = {
listWorkspaceSessions: () => [],
spawnOrAttach: vi.fn().mockResolvedValue({}),
async *subscribeEvents() {},
getSessionStatsStatus: vi.fn().mockResolvedValue({ models: {} }),
sendPrompt: vi.fn().mockRejectedValue({
code: -32603,
message: 'Internal error',
data: { details: '400 PR12582_PROVIDER_400_READABLE_CAUSE' },
}),
closeSession: vi.fn().mockResolvedValue({}),
} as unknown as AcpSessionBridge;
vi.stubEnv('QWEN_HOME', workspaceCwd);
vi.stubGlobal(
'fetch',
vi.fn(async (url: string, init?: RequestInit) => {
if (url.endsWith('/enroll')) {
return Response.json({ host: { id: 'host-test' }, secret: 'secret' });
}
if (url.endsWith('/pickup')) {
if (++pickups > 1) {
return Response.json(
{ error: 'Invalid Agent Host credential.' },
{ status: 401 },
);
}
return Response.json({
assignment: {
agent: { id: 'agent-test', name: 'test' },
threadId: 'thread-test',
runId: 'run-test',
attempt: 1,
lease: { leaseId: 'lease-test' },
prompt: 'Read the fixture.',
},
});
}
if (url.endsWith('/result')) {
result = JSON.parse(init?.body as string) as HostRunResult;
}
if (url.endsWith('/heartbeat')) {
return Response.json({ lease: { leaseId: 'lease-test' } });
}
return Response.json({ ok: true });
}),
);
try {
await startAgentHostConnection({
bridge,
serverUrl: 'http://127.0.0.1:18583',
workspaceId: 'ws-test',
workspaceCwd,
enrollmentToken: 'test-token',
});
await vi.waitFor(() => expect(pickups).toBe(2));
expect(result).toMatchObject({
status: 'failed',
error: '400 PR12582_PROVIDER_400_READABLE_CAUSE',
});
expect(bridge.sendPrompt).toHaveBeenCalledOnce();
expect(bridge.closeSession).toHaveBeenCalledOnce();
await vi.waitFor(async () => {
expect(await fs.readdir(path.join(workspaceCwd, 'agent-hosts'))).toEqual(
[],
);
});
} finally {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
await fs.rm(workspaceCwd, { recursive: true, force: true });
}
});
it('retains a saved credential when rejoining sees only bare bearer 401s', async () => {
const qwenDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pr12582-f1-'));
const serverUrl = 'http://127.0.0.1:18582';

View file

@ -9,6 +9,7 @@ import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { extractErrorMessage } from '@qwen-code/acp-bridge/bridge';
import type {
HostRunAssignment,
HostRunResult,
@ -491,14 +492,8 @@ async function executeAssignment(
);
if (turn?.promptId === promptId) {
if (turn.state === 'error' || turn.state === 'cancelled') {
const message = turn.error?.message;
// A provider failure can arrive as the JSON-RPC error object
// itself; stringifying the field keeps the real cause on the wire
// instead of "[object Object]".
throw new Error(
typeof message === 'string' && message
? message
: (JSON.stringify(turn.error) ?? 'Managed Agent cancelled.'),
extractErrorMessage(turn.error ?? 'Managed Agent cancelled.'),
);
}
if (turn.state === 'completed') {
@ -843,7 +838,7 @@ async function connectAgentHost(
error instanceof Error && error.message === 'not_leasable'
? 'cancelled'
: 'failed',
error: error instanceof Error ? error.message : String(error),
error: extractErrorMessage(error),
};
}
await returnResult(