mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-23 07:35:13 +00:00
fix(cli): make the safeTarget slug space prefix-free at the dash boundary
Review's cleanup sweeps .qwen/tmp/ by qwen-review-<slug>- prefix. A slug that itself carried '-' — natively (pr-6771 vs pr) or via the truncation join — could extend a shorter slug, letting one target's cleanup delete a DISTINCT target's artifacts (R8-7). The truncation branch this PR carries newly lands deep targets inside the cap instead of dying ENAMETOOLONG, which turned the latent collision live. Drop '-' from the slug alphabet entirely (dashes flatten like separators) and join the truncation digest with '_': with '-' out of every slug, the qwen-review-<slug>- boundary is unambiguous by construction — no slug can start with another slug plus '-'. prev-ledger side files keep their hardcoded dashed name on both writer and reader, untouched by the slug. Tests pin the prefix-free property (short-vs-short, short-vs-truncated) and the fixture names follow the new slugs. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
bc3df79d47
commit
539495226d
5 changed files with 41 additions and 16 deletions
|
|
@ -80,7 +80,7 @@ import {
|
|||
import { runCleanup } from './cleanup.js';
|
||||
|
||||
const PLAN = {
|
||||
diffPathAbsolute: '/abs/.qwen/tmp/qwen-review-pr-9206-diff.txt',
|
||||
diffPathAbsolute: '/abs/.qwen/tmp/qwen-review-pr_9206-diff.txt',
|
||||
chunks: [
|
||||
{
|
||||
id: 13,
|
||||
|
|
@ -502,7 +502,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
writeFileSync(planPath, JSON.stringify({ prNumber: '9206' }));
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
|
|
@ -533,7 +533,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
writeFileSync(planPath, JSON.stringify({ prNumber: '9206' }));
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
|
|
@ -564,7 +564,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
mkdirSync(recordDir, { recursive: true });
|
||||
|
|
@ -601,7 +601,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
mkdirSync(recordDir, { recursive: true });
|
||||
|
|
@ -634,7 +634,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
writeFileSync(planPath, JSON.stringify({ prNumber: '9206' }));
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
|
|
@ -657,7 +657,7 @@ describe('issue #9206 — Step 9 cleanup must not destroy a non-converged run’
|
|||
dir,
|
||||
'.qwen',
|
||||
'tmp',
|
||||
'qwen-review-pr-9206-fetch.json',
|
||||
'qwen-review-pr_9206-fetch.json',
|
||||
);
|
||||
writeFileSync(planPath, JSON.stringify({ prNumber: '9206' }));
|
||||
const recordDir = promptRecordDir(planPath);
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ describe('PARSE_ARGS_REPORT', () => {
|
|||
});
|
||||
|
||||
describe('tmpFile — target is a single safe component', () => {
|
||||
it('keeps ordinary labels intact', () => {
|
||||
it('flattens ordinary labels to the prefix-free slug', () => {
|
||||
expect(tmpFile('pr-6771', 'diff.txt')).toContain(
|
||||
'qwen-review-pr-6771-diff.txt',
|
||||
'qwen-review-pr_6771-diff.txt',
|
||||
);
|
||||
expect(tmpFile('local', 'plan.json')).toContain(
|
||||
'qwen-review-local-plan.json',
|
||||
|
|
|
|||
|
|
@ -2781,7 +2781,7 @@ describe('what the reviewer caught in this change', () => {
|
|||
// receipt lands there.
|
||||
describe('submit receipt (producer half of the audit contract)', () => {
|
||||
const receiptPath = () =>
|
||||
join(dir, '.qwen', 'tmp', 'qwen-review-pr-6771-submit-receipt.json');
|
||||
join(dir, '.qwen', 'tmp', 'qwen-review-pr_6771-submit-receipt.json');
|
||||
|
||||
const authorizedPost = (over: Record<string, unknown> = {}) =>
|
||||
args({ userAuthorized: true, ...over });
|
||||
|
|
|
|||
|
|
@ -38,6 +38,23 @@ describe('safeTarget', () => {
|
|||
expect(safeTarget('///')).toBe('target');
|
||||
});
|
||||
|
||||
it('keeps the slug space prefix-free at the dash boundary', () => {
|
||||
// Review's cleanup sweeps `.qwen/tmp/` by `qwen-review-<slug>-` prefix:
|
||||
// if a slug could contain `-` — natively, or via the truncation join —
|
||||
// one target's slug could extend another's, and the shorter target's
|
||||
// cleanup would delete the longer target's artifacts. `-` is therefore
|
||||
// out of the slug alphabet entirely.
|
||||
expect(safeTarget('foo-bar')).toBe('foo_bar');
|
||||
const shortSlug = safeTarget('foo');
|
||||
expect(safeTarget('foo-bar').startsWith(`${shortSlug}-`)).toBe(false);
|
||||
const shared = Array.from({ length: 30 }, (_, i) => `level${i}`).join('/');
|
||||
const deepSlug = safeTarget(`${shared}/alpha`);
|
||||
expect(deepSlug).not.toContain('-');
|
||||
// The short-vs-truncated direction too: a truncated slug never starts
|
||||
// with `<short slug>-`, whatever the short target is.
|
||||
expect(deepSlug.startsWith(`${safeTarget(shared)}-`)).toBe(false);
|
||||
});
|
||||
|
||||
it('bounds deep targets to the one-component cap, keeping them distinct', () => {
|
||||
// Deep nested paths flatten past POSIX's 255-byte filename component cap
|
||||
// (ENAMETOOLONG): truncate and carry a hash of the ORIGINAL target so
|
||||
|
|
|
|||
|
|
@ -29,22 +29,30 @@ const SAFE_TARGET_MAX_CHARS = 200;
|
|||
* packages/core (agent-transcript.ts) answers the same question for
|
||||
* transcript/monitor names and flattens dots instead — the two stay separate
|
||||
* on that deliberate difference.
|
||||
*
|
||||
* Dashes are flattened too, on purpose: the slugs are prefix-scanned with a
|
||||
* dash boundary (`qwen-review-<slug>-` in review's cleanup), and a slug that
|
||||
* itself contained `-` — natively, or via the truncation join — could extend
|
||||
* a shorter slug, letting one target's cleanup sweep a DISTINCT target's
|
||||
* artifacts. With `-` out of the slug alphabet the space is prefix-free at
|
||||
* that boundary by construction.
|
||||
*/
|
||||
export function safeTarget(target: string): string {
|
||||
let flat = target
|
||||
.replace(/[^A-Za-z0-9._-]/g, '_') // separators and anything odd → underscore
|
||||
.replace(/[^A-Za-z0-9._]/g, '_') // separators, dashes, anything odd → underscore
|
||||
.replace(/\.\.+/g, '_'); // no run of dots survives as a traversal token
|
||||
// A deep nested target flattens past the one-component byte cap
|
||||
// (ENAMETOOLONG): truncate and keep uniqueness with a hash of the
|
||||
// ORIGINAL target, so distinct long paths stay distinct.
|
||||
// ORIGINAL target, so distinct long paths stay distinct. The digest joins
|
||||
// with `_` — the one dash in a filename is the prefix-scan boundary.
|
||||
if (flat.length > SAFE_TARGET_MAX_CHARS) {
|
||||
const digest = createHash('sha256')
|
||||
.update(target)
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
flat = `${flat.slice(0, SAFE_TARGET_MAX_CHARS - digest.length - 1)}-${digest}`;
|
||||
flat = `${flat.slice(0, SAFE_TARGET_MAX_CHARS - digest.length - 1)}_${digest}`;
|
||||
}
|
||||
// Leading dashes too: a dash-leading slug is parsed as short flags when a
|
||||
// consumer passes it as a spaced CLI argument value.
|
||||
return flat.replace(/^[._-]+/, '') || 'target';
|
||||
// Leading dots/underscores too: a dash-leading slug is no longer possible
|
||||
// (dashes flatten), and a dot-leading component reads as a hidden file.
|
||||
return flat.replace(/^[._]+/, '') || 'target';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue