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,129 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
COMPRESSION_AUDIT_PREFIX,
|
|
4
|
+
emitCompressionAudit,
|
|
5
|
+
formatCompressionAudit,
|
|
6
|
+
} from '../../src/runtime/audit.ts'
|
|
7
|
+
import type { CompressionAuditRecord } from '../../src/runtime/audit.ts'
|
|
8
|
+
|
|
9
|
+
const custom = {
|
|
10
|
+
version: 3 as const,
|
|
11
|
+
unit: 'tokens' as const,
|
|
12
|
+
fresh: { enabled: true, trigger: 8_192, target: 3_072 },
|
|
13
|
+
aggregate: { enabled: true, trigger: 32_768, target: 12_288 },
|
|
14
|
+
history: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
trigger: 500_000,
|
|
17
|
+
keepRecentToolCalls: 10,
|
|
18
|
+
keepRecentTokens: 64_000,
|
|
19
|
+
minReclaim: 96_000,
|
|
20
|
+
},
|
|
21
|
+
prefixPolicy: 'pressure-break' as const,
|
|
22
|
+
tailTrim: { enabled: true, trigger: 700_000 },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('context-compression audit records', () => {
|
|
26
|
+
it('formats one complete frozen-policy record without model-visible content', () => {
|
|
27
|
+
const record: CompressionAuditRecord = {
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
kind: 'policy-frozen',
|
|
30
|
+
sessionId: 'audit-session',
|
|
31
|
+
settingsSource: 'host-settings',
|
|
32
|
+
settings: {
|
|
33
|
+
profile: 'custom',
|
|
34
|
+
custom,
|
|
35
|
+
autoCompact: { thresholdPercent: 80 },
|
|
36
|
+
codeSkeleton: { enabled: false },
|
|
37
|
+
},
|
|
38
|
+
deploymentConfig: {
|
|
39
|
+
profile: 'balanced',
|
|
40
|
+
headChars: 4_096,
|
|
41
|
+
tailChars: 1_024,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const line = formatCompressionAudit(record)
|
|
46
|
+
|
|
47
|
+
expect(line).toBe(`${COMPRESSION_AUDIT_PREFIX}${JSON.stringify(record)}`)
|
|
48
|
+
expect(JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length))).toEqual(record)
|
|
49
|
+
expect(line).not.toContain('prompt')
|
|
50
|
+
expect(line).not.toContain('toolArguments')
|
|
51
|
+
expect(line).not.toContain('toolResult')
|
|
52
|
+
expect(line).not.toContain('apiKey')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('keeps component provenance and exact token arithmetic on rewrite records', () => {
|
|
56
|
+
const record: CompressionAuditRecord = {
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
kind: 'rewrite',
|
|
59
|
+
sessionId: 'history-session',
|
|
60
|
+
profile: 'cache-strict',
|
|
61
|
+
component: 'history',
|
|
62
|
+
stage: 'pressure',
|
|
63
|
+
reducer: 'historical-tool-result-aging',
|
|
64
|
+
historyMode: 'capacity-pressure',
|
|
65
|
+
manifestEventType: 'compaction/prune',
|
|
66
|
+
manifestSeq: 41,
|
|
67
|
+
replacementSeq: 42,
|
|
68
|
+
sourceSeqs: [12],
|
|
69
|
+
tokensBefore: 600_100,
|
|
70
|
+
tokensAfter: 100,
|
|
71
|
+
tokensRemoved: 600_000,
|
|
72
|
+
tokenizerId: 'mock-tokenizer',
|
|
73
|
+
tokenizerRevision: 'r1',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parsed = JSON.parse(
|
|
77
|
+
formatCompressionAudit(record).slice(COMPRESSION_AUDIT_PREFIX.length),
|
|
78
|
+
) as CompressionAuditRecord
|
|
79
|
+
|
|
80
|
+
expect(parsed).toEqual(record)
|
|
81
|
+
expect(parsed.kind === 'rewrite' && parsed.tokensRemoved)
|
|
82
|
+
.toBe(record.tokensBefore - record.tokensAfter)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('publishes exactly one single-line info message', () => {
|
|
86
|
+
const info = vi.fn()
|
|
87
|
+
const record: CompressionAuditRecord = {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
kind: 'native-auto-compact',
|
|
90
|
+
sessionId: 'native-session',
|
|
91
|
+
manifestEventType: 'compaction/summary',
|
|
92
|
+
manifestSeq: 9,
|
|
93
|
+
reducer: 'llm-summary',
|
|
94
|
+
provider: 'mock',
|
|
95
|
+
model: 'mock',
|
|
96
|
+
tokensBefore: 1_024,
|
|
97
|
+
tokensAfter: null,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
emitCompressionAudit({ info }, record)
|
|
101
|
+
|
|
102
|
+
expect(info).toHaveBeenCalledOnce()
|
|
103
|
+
expect(info).toHaveBeenCalledWith(formatCompressionAudit(record))
|
|
104
|
+
expect(String(info.mock.calls[0]?.[0])).not.toContain('\n')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('does not let logger failure roll back an already committed rewrite', () => {
|
|
108
|
+
const record: CompressionAuditRecord = {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
kind: 'native-auto-compact',
|
|
111
|
+
sessionId: 'native-session',
|
|
112
|
+
manifestEventType: 'compaction/summary',
|
|
113
|
+
manifestSeq: 9,
|
|
114
|
+
reducer: 'llm-summary',
|
|
115
|
+
provider: 'mock',
|
|
116
|
+
model: 'mock',
|
|
117
|
+
tokensBefore: 1_024,
|
|
118
|
+
tokensAfter: null,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
expect(() => {
|
|
122
|
+
emitCompressionAudit({
|
|
123
|
+
info: () => {
|
|
124
|
+
throw new Error('logger unavailable')
|
|
125
|
+
},
|
|
126
|
+
}, record)
|
|
127
|
+
}).not.toThrow()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/** Auto Compact threshold settings schema and History linkage formula. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
AUTO_COMPACT_THRESHOLD_LIMITS,
|
|
6
|
+
ContextCompressionSettingsSchema,
|
|
7
|
+
DEFAULT_CUSTOM_COMPRESSION_POLICY,
|
|
8
|
+
isValidAutoCompactThresholdPercent,
|
|
9
|
+
parseContextCompressionSettings,
|
|
10
|
+
resolveConfig,
|
|
11
|
+
resolvePolicy,
|
|
12
|
+
} from '../../src/pruner.ts'
|
|
13
|
+
import {
|
|
14
|
+
AUTO_COMPACT_THRESHOLD_LIMITS as BROWSER_LIMITS,
|
|
15
|
+
decodeAutoCompactSettings as browserDecodeAutoCompact,
|
|
16
|
+
isValidAutoCompactThresholdPercent as browserIsValid,
|
|
17
|
+
} from '../../src/profiles.ts'
|
|
18
|
+
import { decodeSettings as browserDecodeSettings } from '../../src/client/decode.ts'
|
|
19
|
+
|
|
20
|
+
const BASE_SETTINGS = {
|
|
21
|
+
profile: 'balanced' as const,
|
|
22
|
+
custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('autoCompact settings validation', () => {
|
|
26
|
+
it('defaults legacy settings without an autoCompact section to 80%', () => {
|
|
27
|
+
const settings = parseContextCompressionSettings(structuredClone(BASE_SETTINGS))
|
|
28
|
+
expect(settings.autoCompact).toEqual({ thresholdPercent: 80 })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('accepts any integer threshold from 50 through 90, including non-quick values', () => {
|
|
32
|
+
for (const thresholdPercent of [50, 69, 70, 73, 80, 82, 85, 90]) {
|
|
33
|
+
const settings = parseContextCompressionSettings({
|
|
34
|
+
...structuredClone(BASE_SETTINGS),
|
|
35
|
+
autoCompact: { thresholdPercent },
|
|
36
|
+
} as never)
|
|
37
|
+
expect(settings.autoCompact.thresholdPercent).toBe(thresholdPercent)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it.each([
|
|
42
|
+
49, 91, 0, -1, 100, 72.5, 80.5, Number.NaN, Number.POSITIVE_INFINITY,
|
|
43
|
+
])('rejects the out-of-range or fractional value %s', (thresholdPercent) => {
|
|
44
|
+
expect(() => parseContextCompressionSettings({
|
|
45
|
+
...structuredClone(BASE_SETTINGS),
|
|
46
|
+
autoCompact: { thresholdPercent },
|
|
47
|
+
})).toThrow()
|
|
48
|
+
expect(isValidAutoCompactThresholdPercent(thresholdPercent)).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('rejects malformed autoCompact sections and unknown keys', () => {
|
|
52
|
+
for (const autoCompact of [
|
|
53
|
+
{ thresholdPercent: '80' },
|
|
54
|
+
{ thresholdPercent: null },
|
|
55
|
+
{},
|
|
56
|
+
{ thresholdPercent: 80, extra: true },
|
|
57
|
+
null,
|
|
58
|
+
]) {
|
|
59
|
+
expect(() => parseContextCompressionSettings({
|
|
60
|
+
...structuredClone(BASE_SETTINGS),
|
|
61
|
+
autoCompact,
|
|
62
|
+
})).toThrow()
|
|
63
|
+
}
|
|
64
|
+
expect(() => parseContextCompressionSettings({
|
|
65
|
+
...structuredClone(BASE_SETTINGS),
|
|
66
|
+
autoCompact: { thresholdPercent: 80 },
|
|
67
|
+
unrelated: true,
|
|
68
|
+
} as never)).toThrow(/unknown key/u)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('publishes one shared validation contract for UI, persistence, and runtime', () => {
|
|
72
|
+
expect(AUTO_COMPACT_THRESHOLD_LIMITS).toEqual({
|
|
73
|
+
min: 50,
|
|
74
|
+
max: 90,
|
|
75
|
+
step: 1,
|
|
76
|
+
default: 80,
|
|
77
|
+
})
|
|
78
|
+
expect(isValidAutoCompactThresholdPercent(73)).toBe(true)
|
|
79
|
+
expect(isValidAutoCompactThresholdPercent(80)).toBe(true)
|
|
80
|
+
expect(isValidAutoCompactThresholdPercent('73')).toBe(false)
|
|
81
|
+
expect(isValidAutoCompactThresholdPercent(undefined)).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps the browser-safe mirror of the contract identical to the runtime contract', () => {
|
|
85
|
+
expect(BROWSER_LIMITS).toEqual(AUTO_COMPACT_THRESHOLD_LIMITS)
|
|
86
|
+
for (const value of [50, 73, 80, 90, 49, 91, 72.5, '73', null]) {
|
|
87
|
+
expect(browserIsValid(value)).toBe(isValidAutoCompactThresholdPercent(value))
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('keeps the full settings document browser/runtime parity', () => {
|
|
92
|
+
const documents = [
|
|
93
|
+
['legacy without autoCompact', { profile: 'off', custom: structuredClone(BASE_SETTINGS.custom) }],
|
|
94
|
+
['complete document', {
|
|
95
|
+
profile: 'off',
|
|
96
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
97
|
+
autoCompact: { thresholdPercent: 73 },
|
|
98
|
+
}],
|
|
99
|
+
['top-level unknown key', {
|
|
100
|
+
profile: 'off',
|
|
101
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
102
|
+
autoCompact: { thresholdPercent: 80 },
|
|
103
|
+
unrelated: true,
|
|
104
|
+
}],
|
|
105
|
+
['null document', null],
|
|
106
|
+
['array document', [{ profile: 'off' }]],
|
|
107
|
+
['primitive document', 42],
|
|
108
|
+
['empty object', {}],
|
|
109
|
+
['missing profile', { custom: structuredClone(BASE_SETTINGS.custom) }],
|
|
110
|
+
['invalid profile', { profile: 'turbo', custom: structuredClone(BASE_SETTINGS.custom) }],
|
|
111
|
+
['missing custom', { profile: 'off' }],
|
|
112
|
+
['malformed autoCompact', {
|
|
113
|
+
profile: 'off',
|
|
114
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
115
|
+
autoCompact: { thresholdPercent: '80' },
|
|
116
|
+
}],
|
|
117
|
+
['null profile', { profile: null, custom: structuredClone(BASE_SETTINGS.custom) }],
|
|
118
|
+
['null custom', { profile: 'off', custom: null }],
|
|
119
|
+
['both sections null', { profile: null, custom: null }],
|
|
120
|
+
['own-property undefined profile', { profile: undefined, custom: structuredClone(BASE_SETTINGS.custom) }],
|
|
121
|
+
['own-property undefined custom', { profile: 'off', custom: undefined }],
|
|
122
|
+
['profile off plus malformed autoCompact', {
|
|
123
|
+
profile: 'off',
|
|
124
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
125
|
+
autoCompact: { thresholdPercent: 400 },
|
|
126
|
+
}],
|
|
127
|
+
['codeSkeleton enabled', {
|
|
128
|
+
profile: 'off',
|
|
129
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
130
|
+
codeSkeleton: { enabled: true },
|
|
131
|
+
}],
|
|
132
|
+
['codeSkeleton disabled', {
|
|
133
|
+
profile: 'off',
|
|
134
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
135
|
+
codeSkeleton: { enabled: false },
|
|
136
|
+
}],
|
|
137
|
+
['malformed codeSkeleton section', {
|
|
138
|
+
profile: 'off',
|
|
139
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
140
|
+
codeSkeleton: { enabled: 'on' },
|
|
141
|
+
}],
|
|
142
|
+
['empty codeSkeleton section', {
|
|
143
|
+
profile: 'off',
|
|
144
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
145
|
+
codeSkeleton: {},
|
|
146
|
+
}],
|
|
147
|
+
['codeSkeleton with extra key', {
|
|
148
|
+
profile: 'off',
|
|
149
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
150
|
+
codeSkeleton: { enabled: true, extra: true },
|
|
151
|
+
}],
|
|
152
|
+
['null codeSkeleton section', {
|
|
153
|
+
profile: 'off',
|
|
154
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
155
|
+
codeSkeleton: null,
|
|
156
|
+
}],
|
|
157
|
+
['custom with wrong version', { profile: 'off', custom: { ...structuredClone(BASE_SETTINGS.custom), version: 99 } }],
|
|
158
|
+
] as const
|
|
159
|
+
for (const [label, document] of documents) {
|
|
160
|
+
const runtimeAccepts = (() => {
|
|
161
|
+
try {
|
|
162
|
+
parseContextCompressionSettings(structuredClone(document))
|
|
163
|
+
return true
|
|
164
|
+
} catch {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
})()
|
|
168
|
+
const browserDecoded = browserDecodeSettings(structuredClone(document))
|
|
169
|
+
expect(browserDecoded !== undefined, label).toBe(runtimeAccepts)
|
|
170
|
+
// When both accept, the decoded values must agree on the COMPLETE
|
|
171
|
+
// canonical document — profile, custom, and autoCompact alike.
|
|
172
|
+
if (runtimeAccepts && browserDecoded !== undefined) {
|
|
173
|
+
const runtimeDecoded = parseContextCompressionSettings(structuredClone(document))
|
|
174
|
+
expect(browserDecoded, label).toEqual(runtimeDecoded)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('enforces the same plain-record boundary without erasing prototypes', () => {
|
|
180
|
+
class SettingsDocument {
|
|
181
|
+
profile = 'off' as const
|
|
182
|
+
custom = structuredClone(BASE_SETTINGS.custom)
|
|
183
|
+
autoCompact = { thresholdPercent: 73 }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class AutoCompactDocument {
|
|
187
|
+
thresholdPercent = 73
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const customPrototypeDocument = (): Record<string, unknown> => {
|
|
191
|
+
const custom = structuredClone(BASE_SETTINGS.custom) as unknown as Record<string, unknown>
|
|
192
|
+
Object.setPrototypeOf(custom, { inheritedMarker: true })
|
|
193
|
+
return {
|
|
194
|
+
profile: 'off',
|
|
195
|
+
custom,
|
|
196
|
+
autoCompact: { thresholdPercent: 73 },
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const invalidFactories: ReadonlyArray<readonly [string, () => unknown]> = [
|
|
201
|
+
['Date top-level document', () => new Date(0)],
|
|
202
|
+
['class top-level document', () => new SettingsDocument()],
|
|
203
|
+
['class autoCompact section', () => ({
|
|
204
|
+
...structuredClone(BASE_SETTINGS),
|
|
205
|
+
autoCompact: new AutoCompactDocument(),
|
|
206
|
+
})],
|
|
207
|
+
['custom-prototype Custom section', customPrototypeDocument],
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
for (const [label, createDocument] of invalidFactories) {
|
|
211
|
+
expect(() => ContextCompressionSettingsSchema(createDocument() as never), `host: ${label}`).toThrow()
|
|
212
|
+
expect(() => parseContextCompressionSettings(createDocument()), `runtime: ${label}`).toThrow()
|
|
213
|
+
expect(browserDecodeSettings(createDocument()), `browser: ${label}`).toBeUndefined()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const nullPrototypeDocument = Object.assign(Object.create(null) as Record<string, unknown>, {
|
|
217
|
+
profile: 'off',
|
|
218
|
+
custom: structuredClone(BASE_SETTINGS.custom),
|
|
219
|
+
autoCompact: { thresholdPercent: 73 },
|
|
220
|
+
})
|
|
221
|
+
const hostDecoded = ContextCompressionSettingsSchema(nullPrototypeDocument as never)
|
|
222
|
+
const runtimeDecoded = parseContextCompressionSettings(nullPrototypeDocument)
|
|
223
|
+
const browserDecoded = browserDecodeSettings(nullPrototypeDocument)
|
|
224
|
+
expect(hostDecoded).toEqual(runtimeDecoded)
|
|
225
|
+
expect(browserDecoded).toEqual(runtimeDecoded)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('canonicalizes legacy Custom v1 documents identically on both ends', () => {
|
|
229
|
+
const legacyV1 = {
|
|
230
|
+
version: 1,
|
|
231
|
+
unit: 'tokens',
|
|
232
|
+
fresh: { enabled: true, trigger: 8_192, target: 3_072 },
|
|
233
|
+
aggregate: { enabled: true, trigger: 32_768, target: 12_288 },
|
|
234
|
+
history: {
|
|
235
|
+
enabled: true,
|
|
236
|
+
trigger: 500_000,
|
|
237
|
+
keepRecentTurns: 4,
|
|
238
|
+
keepRecent: 64_000,
|
|
239
|
+
minReclaim: 96_000,
|
|
240
|
+
},
|
|
241
|
+
prefixPolicy: 'pressure-break',
|
|
242
|
+
}
|
|
243
|
+
const runtimeDecoded = parseContextCompressionSettings({
|
|
244
|
+
profile: 'custom',
|
|
245
|
+
custom: structuredClone(legacyV1),
|
|
246
|
+
})
|
|
247
|
+
const browserDecoded = browserDecodeSettings({
|
|
248
|
+
profile: 'custom',
|
|
249
|
+
custom: structuredClone(legacyV1),
|
|
250
|
+
})
|
|
251
|
+
expect(browserDecoded).toEqual(runtimeDecoded)
|
|
252
|
+
// Canonicalization is part of the shared contract: both ends upgrade the
|
|
253
|
+
// legacy document to the same complete v3 policy.
|
|
254
|
+
const custom = runtimeDecoded.custom
|
|
255
|
+
if (custom.version !== 3) throw new Error('runtime did not canonicalize the legacy document to v3')
|
|
256
|
+
expect(custom.version).toBe(3)
|
|
257
|
+
expect(custom.history.keepRecentToolCalls).toBe(10)
|
|
258
|
+
expect(custom.history.keepRecentTokens).toBe(64_000)
|
|
259
|
+
expect(custom.tailTrim).toEqual({ enabled: false, trigger: 700_000 })
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('decodes the browser autoCompact section with exactly the runtime strictness', () => {
|
|
263
|
+
for (const [section, expected] of [
|
|
264
|
+
[undefined, { thresholdPercent: 80 }],
|
|
265
|
+
[{ thresholdPercent: 73 }, { thresholdPercent: 73 }],
|
|
266
|
+
[{ thresholdPercent: 50 }, { thresholdPercent: 50 }],
|
|
267
|
+
[{ thresholdPercent: 90 }, { thresholdPercent: 90 }],
|
|
268
|
+
[null, undefined],
|
|
269
|
+
[[80], undefined],
|
|
270
|
+
[80, undefined],
|
|
271
|
+
['80', undefined],
|
|
272
|
+
[{}, undefined],
|
|
273
|
+
[{ thresholdPercent: 80, extra: true }, undefined],
|
|
274
|
+
[{ thresholdPercent: null }, undefined],
|
|
275
|
+
[{ thresholdPercent: '73' }, undefined],
|
|
276
|
+
[{ thresholdPercent: 91 }, undefined],
|
|
277
|
+
[{ thresholdPercent: 72.5 }, undefined],
|
|
278
|
+
[{ percent: 80 }, undefined],
|
|
279
|
+
] as const) {
|
|
280
|
+
expect(browserDecodeAutoCompact(section), JSON.stringify(section)).toEqual(expected)
|
|
281
|
+
// The runtime schema agrees on accept/reject for the whole section.
|
|
282
|
+
const accepted = (() => {
|
|
283
|
+
try {
|
|
284
|
+
parseContextCompressionSettings({ ...structuredClone(BASE_SETTINGS), ...(section === undefined ? {} : { autoCompact: section }) })
|
|
285
|
+
return true
|
|
286
|
+
} catch {
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
})()
|
|
290
|
+
expect(accepted, JSON.stringify(section)).toBe(expected !== undefined)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('Auto Compact History linkage formula', () => {
|
|
296
|
+
it('restores the exact current preset numbers at the 80% default on a 1M window', () => {
|
|
297
|
+
for (const [profile, expected] of [
|
|
298
|
+
['balanced', { h: 500_000, m: 96_000 }],
|
|
299
|
+
['adaptive', { h: 500_000, m: 96_000 }],
|
|
300
|
+
['savings', { h: 400_000, m: 128_000 }],
|
|
301
|
+
['cache-strict', { h: 600_000, m: 128_000 }],
|
|
302
|
+
] as const) {
|
|
303
|
+
const policy = resolvePolicy(resolveConfig(), profile, undefined, {
|
|
304
|
+
contextWindowTokens: 1_000_000,
|
|
305
|
+
autoCompactThresholdPercent: 80,
|
|
306
|
+
})
|
|
307
|
+
expect(policy.historyTriggerTokens, `${profile} trigger`).toBe(expected.h)
|
|
308
|
+
expect(policy.historyMinReclaimTokens, `${profile} min reclaim`).toBe(expected.m)
|
|
309
|
+
expect(policy.historyKeepRecentTokens, `${profile} tail`).toBe(64_000)
|
|
310
|
+
expect(policy.autoCompactTokens).toBe(800_000)
|
|
311
|
+
expect(policy.microDeadlineTokens).toBe(700_000)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('scales H/M/K and the micro deadline for arbitrary thresholds', () => {
|
|
316
|
+
const balanced73 = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
317
|
+
contextWindowTokens: 1_000_000,
|
|
318
|
+
autoCompactThresholdPercent: 73,
|
|
319
|
+
})
|
|
320
|
+
expect(balanced73.autoCompactTokens).toBe(730_000)
|
|
321
|
+
expect(balanced73.microDeadlineTokens).toBe(638_750)
|
|
322
|
+
expect(balanced73.historyTriggerTokens).toBe(456_250)
|
|
323
|
+
expect(balanced73.historyMinReclaimTokens).toBe(87_600)
|
|
324
|
+
expect(balanced73.historyKeepRecentTokens).toBe(58_400)
|
|
325
|
+
|
|
326
|
+
const savings70 = resolvePolicy(resolveConfig(), 'savings', undefined, {
|
|
327
|
+
contextWindowTokens: 1_000_000,
|
|
328
|
+
autoCompactThresholdPercent: 70,
|
|
329
|
+
})
|
|
330
|
+
expect(savings70.autoCompactTokens).toBe(700_000)
|
|
331
|
+
expect(savings70.microDeadlineTokens).toBe(612_500)
|
|
332
|
+
expect(savings70.historyTriggerTokens).toBe(350_000)
|
|
333
|
+
expect(savings70.historyMinReclaimTokens).toBe(112_000)
|
|
334
|
+
expect(savings70.historyKeepRecentTokens).toBe(56_000)
|
|
335
|
+
|
|
336
|
+
const cacheStrict85 = resolvePolicy(resolveConfig(), 'cache-strict', undefined, {
|
|
337
|
+
contextWindowTokens: 1_000_000,
|
|
338
|
+
autoCompactThresholdPercent: 85,
|
|
339
|
+
})
|
|
340
|
+
expect(cacheStrict85.autoCompactTokens).toBe(850_000)
|
|
341
|
+
expect(cacheStrict85.microDeadlineTokens).toBe(743_750)
|
|
342
|
+
expect(cacheStrict85.historyTriggerTokens).toBe(637_500)
|
|
343
|
+
expect(cacheStrict85.historyMinReclaimTokens).toBe(136_000)
|
|
344
|
+
expect(cacheStrict85.historyKeepRecentTokens).toBe(68_000)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('matches the native engine float evaluation order on rounding-edge pairs', () => {
|
|
348
|
+
// compaction-basic computes floor(window * (p / 100)); integer-first
|
|
349
|
+
// division differs by one token on exactly these pairs.
|
|
350
|
+
for (const [thresholdPercent, contextWindowTokens, expected] of [
|
|
351
|
+
[57, 200_000, 113_999],
|
|
352
|
+
[58, 200_000, 115_999],
|
|
353
|
+
[57, 1_000_000, 570_000],
|
|
354
|
+
] as const) {
|
|
355
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
356
|
+
contextWindowTokens,
|
|
357
|
+
autoCompactThresholdPercent: thresholdPercent,
|
|
358
|
+
})
|
|
359
|
+
expect(policy.autoCompactTokens, `${String(thresholdPercent)}@${String(contextWindowTokens)}`)
|
|
360
|
+
.toBe(expected)
|
|
361
|
+
expect(policy.autoCompactTokens)
|
|
362
|
+
.toBe(Math.floor(contextWindowTokens * (thresholdPercent / 100)))
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('keeps every watermark floor-based and exact on odd context windows', () => {
|
|
367
|
+
const contextWindow = 999_983
|
|
368
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
369
|
+
contextWindowTokens: contextWindow,
|
|
370
|
+
autoCompactThresholdPercent: 73,
|
|
371
|
+
})
|
|
372
|
+
const autoTokens = Math.floor(contextWindow * (73 / 100))
|
|
373
|
+
expect(policy.autoCompactTokens).toBe(autoTokens)
|
|
374
|
+
expect(policy.microDeadlineTokens).toBe(Math.floor(autoTokens * 0.875))
|
|
375
|
+
expect(policy.historyTriggerTokens).toBe(Math.floor(0.625 * autoTokens))
|
|
376
|
+
expect(policy.historyMinReclaimTokens).toBe(Math.floor(0.12 * autoTokens))
|
|
377
|
+
expect(policy.historyKeepRecentTokens).toBe(Math.floor(0.08 * autoTokens))
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('covers the 50% and 90% hard boundaries', () => {
|
|
381
|
+
const low = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
382
|
+
contextWindowTokens: 1_000_000,
|
|
383
|
+
autoCompactThresholdPercent: 50,
|
|
384
|
+
})
|
|
385
|
+
expect(low.autoCompactTokens).toBe(500_000)
|
|
386
|
+
expect(low.microDeadlineTokens).toBe(437_500)
|
|
387
|
+
const high = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
388
|
+
contextWindowTokens: 1_000_000,
|
|
389
|
+
autoCompactThresholdPercent: 90,
|
|
390
|
+
})
|
|
391
|
+
expect(high.autoCompactTokens).toBe(900_000)
|
|
392
|
+
expect(high.microDeadlineTokens).toBe(787_500)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('never links Custom, Off, or Native profiles', () => {
|
|
396
|
+
const custom = resolvePolicy(resolveConfig(), 'custom', structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY), {
|
|
397
|
+
contextWindowTokens: 1_000_000,
|
|
398
|
+
autoCompactThresholdPercent: 73,
|
|
399
|
+
})
|
|
400
|
+
expect(custom.historyTriggerTokens).toBe(500_000)
|
|
401
|
+
expect(custom.autoCompactTokens).toBeUndefined()
|
|
402
|
+
expect(custom.microDeadlineTokens).toBeUndefined()
|
|
403
|
+
for (const profile of ['off', 'native'] as const) {
|
|
404
|
+
const policy = resolvePolicy(resolveConfig(), profile, undefined, {
|
|
405
|
+
contextWindowTokens: 1_000_000,
|
|
406
|
+
autoCompactThresholdPercent: 73,
|
|
407
|
+
})
|
|
408
|
+
expect(policy.autoCompactTokens).toBeUndefined()
|
|
409
|
+
expect(policy.microDeadlineTokens).toBeUndefined()
|
|
410
|
+
expect(policy.historyMode).toBe('disabled')
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('falls back to the fixed preset numbers without a resolved context window', () => {
|
|
415
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, { autoCompactThresholdPercent: 73 })
|
|
416
|
+
expect(policy.historyTriggerTokens).toBe(500_000)
|
|
417
|
+
expect(policy.historyMinReclaimTokens).toBe(96_000)
|
|
418
|
+
expect(policy.historyKeepRecentTokens).toBe(64_000)
|
|
419
|
+
expect(policy.autoCompactTokens).toBeUndefined()
|
|
420
|
+
expect(policy.microDeadlineTokens).toBeUndefined()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('rejects an out-of-range deployment-config threshold percent', () => {
|
|
424
|
+
for (const autoCompactThresholdPercent of [49, 91, 72.5]) {
|
|
425
|
+
expect(() => resolveConfig({ autoCompactThresholdPercent }), String(autoCompactThresholdPercent))
|
|
426
|
+
.toThrow(/autoCompactThresholdPercent/u)
|
|
427
|
+
}
|
|
428
|
+
expect(resolveConfig({ autoCompactThresholdPercent: 73 }).autoCompactThresholdPercent).toBe(73)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('ignores linkage when the context window is zero, fractional, or negative', () => {
|
|
432
|
+
for (const contextWindowTokens of [0, -1_000_000, 999_999.5]) {
|
|
433
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
434
|
+
contextWindowTokens,
|
|
435
|
+
autoCompactThresholdPercent: 73,
|
|
436
|
+
})
|
|
437
|
+
expect(policy.historyTriggerTokens).toBe(500_000)
|
|
438
|
+
expect(policy.autoCompactTokens).toBeUndefined()
|
|
439
|
+
expect(policy.microDeadlineTokens).toBeUndefined()
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('ignores linkage when the threshold is missing or invalid', () => {
|
|
444
|
+
for (const autoCompactThresholdPercent of [undefined, 49, 91, 72.5]) {
|
|
445
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
446
|
+
contextWindowTokens: 1_000_000,
|
|
447
|
+
...(autoCompactThresholdPercent === undefined ? {} : { autoCompactThresholdPercent }),
|
|
448
|
+
})
|
|
449
|
+
expect(policy.historyTriggerTokens).toBe(500_000)
|
|
450
|
+
expect(policy.microDeadlineTokens).toBeUndefined()
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('keeps explicit deployment overrides ahead of linkage values', () => {
|
|
455
|
+
const config = resolveConfig({ historyTriggerTokens: 123_456, historyMinReclaimTokens: 12_345 })
|
|
456
|
+
const policy = resolvePolicy(config, 'balanced', undefined, {
|
|
457
|
+
contextWindowTokens: 1_000_000,
|
|
458
|
+
autoCompactThresholdPercent: 73,
|
|
459
|
+
})
|
|
460
|
+
expect(policy.historyTriggerTokens).toBe(123_456)
|
|
461
|
+
expect(policy.historyMinReclaimTokens).toBe(12_345)
|
|
462
|
+
expect(policy.historyKeepRecentTokens).toBe(58_400)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('keeps the 10-call working set and Fresh/Aggregate budgets independent of the threshold', () => {
|
|
466
|
+
for (const autoCompactThresholdPercent of [50, 73, 90]) {
|
|
467
|
+
const policy = resolvePolicy(resolveConfig(), 'balanced', undefined, {
|
|
468
|
+
contextWindowTokens: 1_000_000,
|
|
469
|
+
autoCompactThresholdPercent,
|
|
470
|
+
})
|
|
471
|
+
expect(policy.historyKeepRecentToolCalls).toBe(10)
|
|
472
|
+
expect(policy.freshTriggerTokens).toBe(8_192)
|
|
473
|
+
expect(policy.freshTargetTokens).toBe(3_072)
|
|
474
|
+
expect(policy.aggregateTriggerTokens).toBe(32_768)
|
|
475
|
+
expect(policy.aggregateTargetTokens).toBe(12_288)
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('codeSkeleton settings validation', () => {
|
|
481
|
+
it('defaults legacy settings without a codeSkeleton section to off', () => {
|
|
482
|
+
const settings = parseContextCompressionSettings(structuredClone(BASE_SETTINGS))
|
|
483
|
+
expect(settings.codeSkeleton).toEqual({ enabled: false })
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('accepts an explicit boolean enabled flag on either side', () => {
|
|
487
|
+
for (const enabled of [true, false]) {
|
|
488
|
+
const settings = parseContextCompressionSettings({
|
|
489
|
+
...structuredClone(BASE_SETTINGS),
|
|
490
|
+
codeSkeleton: { enabled },
|
|
491
|
+
} as never)
|
|
492
|
+
expect(settings.codeSkeleton).toEqual({ enabled })
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('rejects malformed codeSkeleton sections', () => {
|
|
497
|
+
for (const codeSkeleton of [
|
|
498
|
+
{ enabled: 'true' },
|
|
499
|
+
{ enabled: 1 },
|
|
500
|
+
{},
|
|
501
|
+
{ enabled: true, extra: true },
|
|
502
|
+
null,
|
|
503
|
+
'on',
|
|
504
|
+
]) {
|
|
505
|
+
expect(() => parseContextCompressionSettings({
|
|
506
|
+
...structuredClone(BASE_SETTINGS),
|
|
507
|
+
codeSkeleton,
|
|
508
|
+
})).toThrow()
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('mirrors the browser decode for legacy, valid, and malformed documents', () => {
|
|
513
|
+
const legacy = structuredClone(BASE_SETTINGS)
|
|
514
|
+
const enabled = { ...structuredClone(BASE_SETTINGS), codeSkeleton: { enabled: true } }
|
|
515
|
+
const malformed = { ...structuredClone(BASE_SETTINGS), codeSkeleton: { enabled: 'on' } }
|
|
516
|
+
expect(browserDecodeSettings(structuredClone(legacy)))
|
|
517
|
+
.toEqual(parseContextCompressionSettings(structuredClone(legacy)))
|
|
518
|
+
expect(browserDecodeSettings(structuredClone(enabled)))
|
|
519
|
+
.toEqual(parseContextCompressionSettings(structuredClone(enabled) as never))
|
|
520
|
+
expect(browserDecodeSettings(structuredClone(malformed))).toBeUndefined()
|
|
521
|
+
expect(() => parseContextCompressionSettings(structuredClone(malformed))).toThrow()
|
|
522
|
+
})
|
|
523
|
+
})
|