fix(fixture): mirror context breakdown projection
This commit is contained in:
@@ -121,6 +121,21 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
|
||||
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
|
||||
// Resolve the resident approval so the ordinary composer bar (which owns
|
||||
// ContextMeter) resumes without replacing the session shell. This minimal
|
||||
// boot graph intentionally does not mount the separate question UI plugin.
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Allow once' }))
|
||||
|
||||
// The fixture mirrors all three token-meter projections, so the assembled
|
||||
// ContextMeter reaches its composition panel instead of only the occupancy
|
||||
// fallback path.
|
||||
const contextTrigger = await screen.findByRole('button', { name: /of context used/ })
|
||||
fireEvent.click(contextTrigger)
|
||||
const contextPanel = await screen.findByRole('dialog', { name: 'of context used' })
|
||||
within(contextPanel).getByText('System prompt')
|
||||
within(contextPanel).getByText('Tools')
|
||||
within(contextPanel).getByText('Messages')
|
||||
|
||||
// The write/edit turns render a real diff card through the assembled graph
|
||||
// (the keyed FileMutationRow composing ToolRow + DiffBlock), not just the
|
||||
// fixture's raw text. The card is collapsed by default, so expand each edit/
|
||||
|
||||
@@ -27,7 +27,7 @@ import type {
|
||||
// Type-only: the brand constructor is host-side; the fixture casts at its
|
||||
// wire-fabrication boundary (the schema layer's one-cast-point posture).
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import { foldSurface } from '@deepseek-ai/dsh-session/surface'
|
||||
import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface'
|
||||
import type {
|
||||
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
|
||||
ModelProviderGroup, ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
|
||||
@@ -358,6 +358,12 @@ function buildAlphaLog(): SessionEvent[] {
|
||||
events.push({ seq, time: (time += 800), ...authored })
|
||||
return seq
|
||||
}
|
||||
// This resident history represents completed model requests, so retain the
|
||||
// route capacity that accompanied them just as the live prompt path does.
|
||||
push({
|
||||
type: 'request/context',
|
||||
data: { provider: 'deepseek-official', model: 'deepseek-v4-flash', contextWindow: 128_000 },
|
||||
})
|
||||
for (let turn = 0; turn < 60; turn++) {
|
||||
push({ type: 'turn/start', data: { turn } })
|
||||
const userSeq = push({
|
||||
@@ -819,6 +825,65 @@ interface FixtureRequestContext {
|
||||
contextWindow?: number
|
||||
}
|
||||
|
||||
interface FixtureContextBreakdownProjection {
|
||||
systemTokens: number
|
||||
toolsTokens: number
|
||||
messageTokens: number
|
||||
}
|
||||
|
||||
/** Fixed token-meter heuristic constants mirrored by this client-only fixture. */
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const BLOCK_OVERHEAD = 4
|
||||
const ROLE_OVERHEAD = 4
|
||||
|
||||
/** Price fixture content with token-meter's fixed-density heuristic. */
|
||||
function estimateFixtureContent(blocks: readonly ContentBlock[]): number {
|
||||
let tokens = 0
|
||||
for (const block of blocks) {
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
case 'reasoning':
|
||||
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD
|
||||
break
|
||||
case 'tool-call':
|
||||
tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN)
|
||||
+ Math.ceil(block.arguments.length / CHARS_PER_TOKEN)
|
||||
+ BLOCK_OVERHEAD
|
||||
break
|
||||
case 'tool-result':
|
||||
tokens += estimateFixtureContent(block.content) + BLOCK_OVERHEAD
|
||||
break
|
||||
default:
|
||||
tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
/** Fixture parallel of token-meter's heuristic context-composition projection. */
|
||||
function contextBreakdownOf(log: readonly SessionEvent[]): FixtureContextBreakdownProjection {
|
||||
const headerEvent = log.findLast(event => event.type === 'request/header')
|
||||
const header = headerEvent === undefined
|
||||
? undefined
|
||||
: headerEvent.data.header
|
||||
let messageTokens = 0
|
||||
for (const seq of foldSurface(log).nodes) {
|
||||
const event = log[seq]
|
||||
if (event === undefined) continue
|
||||
const message = deriveEventMessage(event)
|
||||
if (message !== null) messageTokens += estimateFixtureContent(message.content) + ROLE_OVERHEAD
|
||||
}
|
||||
return {
|
||||
systemTokens: header?.system === undefined
|
||||
? 0
|
||||
: Math.ceil(header.system.length / CHARS_PER_TOKEN) + ROLE_OVERHEAD,
|
||||
toolsTokens: header?.tools === undefined || header.tools.length === 0
|
||||
? 0
|
||||
: Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD,
|
||||
messageTokens,
|
||||
}
|
||||
}
|
||||
|
||||
/** Latest log-only route context, or undefined before any request ran. */
|
||||
function lastRequestContext(
|
||||
log: readonly SessionEvent[],
|
||||
@@ -874,28 +939,44 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
|
||||
values['tokenUsage'] = tokenUsageOf(log)
|
||||
// Always present (token-meter composed): last request pressure and capacity.
|
||||
values['contextPressure'] = contextPressureOf(log)
|
||||
// Always present (token-meter composed): heuristic request composition.
|
||||
values['contextBreakdown'] = contextBreakdownOf(log)
|
||||
return values
|
||||
}
|
||||
|
||||
/** Host push-frame parallel: emit one session/projection frame per key the given event advanced. */
|
||||
function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event: SessionEvent): Extract<MuxFrame, { type: 'session/projection' }>[] {
|
||||
const type = (event as { type: string }).type
|
||||
const frames: Extract<MuxFrame, { type: 'session/projection' }>[] = []
|
||||
// One usage sample advances both token-meter units.
|
||||
if (usageSampleOf(event) !== undefined) {
|
||||
return [
|
||||
frames.push(
|
||||
{ type: 'session/projection', sessionId: id, key: 'tokenUsage', value: tokenUsageOf(log), seq: event.seq },
|
||||
{ type: 'session/projection', sessionId: id, key: 'contextPressure', value: contextPressureOf(log), seq: event.seq },
|
||||
]
|
||||
)
|
||||
}
|
||||
if (type === 'request/context') {
|
||||
return [{
|
||||
frames.push({
|
||||
type: 'session/projection',
|
||||
sessionId: id,
|
||||
key: 'contextPressure',
|
||||
value: contextPressureOf(log),
|
||||
seq: event.seq,
|
||||
}]
|
||||
})
|
||||
}
|
||||
if (type === 'request/header'
|
||||
|| type === 'user/message'
|
||||
|| type === 'assistant/message'
|
||||
|| type === 'tool/result') {
|
||||
frames.push({
|
||||
type: 'session/projection',
|
||||
sessionId: id,
|
||||
key: 'contextBreakdown',
|
||||
value: contextBreakdownOf(log),
|
||||
seq: event.seq,
|
||||
})
|
||||
}
|
||||
if (frames.length > 0) return frames
|
||||
if (type === 'session/title') {
|
||||
const values = projectionValuesOf(log)
|
||||
/* v8 ignore next -- the advancing title event is in the log, so the key is present. */
|
||||
|
||||
@@ -159,6 +159,11 @@ describe('createFixtureApi', () => {
|
||||
},
|
||||
// No request ran, so neither pressure nor capacity is known yet.
|
||||
contextPressure: {},
|
||||
contextBreakdown: {
|
||||
systemTokens: 0,
|
||||
toolsTokens: 0,
|
||||
messageTokens: 0,
|
||||
},
|
||||
} },
|
||||
})
|
||||
})
|
||||
@@ -304,6 +309,10 @@ describe('createFixtureApi', () => {
|
||||
frame.type === 'session/projection'
|
||||
&& frame.key === 'contextPressure'
|
||||
&& (frame.value as { contextWindow?: number }).contextWindow === 128_000)).toBe(true)
|
||||
expect(frames.some(frame =>
|
||||
frame.type === 'session/projection'
|
||||
&& frame.key === 'contextBreakdown'
|
||||
&& (frame.value as { messageTokens?: number }).messageTokens! > 0)).toBe(true)
|
||||
const finalize = frames.find((f): f is Extract<MuxFrame, { type: 'session/event' }> => f.type === 'session/event' && f.event.type === 'assistant/message')
|
||||
expect(JSON.stringify(finalize?.event.data)).toContain('(已中断)')
|
||||
// Idle cancel: no replay in flight, must not explode; running flips false.
|
||||
@@ -335,7 +344,7 @@ describe('createFixtureApi', () => {
|
||||
const envelopes: RpcRequest<MuxFrame>[] = []
|
||||
for await (const envelope of api.events.mux(req({}), abort.signal)) {
|
||||
envelopes.push(envelope)
|
||||
if (envelopes.length >= 10) abort.abort()
|
||||
if (envelopes.length >= 11) abort.abort()
|
||||
}
|
||||
return envelopes
|
||||
}
|
||||
@@ -351,10 +360,15 @@ describe('createFixtureApi', () => {
|
||||
expect(first[5]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'goal', value: null })
|
||||
expect(first[6]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'tokenUsage' })
|
||||
expect(first[7]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'contextPressure' })
|
||||
expect(first[8]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
||||
expect(second[8]?.rpcId).toBe(first[8]?.rpcId) // stable rpcId across replays (host replay semantics)
|
||||
expect(first[9]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
||||
expect(second[9]?.rpcId).toBe(first[9]?.rpcId)
|
||||
expect(first[8]?.payload).toMatchObject({
|
||||
type: 'session/projection', sessionId: 'fx-alpha', key: 'contextBreakdown',
|
||||
value: { systemTokens: 0, toolsTokens: 0 },
|
||||
})
|
||||
expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0)
|
||||
expect(first[9]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
|
||||
expect(second[9]?.rpcId).toBe(first[9]?.rpcId) // stable rpcId across replays (host replay semantics)
|
||||
expect(first[10]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
|
||||
expect(second[10]?.rpcId).toBe(first[10]?.rpcId)
|
||||
})
|
||||
|
||||
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {
|
||||
|
||||
Reference in New Issue
Block a user