Files
obelisk/tests/session-reader-state.test.mjs
T
tommy0103 d9f31bbe8a fix(app): preserve session reader state across navigation
Cache semantic timeline anchors and disclosures per session instead of relying on KeepAlive. Restore state after virtualized layout stabilization while preserving explicit focus and tail-follow behavior.
2026-07-16 23:22:47 +08:00

89 lines
2.9 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createSessionReaderStateCache } from '../app/src/renderer/src/session-reader-state.mjs';
import { resolveReaderAnchorIndex } from '../app/src/renderer/src/session-timeline-viewport.mjs';
test('reader state cache stores only semantic state in independent session snapshots', () => {
const cache = createSessionReaderStateCache({ maxEntries: 4 });
cache.set('session-a', {
mode: 'anchor',
anchor: {
itemKey: 'message:a-120',
messageUuid: 'a-120',
offset: 18,
fallbackIndex: 120,
},
disclosures: [{ key: 'tool:a-call', messageUuid: 'a-120', open: true, raw: false }],
expandedMessageIds: ['a-120'],
scrollTop: 98_000,
currentMsgIdx: 120,
});
const restored = cache.get('session-a');
assert.deepEqual(restored, {
mode: 'anchor',
anchor: {
itemKey: 'message:a-120',
messageUuid: 'a-120',
offset: 18,
fallbackIndex: 120,
},
disclosures: [{ key: 'tool:a-call', messageUuid: 'a-120', open: true, raw: false }],
expandedMessageIds: ['a-120'],
});
assert.equal(cache.get('session-b'), null);
restored.anchor.offset = 999;
restored.disclosures[0].open = false;
restored.expandedMessageIds.push('mutated');
assert.equal(cache.get('session-a').anchor.offset, 18);
assert.equal(cache.get('session-a').disclosures[0].open, true);
assert.deepEqual(cache.get('session-a').expandedMessageIds, ['a-120']);
});
test('reader state cache evicts the least recently used session', () => {
const cache = createSessionReaderStateCache({ maxEntries: 2 });
const state = messageUuid => ({
mode: 'anchor',
anchor: { itemKey: `message:${messageUuid}`, messageUuid, offset: 0, fallbackIndex: 0 },
});
cache.set('session-a', state('a'));
cache.set('session-b', state('b'));
cache.get('session-a');
cache.set('session-c', state('c'));
assert.equal(cache.get('session-b'), null);
assert.equal(cache.get('session-a').anchor.messageUuid, 'a');
assert.equal(cache.get('session-c').anchor.messageUuid, 'c');
});
test('reader anchors resolve by stable identity before falling back to position', () => {
const items = [
{ key: 'message:first', messageUuid: 'first' },
{ key: 'workflow:shared', messageUuid: 'shared' },
{ key: 'message:shared', messageUuid: 'shared' },
{ key: 'message:last', messageUuid: 'last' },
];
assert.equal(resolveReaderAnchorIndex({
itemKey: 'message:shared',
messageUuid: 'shared',
fallbackIndex: 0,
}, items), 2);
assert.equal(resolveReaderAnchorIndex({
itemKey: 'missing',
messageUuid: 'shared',
fallbackIndex: 0,
}, items), 1);
assert.equal(resolveReaderAnchorIndex({
itemKey: 'missing',
messageUuid: 'missing',
fallbackIndex: 99,
}, items), 3);
assert.equal(resolveReaderAnchorIndex(null, items), 0);
assert.equal(resolveReaderAnchorIndex(null, []), null);
});