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.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user