mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-11 01:36:35 +00:00
Some checks are pending
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
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* fix(web-shell): isolate history and session drafts * fix(web-shell): reset history-browse flag on early commit return (#7810) * fix(web-shell): address review feedback on paste and draft handling (#7810) * fix(web-shell): address review feedback on paste pruning, draft flush, and mobile draft notify (#7810) * fix(web-shell): update smoke test for large paste placeholder behavior (#7810) --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
225 lines
7.4 KiB
TypeScript
225 lines
7.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
buildComposerPrompt,
|
|
buildComposerPromptWithInlineTagPlacements,
|
|
createLargePastePlaceholder,
|
|
expandLargePastePlaceholders,
|
|
getComposerTagDisplay,
|
|
getComposerTagLabel,
|
|
getComposerTagValue,
|
|
getFollowupCompletion,
|
|
isLargePaste,
|
|
normalizePastedText,
|
|
prunePendingPastes,
|
|
replaceInlineTagPlacements,
|
|
serializeComposerTag,
|
|
} from './useComposerCore';
|
|
|
|
describe('follow-up completion helpers', () => {
|
|
it('uses the full suggestion when the editor is empty', () => {
|
|
expect(getFollowupCompletion('', 'show me tests')).toBe('show me tests');
|
|
});
|
|
|
|
it('keeps a suggestion available while the user types a matching prefix', () => {
|
|
expect(getFollowupCompletion('show', 'show me tests')).toBe(
|
|
'show me tests',
|
|
);
|
|
});
|
|
|
|
it('ignores suggestions that no longer match the editor text', () => {
|
|
expect(getFollowupCompletion('run', 'show me tests')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('composer tag serialization', () => {
|
|
it('prefers value, then label, then id', () => {
|
|
expect(
|
|
serializeComposerTag({
|
|
id: 'file',
|
|
label: ' File ',
|
|
value: ' src/a.ts ',
|
|
}),
|
|
).toBe('src/a.ts');
|
|
expect(
|
|
serializeComposerTag({
|
|
id: 'ext',
|
|
label: 'Extension',
|
|
value: 'clickhouse',
|
|
serialized: '@ext:clickhouse',
|
|
}),
|
|
).toBe('@ext:clickhouse');
|
|
expect(serializeComposerTag({ id: 'mode', label: ' Plan ' })).toBe('Plan');
|
|
expect(serializeComposerTag({ id: 'plain' })).toBe('plain');
|
|
});
|
|
|
|
it('returns trimmed label, value, and display text', () => {
|
|
const tag = { id: 'file', label: ' File ', value: ' src/a.ts ' };
|
|
|
|
expect(getComposerTagLabel(tag)).toBe('File');
|
|
expect(getComposerTagValue(tag)).toBe('src/a.ts');
|
|
expect(getComposerTagDisplay(tag)).toBe('src/a.ts');
|
|
expect(getComposerTagDisplay({ id: 'goal', label: ' Goal ' })).toBe('Goal');
|
|
});
|
|
|
|
it('builds prompts with tags before user text', () => {
|
|
expect(
|
|
buildComposerPrompt('do it', [
|
|
{ id: 'file', label: 'File', value: 'src/a.ts' },
|
|
{ id: 'goal', label: 'Goal' },
|
|
]),
|
|
).toBe('src/a.ts\nGoal\n\ndo it');
|
|
expect(buildComposerPrompt('', [{ id: 'file', value: 'src/a.ts' }])).toBe(
|
|
'src/a.ts',
|
|
);
|
|
expect(buildComposerPrompt('do it', [])).toBe('do it');
|
|
});
|
|
|
|
it('keeps inline tags at their editor positions', () => {
|
|
expect(
|
|
buildComposerPromptWithInlineTagPlacements(
|
|
'explain @orders now',
|
|
[{ id: 'top', serialized: '<top />' }],
|
|
[
|
|
{
|
|
start: 8,
|
|
end: 15,
|
|
tag: { id: 'table', value: 'orders', serialized: '<table />' },
|
|
},
|
|
],
|
|
),
|
|
).toBe('<top />\n\nexplain <table /> now');
|
|
});
|
|
|
|
it('ignores invalid and overlapping inline tag placements', () => {
|
|
expect(
|
|
replaceInlineTagPlacements('a @one and @two', [
|
|
{
|
|
start: -1,
|
|
end: 2,
|
|
tag: { id: 'negative', serialized: '<negative />' },
|
|
},
|
|
{
|
|
start: 2,
|
|
end: 6,
|
|
tag: { id: 'one', serialized: '<one />' },
|
|
},
|
|
{
|
|
start: 4,
|
|
end: 12,
|
|
tag: { id: 'overlap', serialized: '<overlap />' },
|
|
},
|
|
{
|
|
start: 11,
|
|
end: 15,
|
|
tag: { id: 'two', serialized: '<two />' },
|
|
},
|
|
{
|
|
start: 14,
|
|
end: 99,
|
|
tag: { id: 'beyond', serialized: '<beyond />' },
|
|
},
|
|
]),
|
|
).toBe('a <one /> and <two />');
|
|
});
|
|
});
|
|
|
|
describe('large paste helpers', () => {
|
|
it('normalizes CRLF and CR line endings to LF', () => {
|
|
expect(normalizePastedText('a\r\nb\rc\n')).toBe('a\nb\nc\n');
|
|
});
|
|
|
|
it('treats pastes over 1000 chars or 10 lines as large', () => {
|
|
expect(isLargePaste('a'.repeat(1000))).toBe(false);
|
|
expect(isLargePaste('a'.repeat(1001))).toBe(true);
|
|
// 10 lines => 9 newlines => split length 10, not large.
|
|
expect(isLargePaste('a\n'.repeat(9) + 'a')).toBe(false);
|
|
// 11 lines => split length 11, large.
|
|
expect(isLargePaste('a\n'.repeat(10) + 'a')).toBe(true);
|
|
});
|
|
|
|
it('counts code points, not UTF-16 units, for the char threshold', () => {
|
|
// 1001 emoji are 1001 code points but 2002 UTF-16 units.
|
|
expect(isLargePaste('😀'.repeat(1001))).toBe(true);
|
|
expect(isLargePaste('😀'.repeat(1000))).toBe(false);
|
|
});
|
|
|
|
it('creates incrementing placeholders keyed by code-point count', () => {
|
|
const pending = new Map<string, string>();
|
|
const first = createLargePastePlaceholder(pending, 1, 'hello');
|
|
expect(first.placeholderText).toBe('[Pasted Content 5 chars]');
|
|
expect(first.nextPasteId).toBe(2);
|
|
const second = createLargePastePlaceholder(
|
|
pending,
|
|
first.nextPasteId,
|
|
'hi',
|
|
);
|
|
expect(second.placeholderText).toBe('[Pasted Content 2 chars] #2');
|
|
expect(second.nextPasteId).toBe(3);
|
|
expect(pending.get('[Pasted Content 5 chars]')).toBe('hello');
|
|
expect(pending.get('[Pasted Content 2 chars] #2')).toBe('hi');
|
|
});
|
|
|
|
it('prunes placeholders absent from the doc and resets the id when empty', () => {
|
|
const pending = new Map<string, string>([
|
|
['[Pasted Content 5 chars]', 'hello'],
|
|
['[Pasted Content 2 chars] #2', 'hi'],
|
|
]);
|
|
// Only the first placeholder remains in the doc.
|
|
expect(
|
|
prunePendingPastes(pending, 'x [Pasted Content 5 chars] y'),
|
|
).toBeNull();
|
|
expect(pending.size).toBe(1);
|
|
expect(pending.has('[Pasted Content 2 chars] #2')).toBe(false);
|
|
// Removing the last placeholder resets the next id to 1.
|
|
expect(prunePendingPastes(pending, 'no placeholders here')).toBe(1);
|
|
expect(pending.size).toBe(0);
|
|
});
|
|
|
|
it('prunes by exact placeholder match, not substring', () => {
|
|
const pending = new Map<string, string>([
|
|
['[Pasted Content 5 chars]', 'aaaaa'],
|
|
['[Pasted Content 5 chars] #2', 'bbbbb'],
|
|
]);
|
|
// Only the longer placeholder is in the doc; the shorter one must be
|
|
// pruned even though it is a substring of the longer one.
|
|
expect(
|
|
prunePendingPastes(pending, '[Pasted Content 5 chars] #2'),
|
|
).toBeNull();
|
|
expect(pending.size).toBe(1);
|
|
expect(pending.has('[Pasted Content 5 chars]')).toBe(false);
|
|
expect(pending.has('[Pasted Content 5 chars] #2')).toBe(true);
|
|
});
|
|
|
|
it('expands placeholders back to their pasted content', () => {
|
|
const pending = new Map<string, string>([
|
|
['[Pasted Content 5 chars]', 'hello'],
|
|
]);
|
|
expect(
|
|
expandLargePastePlaceholders(pending, 'a [Pasted Content 5 chars] b'),
|
|
).toBe('a hello b');
|
|
expect(expandLargePastePlaceholders(pending, 'no placeholder')).toBe(
|
|
'no placeholder',
|
|
);
|
|
expect(
|
|
expandLargePastePlaceholders(new Map(), '[Pasted Content 5 chars]'),
|
|
).toBe('[Pasted Content 5 chars]');
|
|
});
|
|
|
|
it('replaces a placeholder that is a substring of another', () => {
|
|
// "[Pasted Content 5 chars]" is a prefix of "[Pasted Content 5 chars] #2";
|
|
// the longer placeholder must win regardless of map insertion order.
|
|
const pending = new Map<string, string>([
|
|
['[Pasted Content 5 chars]', 'aaaaa'],
|
|
['[Pasted Content 5 chars] #2', 'bbbbb'],
|
|
]);
|
|
expect(
|
|
expandLargePastePlaceholders(pending, '[Pasted Content 5 chars] #2'),
|
|
).toBe('bbbbb');
|
|
expect(
|
|
expandLargePastePlaceholders(
|
|
pending,
|
|
'[Pasted Content 5 chars] and [Pasted Content 5 chars] #2',
|
|
),
|
|
).toBe('aaaaa and bbbbb');
|
|
});
|
|
});
|