Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

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

Expand Down Expand Up @@ -278,4 +281,65 @@ describe('LlmRewriter', () => {
expect(input).not.toContain('上一轮改写结果');
});
});

describe('promptFile', () => {
let tempDir: string;

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'llm-rewriter-promptfile-'));
});

afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});

function promptOf(rewriter: unknown): string {
return (rewriter as { prompt: string }).prompt;
}

it('loads a custom prompt from a readable file', () => {
const filePath = join(tempDir, 'prompt.md');
writeFileSync(filePath, ' custom rewrite prompt ');

const rewriter = new LlmRewriter(makeConfig(), {
enabled: true,
target: 'all',
promptFile: filePath,
} as MessageRewriteConfig);

expect(promptOf(rewriter)).toBe('custom rewrite prompt');
});

it('falls back to the default prompt when the file is missing', () => {
const rewriter = new LlmRewriter(makeConfig(), {
enabled: true,
target: 'all',
promptFile: join(tempDir, 'does-not-exist.md'),
} as MessageRewriteConfig);

expect(promptOf(rewriter)).toContain('rewrites raw coding-agent output');
});

// Regression for #9752: promptFile pointing at a path that exists but
// cannot be read as a file (a directory) used to throw EISDIR from the
// constructor, crashing ACP session startup.
it('falls back to the default prompt when promptFile is a directory', () => {
expect(
() =>
new LlmRewriter(makeConfig(), {
enabled: true,
target: 'all',
promptFile: tempDir,
} as MessageRewriteConfig),
).not.toThrow();

const rewriter = new LlmRewriter(makeConfig(), {
enabled: true,
target: 'all',
promptFile: tempDir,
} as MessageRewriteConfig);

expect(promptOf(rewriter)).toContain('rewrites raw coding-agent output');
});
});
});
25 changes: 19 additions & 6 deletions packages/cli/src/acp-integration/session/rewrite/LlmRewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,29 @@ export class LlmRewriter {
// promptFile takes precedence over inline prompt
if (rewriteConfig.promptFile) {
const filePath = resolve(rewriteConfig.promptFile);
if (existsSync(filePath)) {
this.prompt = readFileSync(filePath, 'utf-8').trim();
debugLogger.info(
`Loaded rewrite prompt from file: ${filePath} (${this.prompt.length} chars)`,
);
} else {
if (!existsSync(filePath)) {
debugLogger.warn(
`Rewrite prompt file not found: ${filePath}, using default`,
);
this.prompt = DEFAULT_REWRITE_PROMPT;
} else {
// existsSync passes for directories and says nothing about
// readability, so the read itself can still fail (EISDIR, EACCES,
// ...). Degrade like the missing-file case instead of throwing,
// which would crash ACP session startup (#9752).
try {
this.prompt = readFileSync(filePath, 'utf-8').trim();
debugLogger.info(
`Loaded rewrite prompt from file: ${filePath} (${this.prompt.length} chars)`,
);
} catch (error) {
debugLogger.warn(
`Rewrite prompt file could not be read: ${filePath} (${
error instanceof Error ? error.message : String(error)
}), using default`,
);
Comment thread
yiliang114 marked this conversation as resolved.
this.prompt = DEFAULT_REWRITE_PROMPT;
}
}
} else {
this.prompt = rewriteConfig.prompt || DEFAULT_REWRITE_PROMPT;
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/acp-integration/session/rewrite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ Add to `settings.json`:
```

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