dsh-context-compression-improved 0.1.1 → 0.2.0
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.
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +84 -81
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type { CallId } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
|
|
3
|
+
/** User-facing mixed strategy profile. */
|
|
4
|
+
export const COMPRESSION_PROFILES = [
|
|
5
|
+
'off',
|
|
6
|
+
'native',
|
|
7
|
+
'balanced',
|
|
8
|
+
'cache-strict',
|
|
9
|
+
'savings',
|
|
10
|
+
'adaptive',
|
|
11
|
+
'tokenpilot-inspired',
|
|
12
|
+
'custom',
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
/** Public compression strategy selected for one Session. */
|
|
16
|
+
export type CompressionProfile = typeof COMPRESSION_PROFILES[number]
|
|
17
|
+
|
|
18
|
+
/** When historical tool results may be aged for one Session policy. */
|
|
19
|
+
export type HistoryMode = 'disabled' | 'routine' | 'capacity-pressure' | 'adaptive'
|
|
20
|
+
|
|
21
|
+
/** Canonical unit stored by one versioned Custom policy. */
|
|
22
|
+
export type CustomCompressionUnit = 'tokens' | 'context-percent'
|
|
23
|
+
|
|
24
|
+
/** Whether routine History may rewrite a previously sent Harness prefix. */
|
|
25
|
+
export type CustomPrefixPolicy = 'preserve' | 'pressure-break'
|
|
26
|
+
|
|
27
|
+
/** One independently selectable Custom Fresh or Aggregate stage. */
|
|
28
|
+
export interface CustomCompressionBudget {
|
|
29
|
+
enabled: boolean
|
|
30
|
+
trigger: number
|
|
31
|
+
target: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Legacy Custom History gate and turn/token working-set protection. */
|
|
35
|
+
export interface LegacyCustomHistoryPolicy {
|
|
36
|
+
enabled: boolean
|
|
37
|
+
trigger: number
|
|
38
|
+
keepRecentTurns: number
|
|
39
|
+
keepRecent: number
|
|
40
|
+
minReclaim: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Custom History gate and recent tool-call/token working-set protection. */
|
|
44
|
+
export interface CustomHistoryPolicy {
|
|
45
|
+
enabled: boolean
|
|
46
|
+
trigger: number
|
|
47
|
+
keepRecentToolCalls: number
|
|
48
|
+
keepRecentTokens: number
|
|
49
|
+
minReclaim: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Custom-only experimental TailTrim gate. */
|
|
53
|
+
export interface CustomTailTrimPolicy {
|
|
54
|
+
enabled: boolean
|
|
55
|
+
trigger: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** TokenPilot-inspired preset sub-capability switches (only injected for `tokenpilot-inspired`). */
|
|
59
|
+
export interface PresetOptions {
|
|
60
|
+
/** Reject replacements whose text is not smaller than the original (G1). */
|
|
61
|
+
readonly noNetSavingsGuard: boolean
|
|
62
|
+
/** Permanently exempt recovery content from further reduction (G2). */
|
|
63
|
+
readonly skipReductionRecovery: boolean
|
|
64
|
+
/** Replace byte-identical repeated tool results with first-occurrence pointers (A1). */
|
|
65
|
+
readonly dedupeToolResults: boolean
|
|
66
|
+
/** Append Exact Sources locator blocks after Auto Compact completes (A2). */
|
|
67
|
+
readonly summaryLocator: boolean
|
|
68
|
+
/** Volatile-line demotion, deterministic tool ordering, and prefix fingerprint audit (S1/S2). */
|
|
69
|
+
readonly prefixStabilizer: boolean
|
|
70
|
+
/** Fresh/superseded read-state classification with clustered omission markers (R2/R3). */
|
|
71
|
+
readonly readState: boolean
|
|
72
|
+
/** Optional estimator channel; `''` keeps every estimator consumer on rule-only fallbacks (E1/E2). */
|
|
73
|
+
readonly estimator: { readonly mode: '' | 'host' | 'direct' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Common user-authored Custom stages shared by persisted policy versions. */
|
|
77
|
+
interface CustomCompressionPolicyFields<HistoryPolicy> {
|
|
78
|
+
unit: CustomCompressionUnit
|
|
79
|
+
fresh: CustomCompressionBudget
|
|
80
|
+
aggregate: CustomCompressionBudget
|
|
81
|
+
history: HistoryPolicy
|
|
82
|
+
prefixPolicy: CustomPrefixPolicy
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Legacy R4 Custom policy, accepted without migration. */
|
|
86
|
+
export interface CustomCompressionPolicyV1 extends CustomCompressionPolicyFields<LegacyCustomHistoryPolicy> {
|
|
87
|
+
version: 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** R5 Custom policy with an explicit default-off TailTrim stage. */
|
|
91
|
+
export interface CustomCompressionPolicyV2 extends CustomCompressionPolicyFields<LegacyCustomHistoryPolicy> {
|
|
92
|
+
version: 2
|
|
93
|
+
tailTrim: CustomTailTrimPolicy
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Custom policy with tool-call working-set protection. */
|
|
97
|
+
export interface CustomCompressionPolicyV3 extends CustomCompressionPolicyFields<CustomHistoryPolicy> {
|
|
98
|
+
version: 3
|
|
99
|
+
tailTrim: CustomTailTrimPolicy
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Strict persisted Custom policy union. */
|
|
103
|
+
export type CustomCompressionPolicy = CustomCompressionPolicyV1 | CustomCompressionPolicyV2 | CustomCompressionPolicyV3
|
|
104
|
+
|
|
105
|
+
/** User-tunable Auto Compact coordination preferences. */
|
|
106
|
+
export interface AutoCompactSettings {
|
|
107
|
+
/** Routed-context percentage that triggers model-driven Auto Compact. */
|
|
108
|
+
thresholdPercent: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Orthogonal evidence-based code-skeleton reducer gate. Independent of every
|
|
113
|
+
* profile: when enabled, fresh oversized source-code results may take the
|
|
114
|
+
* `hypa-code-skeleton` reducer before the head/tail fallbacks.
|
|
115
|
+
*/
|
|
116
|
+
export interface CodeSkeletonSettings {
|
|
117
|
+
enabled: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Persisted sub-capability overrides for the `tokenpilot-inspired` preset.
|
|
122
|
+
* Absent fields inherit the preset defaults; the section is only meaningful
|
|
123
|
+
* while the resolved profile is `tokenpilot-inspired`.
|
|
124
|
+
*/
|
|
125
|
+
export interface PresetOptionsSettings {
|
|
126
|
+
readonly dedupeToolResults?: boolean
|
|
127
|
+
readonly summaryLocator?: boolean
|
|
128
|
+
readonly prefixStabilizer?: boolean
|
|
129
|
+
readonly readState?: boolean
|
|
130
|
+
readonly estimatorMode?: '' | 'host' | 'direct'
|
|
131
|
+
/**
|
|
132
|
+
* Estimator endpoint fields. Persisted-settings only: they never enter the
|
|
133
|
+
* frozen CompressionPolicy, which is emitted verbatim by policy-resolved
|
|
134
|
+
* audits, so the API key cannot leak into logs.
|
|
135
|
+
*/
|
|
136
|
+
readonly estimatorProvider?: string
|
|
137
|
+
readonly estimatorModel?: string
|
|
138
|
+
readonly estimatorBaseUrl?: string
|
|
139
|
+
readonly estimatorApiKey?: string
|
|
140
|
+
readonly estimatorTimeoutMs?: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Durable global preference exposed through `ctx.settings`. */
|
|
144
|
+
export interface ContextCompressionSettings {
|
|
145
|
+
/** Default strategy snapped when a Session first reaches the pruner. */
|
|
146
|
+
profile: CompressionProfile
|
|
147
|
+
/** Versioned Custom policy snapped with `profile` for a newly observed Session. */
|
|
148
|
+
custom: CustomCompressionPolicy
|
|
149
|
+
/** Auto Compact trigger preference snapped with `profile` for a newly observed Session. */
|
|
150
|
+
autoCompact: AutoCompactSettings
|
|
151
|
+
/** Code-skeleton reducer gate snapped independently of `profile`. */
|
|
152
|
+
codeSkeleton: CodeSkeletonSettings
|
|
153
|
+
/** Optional tokenpilot-inspired sub-capability overrides; absent inherits preset defaults. */
|
|
154
|
+
presetOptions?: PresetOptionsSettings
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Token-gated policy with character fields limited to reducer candidate shape. */
|
|
158
|
+
export interface ToolResultPruneConfig {
|
|
159
|
+
/** Composition fallback when Host settings are unavailable. Defaults to `balanced`. */
|
|
160
|
+
profile?: CompressionProfile
|
|
161
|
+
/** Native fallback leading Unicode code points. Defaults to `4096`. */
|
|
162
|
+
headChars?: number
|
|
163
|
+
/** Native fallback trailing Unicode code points. Defaults to `1024`. */
|
|
164
|
+
tailChars?: number
|
|
165
|
+
/** Native original-content token trigger. Profile default when omitted. */
|
|
166
|
+
nativeTriggerTokens?: number
|
|
167
|
+
/** Native replacement token target. Profile default when omitted. */
|
|
168
|
+
nativeTargetTokens?: number
|
|
169
|
+
/** Fresh-result exact-token trigger. Profile default when omitted. */
|
|
170
|
+
freshTriggerTokens?: number
|
|
171
|
+
/** Maximum fresh-result exact-token replacement size. Profile default when omitted. */
|
|
172
|
+
freshTargetTokens?: number
|
|
173
|
+
/** Combined completed-step token pressure that starts aggregate reduction. */
|
|
174
|
+
aggregateTriggerTokens?: number
|
|
175
|
+
/** Aggregate token target after completed-step pressure exceeds its trigger. */
|
|
176
|
+
aggregateTargetTokens?: number
|
|
177
|
+
/** Total live tool-result tokens that permit historical aging. Profile default when omitted. */
|
|
178
|
+
historyTriggerTokens?: number
|
|
179
|
+
/** Recent completed agent tool calls protected from historical aging. Profile default when omitted. */
|
|
180
|
+
historyKeepRecentToolCalls?: number
|
|
181
|
+
/** Recent tool-result token tail protected in addition to tool calls. Profile default when omitted. */
|
|
182
|
+
historyKeepRecentTokens?: number
|
|
183
|
+
/** Minimum reclaim required before historical aging is worth a cache break. Profile default when omitted. */
|
|
184
|
+
historyMinReclaimTokens?: number
|
|
185
|
+
/**
|
|
186
|
+
* Auto Compact threshold percent frozen into this deployment by the preset
|
|
187
|
+
* overlay generation (50–90 integer). When present it supersedes the live
|
|
188
|
+
* Host setting so one generation never splits Auto Compact and micro
|
|
189
|
+
* compact across two thresholds.
|
|
190
|
+
*/
|
|
191
|
+
autoCompactThresholdPercent?: number
|
|
192
|
+
/** Optional tokenpilot-inspired sub-capability overrides (deploy-level). */
|
|
193
|
+
presetOptions?: PresetOptionsSettings
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Resolved per-profile behavior. */
|
|
197
|
+
export interface CompressionPolicy {
|
|
198
|
+
readonly profile: CompressionProfile
|
|
199
|
+
/** Whether this Session may use the selector's native-style head/middle/tail reducer. */
|
|
200
|
+
readonly nativeToolResultEnabled: boolean
|
|
201
|
+
readonly freshEnabled: boolean
|
|
202
|
+
readonly aggregateEnabled: boolean
|
|
203
|
+
readonly historyMode: HistoryMode
|
|
204
|
+
readonly nativeTriggerTokens: number
|
|
205
|
+
readonly nativeTargetTokens: number
|
|
206
|
+
readonly freshTriggerTokens: number
|
|
207
|
+
readonly freshTargetTokens: number
|
|
208
|
+
readonly aggregateTriggerTokens: number
|
|
209
|
+
readonly aggregateTargetTokens: number
|
|
210
|
+
readonly historyTriggerTokens: number
|
|
211
|
+
readonly historyKeepRecentToolCalls: number
|
|
212
|
+
readonly historyKeepRecentTokens: number
|
|
213
|
+
readonly historyMinReclaimTokens: number
|
|
214
|
+
/**
|
|
215
|
+
* Auto Compact token watermark `A = floor(C × a)` when the standard-profile
|
|
216
|
+
* History linkage resolved for this Session; absent for Custom, Off, Native,
|
|
217
|
+
* or unresolved routed capacity.
|
|
218
|
+
*/
|
|
219
|
+
readonly autoCompactTokens?: number
|
|
220
|
+
/**
|
|
221
|
+
* Micro-compact last-chance watermark `D = floor(A × 0.875)`. Absent when
|
|
222
|
+
* {@link autoCompactTokens} is absent; capacity-pressure gates fall back to
|
|
223
|
+
* the fixed 0.7 routed-context ratio in that case.
|
|
224
|
+
*/
|
|
225
|
+
readonly microDeadlineTokens?: number
|
|
226
|
+
/** Present for Custom v3; standard profiles and legacy Custom policies carry no TailTrim policy. */
|
|
227
|
+
readonly tailTrim?: {
|
|
228
|
+
readonly enabled: boolean
|
|
229
|
+
readonly triggerTokens: number
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* TokenPilot-inspired sub-capability switches. Present only for the
|
|
233
|
+
* `tokenpilot-inspired` preset; every other profile stays byte-identical.
|
|
234
|
+
*/
|
|
235
|
+
readonly presetOptions?: PresetOptions
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Validated, detached, deeply immutable configuration. */
|
|
239
|
+
export interface ResolvedConfig {
|
|
240
|
+
readonly profile: CompressionProfile
|
|
241
|
+
readonly headChars: number
|
|
242
|
+
readonly tailChars: number
|
|
243
|
+
readonly nativeTriggerTokens?: number
|
|
244
|
+
readonly nativeTargetTokens?: number
|
|
245
|
+
readonly freshTriggerTokens?: number
|
|
246
|
+
readonly freshTargetTokens?: number
|
|
247
|
+
readonly aggregateTriggerTokens?: number
|
|
248
|
+
readonly aggregateTargetTokens?: number
|
|
249
|
+
readonly historyTriggerTokens?: number
|
|
250
|
+
readonly historyKeepRecentToolCalls?: number
|
|
251
|
+
readonly historyKeepRecentTokens?: number
|
|
252
|
+
readonly historyMinReclaimTokens?: number
|
|
253
|
+
/**
|
|
254
|
+
* Auto Compact threshold percent frozen into this deployment by the preset
|
|
255
|
+
* overlay generation (50-90 integer). Supersedes the live Host setting.
|
|
256
|
+
*/
|
|
257
|
+
readonly autoCompactThresholdPercent?: number
|
|
258
|
+
/** Optional tokenpilot-inspired sub-capability overrides resolved from the deployment config. */
|
|
259
|
+
readonly presetOptions?: PresetOptionsSettings
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Why a pruning pass runs. */
|
|
263
|
+
export type PruneStage = 'fresh' | 'pressure'
|
|
264
|
+
|
|
265
|
+
/** Optional control over one pruning pass. */
|
|
266
|
+
export interface PruneSessionOptions {
|
|
267
|
+
/** `fresh` only reduces never-before-seen oversized results; `pressure` may age older results too. */
|
|
268
|
+
stage?: PruneStage
|
|
269
|
+
/** Routed model context capacity used only to resolve a context-percent Custom snapshot. */
|
|
270
|
+
contextWindowTokens?: number
|
|
271
|
+
/** Proposed turn at the pre-step boundary. Used with `freshStep` to freeze keep/reduce decisions. */
|
|
272
|
+
freshTurn?: number
|
|
273
|
+
/** The immediately preceding completed step whose tool results have not yet entered a model request. */
|
|
274
|
+
freshStep?: number
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cited source event and size accounting for one landed surface replacement. */
|
|
278
|
+
export interface PrunedEntry {
|
|
279
|
+
/** Current surface event shadowed by this replacement. */
|
|
280
|
+
readonly originalSeq: number
|
|
281
|
+
/** Root full-fidelity source event used in the recovery reference. */
|
|
282
|
+
readonly sourceSeq: number
|
|
283
|
+
/** Newly appended compressed tool-result event. */
|
|
284
|
+
readonly replacementSeq: number
|
|
285
|
+
/** Tool call shared by the original and replacement. */
|
|
286
|
+
readonly callId: CallId
|
|
287
|
+
/** Reducer or aging strategy that produced the replacement. */
|
|
288
|
+
readonly reducer: string
|
|
289
|
+
/** Pass stage that landed the replacement. */
|
|
290
|
+
readonly stage: PruneStage
|
|
291
|
+
/** Original deterministic pressure cost. */
|
|
292
|
+
readonly charsBefore: number
|
|
293
|
+
/** Replacement deterministic pressure cost. */
|
|
294
|
+
readonly charsAfter: number
|
|
295
|
+
/** Authoritative exact canonical content tokens before replacement. */
|
|
296
|
+
readonly tokensBefore: number
|
|
297
|
+
/** Authoritative exact canonical content tokens after replacement. */
|
|
298
|
+
readonly tokensAfter: number
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Aggregate outcome of one stable-surface pruning pass. */
|
|
302
|
+
export interface PruneResult {
|
|
303
|
+
/** Replacements in landing order. */
|
|
304
|
+
readonly pruned: readonly PrunedEntry[]
|
|
305
|
+
/** Total deterministic pressure cost removed across replacements. */
|
|
306
|
+
readonly charsRemoved: number
|
|
307
|
+
/** Authoritative exact canonical content tokens removed. */
|
|
308
|
+
readonly tokensRemoved: number
|
|
309
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Version-neutral immutable-value and closed-union helpers for the plugin runtime. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Freeze an object graph in place without relying on a Harness utility export.
|
|
5
|
+
* Live AbortSignals remain mutable so request cancellation continues to work.
|
|
6
|
+
* @param value - Value to freeze recursively.
|
|
7
|
+
* @returns The same deeply frozen value.
|
|
8
|
+
*/
|
|
9
|
+
export function deepFreeze<T>(value: T): T {
|
|
10
|
+
const seen = new WeakSet<object>()
|
|
11
|
+
const pending: Array<{ readonly kind: 'visit', readonly node: unknown } | {
|
|
12
|
+
readonly kind: 'property'
|
|
13
|
+
readonly source: Record<string, unknown>
|
|
14
|
+
readonly key: string
|
|
15
|
+
}> = [{ kind: 'visit', node: value }]
|
|
16
|
+
while (pending.length > 0) {
|
|
17
|
+
const task = pending.pop()
|
|
18
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
19
|
+
if (task === undefined) continue
|
|
20
|
+
if (task.kind === 'property') {
|
|
21
|
+
pending.push({ kind: 'visit', node: task.source[task.key] })
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
const node = task.node
|
|
25
|
+
if (node === null || typeof node !== 'object' || node instanceof AbortSignal || seen.has(node)) continue
|
|
26
|
+
seen.add(node)
|
|
27
|
+
Object.freeze(node)
|
|
28
|
+
const keys = Object.keys(node)
|
|
29
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
30
|
+
const key = keys[index]
|
|
31
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
32
|
+
if (key === undefined) continue
|
|
33
|
+
pending.push({ kind: 'property', source: node as Record<string, unknown>, key })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Throw for an impossible member of a closed discriminated union.
|
|
41
|
+
* @param value - Value that escaped its closed union type.
|
|
42
|
+
* @param context - Optional switch-site label.
|
|
43
|
+
* @returns Never returns.
|
|
44
|
+
*/
|
|
45
|
+
export function assertNever(value: never, context?: string): never {
|
|
46
|
+
const rendered = JSON.stringify(value) ?? String(value)
|
|
47
|
+
throw new Error(`unreachable variant${context === undefined ? '' : ` in ${context}`}: ${rendered}`)
|
|
48
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
CompressionProfileSelector,
|
|
8
|
+
ContextCompressionSettingsSection,
|
|
9
|
+
type CompressionProfileSelectorProps,
|
|
10
|
+
type ContextCompressionSettings,
|
|
11
|
+
} from '../src/client/CompressionProfileSelector.tsx'
|
|
12
|
+
import { en } from '../src/client/locales.ts'
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup)
|
|
15
|
+
|
|
16
|
+
interface SettingsScopeSnapshot<T> {
|
|
17
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
18
|
+
value: T | undefined
|
|
19
|
+
base: T | undefined
|
|
20
|
+
user: T | undefined
|
|
21
|
+
revision: number | undefined
|
|
22
|
+
writable: boolean
|
|
23
|
+
mode: 'host' | 'memory'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SnapshotStore<T> {
|
|
27
|
+
readonly getSnapshot: () => T
|
|
28
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
29
|
+
readonly update: (recipe: (draft: T) => void) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createSnapshotStore<T>(initial: T): SnapshotStore<T> {
|
|
33
|
+
let snapshot = structuredClone(initial)
|
|
34
|
+
const listeners = new Set<() => void>()
|
|
35
|
+
return {
|
|
36
|
+
getSnapshot: () => snapshot,
|
|
37
|
+
subscribe(listener) {
|
|
38
|
+
listeners.add(listener)
|
|
39
|
+
return () => { listeners.delete(listener) }
|
|
40
|
+
},
|
|
41
|
+
update(recipe) {
|
|
42
|
+
const draft = structuredClone(snapshot)
|
|
43
|
+
recipe(draft)
|
|
44
|
+
snapshot = draft
|
|
45
|
+
for (const listener of listeners) listener()
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bindSnapshotSelector<T>(store: SnapshotStore<T>) {
|
|
51
|
+
return function useSnapshotSelector<U>(selector: (snapshot: T) => U): U {
|
|
52
|
+
return useSyncExternalStore(
|
|
53
|
+
store.subscribe,
|
|
54
|
+
() => selector(store.getSnapshot()),
|
|
55
|
+
() => selector(store.getSnapshot()),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_CUSTOM = {
|
|
61
|
+
version: 1,
|
|
62
|
+
unit: 'tokens',
|
|
63
|
+
fresh: { enabled: true, trigger: 4_096, target: 2_048 },
|
|
64
|
+
aggregate: { enabled: true, trigger: 16_384, target: 8_192 },
|
|
65
|
+
history: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
trigger: 32_768,
|
|
68
|
+
keepRecentTurns: 2,
|
|
69
|
+
keepRecent: 16_384,
|
|
70
|
+
minReclaim: 8_192,
|
|
71
|
+
},
|
|
72
|
+
prefixPolicy: 'pressure-break',
|
|
73
|
+
} as const satisfies ContextCompressionSettings['custom']
|
|
74
|
+
|
|
75
|
+
const t = (key: string) => en[key as keyof typeof en] ?? key
|
|
76
|
+
|
|
77
|
+
interface MountOptions {
|
|
78
|
+
readonly value?: ContextCompressionSettings
|
|
79
|
+
readonly saveAutoCompact?: (thresholdPercent: number) => Promise<void>
|
|
80
|
+
readonly settingsSection?: boolean
|
|
81
|
+
readonly writable?: boolean
|
|
82
|
+
readonly status?: 'loading' | 'ready'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mountAutoCompact(options: MountOptions = {}) {
|
|
86
|
+
const state = createSnapshotStore<SettingsScopeSnapshot<ContextCompressionSettings>>({
|
|
87
|
+
status: options.status ?? 'ready',
|
|
88
|
+
value: options.value ?? {
|
|
89
|
+
profile: 'balanced',
|
|
90
|
+
custom: structuredClone(DEFAULT_CUSTOM),
|
|
91
|
+
autoCompact: { thresholdPercent: 80 },
|
|
92
|
+
codeSkeleton: { enabled: false },
|
|
93
|
+
},
|
|
94
|
+
base: undefined,
|
|
95
|
+
user: undefined,
|
|
96
|
+
revision: 0,
|
|
97
|
+
writable: options.writable ?? true,
|
|
98
|
+
mode: 'host',
|
|
99
|
+
})
|
|
100
|
+
const sessions = createSnapshotStore({
|
|
101
|
+
ids: ['s1'],
|
|
102
|
+
byId: { s1: { id: 's1', displayTitle: 's1', running: false, blank: true, updatedAt: 0, agentPreset: 'standard' } },
|
|
103
|
+
current: 's1',
|
|
104
|
+
phase: 'ready',
|
|
105
|
+
subagentsByParent: {},
|
|
106
|
+
jobsBySession: {},
|
|
107
|
+
currentAddress: undefined,
|
|
108
|
+
})
|
|
109
|
+
const saveAutoCompact = options.saveAutoCompact ?? vi.fn(() => Promise.resolve())
|
|
110
|
+
const Component = options.settingsSection === false ? CompressionProfileSelector : ContextCompressionSettingsSection
|
|
111
|
+
render(<Component {...({
|
|
112
|
+
useCompression: bindSnapshotSelector(state),
|
|
113
|
+
useSessions: bindSnapshotSelector(sessions),
|
|
114
|
+
select: vi.fn(() => Promise.resolve()),
|
|
115
|
+
saveCustom: vi.fn(() => Promise.resolve()),
|
|
116
|
+
resetCustom: vi.fn(() => Promise.resolve()),
|
|
117
|
+
saveAutoCompact,
|
|
118
|
+
saveCodeSkeleton: vi.fn(() => Promise.resolve()),
|
|
119
|
+
savePresetOptions: vi.fn(() => Promise.resolve()),
|
|
120
|
+
t,
|
|
121
|
+
} as unknown as CompressionProfileSelectorProps)} />)
|
|
122
|
+
return { state, saveAutoCompact }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const INPUT_LABEL = 'Auto Compact threshold (%)'
|
|
126
|
+
|
|
127
|
+
describe('Auto Compact threshold controls', () => {
|
|
128
|
+
it('renders the editor only inside the context-compression settings section', () => {
|
|
129
|
+
mountAutoCompact({ settingsSection: true })
|
|
130
|
+
expect(screen.getByLabelText(INPUT_LABEL)).not.toBeNull()
|
|
131
|
+
expect(screen.queryByRole('slider')).toBeNull()
|
|
132
|
+
for (const quick of ['70%', '80%', '85%']) expect(screen.queryByRole('button', { name: quick })).toBeNull()
|
|
133
|
+
|
|
134
|
+
// The compact workspace selector shows a summary, never a second editor.
|
|
135
|
+
cleanup()
|
|
136
|
+
mountAutoCompact({ settingsSection: false })
|
|
137
|
+
expect(screen.queryByLabelText(INPUT_LABEL)).toBeNull()
|
|
138
|
+
expect(screen.queryByRole('slider')).toBeNull()
|
|
139
|
+
expect(screen.getByText(/Auto Compact threshold: 80%/)).not.toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Regression guard: the settings surface must keep BOTH section-level gates.
|
|
143
|
+
// A refactor once dropped CodeSkeletonControls from the settings section while
|
|
144
|
+
// leaving the injected save path intact, so the code-skeleton save contract
|
|
145
|
+
// stayed green with the control missing from the UI.
|
|
146
|
+
it('keeps the code-skeleton gate alongside the Auto Compact editor in the settings section', () => {
|
|
147
|
+
mountAutoCompact({ settingsSection: true })
|
|
148
|
+
expect(screen.getByRole('heading', { name: en['codeSkeleton.title'] })).not.toBeNull()
|
|
149
|
+
expect(screen.getByLabelText(en['codeSkeleton.enabled'])).not.toBeNull()
|
|
150
|
+
expect(screen.getByRole('heading', { name: en['autoCompact.title'] })).not.toBeNull()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('saves a non-quick value like 73 and reads the same value back after remount', async () => {
|
|
154
|
+
const saving = vi.fn((thresholdPercent: number): Promise<void> => {
|
|
155
|
+
mounted.state.update(draft => {
|
|
156
|
+
if (draft.value !== undefined) draft.value.autoCompact = { thresholdPercent }
|
|
157
|
+
})
|
|
158
|
+
return Promise.resolve()
|
|
159
|
+
})
|
|
160
|
+
const mounted = mountAutoCompact({ settingsSection: true, saveAutoCompact: saving })
|
|
161
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: '73' } })
|
|
162
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save Auto Compact threshold' }))
|
|
163
|
+
await waitFor(() => { expect(mounted.state.getSnapshot().value?.autoCompact).toEqual({ thresholdPercent: 73 }) })
|
|
164
|
+
|
|
165
|
+
// A fresh mount reads the same persisted settings.
|
|
166
|
+
cleanup()
|
|
167
|
+
mountAutoCompact({
|
|
168
|
+
settingsSection: true,
|
|
169
|
+
value: { profile: 'balanced', custom: structuredClone(DEFAULT_CUSTOM), autoCompact: { thresholdPercent: 73 }, codeSkeleton: { enabled: false } },
|
|
170
|
+
})
|
|
171
|
+
expect(screen.getByLabelText<HTMLInputElement>(INPUT_LABEL).value).toBe('73')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it.each(['49', '91', '72.5', 'abc', ''])('blocks saving the invalid draft %s', (draft) => {
|
|
175
|
+
const { saveAutoCompact } = mountAutoCompact({ settingsSection: true })
|
|
176
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: draft } })
|
|
177
|
+
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Save Auto Compact threshold' }).disabled).toBe(true)
|
|
178
|
+
expect(screen.getByText(/must be an integer between 50 and 90/)).not.toBeNull()
|
|
179
|
+
expect(saveAutoCompact).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('uses the typed number input as the only threshold editor', () => {
|
|
183
|
+
mountAutoCompact({ settingsSection: true })
|
|
184
|
+
const input = screen.getByLabelText<HTMLInputElement>(INPUT_LABEL)
|
|
185
|
+
fireEvent.change(input, { target: { value: '73' } })
|
|
186
|
+
expect(input.value).toBe('73')
|
|
187
|
+
expect(screen.queryByRole('slider')).toBeNull()
|
|
188
|
+
for (const quick of ['70%', '80%', '85%']) expect(screen.queryByRole('button', { name: quick })).toBeNull()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('explains the risk outside the recommended 70–85 band but keeps saving available', () => {
|
|
192
|
+
mountAutoCompact({ settingsSection: true })
|
|
193
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: '65' } })
|
|
194
|
+
expect(screen.getByText(/summarization calls and prefix rebuilds/)).not.toBeNull()
|
|
195
|
+
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Save Auto Compact threshold' }).disabled).toBe(false)
|
|
196
|
+
|
|
197
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: '90' } })
|
|
198
|
+
expect(screen.getByText(/shared by requests and output/)).not.toBeNull()
|
|
199
|
+
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Save Auto Compact threshold' }).disabled).toBe(false)
|
|
200
|
+
|
|
201
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: '80' } })
|
|
202
|
+
expect(screen.queryByText(/summarization calls and prefix rebuilds/)).toBeNull()
|
|
203
|
+
expect(screen.queryByText(/shared by requests and output/)).toBeNull()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('disables every control while saving or when the scope is not writable', () => {
|
|
207
|
+
const { saveAutoCompact } = mountAutoCompact({ settingsSection: true, status: 'loading' })
|
|
208
|
+
expect(screen.getByLabelText<HTMLInputElement>(INPUT_LABEL).disabled).toBe(true)
|
|
209
|
+
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Save Auto Compact threshold' }).disabled).toBe(true)
|
|
210
|
+
expect(saveAutoCompact).not.toHaveBeenCalled()
|
|
211
|
+
|
|
212
|
+
cleanup()
|
|
213
|
+
mountAutoCompact({ settingsSection: true, writable: false })
|
|
214
|
+
expect(screen.getByLabelText<HTMLInputElement>(INPUT_LABEL).disabled).toBe(true)
|
|
215
|
+
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Save Auto Compact threshold' }).disabled).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('shows a failed threshold save inline', async () => {
|
|
219
|
+
const rejected = Promise.reject(new Error('threshold write failed'))
|
|
220
|
+
void rejected.catch(() => {})
|
|
221
|
+
mountAutoCompact({ settingsSection: true, saveAutoCompact: () => rejected })
|
|
222
|
+
fireEvent.change(screen.getByLabelText(INPUT_LABEL), { target: { value: '73' } })
|
|
223
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save Auto Compact threshold' }))
|
|
224
|
+
await waitFor(() => { expect(screen.getByRole('alert').textContent).toContain('threshold write failed') })
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import * as jsxRuntime from 'react/jsx-runtime'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
const artifact = resolve(import.meta.dirname, '../../lib/client.js')
|
|
8
|
+
|
|
9
|
+
describe('built Harness client artifact', () => {
|
|
10
|
+
it('registers a self-contained lazy-CJS factory and injects its CSS', () => {
|
|
11
|
+
const code = readFileSync(artifact, 'utf8')
|
|
12
|
+
let registration: { id: string, factory: (require: (id: string) => unknown) => unknown } | undefined
|
|
13
|
+
const moduleLoader = {
|
|
14
|
+
load(value: typeof registration) {
|
|
15
|
+
registration = value
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(window, '__ModuleLoader__', { configurable: true, value: moduleLoader })
|
|
19
|
+
|
|
20
|
+
expect(() => Function(code)()).not.toThrow()
|
|
21
|
+
expect(registration?.id).toBe('dsh-context-compression-improved')
|
|
22
|
+
|
|
23
|
+
const modules = new Map<string, unknown>([
|
|
24
|
+
['react', React],
|
|
25
|
+
['react/jsx-runtime', jsxRuntime],
|
|
26
|
+
// The current selector only keeps this official package as a side-effect
|
|
27
|
+
// external. A minimal public-module stub proves the artifact asks the
|
|
28
|
+
// Harness loader for it without importing the package's raw CSS in Node.
|
|
29
|
+
['@deepseek-ai/dsh-client-ui-primitives', {}],
|
|
30
|
+
])
|
|
31
|
+
const exported = registration?.factory((id) => {
|
|
32
|
+
if (!modules.has(id)) throw new Error(`unexpected client dependency: ${id}`)
|
|
33
|
+
return modules.get(id)
|
|
34
|
+
}) as { apply?: unknown, inject?: unknown }
|
|
35
|
+
|
|
36
|
+
expect(exported.apply).toBeTypeOf('function')
|
|
37
|
+
expect(exported.inject).toEqual(['slots', 'locale', 'settingsScope'])
|
|
38
|
+
const style = document.querySelector<HTMLStyleElement>(
|
|
39
|
+
'style[data-plugin-css="dsh-context-compression-improved/CompressionProfileSelector.module.css"]',
|
|
40
|
+
)
|
|
41
|
+
expect(style?.dataset.plugin).toBe('dsh-context-compression-improved')
|
|
42
|
+
expect(style?.textContent).toContain('profileGrid')
|
|
43
|
+
expect(document.querySelectorAll('style[data-plugin-css]').length).toBe(1)
|
|
44
|
+
|
|
45
|
+
registration?.factory((id) => modules.get(id))
|
|
46
|
+
expect(document.querySelectorAll('style[data-plugin-css]').length).toBe(1)
|
|
47
|
+
expect(code).not.toMatch(/^\s*(?:import|export)\s/mu)
|
|
48
|
+
expect(code).not.toMatch(/(?:\/home\/|[A-Za-z]:\\Users\\)/u)
|
|
49
|
+
expect(code).not.toContain('sourceMappingURL')
|
|
50
|
+
})
|
|
51
|
+
})
|