dsh-context-compression-improved 0.3.1 → 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-push +37 -0
- package/package.json +2 -1
- package/packages/selector/cordis.patch.yml +12 -5
- package/packages/selector/lib/client.d.ts +24 -0
- package/packages/selector/lib/client.js +506 -5
- package/packages/selector/lib/config.js +27 -4
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +229 -1
- package/packages/selector/lib/pruner.d.ts +254 -0
- package/packages/selector/lib/pruner.js +714 -25
- package/packages/selector/package.json +0 -1
- package/packages/selector/src/client/EstimatorControls.tsx +101 -0
- package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
- package/packages/selector/src/client/index.ts +17 -0
- package/packages/selector/src/client/locales.ts +38 -0
- package/packages/selector/src/client/preset-options.ts +1 -0
- package/packages/selector/src/client/review-scope.ts +16 -0
- package/packages/selector/src/client/settings-section.tsx +17 -8
- package/packages/selector/src/index.ts +308 -0
- package/packages/selector/src/profiles.ts +28 -1
- package/packages/selector/src/pruner/state.ts +27 -0
- package/packages/selector/src/pruner.ts +430 -10
- package/packages/selector/src/runtime/audit.ts +27 -0
- package/packages/selector/src/runtime/config.ts +33 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
- package/packages/selector/src/runtime/types.ts +17 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
- package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
- package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
- package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
- package/packages/selector/tests/review-routes.host.spec.ts +290 -0
- package/packages/selector/tests/runtime/audit.spec.ts +44 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
- package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
|
@@ -71,6 +71,18 @@ export interface PresetOptions {
|
|
|
71
71
|
readonly readState: boolean
|
|
72
72
|
/** Optional estimator channel; `''` keeps every estimator consumer on rule-only fallbacks (E1/E2). */
|
|
73
73
|
readonly estimator: { readonly mode: '' | 'host' | 'direct' }
|
|
74
|
+
/**
|
|
75
|
+
* Human-gated review pipeline (beta): edge/high-impact candidates queue for
|
|
76
|
+
* manual approval and execute in one merged batch at the next turn boundary
|
|
77
|
+
* instead of the automatic path (R4).
|
|
78
|
+
*/
|
|
79
|
+
readonly reviewMode: boolean
|
|
80
|
+
/** Turn-boundary patience: pending review proposals older than this many turns auto-expire (R4). */
|
|
81
|
+
readonly reviewTimeoutTurns: number
|
|
82
|
+
/** Cache-hit discount rate α in the benefit model; expectedSaving = α·R·Ŝ − (1−α)·tail. */
|
|
83
|
+
readonly cacheHitDiscountAlpha: number
|
|
84
|
+
/** Candidates whose tokenBefore reaches this threshold bypass payback triage and always enter review (R4). */
|
|
85
|
+
readonly reviewHighImpactTokens: number
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
/** Common user-authored Custom stages shared by persisted policy versions. */
|
|
@@ -128,6 +140,11 @@ export interface PresetOptionsSettings {
|
|
|
128
140
|
readonly prefixStabilizer?: boolean
|
|
129
141
|
readonly readState?: boolean
|
|
130
142
|
readonly estimatorMode?: '' | 'host' | 'direct'
|
|
143
|
+
/** Review-mode overrides (beta); see PresetOptions.reviewMode. */
|
|
144
|
+
readonly reviewMode?: boolean
|
|
145
|
+
readonly reviewTimeoutTurns?: number
|
|
146
|
+
readonly cacheHitDiscountAlpha?: number
|
|
147
|
+
readonly reviewHighImpactTokens?: number
|
|
131
148
|
/**
|
|
132
149
|
* Estimator endpoint fields. Persisted-settings only: they never enter the
|
|
133
150
|
* frozen CompressionPolicy, which is emitted verbatim by policy-resolved
|
|
@@ -56,8 +56,9 @@ describe('codeSkeleton confirm-on-write client contract', () => {
|
|
|
56
56
|
settingsScope: { bind: vi.fn(() => scope) },
|
|
57
57
|
slots: {
|
|
58
58
|
inject: (_slot: string, install: () => unknown) => { install() },
|
|
59
|
-
register: (registration: { inject
|
|
60
|
-
|
|
59
|
+
register: (registration: { inject?: () => CompressionSelectorInjected }) => {
|
|
60
|
+
// The R4 overlay registration carries no inject face; keep the card's.
|
|
61
|
+
if (typeof registration.inject === 'function') injected = registration.inject
|
|
61
62
|
return () => {}
|
|
62
63
|
},
|
|
63
64
|
},
|
|
@@ -148,8 +148,9 @@ describe('context compression browser contract', () => {
|
|
|
148
148
|
settingsScope: { bind: vi.fn(() => scope) },
|
|
149
149
|
slots: {
|
|
150
150
|
inject: (_slot: string, install: () => unknown) => { install() },
|
|
151
|
-
register: (registration: { inject
|
|
152
|
-
|
|
151
|
+
register: (registration: { inject?: () => CompressionSelectorInjected }) => {
|
|
152
|
+
// The R4 overlay registration carries no inject face; keep the card's.
|
|
153
|
+
if (typeof registration.inject === 'function') injected = registration.inject
|
|
153
154
|
return () => {}
|
|
154
155
|
},
|
|
155
156
|
},
|
|
@@ -123,7 +123,9 @@ function bindInjected(
|
|
|
123
123
|
return typeof result === 'function' ? result : () => {}
|
|
124
124
|
},
|
|
125
125
|
register: (registerOptions: Record<string, unknown>) => {
|
|
126
|
-
|
|
126
|
+
// The R4 overlay registration rides the same slots service; only the
|
|
127
|
+
// settings card carries the inject factory these tests drive.
|
|
128
|
+
if (registerOptions.name === 'settings.section') options = registerOptions
|
|
127
129
|
return () => {}
|
|
128
130
|
},
|
|
129
131
|
},
|
|
@@ -146,6 +148,37 @@ describe('presetOptions writes are path-addressed', () => {
|
|
|
146
148
|
expect(writes).toEqual([[{ op: 'set', path: ['presetOptions', 'estimatorProvider'], value: 'local-35b' }]])
|
|
147
149
|
})
|
|
148
150
|
|
|
151
|
+
it('writes and clears the review-mode fields without touching estimator siblings', async () => {
|
|
152
|
+
const { injected, writes, snapshot } = bindInjected({ estimatorMode: 'host' })
|
|
153
|
+
await injected.savePresetOptions({
|
|
154
|
+
reviewMode: true,
|
|
155
|
+
reviewTimeoutTurns: 8,
|
|
156
|
+
cacheHitDiscountAlpha: 0.2,
|
|
157
|
+
reviewHighImpactTokens: 6000,
|
|
158
|
+
})
|
|
159
|
+
expect(snapshot()['presetOptions']).toEqual({
|
|
160
|
+
estimatorMode: 'host',
|
|
161
|
+
reviewMode: true,
|
|
162
|
+
reviewTimeoutTurns: 8,
|
|
163
|
+
cacheHitDiscountAlpha: 0.2,
|
|
164
|
+
reviewHighImpactTokens: 6000,
|
|
165
|
+
})
|
|
166
|
+
await injected.savePresetOptions({ reviewMode: undefined })
|
|
167
|
+
expect(snapshot()['presetOptions']).toEqual({
|
|
168
|
+
estimatorMode: 'host',
|
|
169
|
+
reviewTimeoutTurns: 8,
|
|
170
|
+
cacheHitDiscountAlpha: 0.2,
|
|
171
|
+
reviewHighImpactTokens: 6000,
|
|
172
|
+
})
|
|
173
|
+
for (const batch of writes) {
|
|
174
|
+
for (const op of batch) {
|
|
175
|
+
expect(op.path).toEqual(['presetOptions', op.path[1] ?? ''])
|
|
176
|
+
expect(['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens'])
|
|
177
|
+
.toContain(op.path[1])
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
149
182
|
it('never replaces the whole section, so sibling overrides survive', async () => {
|
|
150
183
|
const { injected, writes, snapshot } = bindInjected({
|
|
151
184
|
estimatorMode: 'direct',
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ReviewOverlay } from '../src/client/ReviewOverlay.tsx'
|
|
6
|
+
import type { SettingsScopeLike } from '../src/client/review-scope.ts'
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup()
|
|
10
|
+
vi.unstubAllGlobals()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const QUEUE_ROUTE = '/api/dsh-context-compression-improved/review-queue'
|
|
14
|
+
const DECIDE_ROUTE = '/api/dsh-context-compression-improved/review-decide'
|
|
15
|
+
|
|
16
|
+
const PROPOSAL = {
|
|
17
|
+
sessionId: 's1',
|
|
18
|
+
id: 'abc123def456',
|
|
19
|
+
kind: 'read-state',
|
|
20
|
+
items: [{ seq: 6, tokensBefore: 2401, tokensAfter: 134 }],
|
|
21
|
+
benefit: { recoveredTokens: 400, paybackTurns: 2.25, expectedSaving: 312 },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scopeStub(reviewMode: boolean): SettingsScopeLike {
|
|
25
|
+
return {
|
|
26
|
+
getSnapshot: () => ({
|
|
27
|
+
status: 'ready',
|
|
28
|
+
value: { presetOptions: { reviewMode } },
|
|
29
|
+
}),
|
|
30
|
+
subscribe: () => () => {},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type FetchCall = { input: string | URL | Request, init?: RequestInit | undefined }
|
|
35
|
+
|
|
36
|
+
function stubFetch(responses: Array<{ match: (input: string) => boolean, body: unknown, status?: number }>): {
|
|
37
|
+
calls: FetchCall[]
|
|
38
|
+
} {
|
|
39
|
+
const calls: FetchCall[] = []
|
|
40
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
41
|
+
calls.push({ input, init })
|
|
42
|
+
const url = String(input)
|
|
43
|
+
const match = responses.find(entry => entry.match(url))
|
|
44
|
+
return {
|
|
45
|
+
ok: (match?.status ?? 200) < 400,
|
|
46
|
+
status: match?.status ?? 200,
|
|
47
|
+
json: async () => match?.body,
|
|
48
|
+
} as Response
|
|
49
|
+
}))
|
|
50
|
+
return { calls }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const queueBody = {
|
|
54
|
+
ok: true,
|
|
55
|
+
total: 1,
|
|
56
|
+
pending: [PROPOSAL],
|
|
57
|
+
summary: { autoApplied: 2, reviewApplied: 1, expired: 3, voided: 0 },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('review overlay (client)', () => {
|
|
61
|
+
it('renders the pending list and the four-state summary row', async () => {
|
|
62
|
+
stubFetch([{ match: url => url.includes(QUEUE_ROUTE), body: queueBody }])
|
|
63
|
+
render(<ReviewOverlay scope={scopeStub(true)} t={key => key} />)
|
|
64
|
+
|
|
65
|
+
await waitFor(() => { expect(screen.getByText('review.badge 1')).toBeDefined() })
|
|
66
|
+
fireEvent.click(screen.getByText('review.badge 1'))
|
|
67
|
+
expect(screen.getByText('review.title')).toBeDefined()
|
|
68
|
+
expect(screen.getByText(/review.summary.autoApplied: 2/)).toBeDefined()
|
|
69
|
+
expect(screen.getByText(/review.summary.expired: 3/)).toBeDefined()
|
|
70
|
+
// Estimated saving is labelled as an estimate.
|
|
71
|
+
expect(screen.getByText(/review.row.estimated/)).toBeDefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('posts the three decisions through the decide route and refreshes', async () => {
|
|
75
|
+
const { calls } = stubFetch([
|
|
76
|
+
{ match: url => url.includes(QUEUE_ROUTE), body: queueBody },
|
|
77
|
+
{ match: url => url.includes(DECIDE_ROUTE), body: { ok: true } },
|
|
78
|
+
])
|
|
79
|
+
render(<ReviewOverlay scope={scopeStub(true)} t={key => key} />)
|
|
80
|
+
await waitFor(() => { expect(screen.getByText('review.badge 1')).toBeDefined() })
|
|
81
|
+
fireEvent.click(screen.getByText('review.badge 1'))
|
|
82
|
+
|
|
83
|
+
fireEvent.click(screen.getByText('review.action.approve'))
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(calls.some(call => String(call.input) === DECIDE_ROUTE && call.init?.method === 'POST')).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
const posted = JSON.parse(String(calls.find(call => call.init?.method === 'POST')?.init?.body))
|
|
88
|
+
expect(posted).toEqual({ sessionId: 's1', proposalId: 'abc123def456', decision: 'approved' })
|
|
89
|
+
|
|
90
|
+
fireEvent.click(screen.getByText('review.action.reject'))
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(calls.some(call => String(call.input) === DECIDE_ROUTE
|
|
93
|
+
&& JSON.parse(String(call.init?.body)).decision === 'rejected')).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
fireEvent.click(screen.getByText('review.action.ignore'))
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(calls.some(call => String(call.input) === DECIDE_ROUTE
|
|
98
|
+
&& JSON.parse(String(call.init?.body)).decision === 'ignored')).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('renders nothing while nothing is pending', async () => {
|
|
103
|
+
stubFetch([{ match: url => url.includes(QUEUE_ROUTE), body: { ok: true, total: 0, pending: [] } }])
|
|
104
|
+
const { container } = render(<ReviewOverlay scope={scopeStub(true)} t={key => key} />)
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect((container.querySelector('.dsh-cc-review-badge'))).toBeNull()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('renders nothing while review mode is off', async () => {
|
|
111
|
+
const fetchMock = vi.fn()
|
|
112
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
113
|
+
const { container } = render(<ReviewOverlay scope={scopeStub(false)} t={key => key} />)
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 30))
|
|
115
|
+
expect(container.querySelector('.dsh-cc-review-badge')).toBeNull()
|
|
116
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side guard for the review pipeline's HTTP transport (R4).
|
|
3
|
+
*
|
|
4
|
+
* Pins the client↔runtime contract: the two routes (queue read, decide write)
|
|
5
|
+
* register under both prefixes, the happy path answers a sanitized payload
|
|
6
|
+
* (ids/seqs/counts, never digests or content), and every error path answers a
|
|
7
|
+
* precise status — 400 for malformed input, 404 for unknown session/proposal,
|
|
8
|
+
* 503 when the review pipeline is not serving the session.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
13
|
+
import { apply } from '../src/index.ts'
|
|
14
|
+
|
|
15
|
+
const QUEUE_ROUTE = '/api/dsh-context-compression-improved/review-queue'
|
|
16
|
+
const DECIDE_ROUTE = '/api/dsh-context-compression-improved/review-decide'
|
|
17
|
+
const LEGACY_QUEUE_ROUTE = '/endpoint/dsh-context-compression-improved/review-queue'
|
|
18
|
+
const LEGACY_DECIDE_ROUTE = '/endpoint/dsh-context-compression-improved/review-decide'
|
|
19
|
+
|
|
20
|
+
interface RegisteredRoute {
|
|
21
|
+
kind: string
|
|
22
|
+
path: string
|
|
23
|
+
handler: (req: unknown, res: unknown) => unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FakeResponse {
|
|
27
|
+
status?: number
|
|
28
|
+
body?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let ctx: Context | undefined
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await ctx?.fiber.dispose()
|
|
35
|
+
ctx = undefined
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 20))
|
|
39
|
+
|
|
40
|
+
async function mountWebServer(runtime: Context, routes: RegisteredRoute[]): Promise<void> {
|
|
41
|
+
await runtime.plugin({
|
|
42
|
+
name: 'fake-webserver',
|
|
43
|
+
apply(webCtx) {
|
|
44
|
+
webCtx.provide('webServer', {
|
|
45
|
+
tables: { exact: new Map<string, RegisteredRoute>() },
|
|
46
|
+
register(this: { tables: { exact: Map<string, RegisteredRoute> } }, route: RegisteredRoute) {
|
|
47
|
+
const table = this.tables.exact
|
|
48
|
+
if (table.has(route.path)) {
|
|
49
|
+
throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`)
|
|
50
|
+
}
|
|
51
|
+
table.set(route.path, route)
|
|
52
|
+
routes.push(route)
|
|
53
|
+
return () => {}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface PrunerBehavior {
|
|
61
|
+
readonly reviewOn: boolean
|
|
62
|
+
decideOutcome?: { ok: true } | { ok: false, reason: string } | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mountReviewServices(runtime: Context, behavior: PrunerBehavior): void {
|
|
66
|
+
void runtime.plugin({
|
|
67
|
+
name: 'fake-review-services',
|
|
68
|
+
apply(serviceCtx) {
|
|
69
|
+
serviceCtx.provide('agents', {
|
|
70
|
+
get: (id: unknown) => (id === 's1' ? { session: { id: 's1' } } : undefined),
|
|
71
|
+
})
|
|
72
|
+
const sampleProposal = {
|
|
73
|
+
id: 'abc123def456',
|
|
74
|
+
kind: 'read-state',
|
|
75
|
+
status: 'pending',
|
|
76
|
+
items: [{
|
|
77
|
+
seq: 6, kind: 'read-state', component: 'history', tokensBefore: 2401, tokensAfter: 134,
|
|
78
|
+
digest: 'dd'.repeat(32),
|
|
79
|
+
}],
|
|
80
|
+
benefit: { recoveredTokens: 400, penaltyTokens: 900, paybackTurns: 2.25 },
|
|
81
|
+
enqueuedTurn: 3,
|
|
82
|
+
lastTurnIndex: 3,
|
|
83
|
+
}
|
|
84
|
+
serviceCtx.provide('toolResultPruner', {
|
|
85
|
+
listReviewProposals: (session: unknown) => {
|
|
86
|
+
const id = (session as { id?: string }).id
|
|
87
|
+
return id === 's1' && behavior.reviewOn ? [sampleProposal] : []
|
|
88
|
+
},
|
|
89
|
+
listAllReviewProposals: () => (behavior.reviewOn
|
|
90
|
+
? [{ sessionId: 's1', proposals: [sampleProposal] }]
|
|
91
|
+
: []),
|
|
92
|
+
reviewSummary: () => ({ autoApplied: 2, reviewApplied: 1, expired: 3, voided: 0 }),
|
|
93
|
+
decideReviewProposal: (_session: unknown, proposalId: string) => {
|
|
94
|
+
if (behavior.reviewOn === false) return undefined
|
|
95
|
+
if (proposalId !== 'abc123def456') return { ok: false, reason: 'unknown-proposal' }
|
|
96
|
+
return behavior.decideOutcome ?? { ok: true }
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function invoke(route: RegisteredRoute, req?: unknown): Promise<FakeResponse> {
|
|
104
|
+
const captured: FakeResponse = {}
|
|
105
|
+
const res = {
|
|
106
|
+
writeHead(code: number) { captured.status = code },
|
|
107
|
+
end(body?: string) { if (body !== undefined) captured.body = body },
|
|
108
|
+
}
|
|
109
|
+
await route.handler(req ?? { method: 'GET' }, res)
|
|
110
|
+
await settle()
|
|
111
|
+
return captured
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe('review queue route registration', () => {
|
|
115
|
+
it('registers both route prefixes only when the row opts in', async () => {
|
|
116
|
+
const routes: RegisteredRoute[] = []
|
|
117
|
+
const runtime = new Context()
|
|
118
|
+
ctx = runtime
|
|
119
|
+
await mountWebServer(runtime, routes)
|
|
120
|
+
|
|
121
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
122
|
+
await settle()
|
|
123
|
+
|
|
124
|
+
expect(routes.map(route => route.path)).toEqual([
|
|
125
|
+
LEGACY_QUEUE_ROUTE, QUEUE_ROUTE, LEGACY_DECIDE_ROUTE, DECIDE_ROUTE,
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
const routesAfterOptOut: RegisteredRoute[] = []
|
|
129
|
+
const plain = new Context()
|
|
130
|
+
ctx = plain
|
|
131
|
+
await mountWebServer(plain, routesAfterOptOut)
|
|
132
|
+
apply(plain, { presetOverlay: false })
|
|
133
|
+
await settle()
|
|
134
|
+
expect(routesAfterOptOut).toHaveLength(0)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('answers the queue read with a sanitized payload', async () => {
|
|
138
|
+
const routes: RegisteredRoute[] = []
|
|
139
|
+
const runtime = new Context()
|
|
140
|
+
ctx = runtime
|
|
141
|
+
await mountWebServer(runtime, routes)
|
|
142
|
+
mountReviewServices(runtime, { reviewOn: true })
|
|
143
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
144
|
+
await settle()
|
|
145
|
+
|
|
146
|
+
const route = routes.find(candidate => candidate.path === QUEUE_ROUTE)
|
|
147
|
+
const response = await invoke(route as RegisteredRoute, { url: `${QUEUE_ROUTE}?sessionId=s1` })
|
|
148
|
+
|
|
149
|
+
expect(response.status).toBe(200)
|
|
150
|
+
const body = JSON.parse(String(response.body)) as {
|
|
151
|
+
ok: boolean
|
|
152
|
+
summary?: Record<string, number>
|
|
153
|
+
pending: readonly { id: string, items: readonly Record<string, unknown>[] }[]
|
|
154
|
+
}
|
|
155
|
+
expect(body.ok).toBe(true)
|
|
156
|
+
expect(body.pending).toHaveLength(1)
|
|
157
|
+
expect(body.pending[0]!.id).toBe('abc123def456')
|
|
158
|
+
expect(body.summary).toEqual({ autoApplied: 2, reviewApplied: 1, expired: 3, voided: 0 })
|
|
159
|
+
// Digests never leave the runtime: the client only needs ids and counts.
|
|
160
|
+
expect(String(response.body)).not.toContain('digest')
|
|
161
|
+
expect(String(response.body)).not.toContain('"content"')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('aggregates every live session when the read carries no sessionId', async () => {
|
|
165
|
+
const routes: RegisteredRoute[] = []
|
|
166
|
+
const runtime = new Context()
|
|
167
|
+
ctx = runtime
|
|
168
|
+
await mountWebServer(runtime, routes)
|
|
169
|
+
mountReviewServices(runtime, { reviewOn: true })
|
|
170
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
171
|
+
await settle()
|
|
172
|
+
|
|
173
|
+
const route = routes.find(candidate => candidate.path === QUEUE_ROUTE) as RegisteredRoute
|
|
174
|
+
const response = await invoke(route, { url: QUEUE_ROUTE })
|
|
175
|
+
|
|
176
|
+
expect(response.status).toBe(200)
|
|
177
|
+
const body = JSON.parse(String(response.body)) as {
|
|
178
|
+
ok: boolean
|
|
179
|
+
total: number
|
|
180
|
+
pending: readonly { sessionId: string, id: string }[]
|
|
181
|
+
}
|
|
182
|
+
expect(body).toMatchObject({ ok: true, total: 1 })
|
|
183
|
+
expect(body.pending[0]).toMatchObject({ sessionId: 's1', id: 'abc123def456' })
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('rejects a queue read for an unknown session', async () => {
|
|
187
|
+
const routes: RegisteredRoute[] = []
|
|
188
|
+
const runtime = new Context()
|
|
189
|
+
ctx = runtime
|
|
190
|
+
await mountWebServer(runtime, routes)
|
|
191
|
+
mountReviewServices(runtime, { reviewOn: true })
|
|
192
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
193
|
+
await settle()
|
|
194
|
+
const route = routes.find(candidate => candidate.path === QUEUE_ROUTE) as RegisteredRoute
|
|
195
|
+
|
|
196
|
+
const unknown = await invoke(route, { url: `${QUEUE_ROUTE}?sessionId=nope` })
|
|
197
|
+
expect(unknown.status).toBe(404)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('answers 503 when the pruner is not serving review proposals', async () => {
|
|
201
|
+
const routes: RegisteredRoute[] = []
|
|
202
|
+
const runtime = new Context()
|
|
203
|
+
ctx = runtime
|
|
204
|
+
await mountWebServer(runtime, routes)
|
|
205
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
206
|
+
await settle()
|
|
207
|
+
|
|
208
|
+
const route = routes.find(candidate => candidate.path === QUEUE_ROUTE) as RegisteredRoute
|
|
209
|
+
const response = await invoke(route, { url: `${QUEUE_ROUTE}?sessionId=s1` })
|
|
210
|
+
expect(response.status).toBe(503)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('rejects malformed decide bodies with 400', async () => {
|
|
214
|
+
const routes: RegisteredRoute[] = []
|
|
215
|
+
const runtime = new Context()
|
|
216
|
+
ctx = runtime
|
|
217
|
+
await mountWebServer(runtime, routes)
|
|
218
|
+
mountReviewServices(runtime, { reviewOn: true })
|
|
219
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
220
|
+
await settle()
|
|
221
|
+
const route = routes.find(candidate => candidate.path === DECIDE_ROUTE) as RegisteredRoute
|
|
222
|
+
|
|
223
|
+
const notJson = await invoke(route, { on: (event: string, listener: (chunk?: Buffer) => void) => {
|
|
224
|
+
if (event === 'end') listener()
|
|
225
|
+
} })
|
|
226
|
+
expect(notJson.status).toBe(400)
|
|
227
|
+
|
|
228
|
+
const missingDecision = await invoke(route, { on: (event: string, listener: (chunk?: Buffer) => void) => {
|
|
229
|
+
if (event === 'data') listener(Buffer.from(JSON.stringify({ sessionId: 's1', proposalId: 'abc123def456' })))
|
|
230
|
+
if (event === 'end') listener()
|
|
231
|
+
} })
|
|
232
|
+
expect(missingDecision.status).toBe(400)
|
|
233
|
+
|
|
234
|
+
const badDecision = await invoke(route, { on: (event: string, listener: (chunk?: Buffer) => void) => {
|
|
235
|
+
if (event === 'data') listener(Buffer.from(JSON.stringify({ sessionId: 's1', proposalId: 'abc123def456', decision: 'maybe' })))
|
|
236
|
+
if (event === 'end') listener()
|
|
237
|
+
} })
|
|
238
|
+
expect(badDecision.status).toBe(400)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('maps decide outcomes to 200, 404, and 503', async () => {
|
|
242
|
+
const routes: RegisteredRoute[] = []
|
|
243
|
+
const runtime = new Context()
|
|
244
|
+
ctx = runtime
|
|
245
|
+
await mountWebServer(runtime, routes)
|
|
246
|
+
mountReviewServices(runtime, { reviewOn: true })
|
|
247
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
248
|
+
await settle()
|
|
249
|
+
const route = routes.find(candidate => candidate.path === DECIDE_ROUTE) as RegisteredRoute
|
|
250
|
+
|
|
251
|
+
const post = (body: unknown): unknown => ({
|
|
252
|
+
on: (event: string, listener: (chunk?: Buffer) => void) => {
|
|
253
|
+
if (event === 'data') listener(Buffer.from(JSON.stringify(body)))
|
|
254
|
+
if (event === 'end') listener()
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const ok = await invoke(route, post({ sessionId: 's1', proposalId: 'abc123def456', decision: 'approved' }))
|
|
259
|
+
expect(ok.status).toBe(200)
|
|
260
|
+
expect(JSON.parse(String(ok.body))).toMatchObject({ ok: true, decision: 'approved' })
|
|
261
|
+
|
|
262
|
+
const unknownProposal = await invoke(route, post({ sessionId: 's1', proposalId: 'nope', decision: 'approved' }))
|
|
263
|
+
expect(unknownProposal.status).toBe(404)
|
|
264
|
+
|
|
265
|
+
const unknownSession = await invoke(route, post({ sessionId: 'nope', proposalId: 'abc123def456', decision: 'approved' }))
|
|
266
|
+
expect(unknownSession.status).toBe(404)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('answers 503 when review mode is off for the session', async () => {
|
|
270
|
+
const routes: RegisteredRoute[] = []
|
|
271
|
+
const runtime = new Context()
|
|
272
|
+
ctx = runtime
|
|
273
|
+
await mountWebServer(runtime, routes)
|
|
274
|
+
mountReviewServices(runtime, { reviewOn: false })
|
|
275
|
+
apply(runtime, { reviewQueueRoute: true })
|
|
276
|
+
await settle()
|
|
277
|
+
|
|
278
|
+
const queueRoute = routes.find(candidate => candidate.path === QUEUE_ROUTE) as RegisteredRoute
|
|
279
|
+
expect((await invoke(queueRoute, { url: `${QUEUE_ROUTE}?sessionId=s1` })).status).toBe(200)
|
|
280
|
+
|
|
281
|
+
const decideRoute = routes.find(candidate => candidate.path === DECIDE_ROUTE) as RegisteredRoute
|
|
282
|
+
const response = await invoke(decideRoute, {
|
|
283
|
+
on: (event: string, listener: (chunk?: Buffer) => void) => {
|
|
284
|
+
if (event === 'data') listener(Buffer.from(JSON.stringify({ sessionId: 's1', proposalId: 'abc123def456', decision: 'approved' })))
|
|
285
|
+
if (event === 'end') listener()
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
expect(response.status).toBe(503)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -126,4 +126,48 @@ describe('context-compression audit records', () => {
|
|
|
126
126
|
}, record)
|
|
127
127
|
}).not.toThrow()
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('keeps review-outcome records free of proposal content', () => {
|
|
131
|
+
const record: CompressionAuditRecord = {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
kind: 'review-outcome',
|
|
134
|
+
sessionId: 'review-session',
|
|
135
|
+
proposalId: 'a1b2c3d4e5f6',
|
|
136
|
+
proposalKind: 'read-state',
|
|
137
|
+
event: 'apply-receipt',
|
|
138
|
+
receiptStatus: 'applied',
|
|
139
|
+
itemSeqs: [7, 9],
|
|
140
|
+
tokensBefore: 1400,
|
|
141
|
+
tokensAfter: 1000,
|
|
142
|
+
turnIndex: 12,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const line = formatCompressionAudit(record)
|
|
146
|
+
const parsed = JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord
|
|
147
|
+
expect(parsed).toEqual(record)
|
|
148
|
+
// Only ids, enums, and numbers: no digests, no content fields.
|
|
149
|
+
expect(line).not.toContain('digest')
|
|
150
|
+
expect(line).not.toContain('"content"')
|
|
151
|
+
expect(line).not.toContain('"text"')
|
|
152
|
+
expect(line).not.toContain('apiKey')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('carries reason codes on void and deferred review events', () => {
|
|
156
|
+
const record: CompressionAuditRecord = {
|
|
157
|
+
schemaVersion: 1,
|
|
158
|
+
kind: 'review-outcome',
|
|
159
|
+
sessionId: 'review-session',
|
|
160
|
+
proposalId: 'b2c3d4e5f6a1',
|
|
161
|
+
proposalKind: 'dedup',
|
|
162
|
+
event: 'apply-void',
|
|
163
|
+
reasonCode: 'review_receipt_digest_invalid',
|
|
164
|
+
itemSeqs: [3],
|
|
165
|
+
tokensBefore: 5000,
|
|
166
|
+
tokensAfter: 4000,
|
|
167
|
+
}
|
|
168
|
+
const parsed = JSON.parse(
|
|
169
|
+
formatCompressionAudit(record).slice(COMPRESSION_AUDIT_PREFIX.length),
|
|
170
|
+
) as CompressionAuditRecord
|
|
171
|
+
expect(parsed).toEqual(record)
|
|
172
|
+
})
|
|
129
173
|
})
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildEstimatorUserPrompt,
|
|
10
10
|
isCoolingDown,
|
|
11
11
|
parseEstimatorAnswer,
|
|
12
|
+
parseEstimatorAnswerDetailed,
|
|
12
13
|
} from '../../../src/runtime/tokenpilot/estimator.ts'
|
|
13
14
|
|
|
14
15
|
describe('tokenpilot estimator helpers', () => {
|
|
@@ -30,6 +31,28 @@ describe('tokenpilot estimator helpers', () => {
|
|
|
30
31
|
expect(parseEstimatorAnswer('not json [')).toEqual([])
|
|
31
32
|
})
|
|
32
33
|
|
|
34
|
+
it('reads the optional expectedRemainingTurns from the extended object form', () => {
|
|
35
|
+
const answer = parseEstimatorAnswerDetailed(
|
|
36
|
+
'Result: {"expectedRemainingTurns":7.9,"items":[{"seq":3,"expired":false}]} end',
|
|
37
|
+
)
|
|
38
|
+
expect(answer.verdicts).toEqual([{ seq: 3, expired: false }])
|
|
39
|
+
// Fractional estimates floor to whole turns.
|
|
40
|
+
expect(answer.expectedRemainingTurns).toBe(7)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('stays Ŝ-less for legacy arrays and malformed turn fields', () => {
|
|
44
|
+
const legacy = parseEstimatorAnswerDetailed('Verdicts: [{"seq":7,"expired":true}] done')
|
|
45
|
+
expect(legacy.verdicts).toEqual([{ seq: 7, expired: true }])
|
|
46
|
+
expect(legacy.expectedRemainingTurns).toBeUndefined()
|
|
47
|
+
const malformed = parseEstimatorAnswerDetailed('{"expectedRemainingTurns":"soon","items":[{"seq":1,"expired":true}]}')
|
|
48
|
+
expect(malformed.verdicts).toEqual([{ seq: 1, expired: true }])
|
|
49
|
+
expect(malformed.expectedRemainingTurns).toBeUndefined()
|
|
50
|
+
// Negative estimates are rejected like any other non-numeric field.
|
|
51
|
+
const negative = parseEstimatorAnswerDetailed('{"expectedRemainingTurns":-2,"items":[{"seq":1,"expired":true}]}')
|
|
52
|
+
expect(negative.expectedRemainingTurns).toBeUndefined()
|
|
53
|
+
expect(negative.verdicts).toEqual([{ seq: 1, expired: true }])
|
|
54
|
+
})
|
|
55
|
+
|
|
33
56
|
it('builds a path-only user prompt without file content', () => {
|
|
34
57
|
const prompt = buildEstimatorUserPrompt([
|
|
35
58
|
{ seq: 4, path: 'src/a.ts', turn: 2 },
|
|
@@ -61,6 +61,11 @@ 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,
|
|
64
69
|
})
|
|
65
70
|
// Other profiles never carry the capability matrix.
|
|
66
71
|
expect(resolvePolicy(config, 'balanced').presetOptions).toBeUndefined()
|