test(session-projection): cover view transition matrix

This commit is contained in:
imccyu committed 2026-08-31 20:30:43 +08:00
1 parent 6f0daff1dd
commit 9ba9a35c72
2 files changed
+172

No files matched your search

@@ -656,6 +656,8 @@ export class SessionProjectionRegistry extends Service {
views[1] = undefined
}
}
// An unchanged state keeps its current view as the valid comparison
// value for the next state change.
}
}
@@ -102,6 +102,47 @@ async function harness(): Promise<{ ctx: Context; session: Session }> {
const mark = (session: Session, marks: string[]): SessionEvent =>
session.append('test/mark', { marks })
const STATE_SEQUENCES = [
[0, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0],
[0, 0, 1, 1],
[0, 0, 1, 2],
[0, 1, 0, 0],
[0, 1, 0, 1],
[0, 1, 0, 2],
[0, 1, 1, 0],
[0, 1, 1, 1],
[0, 1, 1, 2],
[0, 1, 2, 0],
[0, 1, 2, 1],
[0, 1, 2, 2],
[0, 1, 2, 3],
] as const
function identitySequences(length: number): number[][] {
const sequences: number[][] = []
const visit = (sequence: number[], highest: number): void => {
if (sequence.length === length) {
sequences.push(sequence)
return
}
for (let value = 0; value <= highest + 1; value++) {
visit([...sequence, value], Math.max(highest, value))
}
}
visit([0], 0)
return sequences
}
function sameIdentities(left: readonly unknown[], right: readonly unknown[]): boolean {
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]))
}
function sequenceName(sequence: readonly number[], prefix: string): string {
return sequence.map(value => `${prefix}${String(value + 1)}`).join(',')
}
describe('SessionProjectionRegistry drive', () => {
it('drives a registered unit over committed events and snapshots the current value', async () => {
const { ctx, session } = await harness()
@@ -202,6 +243,135 @@ describe('SessionProjectionRegistry drive', () => {
expect(view).toHaveBeenCalledTimes(2)
})
it('matches every four-state identity sequence across listener gaps and raw-view identities', async () => {
const { ctx } = await harness()
const initialState: MarksState = { marks: ['initial'] }
const stateByEvent = new Map<string, MarksState>()
const viewByState = new Map<MarksState, MarksView>()
const computedViews: MarksView[] = []
ctx.sessionProjections.register({
key: 'test/marks',
stateSchema: marksViewSchema.nullable(),
init: () => initialState,
apply: (state, event) => {
if (event.type !== 'test/mark') return state
const token = event.data.marks[0]
if (token === undefined || !stateByEvent.has(token)) return state
return stateByEvent.get(token) as MarksState
},
wire: {
viewSchema: marksViewSchema,
view: (state) => {
const value = viewByState.get(state)
if (value === undefined) throw new Error('test state lacks a raw view')
computedViews.push(value)
return value
},
},
stateVersion: 1,
})
const failures = new Map<string, unknown>()
let mismatchCount = 0
let checked = 0
for (const stateSequence of STATE_SEQUENCES) {
const stateCount = Math.max(...stateSequence) + 1
for (const viewSequence of identitySequences(stateCount)) {
for (const baselineKnown of [false, true]) {
for (let listenerMask = 0; listenerMask < 8; listenerMask++) {
const scenario = String(checked++)
const states = Array.from(
{ length: stateCount },
(_, index): MarksState => ({ marks: [`state-${scenario}-${String(index)}`] }),
)
const views = Array.from(
{ length: Math.max(...viewSequence) + 1 },
(): MarksView => ({ marks: [] }),
)
for (let index = 0; index < stateCount; index++) {
viewByState.set(states[index] as MarksState, views[viewSequence[index] as number] as MarksView)
}
for (let index = 0; index < stateSequence.length; index++) {
stateByEvent.set(`${scenario}:${String(index)}`, states[stateSequence[index] as number] as MarksState)
}
const session = ctx.sessions.create()
const notifications: number[] = []
let stop: (() => void) | undefined
const setListening = (listening: boolean): void => {
if (listening && stop === undefined) {
stop = ctx.sessionProjections.onChanged((changedSession, key, _value, seq) => {
if (changedSession === session && key === 'test/marks') notifications.push(seq)
})
} else if (!listening && stop !== undefined) {
stop()
stop = undefined
}
}
setListening(baselineKnown)
mark(session, [`${scenario}:0`])
computedViews.length = 0
notifications.length = 0
const expectedViews: MarksView[] = []
const expectedNotifications: number[] = []
let comparable = baselineKnown
? views[viewSequence[stateSequence[0] as number] as number] as MarksView
: undefined
for (let index = 1; index < stateSequence.length; index++) {
const listening = (listenerMask & (1 << (index - 1))) !== 0
setListening(listening)
const changed = stateSequence[index] !== stateSequence[index - 1]
if (changed) {
if (listening) {
const current = views[viewSequence[stateSequence[index] as number] as number] as MarksView
expectedViews.push(current)
if (comparable === undefined || !Object.is(comparable, current)) {
expectedNotifications.push(index)
}
comparable = current
} else {
comparable = undefined
}
}
mark(session, [`${scenario}:${String(index)}`])
}
setListening(false)
if (!sameIdentities(computedViews, expectedViews)
|| notifications.length !== expectedNotifications.length
|| notifications.some((seq, index) => seq !== expectedNotifications[index])) {
mismatchCount += 1
const stateName = sequenceName(stateSequence, 'v')
if (!failures.has(stateName) || (baselineKnown && listenerMask === 7)) {
failures.set(stateName, {
state: stateName,
view: stateSequence.map(value => `r${String((viewSequence[value] as number) + 1)}`).join(','),
baseline: baselineKnown ? 'known' : 'unknown',
listeners: [0, 1, 2]
.map(index => (listenerMask & (1 << index)) === 0 ? 'off' : 'on')
.join(','),
expectedViewCalls: expectedViews.length,
actualViewCalls: computedViews.length,
expectedNotifications,
actualNotifications: [...notifications],
})
}
}
computedViews.length = 0
}
}
}
}
expect({ checked, mismatchCount, failures: [...failures.values()] }).toEqual({
checked: 960,
mismatchCount: 0,
failures: [],
})
})
it('drives independently per session (cells are per-session watermarks)', async () => {
const { ctx, session } = await harness()
const other = ctx.sessions.create()