mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-11 12:12:19 -10:00
* feat(web-shell): render the compression outcome from structured meta The compression commands stream their outcome as English sentences over ACP, which a host that owns its own UI language cannot render. They now carry the outcome as machine-readable `_meta` next to the sentence, the transcript record and replay pass it through, and the web shell renders it in the user's language with the token counts formatted for that language. A compression that finds nothing to strip reports a terminal no-op phase rather than reusing the pending one: the payload-driven row has to state the outcome, and a pending row is dropped once the turn ends. The invocation note rides its own `_meta` key rather than sharing the result's. The daemon UI reducer folds a turn's text frames into one block and spreads `_meta` key by key, so a shared key would be overwritten by the frame that follows. Nothing else reads the new keys and no frame gains the discrete-message marker, so a host that renders text as-is sees the same sentences it saw before. Opening a session whose last action was a compression also reconciles the composer's context counters once. They are seeded from the last usage frame in the replay, and a compression emits none, so they described the context it replaced; a read is only written back while the session is live and its model and context window have settled, which is what the reconcile waits for. * docs(design): describe the structured compression outcome The design records why the outcome travels as metadata beside the sentence, why the invocation note needs its own key, why the no-op is a terminal phase, and how the composer counters reconcile on an attach that ends on a compression. It also keeps the compatibility analysis for other clients, which is a constraint of this change rather than a follow-up.
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
formatCompressionTokens,
|
|
parseContextCompressionMeta,
|
|
} from './contextCompression';
|
|
|
|
describe('parseContextCompressionMeta', () => {
|
|
it('reads a progress frame', () => {
|
|
expect(parseContextCompressionMeta({ phase: 'progress' })).toEqual({
|
|
phase: 'progress',
|
|
});
|
|
});
|
|
|
|
it('reads a no-op frame', () => {
|
|
expect(parseContextCompressionMeta({ phase: 'noop' })).toEqual({
|
|
phase: 'noop',
|
|
});
|
|
});
|
|
|
|
it('reads a notice frame', () => {
|
|
expect(
|
|
parseContextCompressionMeta({ phase: 'notice', instructionsLimit: 2000 }),
|
|
).toEqual({ phase: 'notice', instructionsLimit: 2000 });
|
|
});
|
|
|
|
it.each([
|
|
['a missing limit', { phase: 'notice' }],
|
|
['a non-numeric limit', { phase: 'notice', instructionsLimit: '2000' }],
|
|
['a negative limit', { phase: 'notice', instructionsLimit: -1 }],
|
|
])('rejects a notice with %s', (_label, value) => {
|
|
expect(parseContextCompressionMeta(value)).toBeUndefined();
|
|
});
|
|
|
|
it('reads a done frame with explicit estimate flags', () => {
|
|
expect(
|
|
parseContextCompressionMeta({
|
|
phase: 'done',
|
|
originalTokenCount: 263195,
|
|
newTokenCount: 99799,
|
|
originalTokenCountIsEstimated: false,
|
|
newTokenCountIsEstimated: true,
|
|
warning: 'History before the marker was summarized.',
|
|
}),
|
|
).toEqual({
|
|
phase: 'done',
|
|
result: {
|
|
originalTokenCount: 263195,
|
|
newTokenCount: 99799,
|
|
originalTokenCountIsEstimated: false,
|
|
newTokenCountIsEstimated: true,
|
|
warning: 'History before the marker was summarized.',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('treats a missing estimate flag as measured', () => {
|
|
expect(
|
|
parseContextCompressionMeta({
|
|
phase: 'done',
|
|
originalTokenCount: 10,
|
|
newTokenCount: 5,
|
|
}),
|
|
).toEqual({
|
|
phase: 'done',
|
|
result: {
|
|
originalTokenCount: 10,
|
|
newTokenCount: 5,
|
|
originalTokenCountIsEstimated: false,
|
|
newTokenCountIsEstimated: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
['null', null],
|
|
['a string', 'done'],
|
|
['an array', [{ phase: 'done' }]],
|
|
['an unknown phase', { phase: 'later' }],
|
|
['a missing phase', { originalTokenCount: 1, newTokenCount: 1 }],
|
|
[
|
|
'a non-numeric count',
|
|
{ phase: 'done', originalTokenCount: 'many', newTokenCount: 1 },
|
|
],
|
|
[
|
|
'a fractional count',
|
|
{ phase: 'done', originalTokenCount: 1.5, newTokenCount: 1 },
|
|
],
|
|
[
|
|
'a negative count',
|
|
{ phase: 'done', originalTokenCount: -1, newTokenCount: 1 },
|
|
],
|
|
['a finite-check dodge', { phase: 'done', originalTokenCount: NaN }],
|
|
])('rejects %s', (_label, value) => {
|
|
expect(parseContextCompressionMeta(value)).toBeUndefined();
|
|
});
|
|
|
|
it('ignores an empty warning', () => {
|
|
expect(
|
|
parseContextCompressionMeta({
|
|
phase: 'done',
|
|
originalTokenCount: 1,
|
|
newTokenCount: 1,
|
|
warning: '',
|
|
}),
|
|
).toEqual({
|
|
phase: 'done',
|
|
result: {
|
|
originalTokenCount: 1,
|
|
newTokenCount: 1,
|
|
originalTokenCountIsEstimated: false,
|
|
newTokenCountIsEstimated: false,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('formatCompressionTokens', () => {
|
|
it('groups thousands for the UI language, not the host locale', () => {
|
|
expect(formatCompressionTokens(263195, false, 'en')).toBe('263,195');
|
|
expect(formatCompressionTokens(263195, false, 'zh-CN')).toBe('263,195');
|
|
expect(formatCompressionTokens(99799, true, 'zh-CN')).toBe('~99,799');
|
|
// A language that groups differently from the host locale: this can only
|
|
// pass if the argument is really what formats the number.
|
|
expect(formatCompressionTokens(263195, false, 'de')).toBe('263.195');
|
|
});
|
|
});
|