fix(schedule): validate snapshot marker arrays

This commit is contained in:
pku-xht
2026-08-07 19:47:12 +08:00
committed by Tianyi Cui
parent b8d4e004ed
commit 9dc4ad91fa
4 changed files with 64 additions and 24 deletions

View File

@@ -214,21 +214,19 @@ export function apply(ctx: Context, config: Config): () => void {
const formatter = sessionTimeZone === undefined const formatter = sessionTimeZone === undefined
? fallbackFormatter ? fallbackFormatter
: formatterFor(sessionTimeZone) : formatterFor(sessionTimeZone)
const text = renderText(
now,
turn,
step,
previous,
formatter,
displayTimeZone,
sessionTimeZone,
requestMessages(agent, turn, messages),
)
return createUserMessage({ return createUserMessage({
content: [{ content: [{ type: 'text', text }],
type: 'text', source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
text: renderText(
now,
turn,
step,
previous,
formatter,
displayTimeZone,
sessionTimeZone,
requestMessages(agent, turn, messages),
),
}],
source: { kind: 'plugin', plugin: name },
}) })
} }

View File

@@ -82,8 +82,24 @@ function validateReading(
if (turn !== expected.turn || step !== expected.step) { if (turn !== expected.turn || step !== expected.step) {
fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`) fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`)
} }
if (Object.keys(event.data.source).length !== 2) { const source = event.data.source
fail('time-context source must not duplicate request authority') /* v8 ignore next 2 -- replay and dispatch callers select this exact package-owned source before validation. */
if (source.kind !== 'plugin' || source.plugin !== SOURCE_NAME) {
fail('time-context source must retain package ownership')
}
const sections: unknown = 'sections' in source ? source.sections : undefined
const section: unknown = Array.isArray(sections) ? sections[0] : undefined
if (Object.keys(source).length !== 4
|| source.form !== 'snapshot'
|| !Array.isArray(sections)
|| sections.length !== 1
|| typeof section !== 'object'
|| section === null
|| !('name' in section)
|| section.name !== SOURCE_NAME
|| !('text' in section)
|| section.text !== block.text) {
fail('time-context source must carry only the exact snapshot text, not request authority')
} }
const renderedAuthority = `Session time zone: ${match[4]}.\nClient time zone for this request: ${match[5]}.` const renderedAuthority = `Session time zone: ${match[4]}.\nClient time zone for this request: ${match[5]}.`
const expectedAuthority = renderTimeZoneContext( const expectedAuthority = renderTimeZoneContext(

View File

@@ -32,6 +32,8 @@ function event(
? { ? {
kind: 'plugin', kind: 'plugin',
plugin, plugin,
form: 'snapshot',
sections: [{ name: plugin, text }],
} }
: { kind: 'plugin', plugin }, : { kind: 'plugin', plugin },
}), }),
@@ -77,6 +79,8 @@ function appendReading(session: Session, text: string): void {
source: { source: {
kind: 'plugin', kind: 'plugin',
plugin: 'time-context', plugin: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context', text }],
}, },
}), { surfaceOp: 'append' }) }), { surfaceOp: 'append' })
} }
@@ -148,7 +152,22 @@ describe('time-context invariants', () => {
} }
expect(() => { expect(() => {
ctx.emit('session/event', preparing(1, 1), duplicate) ctx.emit('session/event', preparing(1, 1), duplicate)
}).toThrow(/must not duplicate request authority/) }).toThrow(/must carry only the exact snapshot text/)
})
it('rejects package-owned provenance without snapshot sections', async () => {
const ctx = await setup()
const base = event(reading())
const unformed: SessionEvent<'user/message'> = {
...base,
data: {
...base.data,
source: { kind: 'plugin', plugin: 'time-context' },
},
}
expect(() => {
ctx.emit('session/event', preparing(1, 1), unformed)
}).toThrow(/must carry only the exact snapshot text/)
}) })
it('validates each existing reading against its preceding durable prefix', async () => { it('validates each existing reading against its preceding durable prefix', async () => {

View File

@@ -222,16 +222,23 @@ interface AtTimeZoneContext {
function isTimeContextReading(event: SessionEvent): boolean { function isTimeContextReading(event: SessionEvent): boolean {
if (event.type !== 'user/message') return false if (event.type !== 'user/message') return false
const source = event.data.source const source = event.data.source
if (source.kind !== 'plugin'
|| source.plugin !== 'time-context'
|| Object.keys(source).length !== 4
|| source.form !== 'snapshot') return false
const [block] = event.data.content const [block] = event.data.content
const sections: unknown = source.sections
const section: unknown = Array.isArray(sections) ? sections[0] : undefined
return event.data.content.length === 1 return event.data.content.length === 1
&& block?.type === 'text' && block?.type === 'text'
&& source.kind === 'plugin' && Array.isArray(sections)
&& source.plugin === 'time-context' && sections.length === 1
&& Object.keys(source).length === 4 && typeof section === 'object'
&& source.form === 'snapshot' && section !== null
&& source.sections.length === 1 && 'name' in section
&& source.sections[0]?.name === 'time-context' && section.name === 'time-context'
&& source.sections[0].text === block.text && 'text' in section
&& section.text === block.text
} }
/** Derive request zones only while the current open turn contains a time-context reading. */ /** Derive request zones only while the current open turn contains a time-context reading. */