Skip to content

Commit 8be0715

Browse files
authored
fix(cli): degrade gracefully when messageRewrite.promptFile cannot be read (#9753)
* fix(cli): degrade gracefully when messageRewrite.promptFile cannot be read * docs(cli): document rewrite prompt fallback
1 parent 3133e83 commit 8be0715

3 files changed

Lines changed: 87 additions & 7 deletions

File tree

packages/cli/src/acp-integration/session/rewrite/LlmRewriter.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { describe, it, expect, vi, beforeEach } from 'vitest';
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9+
import { tmpdir } from 'node:os';
10+
import { join } from 'node:path';
811
import type { Config } from '@qwen-code/qwen-code-core';
912
import type { TurnContent, MessageRewriteConfig } from './types.js';
1013

@@ -278,4 +281,65 @@ describe('LlmRewriter', () => {
278281
expect(input).not.toContain('上一轮改写结果');
279282
});
280283
});
284+
285+
describe('promptFile', () => {
286+
let tempDir: string;
287+
288+
beforeEach(() => {
289+
tempDir = mkdtempSync(join(tmpdir(), 'llm-rewriter-promptfile-'));
290+
});
291+
292+
afterEach(() => {
293+
rmSync(tempDir, { recursive: true, force: true });
294+
});
295+
296+
function promptOf(rewriter: unknown): string {
297+
return (rewriter as { prompt: string }).prompt;
298+
}
299+
300+
it('loads a custom prompt from a readable file', () => {
301+
const filePath = join(tempDir, 'prompt.md');
302+
writeFileSync(filePath, ' custom rewrite prompt ');
303+
304+
const rewriter = new LlmRewriter(makeConfig(), {
305+
enabled: true,
306+
target: 'all',
307+
promptFile: filePath,
308+
} as MessageRewriteConfig);
309+
310+
expect(promptOf(rewriter)).toBe('custom rewrite prompt');
311+
});
312+
313+
it('falls back to the default prompt when the file is missing', () => {
314+
const rewriter = new LlmRewriter(makeConfig(), {
315+
enabled: true,
316+
target: 'all',
317+
promptFile: join(tempDir, 'does-not-exist.md'),
318+
} as MessageRewriteConfig);
319+
320+
expect(promptOf(rewriter)).toContain('rewrites raw coding-agent output');
321+
});
322+
323+
// Regression for #9752: promptFile pointing at a path that exists but
324+
// cannot be read as a file (a directory) used to throw EISDIR from the
325+
// constructor, crashing ACP session startup.
326+
it('falls back to the default prompt when promptFile is a directory', () => {
327+
expect(
328+
() =>
329+
new LlmRewriter(makeConfig(), {
330+
enabled: true,
331+
target: 'all',
332+
promptFile: tempDir,
333+
} as MessageRewriteConfig),
334+
).not.toThrow();
335+
336+
const rewriter = new LlmRewriter(makeConfig(), {
337+
enabled: true,
338+
target: 'all',
339+
promptFile: tempDir,
340+
} as MessageRewriteConfig);
341+
342+
expect(promptOf(rewriter)).toContain('rewrites raw coding-agent output');
343+
});
344+
});
281345
});

packages/cli/src/acp-integration/session/rewrite/LlmRewriter.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,29 @@ export class LlmRewriter {
5757
// promptFile takes precedence over inline prompt
5858
if (rewriteConfig.promptFile) {
5959
const filePath = resolve(rewriteConfig.promptFile);
60-
if (existsSync(filePath)) {
61-
this.prompt = readFileSync(filePath, 'utf-8').trim();
62-
debugLogger.info(
63-
`Loaded rewrite prompt from file: ${filePath} (${this.prompt.length} chars)`,
64-
);
65-
} else {
60+
if (!existsSync(filePath)) {
6661
debugLogger.warn(
6762
`Rewrite prompt file not found: ${filePath}, using default`,
6863
);
6964
this.prompt = DEFAULT_REWRITE_PROMPT;
65+
} else {
66+
// existsSync passes for directories and says nothing about
67+
// readability, so the read itself can still fail (EISDIR, EACCES,
68+
// ...). Degrade like the missing-file case instead of throwing,
69+
// which would crash ACP session startup (#9752).
70+
try {
71+
this.prompt = readFileSync(filePath, 'utf-8').trim();
72+
debugLogger.info(
73+
`Loaded rewrite prompt from file: ${filePath} (${this.prompt.length} chars)`,
74+
);
75+
} catch (error) {
76+
debugLogger.warn(
77+
`Rewrite prompt file could not be read: ${filePath} (${
78+
error instanceof Error ? error.message : String(error)
79+
}), using default`,
80+
);
81+
this.prompt = DEFAULT_REWRITE_PROMPT;
82+
}
7083
}
7184
} else {
7285
this.prompt = rewriteConfig.prompt || DEFAULT_REWRITE_PROMPT;

packages/cli/src/acp-integration/session/rewrite/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ Add to `settings.json`:
3333
```
3434

3535
`timeoutMs` sets the per-rewrite LLM call timeout in milliseconds. Defaults to 30000.
36+
If `promptFile` is missing or cannot be read, rewriting falls back to the
37+
built-in default prompt. Set `QWEN_DEBUG_LOG_FILE` to capture the fallback
38+
warning.

0 commit comments

Comments
 (0)