Files
qwen-code/packages/web-shell/client/utils/contextCompression.test.ts
ytahdn a9ff4567f7 feat(web-shell): render the compression outcome from structured meta (#12141)
* 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.
2026-09-18 07:06:37 +00:00

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');
});
});