fix(session-query): reject misplaced surface ops

This commit is contained in:
Hypatia May
2026-07-13 16:05:11 +08:00
parent 3d3789bf2d
commit 84e6f72ef5
12 changed files with 182 additions and 114 deletions

View File

@@ -313,10 +313,13 @@ describe('Session', () => {
expect(event.surfaceOp).toEqual({ op: 'replace', start: 0, end: 0 })
})
it('adds seed context when surface validation throws a non-Error value', () => {
it.each([
['an Error', new Error('validator failed'), 'validator failed'],
['a non-Error value', 'validator failed', 'invalid surface metadata'],
] as const)('adds seed context when surface validation throws %s', (_name, failure, expected) => {
const originalHasOwn = Object.hasOwn
const hasOwn = vi.spyOn(Object, 'hasOwn').mockImplementation((object: object, property: PropertyKey): boolean => {
if ((object as Record<string, unknown>)['op'] === 'replace') throw 'validator failed'
if ((object as Record<string, unknown>)['op'] === 'replace') throw failure
return originalHasOwn(object, property)
})
const seed = [{
@@ -329,7 +332,7 @@ describe('Session', () => {
try {
expect(() => new Session(SessionId('seed-non-error-metadata-failure'), seed))
.toThrow('invalid seed event at index 0: invalid surface metadata')
.toThrow(`invalid seed event at index 0: ${expected}`)
} finally {
hasOwn.mockRestore()
}
@@ -468,7 +471,7 @@ describe('Session', () => {
'turn/start',
{ turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
{ surfaceOp: 'append' },
)).toThrow(/not surface-eligible and cannot carry surface metadata/)
)).toThrow(/not surface-eligible and cannot carry surfaceOp/)
expect(() => new Session(SessionId('non-surface-metadata-seed'), [{
type: 'turn/start',
seq: 0,

View File

@@ -6,7 +6,7 @@ import {
foldSurface,
isSurfaceEligibleType,
isSurfaceEvent,
validateSurfaceProvenance,
validateSurfaceMetadata,
} from '@deepseek-ai/dsh-session'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -31,11 +31,11 @@ function provenanceEvent(seq: number, sourceEventSeqs: unknown): SessionEvent {
} as unknown as SessionEvent
}
describe('validateSurfaceProvenance', () => {
describe('validateSurfaceMetadata', () => {
it('accepts absent or valid provenance and complete replacement coverage', () => {
expect(validateSurfaceProvenance(provenanceEvent(0, undefined), new Set()))
expect(validateSurfaceMetadata(provenanceEvent(0, undefined), new Set()))
.toBeUndefined()
expect(validateSurfaceProvenance(provenanceEvent(2, [0, 1]), new Set([0, 1]), [1]))
expect(validateSurfaceMetadata(provenanceEvent(2, [0, 1]), new Set([0, 1]), [1]))
.toBeUndefined()
})
@@ -47,28 +47,33 @@ describe('validateSurfaceProvenance', () => {
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
sourceEventSeqs: [0],
} as unknown as SessionEvent
expect(validateSurfaceProvenance(event, new Set([0])))
.toMatch(/cannot carry sourceEventSeqs/)
expect(validateSurfaceMetadata(event, new Set([0])))
.toEqual({
kind: 'provenance',
message: 'turn/start cannot carry sourceEventSeqs (non-surface event)',
})
})
it.each([
['a non-array', 1, 'invalid', new Set([0]), [], /must be an array/],
['an empty array', 1, [], new Set([0]), [], /must not be empty/],
['duplicates', 1, [0, 0], new Set([0]), [], /must not contain duplicates/],
['a non-number', 1, ['0'], new Set([0]), [], /invalid seq 0/],
['a fractional number', 1, [0.5], new Set([0]), [], /invalid seq 0\.5/],
['a negative number', 1, [-1], new Set([0]), [], /invalid seq -1/],
['a non-number', 1, ['0'], new Set([0]), [], /non-negative safe integers/],
['a fractional number', 1, [0.5], new Set([0]), [], /non-negative safe integers/],
['a negative number', 1, [-1], new Set([0]), [], /non-negative safe integers/],
['a self reference', 1, [1], new Set([0]), [], /must reference earlier events/],
['an unknown earlier seq', 2, [1], new Set([0]), [], /references unknown seq 1/],
['incomplete replacement coverage', 2, [0], new Set([0, 1]), [0, 1], /missing 1/],
] as const)(
'returns the first violation for %s',
(_name, seq, sources, knownSeqs, shadowedSeqs, expected) => {
expect(validateSurfaceProvenance(
const violation = validateSurfaceMetadata(
provenanceEvent(seq, sources),
knownSeqs,
shadowedSeqs,
)).toMatch(expected)
)
expect(violation?.kind).toBe('provenance')
expect(violation?.message).toMatch(expected)
},
)
})
@@ -124,7 +129,20 @@ describe('SurfaceManager', () => {
}
expect(() => foldSurface([malformed]))
.toThrow(/surface event "user\/message" \(seq 0\) carries no surfaceOp marker/)
.toThrow(/surface-eligible and requires a surfaceOp marker/)
})
it('foldSurface rejects surfaceOp on a non-surface event', () => {
const malformed = {
type: 'turn/start',
seq: 0,
time: 1,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
surfaceOp: 'append',
} as unknown as SessionEvent
expect(() => foldSurface([malformed]))
.toThrow(/not surface-eligible and cannot carry surfaceOp/)
})
it('rebuilds a linked list from surfaceOp: append markers', () => {