Drop window-cache payloads that lack a schema field. Prove a non-owner listing another user's sessions is forbidden. Correct the stale test schema note in CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
saveSessionWindowCache,
|
|
loadSessionWindowCache,
|
|
clearSessionWindowCache,
|
|
} from './storage';
|
|
import type { ChatDayBucket } from '../types/workbench';
|
|
|
|
function installMemoryLocalStorage(): { store: Map<string, string>; restore: () => void } {
|
|
const store = new Map<string, string>();
|
|
const originalWindow = (globalThis as { window?: unknown }).window;
|
|
const localStorage = {
|
|
getItem(key: string) {
|
|
return store.has(key) ? store.get(key)! : null;
|
|
},
|
|
setItem(key: string, value: string) {
|
|
store.set(key, value);
|
|
},
|
|
removeItem(key: string) {
|
|
store.delete(key);
|
|
},
|
|
};
|
|
(globalThis as { window: unknown }).window = { localStorage };
|
|
return {
|
|
store,
|
|
restore: () => {
|
|
if (originalWindow === undefined) {
|
|
delete (globalThis as { window?: unknown }).window;
|
|
} else {
|
|
(globalThis as { window: unknown }).window = originalWindow;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function sampleBucket(dayKey: string): ChatDayBucket {
|
|
return {
|
|
dayKey,
|
|
count: 1,
|
|
loadedCount: 1,
|
|
truncated: false,
|
|
firstMessageId: 1,
|
|
lastMessageId: 1,
|
|
messages: [
|
|
{
|
|
id: `msg-${dayKey}`,
|
|
sessionId: 's',
|
|
role: 'user',
|
|
content: 'hi',
|
|
timestamp: '12:00',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test('window cache LRU: 驱逐带 userId 前缀的完整 cache key,不留孤儿条目', () => {
|
|
const { store, restore } = installMemoryLocalStorage();
|
|
try {
|
|
const userId = 42;
|
|
// MAX_CACHED_WINDOWS = 3;写入 4 个不同 session 应驱逐最早的带前缀 key
|
|
const sessions = ['sess-a', 'sess-b', 'sess-c', 'sess-d'];
|
|
for (const sid of sessions) {
|
|
saveSessionWindowCache(sid, [sampleBucket(`2026-09-18-${sid}`)], userId);
|
|
}
|
|
|
|
const prefixed = [...store.keys()].filter((k) => k.startsWith('smdf_chat_window_u'));
|
|
assert.equal(prefixed.length, 3, `expected 3 prefixed window keys, got ${prefixed.join(',')}`);
|
|
assert.equal(
|
|
loadSessionWindowCache('sess-a', userId),
|
|
null,
|
|
'oldest sess-a with user prefix must be evicted'
|
|
);
|
|
assert.ok(loadSessionWindowCache('sess-b', userId));
|
|
assert.ok(loadSessionWindowCache('sess-c', userId));
|
|
assert.ok(loadSessionWindowCache('sess-d', userId));
|
|
|
|
// 无前缀 key 不应被误认为存活缓存
|
|
assert.equal(store.has('smdf_chat_window_sess-a'), false);
|
|
assert.equal(store.has('smdf_chat_window_sess-d'), false);
|
|
|
|
clearSessionWindowCache('sess-d', userId);
|
|
assert.equal(loadSessionWindowCache('sess-d', userId), null);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test('window cache: 无 schema 的旧载荷不当作有效缓存', () => {
|
|
const { store, restore } = installMemoryLocalStorage();
|
|
try {
|
|
store.set('smdf_chat_window_u7_sess-old', JSON.stringify([{ dayKey: 'stale' }]));
|
|
assert.equal(loadSessionWindowCache('sess-old', 7), null);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test('window cache: 读写同一 userId 前缀,跨用户不串读', () => {
|
|
const { restore } = installMemoryLocalStorage();
|
|
try {
|
|
saveSessionWindowCache('sess-shared', [sampleBucket('2026-09-18')], 1);
|
|
saveSessionWindowCache('sess-shared', [sampleBucket('2026-09-19')], 2);
|
|
const u1 = loadSessionWindowCache('sess-shared', 1);
|
|
const u2 = loadSessionWindowCache('sess-shared', 2);
|
|
assert.equal(u1?.[0]?.dayKey, '2026-09-18');
|
|
assert.equal(u2?.[0]?.dayKey, '2026-09-19');
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|