Merge pull request #2536 from LaZzyMan/fix/test-artifacts-and-shell-permissions
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

Fix shell permission parsing and test-created debug artifacts
This commit is contained in:
顾盼 2026-03-23 11:34:16 +08:00 committed by GitHub
commit a0041191a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 62 additions and 2 deletions

View file

@ -9,4 +9,10 @@ if (process.env['NO_COLOR'] !== undefined) {
delete process.env['NO_COLOR'];
}
// Avoid writing per-session debug log files during CLI tests.
// Individual tests can still opt in by overriding this env var explicitly.
if (process.env['QWEN_DEBUG_LOG_FILE'] === undefined) {
process.env['QWEN_DEBUG_LOG_FILE'] = '0';
}
import './src/test-utils/customMatchers.js';

View file

@ -949,6 +949,24 @@ describe('ShellTool', () => {
);
});
it('should not surface file descriptor redirects as standalone commands in confirmation details', async () => {
const params = {
command: 'npm run build 2>&1 | head -100',
is_background: false,
};
const invocation = shellTool.build(params);
const permission = await invocation.getDefaultPermission();
expect(permission).toBe('ask');
const details = (await invocation.getConfirmationDetails(
new AbortController().signal,
)) as { rootCommand: string; permissionRules: string[] };
expect(details.rootCommand).toBe('npm');
expect(details.permissionRules).toEqual(['Bash(npm run *)']);
});
it('should throw an error if validation fails', () => {
expect(() =>
shellTool.build({ command: '', is_background: false }),

View file

@ -440,6 +440,16 @@ describe('getCommandRoots', () => {
const result = getCommandRoots('ls\n\ngrep foo');
expect(result).toEqual(['ls', 'grep']);
});
it('should not treat file descriptor redirection as a command separator', () => {
const result = getCommandRoots('npm run build 2>&1 | head -100');
expect(result).toEqual(['npm', 'head']);
});
it('should not treat >| redirection as a pipeline separator', () => {
const result = getCommandRoots('echo hello >| out.txt');
expect(result).toEqual(['echo']);
});
});
describe('stripShellWrapper', () => {

View file

@ -126,6 +126,16 @@ export function splitCommands(command: string): string[] {
let inDoubleQuotes = false;
let i = 0;
const previousNonWhitespaceChar = (index: number): string | undefined => {
for (let j = index - 1; j >= 0; j--) {
const ch = command[j];
if (ch && !/\s/.test(ch)) {
return ch;
}
}
return undefined;
};
while (i < command.length) {
const char = command[i];
const nextChar = command[i + 1];
@ -145,14 +155,30 @@ export function splitCommands(command: string): string[] {
if (!inSingleQuotes && !inDoubleQuotes) {
if (
(char === '&' && nextChar === '&') ||
(char === '|' && nextChar === '|')
(char === '|' && (nextChar === '|' || nextChar === '&'))
) {
commands.push(currentCommand.trim());
currentCommand = '';
i++; // Skip the next character
} else if (char === ';' || char === '&' || char === '|') {
} else if (char === ';') {
commands.push(currentCommand.trim());
currentCommand = '';
} else if (char === '&') {
const prevChar = previousNonWhitespaceChar(i);
if (prevChar === '>' || prevChar === '<') {
currentCommand += char;
} else {
commands.push(currentCommand.trim());
currentCommand = '';
}
} else if (char === '|') {
const prevChar = previousNonWhitespaceChar(i);
if (prevChar === '>') {
currentCommand += char;
} else {
commands.push(currentCommand.trim());
currentCommand = '';
}
} else if (char === '\r' && nextChar === '\n') {
// Windows-style \r\n newline - treat as command separator
commands.push(currentCommand.trim());