dsh-context-compression-improved 0.5.1 → 0.5.3
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/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade safety for the retired review gate's settings keys.
|
|
3
|
+
*
|
|
4
|
+
* The gate is gone, but its keys are PERSISTED: a live settings document still
|
|
5
|
+
* carries `reviewMode: false`, and both the runtime parser and the browser
|
|
6
|
+
* decoder reject unknown keys. Removing the keys from the accepted set would
|
|
7
|
+
* therefore make the plugin fail to load (runtime) or report the whole settings
|
|
8
|
+
* card as unreadable (client). This spec pins the accept-and-ignore contract in
|
|
9
|
+
* both directions, and pins that the keys never reach the resolved policy.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from 'vitest'
|
|
12
|
+
import {
|
|
13
|
+
parsePresetOptionsSettings,
|
|
14
|
+
resolveConfig,
|
|
15
|
+
resolvePolicy,
|
|
16
|
+
} from '../../src/runtime/config.ts'
|
|
17
|
+
import { decodePresetOptionsSettings } from '../../src/profiles.ts'
|
|
18
|
+
|
|
19
|
+
/** The exact legacy section a pre-0.5.2 install has on disk. */
|
|
20
|
+
const LEGACY_SECTION = {
|
|
21
|
+
estimatorMode: 'host',
|
|
22
|
+
estimatorProvider: 'local-35b',
|
|
23
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
24
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
25
|
+
reviewMode: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('retired review-gate keys are accepted and ignored (runtime)', () => {
|
|
29
|
+
it('parses the legacy section without throwing and keeps the live keys', () => {
|
|
30
|
+
const parsed = parsePresetOptionsSettings(LEGACY_SECTION)
|
|
31
|
+
expect(parsed).toEqual({
|
|
32
|
+
estimatorMode: 'host',
|
|
33
|
+
estimatorProvider: 'local-35b',
|
|
34
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
35
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('drops every retired key, even with values the old schema rejected', () => {
|
|
40
|
+
// The retired keys are not validated any more — a stale invalid value in an
|
|
41
|
+
// existing document must not be able to break the load.
|
|
42
|
+
const parsed = parsePresetOptionsSettings({
|
|
43
|
+
reviewMode: 'yes',
|
|
44
|
+
reviewTimeoutTurns: -1,
|
|
45
|
+
cacheHitDiscountAlpha: 'wide',
|
|
46
|
+
reviewHighImpactTokens: null,
|
|
47
|
+
})
|
|
48
|
+
expect(parsed).toEqual({})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('still rejects a genuinely unknown key', () => {
|
|
52
|
+
expect(() => parsePresetOptionsSettings({ reviewModes: true })).toThrow(/unknown key/)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('never lets a retired key reach the resolved policy matrix', () => {
|
|
56
|
+
// The section arrives as `unknown` from the settings document, so the cast
|
|
57
|
+
// mirrors the production path rather than widening the config surface.
|
|
58
|
+
const policy = resolvePolicy(
|
|
59
|
+
resolveConfig({
|
|
60
|
+
presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
|
|
61
|
+
} as unknown as Parameters<typeof resolveConfig>[0]),
|
|
62
|
+
'tokenpilot-inspired',
|
|
63
|
+
)
|
|
64
|
+
const matrix = policy.presetOptions
|
|
65
|
+
expect(matrix).toBeDefined()
|
|
66
|
+
for (const gone of ['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens']) {
|
|
67
|
+
expect(Object.hasOwn(matrix as object, gone)).toBe(false)
|
|
68
|
+
}
|
|
69
|
+
// The surviving matrix is exactly the live capability set.
|
|
70
|
+
expect(Object.keys(matrix as object).sort()).toEqual([
|
|
71
|
+
'advisor', 'dedupeToolResults', 'estimator', 'noNetSavingsGuard',
|
|
72
|
+
'prefixStabilizer', 'readState', 'skipReductionRecovery', 'summaryLocator',
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('retired review-gate keys are accepted and ignored (client mirror)', () => {
|
|
78
|
+
it('decodes a legacy section instead of reporting the card unreadable', () => {
|
|
79
|
+
const decoded = decodePresetOptionsSettings(LEGACY_SECTION)
|
|
80
|
+
// `undefined` here would blank the settings card's presetOptions section.
|
|
81
|
+
expect(decoded).toEqual({
|
|
82
|
+
estimatorMode: 'host',
|
|
83
|
+
estimatorProvider: 'local-35b',
|
|
84
|
+
estimatorModel: 'Qwen3.6-35B-A3B',
|
|
85
|
+
estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('tolerates legacy values the old decoder rejected', () => {
|
|
90
|
+
expect(decodePresetOptionsSettings({ reviewMode: 'yes', reviewHighImpactTokens: -5 })).toEqual({})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('still refuses an unknown key', () => {
|
|
94
|
+
expect(decodePresetOptionsSettings({ reviewModes: true })).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
adviseCandidates,
|
|
4
|
+
computeBenefit,
|
|
5
|
+
DEFAULT_ADVICE_ALPHA,
|
|
6
|
+
DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
|
|
7
|
+
} from '../../../src/runtime/tokenpilot/benefit.ts'
|
|
8
|
+
|
|
9
|
+
/** Reference values follow the spec formulas exactly:
|
|
10
|
+
* R = Σ(before − after); penalty = (1−α)·tail; payback = penalty / (α·R);
|
|
11
|
+
* expectedSaving = α·R·max(0, Ŝ − payback). */
|
|
12
|
+
|
|
13
|
+
describe('computeBenefit', () => {
|
|
14
|
+
it('returns zero recovery and no payback for an empty batch', () => {
|
|
15
|
+
const result = computeBenefit([], { alpha: 0.1, tailTokens: 4000 })
|
|
16
|
+
expect(result.recoveredTokens).toBe(0)
|
|
17
|
+
expect(result.penaltyTokens).toBe(3600)
|
|
18
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
19
|
+
expect(result.expectedSaving).toBeUndefined()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('computes the single-candidate benefit with a known remaining-turn estimate', () => {
|
|
23
|
+
// R = 4000 − 400 = 3600; penalty = 0.9·4000 = 3600; per-turn = 0.1·3600 = 360
|
|
24
|
+
const result = computeBenefit(
|
|
25
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
26
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 12 },
|
|
27
|
+
)
|
|
28
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
29
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
30
|
+
expect(result.paybackTurns).toBeCloseTo(10)
|
|
31
|
+
// α·R·max(0, 12 − 10) = 360·2
|
|
32
|
+
expect(result.expectedSaving).toBeCloseTo(720)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('omits expectedSaving while Ŝ is unknown', () => {
|
|
36
|
+
const result = computeBenefit(
|
|
37
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
38
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
39
|
+
)
|
|
40
|
+
expect(result.paybackTurns).toBeCloseTo(10)
|
|
41
|
+
expect(result.expectedSaving).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('merges candidates into one batch so the penalty is paid once', () => {
|
|
45
|
+
const result = computeBenefit(
|
|
46
|
+
[
|
|
47
|
+
{ sourceSeq: 1, tokensBefore: 2000, tokensAfter: 500 },
|
|
48
|
+
{ sourceSeq: 2, tokensBefore: 3000, tokensAfter: 1000 },
|
|
49
|
+
],
|
|
50
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 20 },
|
|
51
|
+
)
|
|
52
|
+
expect(result.recoveredTokens).toBe(3500)
|
|
53
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
54
|
+
expect(result.paybackTurns).toBeCloseTo(3600 / 350)
|
|
55
|
+
expect(result.expectedSaving).toBeCloseTo(350 * (20 - 3600 / 350))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('clamps a growing candidate to zero recovery instead of negative batch credit', () => {
|
|
59
|
+
const result = computeBenefit(
|
|
60
|
+
[{ sourceSeq: 3, tokensBefore: 100, tokensAfter: 400 }],
|
|
61
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
62
|
+
)
|
|
63
|
+
expect(result.recoveredTokens).toBe(0)
|
|
64
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('stays finite as α → 0: no division, payback undefined, expectedSaving negative', () => {
|
|
68
|
+
const result = computeBenefit(
|
|
69
|
+
[{ sourceSeq: 4, tokensBefore: 4000, tokensAfter: 400 }],
|
|
70
|
+
{ alpha: 0, tailTokens: 4000, remainingTurns: 12 },
|
|
71
|
+
)
|
|
72
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
73
|
+
expect(result.paybackTurns).toBeUndefined()
|
|
74
|
+
// With zero per-turn saving the batch can only lose the refill penalty.
|
|
75
|
+
expect(result.expectedSaving).toBeCloseTo(-4000)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('never reports a positive expectedSaving once Ŝ is inside the payback window', () => {
|
|
79
|
+
// payback = 10 turns; Ŝ = 5 → max(0, Ŝ − payback) = 0
|
|
80
|
+
const result = computeBenefit(
|
|
81
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
82
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 5 },
|
|
83
|
+
)
|
|
84
|
+
expect(result.expectedSaving).toBeCloseTo(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('waives the refill penalty for a refill-exempt batch: payback 0, saving α·R·Ŝ', () => {
|
|
88
|
+
// Fresh-stage shaping: the content never entered the KV cache, so no
|
|
89
|
+
// refill penalty applies — the whole discounted recovery is pure gain.
|
|
90
|
+
const result = computeBenefit(
|
|
91
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
92
|
+
{ alpha: 0.1, tailTokens: 4000, remainingTurns: 12, refillPenaltyExempt: true },
|
|
93
|
+
)
|
|
94
|
+
expect(result.recoveredTokens).toBe(3600)
|
|
95
|
+
expect(result.penaltyTokens).toBe(0)
|
|
96
|
+
expect(result.paybackTurns).toBe(0)
|
|
97
|
+
// α·R·max(0, Ŝ − 0) = 360·12
|
|
98
|
+
expect(result.expectedSaving).toBeCloseTo(4320)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('keeps the refill penalty when the exemption flag is absent', () => {
|
|
102
|
+
const result = computeBenefit(
|
|
103
|
+
[{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
|
|
104
|
+
{ alpha: 0.1, tailTokens: 4000 },
|
|
105
|
+
)
|
|
106
|
+
expect(result.penaltyTokens).toBeCloseTo(3600)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** The shipped advice defaults are part of the contract: the audit and the
|
|
111
|
+
* report quote them, so a silent drift would change what the label means. */
|
|
112
|
+
describe('advice defaults', () => {
|
|
113
|
+
it('pins the shipped α and high-impact threshold', () => {
|
|
114
|
+
expect(DEFAULT_ADVICE_ALPHA).toBe(0.1)
|
|
115
|
+
expect(DEFAULT_ADVICE_HIGH_IMPACT_TOKENS).toBe(4_000)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('adviseCandidates', () => {
|
|
120
|
+
// The shipped threshold is exercised by its own case; every other case lifts
|
|
121
|
+
// it out of the way so one band can be asserted at a time.
|
|
122
|
+
const base = {
|
|
123
|
+
alpha: 0.1,
|
|
124
|
+
tailTokens: 4000,
|
|
125
|
+
highImpactTokens: 1_000_000,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('returns undefined when no candidate carries a positive recovery', () => {
|
|
129
|
+
expect(adviseCandidates([], base)).toBeUndefined()
|
|
130
|
+
expect(adviseCandidates([{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 }], base)).toBeUndefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('labels a fast-payback batch profitable', () => {
|
|
134
|
+
// R = 3600, penalty = 3600, α·R = 360 → payback 10 ≤ 0.25·Ŝ requires Ŝ ≥ 40.
|
|
135
|
+
const advice = adviseCandidates(
|
|
136
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
137
|
+
{ ...base, remainingTurns: 60 },
|
|
138
|
+
)
|
|
139
|
+
expect(advice?.band).toBe('profitable')
|
|
140
|
+
expect(advice?.priced).toBe(1)
|
|
141
|
+
expect(advice?.maxTokensBefore).toBe(4_000)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('labels an unpriceable batch when α prices no recovery', () => {
|
|
145
|
+
const advice = adviseCandidates(
|
|
146
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
147
|
+
{ ...base, alpha: 0, remainingTurns: 12 },
|
|
148
|
+
)
|
|
149
|
+
expect(advice?.band).toBe('unpriceable')
|
|
150
|
+
expect(advice?.benefit.expectedSaving).toBeCloseTo(-4000)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('lets one high-impact candidate label the whole batch, ahead of a good payback', () => {
|
|
154
|
+
// The 8k candidate alone reaches the shipped threshold; the small sibling
|
|
155
|
+
// would be profitable on its own. One label per batch, high impact wins.
|
|
156
|
+
const advice = adviseCandidates(
|
|
157
|
+
[
|
|
158
|
+
{ sourceSeq: 1, tokensBefore: 200, tokensAfter: 100 },
|
|
159
|
+
{ sourceSeq: 2, tokensBefore: 8_192, tokensAfter: 1_000 },
|
|
160
|
+
],
|
|
161
|
+
{ ...base, highImpactTokens: DEFAULT_ADVICE_HIGH_IMPACT_TOKENS, alpha: 1, remainingTurns: 60 },
|
|
162
|
+
)
|
|
163
|
+
expect(advice?.band).toBe('high-impact')
|
|
164
|
+
expect(advice?.maxTokensBefore).toBe(8_192)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('labels a slowly paying batch slow-payback when Ŝ is known', () => {
|
|
168
|
+
// R = 12000 → penalty 3600, α·R = 1200 → payback 3. Ŝ = 11 keeps
|
|
169
|
+
// 0.25·Ŝ = 2.75 strictly below the payback, so the batch is not profitable.
|
|
170
|
+
const advice = adviseCandidates(
|
|
171
|
+
[{ sourceSeq: 7, tokensBefore: 12_000, tokensAfter: 0 }],
|
|
172
|
+
{ ...base, remainingTurns: 11 },
|
|
173
|
+
)
|
|
174
|
+
expect(advice?.band).toBe('slow-payback')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('labels a batch that never pays back not-worth-it, and still returns its benefit', () => {
|
|
178
|
+
const advice = adviseCandidates(
|
|
179
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
180
|
+
{ ...base, remainingTurns: 2 },
|
|
181
|
+
)
|
|
182
|
+
expect(advice?.band).toBe('not-worth-it')
|
|
183
|
+
expect(advice?.benefit.recoveredTokens).toBe(3600)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('never closes a band on Ŝ: an unknown remaining-turn count keeps the batch priceable', () => {
|
|
187
|
+
const advice = adviseCandidates(
|
|
188
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
189
|
+
base,
|
|
190
|
+
)
|
|
191
|
+
// payback 10 > 1 and Ŝ is unknown → the profit bands cannot be argued, so
|
|
192
|
+
// the batch is labelled as not worth the cache break rather than fabricated.
|
|
193
|
+
expect(advice?.band).toBe('not-worth-it')
|
|
194
|
+
expect(advice?.benefit.expectedSaving).toBeUndefined()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('prices a fresh-stage batch without the refill penalty', () => {
|
|
198
|
+
const advice = adviseCandidates(
|
|
199
|
+
[{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
|
|
200
|
+
{ ...base, highImpactTokens: 1_000_000, stage: 'fresh', remainingTurns: 12 },
|
|
201
|
+
)
|
|
202
|
+
expect(advice?.benefit.penaltyTokens).toBe(0)
|
|
203
|
+
expect(advice?.band).toBe('profitable')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('counts only the positively-recovering candidates it priced', () => {
|
|
207
|
+
const advice = adviseCandidates(
|
|
208
|
+
[
|
|
209
|
+
{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 },
|
|
210
|
+
{ sourceSeq: 2, tokensBefore: 4_000, tokensAfter: 400 },
|
|
211
|
+
],
|
|
212
|
+
{ ...base, remainingTurns: 60 },
|
|
213
|
+
)
|
|
214
|
+
expect(advice?.priced).toBe(1)
|
|
215
|
+
expect(advice?.maxTokensBefore).toBe(4_000)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -61,15 +61,14 @@ describe('tokenpilot-inspired preset', () => {
|
|
|
61
61
|
prefixStabilizer: true,
|
|
62
62
|
readState: true,
|
|
63
63
|
estimator: { mode: '' },
|
|
64
|
-
// Review pipeline (beta) ships off with its documented defaults.
|
|
65
|
-
reviewMode: false,
|
|
66
|
-
reviewTimeoutTurns: 6,
|
|
67
|
-
cacheHitDiscountAlpha: 0.1,
|
|
68
|
-
reviewHighImpactTokens: 4000,
|
|
69
64
|
// Advisory advisor ships off with its documented defaults. The
|
|
70
65
|
// resolved matrix intentionally gains this group so it follows the
|
|
71
66
|
// estimator's resolved-consumption pattern; see
|
|
72
67
|
// .agents/plans/ctx-relevance-advisor/spec.md decision record.
|
|
68
|
+
//
|
|
69
|
+
// The retired review gate's four keys are intentionally absent here:
|
|
70
|
+
// they are still ACCEPTED by the parser for upgrade safety but never
|
|
71
|
+
// reach the resolved policy (a reduction must not be gated).
|
|
73
72
|
advisor: {
|
|
74
73
|
mode: '',
|
|
75
74
|
timeoutMs: 8000,
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This spec locks WHERE the compression settings panel mounts: exactly ONE
|
|
5
5
|
* seat — the standalone `settings.section` entry (设置 → 上下文压缩) — and
|
|
6
|
-
* explicitly NOT the Plugins-section surfaces
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* explicitly NOT the Plugins-section surfaces, and NOT a `shell.overlay` float
|
|
7
|
+
* (the retired review panel's seat was removed with the gate). History this
|
|
8
|
+
* pins against: the 0.1.5 line first lost every entry (a lazy `ctx.get` of
|
|
9
|
+
* locale/settingsScope raced the settings client and apply bailed), then showed
|
|
10
|
+
* the panel twice (item card + tab on top of the standalone section). Both were
|
|
11
|
+
* fixed by the declarative-inject + single-section shape below. When a future
|
|
12
|
+
* DSH line moves the seat again, migrate src/client/index.ts AND this file
|
|
13
|
+
* together.
|
|
12
14
|
*/
|
|
13
15
|
import { describe, expect, it, vi } from 'vitest'
|
|
14
16
|
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
@@ -26,8 +28,15 @@ interface CapturedRegistration {
|
|
|
26
28
|
readonly component: unknown
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Drive apply against a stub host and capture every slot registration.
|
|
33
|
+
* @param options - `registerThrows` models an older host that does not declare
|
|
34
|
+
* the seat: the slot boundary rejects the registration, exactly the shape the
|
|
35
|
+
* 0.1.2-era hosts had.
|
|
36
|
+
*/
|
|
37
|
+
function collectRegistrations(
|
|
38
|
+
options: { readonly registerThrows?: boolean } = {},
|
|
39
|
+
): { declared: string[]; registrations: CapturedRegistration[] } {
|
|
31
40
|
const declared: string[] = []
|
|
32
41
|
const registrations: CapturedRegistration[] = []
|
|
33
42
|
const ctx = {
|
|
@@ -43,8 +52,9 @@ function collectRegistrations(): { declared: string[]; registrations: CapturedRe
|
|
|
43
52
|
for (const step of steps) { void step }
|
|
44
53
|
return () => {}
|
|
45
54
|
},
|
|
46
|
-
register: (
|
|
47
|
-
|
|
55
|
+
register: (record: Record<string, unknown>, component: unknown) => {
|
|
56
|
+
if (options.registerThrows === true) throw new Error(`unknown slot ${String(record['name'])}`)
|
|
57
|
+
registrations.push({ slot: String(record['name']), options: record, component })
|
|
48
58
|
return () => {}
|
|
49
59
|
},
|
|
50
60
|
},
|
|
@@ -58,12 +68,16 @@ describe('settings-seat contract (standalone settings.section only)', () => {
|
|
|
58
68
|
expect(inject).toEqual(['slots', 'locale', 'settingsScope'])
|
|
59
69
|
})
|
|
60
70
|
|
|
61
|
-
it('injects exactly the settings.section seat
|
|
71
|
+
it('injects exactly the settings.section seat and nothing else', () => {
|
|
62
72
|
const { declared, registrations } = collectRegistrations()
|
|
63
|
-
expect(declared).toEqual(['settings.section'
|
|
64
|
-
expect(registrations).toHaveLength(
|
|
73
|
+
expect(declared).toEqual(['settings.section'])
|
|
74
|
+
expect(registrations).toHaveLength(1)
|
|
65
75
|
expect(registrations[0]!.slot).toBe('settings.section')
|
|
66
|
-
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('no longer claims a shell.overlay float (the review panel it served is gone)', () => {
|
|
79
|
+
const { declared } = collectRegistrations()
|
|
80
|
+
expect(declared).not.toContain('shell.overlay')
|
|
67
81
|
})
|
|
68
82
|
|
|
69
83
|
it('pins the section identity (id/order/label/locale) and the panel component', () => {
|
|
@@ -84,4 +98,21 @@ describe('settings-seat contract (standalone settings.section only)', () => {
|
|
|
84
98
|
expect(declared).not.toContain('settings.plugins.tab')
|
|
85
99
|
expect(declared).not.toContain('settings.plugin.item')
|
|
86
100
|
})
|
|
101
|
+
|
|
102
|
+
it('survives an older host that does not declare the seat, warning instead of crashing', () => {
|
|
103
|
+
// Cross-version tolerance: a host without this seat rejects the
|
|
104
|
+
// registration at the slot boundary. apply() must swallow that and warn —
|
|
105
|
+
// the historical failure mode was apply() bailing out, which silently took
|
|
106
|
+
// EVERY settings entry down with it.
|
|
107
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
108
|
+
try {
|
|
109
|
+
const { declared } = collectRegistrations({ registerThrows: true })
|
|
110
|
+
// It tried the seat (so the degradation is "loud", not a silent no-op)…
|
|
111
|
+
expect(declared).toEqual(['settings.section'])
|
|
112
|
+
// …and the rejection never escaped apply().
|
|
113
|
+
expect(warn).toHaveBeenCalled()
|
|
114
|
+
} finally {
|
|
115
|
+
warn.mockRestore()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
87
118
|
})
|