fix(schedule): validate snapshot marker arrays
This commit is contained in:
@@ -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 },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
Reference in New Issue
Block a user