dsh-context-compression-improved 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +85 -82
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,434 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
4
+ import { useSyncExternalStore } from 'react'
5
+ import { afterEach, describe, expect, it, vi } from 'vitest'
6
+ import {
7
+ COMPRESSION_PROFILES,
8
+ ContextCompressionSettingsSection,
9
+ CompressionProfileSelector,
10
+ type CompressionProfile,
11
+ type CompressionProfileSelectorProps,
12
+ type ContextCompressionSettings,
13
+ } from '../src/client/CompressionProfileSelector.tsx'
14
+ import { en, zh } from '../src/client/locales.ts'
15
+
16
+ afterEach(cleanup)
17
+
18
+ interface SettingsScopeSnapshot<T> {
19
+ status: 'loading' | 'ready' | 'unavailable'
20
+ value: T | undefined
21
+ base: T | undefined
22
+ user: T | undefined
23
+ revision: number | undefined
24
+ writable: boolean
25
+ mode: 'host' | 'memory'
26
+ }
27
+
28
+ interface SnapshotStore<T> {
29
+ readonly getSnapshot: () => T
30
+ readonly subscribe: (listener: () => void) => () => void
31
+ readonly update: (recipe: (draft: T) => void) => void
32
+ }
33
+
34
+ function createSnapshotStore<T>(initial: T): SnapshotStore<T> {
35
+ let snapshot = structuredClone(initial)
36
+ const listeners = new Set<() => void>()
37
+ return {
38
+ getSnapshot: () => snapshot,
39
+ subscribe(listener) {
40
+ listeners.add(listener)
41
+ return () => { listeners.delete(listener) }
42
+ },
43
+ update(recipe) {
44
+ const draft = structuredClone(snapshot)
45
+ recipe(draft)
46
+ snapshot = draft
47
+ for (const listener of listeners) listener()
48
+ },
49
+ }
50
+ }
51
+
52
+ function bindSnapshotSelector<T>(store: SnapshotStore<T>) {
53
+ return function useSnapshotSelector<U>(selector: (snapshot: T) => U): U {
54
+ return useSyncExternalStore(
55
+ store.subscribe,
56
+ () => selector(store.getSnapshot()),
57
+ () => selector(store.getSnapshot()),
58
+ )
59
+ }
60
+ }
61
+
62
+ const COPY: Record<string, string> = {
63
+ nav: 'Context compression selector',
64
+ 'settings.title': 'Context compression selector',
65
+ 'settings.description': 'Choose the profile and configure its available parameters for the current session.',
66
+ label: 'Context compression',
67
+ 'profile.off': 'Plugin off',
68
+ 'profile.native': 'Native baseline',
69
+ 'profile.balanced': 'Balanced',
70
+ 'profile.cache-strict': 'Cache Strict (prefix protection)',
71
+ 'profile.savings': 'Savings',
72
+ 'profile.adaptive': 'Adaptive (conservative cost)',
73
+ 'profile.custom': 'Custom / Experimental',
74
+ 'profile.current': 'Current profile',
75
+ 'detail.off': 'Disable deterministic compression',
76
+ 'detail.native': 'Use native pruning',
77
+ 'detail.balanced': 'Reduce tool results deterministically',
78
+ 'detail.cache-strict': 'Preserve sent history until confirmed capacity pressure',
79
+ 'detail.savings': 'Use smaller targets without promising lower cost per request',
80
+ 'detail.adaptive': 'Age history only when official usage and prices prove a clear saving',
81
+ 'detail.custom': 'Choose implemented stages and measured thresholds for new sessions',
82
+ 'custom.title': 'Custom policy',
83
+ 'custom.settingsHint': 'Edit detailed parameters in Settings > Context compression selector.',
84
+ 'custom.sessionScope': 'Saved changes apply when the current compression runtime next observes a Session for the first time. A Session already observed by that runtime keeps its frozen policy.',
85
+ 'custom.measurement': 'Exact DeepSeek tokenizer first; tokenizer estimate with calibration fallback. Never chars/4. Cache attribution remains unknown.',
86
+ 'custom.unit': 'Canonical unit',
87
+ 'custom.unit.tokens': 'Tokens',
88
+ 'custom.unit.contextPercent': 'Context percent',
89
+ 'custom.fresh.enabled': 'Enable Fresh',
90
+ 'custom.fresh.trigger': 'Fresh trigger',
91
+ 'custom.fresh.target': 'Fresh target',
92
+ 'custom.aggregate.enabled': 'Enable Aggregate',
93
+ 'custom.aggregate.trigger': 'Aggregate trigger',
94
+ 'custom.aggregate.target': 'Aggregate target',
95
+ 'custom.history.enabled': 'Enable History',
96
+ 'custom.history.trigger': 'History trigger',
97
+ 'custom.history.keepRecentToolCalls': 'Protected recent tool calls',
98
+ 'custom.history.keepRecentTokens': 'Protected recent tool-result tail',
99
+ 'custom.history.minReclaim': 'Minimum reclaim',
100
+ 'custom.prefixPolicy': 'Sent-prefix policy',
101
+ 'custom.prefixPolicy.preserve': 'Preserve until capacity pressure',
102
+ 'custom.prefixPolicy.pressureBreak': 'Allow routine history aging',
103
+ 'custom.experimental': 'Experimental: Custom-only controls',
104
+ 'custom.tailTrim.enabled': 'Enable TailTrim (experimental)',
105
+ 'custom.tailTrim.trigger': 'TailTrim trigger',
106
+ 'status.minimalUnavailable': 'Minimal mode does not load context compression for this session. The selector is effectively off and Harness native behavior remains. Switch to Standard, PTC / Coding, Creative, or a capable custom preset to configure it.',
107
+ 'custom.tailTrim.warning': 'TailTrim requires exact tokens and may reduce cache hits.',
108
+ 'custom.save': 'Save Custom policy',
109
+ 'custom.reset': 'Reset Custom policy',
110
+ 'custom.invalid': 'Custom policy values are invalid.',
111
+ 'status.presetUnavailable': 'This session’s preset does not provide context compression, or availability is not yet confirmed.',
112
+ 'pricing.disclosure': 'DeepSeek official prices checked 2026-08-25. Peak Mon–Fri 09:00–12:00 and 14:00–18:00 Asia/Shanghai; otherwise off-peak. Cross-boundary requests use a cost range.',
113
+ }
114
+
115
+ const DEFAULT_CUSTOM = {
116
+ version: 1,
117
+ unit: 'tokens',
118
+ fresh: { enabled: true, trigger: 4_096, target: 2_048 },
119
+ aggregate: { enabled: true, trigger: 16_384, target: 8_192 },
120
+ history: {
121
+ enabled: true,
122
+ trigger: 32_768,
123
+ keepRecentTurns: 2,
124
+ keepRecent: 16_384,
125
+ minReclaim: 8_192,
126
+ },
127
+ prefixPolicy: 'pressure-break',
128
+ } as const satisfies ContextCompressionSettings['custom']
129
+
130
+ function renderReady(
131
+ select: (profile: CompressionProfile) => Promise<void>,
132
+ contextCompressionAvailable: boolean | undefined,
133
+ t: (key: string) => string,
134
+ options: {
135
+ value?: ContextCompressionSettings
136
+ saveCustom?: (value: ContextCompressionSettings['custom']) => Promise<void>
137
+ resetCustom?: () => Promise<void>
138
+ agentPreset?: string
139
+ settingsSection?: boolean
140
+ } = {},
141
+ ) {
142
+ const state = createSnapshotStore<SettingsScopeSnapshot<ContextCompressionSettings>>({
143
+ status: 'ready',
144
+ value: options.value ?? {
145
+ profile: 'native',
146
+ custom: structuredClone(DEFAULT_CUSTOM),
147
+ autoCompact: { thresholdPercent: 80 },
148
+ codeSkeleton: { enabled: false },
149
+ },
150
+ base: undefined,
151
+ user: undefined,
152
+ revision: 0,
153
+ writable: true,
154
+ mode: 'host',
155
+ })
156
+ const sessions = createSnapshotStore({
157
+ ids: ['s1'],
158
+ byId: {
159
+ s1: {
160
+ id: 's1', displayTitle: 's1', running: false, blank: true, updatedAt: 0,
161
+ agentPreset: options.agentPreset ?? 'standard',
162
+ ...(contextCompressionAvailable === undefined ? {} : { contextCompressionAvailable }),
163
+ },
164
+ },
165
+ current: 's1',
166
+ phase: 'ready',
167
+ subagentsByParent: {},
168
+ jobsBySession: {},
169
+ currentAddress: undefined,
170
+ })
171
+ const Component = options.settingsSection === true ? ContextCompressionSettingsSection : CompressionProfileSelector
172
+ render(<Component {...({
173
+ useCompression: bindSnapshotSelector(state),
174
+ useSessions: bindSnapshotSelector(sessions),
175
+ select,
176
+ saveCustom: options.saveCustom ?? (() => Promise.resolve()),
177
+ resetCustom: options.resetCustom ?? (() => Promise.resolve()),
178
+ t,
179
+ } as unknown as CompressionProfileSelectorProps)} />)
180
+ return { state, sessions }
181
+ }
182
+
183
+ function mount(
184
+ select: (profile: CompressionProfile) => Promise<void>,
185
+ ...availability: [] | [boolean | undefined]
186
+ ) {
187
+ const contextCompressionAvailable = availability.length === 0 ? true : availability[0]
188
+ return renderReady(select, contextCompressionAvailable, key => COPY[key] ?? key)
189
+ }
190
+
191
+ function chooseOff(): void {
192
+ fireEvent.click(screen.getByRole('button', { name: /Native baseline/ }))
193
+ fireEvent.click(screen.getByRole('menuitem', { name: /Plugin off/ }))
194
+ }
195
+
196
+ describe('context compression profiles', () => {
197
+ it('exposes Custom after the established profiles', () => {
198
+ expect(COMPRESSION_PROFILES).toEqual([
199
+ 'off', 'native', 'balanced', 'cache-strict', 'savings', 'adaptive', 'tokenpilot-inspired', 'custom',
200
+ ])
201
+ })
202
+
203
+ it('renders no selector when Host settings are unavailable', () => {
204
+ const state = createSnapshotStore<SettingsScopeSnapshot<ContextCompressionSettings>>({
205
+ status: 'unavailable',
206
+ value: undefined,
207
+ base: undefined,
208
+ user: undefined,
209
+ revision: undefined,
210
+ writable: false,
211
+ mode: 'memory',
212
+ })
213
+ const sessions = createSnapshotStore({
214
+ ids: [], byId: {}, current: undefined, phase: 'ready',
215
+ subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
216
+ })
217
+ render(<CompressionProfileSelector {...({
218
+ useCompression: bindSnapshotSelector(state),
219
+ useSessions: bindSnapshotSelector(sessions),
220
+ select: vi.fn(() => Promise.resolve()),
221
+ t: (key: string) => COPY[key] ?? key,
222
+ } as unknown as CompressionProfileSelectorProps)} />)
223
+
224
+ expect(screen.queryByRole('button', { name: /Context compression/ })).toBeNull()
225
+ })
226
+
227
+ it('keeps all eight profiles selectable for a capable official or custom preset', () => {
228
+ mount(vi.fn(() => Promise.resolve()), true)
229
+
230
+ const button = screen.getByRole('button', { name: /Context compression/ })
231
+ expect((button as HTMLButtonElement).disabled).toBe(false)
232
+ fireEvent.click(button)
233
+ expect(screen.getAllByRole('menuitem')).toHaveLength(8)
234
+ })
235
+
236
+ it.each([false, undefined])(
237
+ 'remains selectable before capability refresh %s when the initial preset is not Minimal',
238
+ (available) => {
239
+ const { state } = mount(vi.fn(() => Promise.resolve()), available)
240
+
241
+ expect(screen.getByRole<HTMLButtonElement>('button', { name: /Context compression/ }).disabled).toBe(false)
242
+ expect(screen.queryByRole('status')).toBeNull()
243
+ expect(state.getSnapshot().value?.profile).toBe('native')
244
+ },
245
+ )
246
+
247
+ it('explains that Minimal keeps the selector effectively off without overwriting the saved profile', () => {
248
+ const { state } = renderReady(
249
+ vi.fn(() => Promise.resolve()), false, key => COPY[key] ?? key,
250
+ { agentPreset: 'minimal' },
251
+ )
252
+
253
+ expect(screen.getByText(/Minimal mode does not load context compression.*effectively off.*native behavior remains/)).not.toBeNull()
254
+ expect(screen.getByRole<HTMLButtonElement>('button', { name: /Context compression/ }).disabled).toBe(true)
255
+ expect(state.getSnapshot().value?.profile).toBe('native')
256
+ })
257
+
258
+ it('renders the full selector as its own Settings section', () => {
259
+ renderReady(
260
+ vi.fn(() => Promise.resolve()), true, key => COPY[key] ?? key,
261
+ { settingsSection: true },
262
+ )
263
+
264
+ expect(screen.getByRole('heading', { name: 'Context compression selector' })).not.toBeNull()
265
+ expect(screen.getByText(/Choose the profile and configure its available parameters/)).not.toBeNull()
266
+ expect(screen.getByRole<HTMLButtonElement>('button', { name: /Native baseline.*Current profile/ }).disabled).toBe(false)
267
+ expect(screen.getByRole('button', { name: /Balanced.*Reduce tool results deterministically/ })).not.toBeNull()
268
+ expect(screen.queryByRole('menu')).toBeNull()
269
+ })
270
+
271
+ it('does not require a preset switch when capability refresh is delayed', async () => {
272
+ const { sessions } = mount(vi.fn(() => Promise.resolve()), true)
273
+ fireEvent.click(screen.getByRole('button', { name: /Context compression/ }))
274
+ expect(screen.getAllByRole('menuitem')).toHaveLength(8)
275
+
276
+ sessions.update((draft) => {
277
+ const current = draft.byId.s1
278
+ if (current !== undefined) current.contextCompressionAvailable = false
279
+ })
280
+
281
+ await waitFor(() => { expect(screen.getAllByRole('menuitem')).toHaveLength(8) })
282
+ expect(screen.getByRole<HTMLButtonElement>('button', { name: /Context compression/ }).disabled).toBe(false)
283
+ })
284
+
285
+ it('persists a selected profile', async () => {
286
+ const select = vi.fn(() => Promise.resolve())
287
+ mount(select)
288
+
289
+ chooseOff()
290
+
291
+ await waitFor(() => { expect(select).toHaveBeenCalledWith('off') })
292
+ })
293
+
294
+ it('persists Cache Strict and exposes its prefix-protection wording', async () => {
295
+ const select = vi.fn(() => Promise.resolve())
296
+ mount(select)
297
+
298
+ fireEvent.click(screen.getByRole('button', { name: /Native baseline/ }))
299
+ fireEvent.click(screen.getByRole('menuitem', { name: /Cache Strict/ }))
300
+
301
+ await waitFor(() => { expect(select).toHaveBeenCalledWith('cache-strict') })
302
+ })
303
+
304
+ it('persists Adaptive and exposes its conservative official-cost wording', async () => {
305
+ const select = vi.fn(() => Promise.resolve())
306
+ mount(select)
307
+
308
+ fireEvent.click(screen.getByRole('button', { name: /Native baseline/ }))
309
+ expect(screen.getByRole('menuitem', {
310
+ name: /Adaptive \(conservative cost\).*official usage and prices prove a clear saving/,
311
+ })).not.toBeNull()
312
+ fireEvent.click(screen.getByRole('menuitem', { name: /Adaptive \(conservative cost\)/ }))
313
+
314
+ await waitFor(() => { expect(select).toHaveBeenCalledWith('adaptive') })
315
+ })
316
+
317
+ it('edits only the implemented Custom stages and saves one canonical document', async () => {
318
+ const custom = {
319
+ version: 1 as const,
320
+ unit: 'tokens' as const,
321
+ fresh: { enabled: true, trigger: 4_096, target: 2_048 },
322
+ aggregate: { enabled: true, trigger: 16_384, target: 8_192 },
323
+ history: {
324
+ enabled: true, trigger: 32_768, keepRecentTurns: 2,
325
+ keepRecent: 16_384, minReclaim: 8_192,
326
+ },
327
+ prefixPolicy: 'pressure-break' as const,
328
+ }
329
+ const saveCustom = vi.fn(() => Promise.resolve())
330
+ const resetCustom = vi.fn(() => Promise.resolve())
331
+ renderReady(vi.fn(() => Promise.resolve()), true, key => COPY[key] ?? key, {
332
+ value: { profile: 'custom', custom, autoCompact: { thresholdPercent: 80 }, codeSkeleton: { enabled: false } },
333
+ saveCustom,
334
+ resetCustom,
335
+ settingsSection: true,
336
+ })
337
+
338
+ expect(screen.getByText('Saved changes apply when the current compression runtime next observes a Session for the first time. A Session already observed by that runtime keeps its frozen policy.')).not.toBeNull()
339
+ expect(screen.getByText(/Exact DeepSeek tokenizer first.*Never chars\/4.*Cache attribution remains unknown/)).not.toBeNull()
340
+ expect(screen.getByLabelText<HTMLSelectElement>('Enable TailTrim (experimental)').value).toBe('off')
341
+ expect(screen.getByLabelText<HTMLInputElement>('TailTrim trigger').value).toBe('700000')
342
+ expect(screen.getByText(/TailTrim requires exact tokens.*cache hits/)).not.toBeNull()
343
+ fireEvent.change(screen.getByLabelText('Fresh trigger'), { target: { value: '8192' } })
344
+ fireEvent.change(screen.getByLabelText('Fresh target'), { target: { value: '4096' } })
345
+ fireEvent.change(screen.getByLabelText('Enable TailTrim (experimental)'), { target: { value: 'on' } })
346
+ fireEvent.change(screen.getByLabelText('TailTrim trigger'), { target: { value: '49152' } })
347
+ fireEvent.click(screen.getByRole('button', { name: 'Save Custom policy' }))
348
+ await waitFor(() => {
349
+ expect(saveCustom).toHaveBeenCalledWith(expect.objectContaining({
350
+ version: 3,
351
+ unit: 'tokens',
352
+ fresh: { enabled: true, trigger: 8_192, target: 4_096 },
353
+ tailTrim: { enabled: true, trigger: 49_152 },
354
+ }))
355
+ })
356
+
357
+ fireEvent.click(screen.getByRole('button', { name: 'Reset Custom policy' }))
358
+ await waitFor(() => { expect(resetCustom).toHaveBeenCalledOnce() })
359
+ })
360
+
361
+ it('applies percent bounds only to measured windows and keeps tool-call counts unbounded', () => {
362
+ renderReady(vi.fn(() => Promise.resolve()), true, key => COPY[key] ?? key, {
363
+ value: {
364
+ profile: 'custom',
365
+ autoCompact: { thresholdPercent: 80 },
366
+ codeSkeleton: { enabled: false },
367
+ custom: {
368
+ version: 1,
369
+ unit: 'context-percent',
370
+ fresh: { enabled: true, trigger: 8, target: 4 },
371
+ aggregate: { enabled: true, trigger: 24, target: 12 },
372
+ history: {
373
+ enabled: true,
374
+ trigger: 60,
375
+ keepRecentTurns: 256,
376
+ keepRecent: 30,
377
+ minReclaim: 10,
378
+ },
379
+ prefixPolicy: 'pressure-break',
380
+ },
381
+ },
382
+ settingsSection: true,
383
+ })
384
+
385
+ expect(screen.getByLabelText<HTMLInputElement>('Fresh trigger').min).toBe('0.01')
386
+ expect(screen.getByLabelText<HTMLInputElement>('Fresh trigger').max).toBe('100')
387
+ expect(screen.getByLabelText<HTMLInputElement>('Protected recent tool calls').min).toBe('0')
388
+ expect(screen.getByLabelText<HTMLInputElement>('Protected recent tool calls').max).toBe('')
389
+ expect(screen.getByLabelText<HTMLInputElement>('Protected recent tool calls').step).toBe('1')
390
+ })
391
+
392
+ it.each([
393
+ ['English', en, /checked 2026-08-25.*Mon–Fri 09:00–12:00.*14:00–18:00.*Asia\/Shanghai.*cost range/],
394
+ ['Simplified Chinese', zh, /2026-08-25.*Asia\/Shanghai.*周一至周五 09:00–12:00.*14:00–18:00.*成本区间/],
395
+ ] as const)('renders the Adaptive price date and schedule in %s', (_label, locale, expected) => {
396
+ const copy = locale as Readonly<Record<string, string>>
397
+ renderReady(vi.fn(() => Promise.resolve()), true, key => copy[key] ?? key)
398
+
399
+ expect(screen.getByText(expected)).not.toBeNull()
400
+ })
401
+
402
+ it('states the mounted-runtime first-observation scope in both locales', () => {
403
+ expect(en['custom.sessionScope']).toBe(
404
+ 'Saved changes apply when the current compression runtime next observes a Session for the first time. A Session already observed by that runtime keeps its frozen policy.',
405
+ )
406
+ expect(zh['custom.sessionScope']).toBe(
407
+ '保存后的修改会在当前压缩运行时随后首次观察某个 Session 时生效;已被该运行时观察的 Session 继续使用其冻结策略。',
408
+ )
409
+ })
410
+
411
+ it('keeps Custom parameters out of the compact sidebar surface', () => {
412
+ renderReady(vi.fn(() => Promise.resolve()), true, key => COPY[key] ?? key, {
413
+ value: { profile: 'custom', custom: structuredClone(DEFAULT_CUSTOM), autoCompact: { thresholdPercent: 80 }, codeSkeleton: { enabled: false } },
414
+ })
415
+
416
+ expect(screen.queryByRole('heading', { name: 'Custom policy' })).toBeNull()
417
+ expect(screen.getByText('Edit detailed parameters in Settings > Context compression selector.')).not.toBeNull()
418
+ })
419
+
420
+ it('shows a rejected save to the user', async () => {
421
+ const rejected = Promise.reject(new Error('settings write failed'))
422
+ // Mark the fixture promise handled so only the component's visible-error
423
+ // contract determines this assertion; an implementation that ignores the
424
+ // returned promise still fails because it renders no alert.
425
+ void rejected.catch(() => {})
426
+ mount(vi.fn(() => rejected))
427
+
428
+ chooseOff()
429
+
430
+ await waitFor(() => {
431
+ expect(screen.getByRole('alert').textContent).not.toBe('')
432
+ })
433
+ })
434
+ })
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ const root = resolve(import.meta.dirname, '../../..')
6
+
7
+ describe('standalone package contract', () => {
8
+ it('ships one install-facing package with no intra-repo dependency edge', () => {
9
+ const selector = JSON.parse(readFileSync(resolve(root, 'selector/package.json'), 'utf8')) as {
10
+ name: string
11
+ version: string
12
+ dependencies?: Record<string, string>
13
+ peerDependencies?: Record<string, string>
14
+ publishConfig?: { tag?: string }
15
+ }
16
+ expect(selector.name).toBe('dsh-context-compression-improved')
17
+ expect(selector.dependencies?.['@huggingface/tokenizers']).toBe('0.1.3')
18
+ expect(Object.keys(selector.dependencies ?? {})).not.toContain('dsh-context-compression-improved-runtime')
19
+ expect(Object.keys(selector.peerDependencies ?? {})).not.toContain('dsh-context-compression-improved-runtime')
20
+ expect(Object.keys(selector.peerDependencies ?? {})).not.toContain('@deepseek-ai/dsh-compaction-tool-result-pruner')
21
+ expect(selector.peerDependencies?.['@deepseek-ai/dsh-compaction-basic'])
22
+ .toBe('>=0.1.1-rc.2 <0.2.0')
23
+ expect(selector.peerDependencies?.['@deepseek-ai/dsh-command-compact'])
24
+ .toBe('>=0.1.1-rc.2 <0.2.0')
25
+ expect(selector.publishConfig?.tag).toBe('latest')
26
+ })
27
+
28
+ it('uses the community package in the one Bundle patch', () => {
29
+ const patch = readFileSync(resolve(root, 'selector/cordis.patch.yml'), 'utf8')
30
+ expect(patch).toContain("name: 'dsh-context-compression-improved'")
31
+ expect(patch).not.toContain('@deepseek-ai/dsh-client-ui-context-compression-selector')
32
+ })
33
+ })
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ decideConservativeAdaptive,
4
+ deriveAdaptiveTokenBounds,
5
+ } from '../../src/runtime/adaptive-cost.ts'
6
+ import type {
7
+ ExactTokenizerTokenCount,
8
+ TokenizerEstimateTokenCount,
9
+ } from '../../src/runtime/token-count.ts'
10
+
11
+ const exact = (
12
+ tokens: number,
13
+ tokenizerRevision = 'DeepSeek-V4-Pro-0813',
14
+ ): ExactTokenizerTokenCount => ({
15
+ kind: 'exact-tokenizer',
16
+ tokens,
17
+ tokenizerId: 'deepseek-v4-official',
18
+ tokenizerRevision,
19
+ })
20
+
21
+ const estimate = (
22
+ input: Omit<TokenizerEstimateTokenCount, 'kind'>,
23
+ ): TokenizerEstimateTokenCount => ({ kind: 'tokenizer-estimate', ...input })
24
+
25
+ describe('Conservative Adaptive interval', () => {
26
+ it('subtracts removed tokens from the affected retained suffix instead of charging them twice', () => {
27
+ expect(deriveAdaptiveTokenBounds({
28
+ exactReclaimedTokens: 20_000,
29
+ earliestChangedSeq: 2,
30
+ previousPromptTokens: 30_500,
31
+ expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
32
+ previousRequestMeasurement: exact(30_500),
33
+ measuredNodes: [
34
+ { seq: 0, count: exact(10_000) },
35
+ { seq: 2, count: exact(20_000) },
36
+ { seq: 3, count: exact(500) },
37
+ ],
38
+ })).toEqual({
39
+ kind: 'available',
40
+ measurementKind: 'exact-tokenizer',
41
+ reclaimedLowerBoundTokens: 20_000,
42
+ affectedRetainedSuffixUpperBoundTokens: 500,
43
+ exactPrefixLowerBoundTokens: 10_000,
44
+ })
45
+ })
46
+
47
+ it('subtracts the tokenizer-estimate conservative margin from reclaimed benefit', () => {
48
+ expect(deriveAdaptiveTokenBounds({
49
+ exactReclaimedTokens: 20_000,
50
+ earliestChangedSeq: 2,
51
+ previousPromptTokens: 30_500,
52
+ expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
53
+ previousRequestMeasurement: estimate({
54
+ tokens: 30_300,
55
+ upperBoundTokens: 30_500,
56
+ estimatorId: 'deepseek-wire',
57
+ estimatorRevision: 'r1',
58
+ calibration: { sampleCount: 1, conservativeMarginTokens: 200 },
59
+ }),
60
+ measuredNodes: [{ seq: 0, count: exact(10_000) }],
61
+ })).toMatchObject({
62
+ kind: 'available',
63
+ measurementKind: 'tokenizer-estimate',
64
+ reclaimedLowerBoundTokens: 19_800,
65
+ affectedRetainedSuffixUpperBoundTokens: 700,
66
+ })
67
+ })
68
+
69
+ it.each([
70
+ [{ kind: 'unavailable', reason: 'no request tokenizer' } as const, 'request-measurement-unavailable'],
71
+ [estimate({
72
+ tokens: 10, upperBoundTokens: 20,
73
+ estimatorId: 'e', estimatorRevision: 'r',
74
+ }), 'estimate-calibration-unavailable'],
75
+ ] as const)('fails closed when D/S cannot be bounded: %s', (measurement, reason) => {
76
+ expect(deriveAdaptiveTokenBounds({
77
+ exactReclaimedTokens: 100,
78
+ earliestChangedSeq: 2,
79
+ previousPromptTokens: 200,
80
+ expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
81
+ previousRequestMeasurement: measurement,
82
+ measuredNodes: [{ seq: 0, count: exact(50) }],
83
+ })).toEqual({ kind: 'unknown', reason })
84
+ })
85
+
86
+ it('fails closed when the exact request count does not match the provider key tokenizer revision', () => {
87
+ expect(deriveAdaptiveTokenBounds({
88
+ exactReclaimedTokens: 100,
89
+ earliestChangedSeq: 2,
90
+ previousPromptTokens: 200,
91
+ expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
92
+ previousRequestMeasurement: exact(200, 'DeepSeek-V4-Flash-0731'),
93
+ measuredNodes: [{ seq: 0, count: exact(50) }],
94
+ })).toEqual({ kind: 'unknown', reason: 'request-tokenizer-revision-mismatch' })
95
+ })
96
+
97
+ it('fails closed before subtracting an exact prefix from another tokenizer revision', () => {
98
+ expect(deriveAdaptiveTokenBounds({
99
+ exactReclaimedTokens: 100,
100
+ earliestChangedSeq: 2,
101
+ previousPromptTokens: 200,
102
+ expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
103
+ previousRequestMeasurement: exact(200),
104
+ measuredNodes: [{ seq: 0, count: exact(50, 'DeepSeek-V4-Flash-0731') }],
105
+ })).toEqual({ kind: 'unknown', reason: 'exact-prefix-tokenizer-revision-mismatch' })
106
+ })
107
+
108
+ it('allows routine History only when minimum removal value beats maximum cache-loss penalty', () => {
109
+ expect(decideConservativeAdaptive({
110
+ capacityPressure: false,
111
+ bounds: {
112
+ kind: 'available', measurementKind: 'exact-tokenizer',
113
+ reclaimedLowerBoundTokens: 20_000,
114
+ affectedRetainedSuffixUpperBoundTokens: 500,
115
+ exactPrefixLowerBoundTokens: 10_000,
116
+ },
117
+ inputCacheHitRate: '0.014',
118
+ inputCacheMissRate: '0.44',
119
+ })).toMatchObject({
120
+ allowHistory: true,
121
+ reason: 'cost-interval-clearly-favourable',
122
+ })
123
+ })
124
+
125
+ it.each([
126
+ [19_000, 1_000, 'cache-risk-not-clearly-paid-back'],
127
+ [20_000, 500, 'adaptive-unknown-price'],
128
+ ] as const)('fails closed for overlap or unknown price', (reclaimed, suffix, expected) => {
129
+ expect(decideConservativeAdaptive({
130
+ capacityPressure: false,
131
+ bounds: {
132
+ kind: 'available', measurementKind: 'exact-tokenizer',
133
+ reclaimedLowerBoundTokens: reclaimed,
134
+ affectedRetainedSuffixUpperBoundTokens: suffix,
135
+ exactPrefixLowerBoundTokens: 0,
136
+ },
137
+ ...(expected === 'adaptive-unknown-price'
138
+ ? {}
139
+ : { inputCacheHitRate: '0.014', inputCacheMissRate: '0.44' }),
140
+ })).toMatchObject({ allowHistory: false, reason: expected })
141
+ })
142
+
143
+ it('lets capacity safety override missing price and telemetry', () => {
144
+ expect(decideConservativeAdaptive({
145
+ capacityPressure: true,
146
+ bounds: { kind: 'unknown', reason: 'usage-unavailable' },
147
+ })).toEqual({ allowHistory: true, reason: 'capacity-override' })
148
+ })
149
+
150
+ it('uses request-level cache hits only as a conservative exposure cap', () => {
151
+ expect(decideConservativeAdaptive({
152
+ capacityPressure: false,
153
+ bounds: {
154
+ kind: 'available', measurementKind: 'exact-tokenizer',
155
+ reclaimedLowerBoundTokens: 20_000,
156
+ affectedRetainedSuffixUpperBoundTokens: 10_000,
157
+ exactPrefixLowerBoundTokens: 0,
158
+ },
159
+ observedCacheReadTokens: 100,
160
+ inputCacheHitRate: '0.014',
161
+ inputCacheMissRate: '0.44',
162
+ })).toMatchObject({
163
+ allowHistory: true,
164
+ maximumCacheLossPenalty: '42600000000',
165
+ })
166
+ })
167
+ })