SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,512 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { MEDIA_DATA_MAX_CHARS, MEDIA_KEY_MAX_CHARS, MediaStore } from '../src/media-store';
|
||||
import { convertEvent, type ConverterContext } from '../src/event-converter';
|
||||
import type { GroupMessage, FriendMessage, MessageElement } from '@snowluma/protocol/events';
|
||||
|
||||
const SELF_ID = 10001;
|
||||
const PEER_UIN = 22222;
|
||||
const GROUP_ID = 99999;
|
||||
|
||||
function imageElement(overrides: Partial<MessageElement> = {}): MessageElement {
|
||||
return {
|
||||
type: 'image',
|
||||
fileId: 'abc.png',
|
||||
fileSize: 4242,
|
||||
imageUrl: 'https://example.com/abc.png',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function recordElement(overrides: Partial<MessageElement> = {}): MessageElement {
|
||||
return {
|
||||
type: 'record',
|
||||
fileId: 'uuid-base64-fake',
|
||||
fileName: 'silk_test.amr',
|
||||
fileSize: 1024,
|
||||
duration: 3,
|
||||
fileHash: 'deadbeef',
|
||||
mediaNode: { fileUuid: 'uuid-base64-fake', storeId: 1 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeGroupMessage(elements: MessageElement[]): GroupMessage {
|
||||
return {
|
||||
kind: 'group_message',
|
||||
groupName: '',
|
||||
time: 1700000000,
|
||||
selfUin: SELF_ID,
|
||||
groupId: GROUP_ID,
|
||||
senderUin: PEER_UIN,
|
||||
senderNick: 'peer',
|
||||
senderCard: '',
|
||||
senderRole: 'member',
|
||||
msgSeq: 1,
|
||||
msgId: 1,
|
||||
elements,
|
||||
};
|
||||
}
|
||||
|
||||
function makeFriendMessage(elements: MessageElement[]): FriendMessage {
|
||||
return {
|
||||
kind: 'friend_message',
|
||||
time: 1700000000,
|
||||
selfUin: SELF_ID,
|
||||
senderUin: PEER_UIN,
|
||||
senderNick: 'peer',
|
||||
msgSeq: 1,
|
||||
msgId: 1,
|
||||
elements,
|
||||
};
|
||||
}
|
||||
|
||||
function tempDbPath(label: string): string {
|
||||
return path.join(
|
||||
'data',
|
||||
'test',
|
||||
`media-${label}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.db`,
|
||||
);
|
||||
}
|
||||
|
||||
describe('MediaStore basic semantics', () => {
|
||||
const dbs: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
dbs.length = 0;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const dbPath of dbs) {
|
||||
for (const ext of ['', '-wal', '-shm']) {
|
||||
try {
|
||||
fs.unlinkSync(dbPath + ext);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function open(label: string, max?: number): MediaStore {
|
||||
const dbPath = tempDbPath(label);
|
||||
dbs.push(dbPath);
|
||||
return max !== undefined ? new MediaStore(dbPath, max) : new MediaStore(dbPath);
|
||||
}
|
||||
|
||||
it('indexes images by file, fileName, and url', async () => {
|
||||
const store = open('img-keys');
|
||||
await store.rememberImage({
|
||||
file: 'abc.png',
|
||||
url: 'https://example.com/abc.png',
|
||||
fileSize: 4242,
|
||||
fileName: 'abc.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: 'https://example.com/abc.png',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
|
||||
expect((await store.findImage('abc.png'))?.fileSize).toBe(4242);
|
||||
expect((await store.findImage('https://example.com/abc.png'))?.url).toBe(
|
||||
'https://example.com/abc.png',
|
||||
);
|
||||
expect(await store.findImage('missing.png')).toBeNull();
|
||||
expect(await store.findImage('')).toBeNull();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('indexes records by file, fileName, fileId, and url', async () => {
|
||||
const store = open('rec-keys');
|
||||
await store.rememberRecord({
|
||||
file: 'silk_test.amr',
|
||||
fileId: 'uuid-base64-fake',
|
||||
url: 'https://example.com/voice.amr',
|
||||
fileSize: 1024,
|
||||
fileName: 'silk_test.amr',
|
||||
duration: 3,
|
||||
fileHash: 'deadbeef',
|
||||
mediaNode: { fileUuid: 'uuid-base64-fake' },
|
||||
isGroup: false,
|
||||
sessionId: PEER_UIN,
|
||||
});
|
||||
|
||||
expect((await store.findRecord('silk_test.amr'))?.duration).toBe(3);
|
||||
expect((await store.findRecord('uuid-base64-fake'))?.fileId).toBe('uuid-base64-fake');
|
||||
expect((await store.findRecord('https://example.com/voice.amr'))?.url).toBe(
|
||||
'https://example.com/voice.amr',
|
||||
);
|
||||
expect(await store.findRecord('nope')).toBeNull();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('updates URLs in place across all alias keys', async () => {
|
||||
const store = open('url-update');
|
||||
await store.rememberRecord({
|
||||
file: 'silk_test.amr',
|
||||
fileId: 'uuid-base64-fake',
|
||||
url: '',
|
||||
fileSize: 1024,
|
||||
fileName: 'silk_test.amr',
|
||||
duration: 3,
|
||||
fileHash: '',
|
||||
mediaNode: { fileUuid: 'uuid-base64-fake' },
|
||||
isGroup: false,
|
||||
sessionId: PEER_UIN,
|
||||
});
|
||||
await store.updateRecordUrl('silk_test.amr', 'https://refreshed.example.com/voice.amr');
|
||||
|
||||
expect((await store.findRecord('silk_test.amr'))?.url).toBe(
|
||||
'https://refreshed.example.com/voice.amr',
|
||||
);
|
||||
expect((await store.findRecord('uuid-base64-fake'))?.url).toBe(
|
||||
'https://refreshed.example.com/voice.amr',
|
||||
);
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('survives close+reopen against the same db path', async () => {
|
||||
const dbPath = tempDbPath('persist');
|
||||
dbs.push(dbPath);
|
||||
|
||||
{
|
||||
const store = new MediaStore(dbPath);
|
||||
await store.initialization;
|
||||
await store.rememberImage({
|
||||
file: 'abc.png',
|
||||
url: 'https://example.com/abc.png',
|
||||
fileSize: 99,
|
||||
fileName: 'abc.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: 'https://example.com/abc.png',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
await store.close();
|
||||
}
|
||||
|
||||
{
|
||||
const store = new MediaStore(dbPath);
|
||||
await store.initialization;
|
||||
const found = await store.findImage('abc.png');
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.fileSize).toBe(99);
|
||||
// Alias index must also persist.
|
||||
expect((await store.findImage('https://example.com/abc.png'))?.fileSize).toBe(99);
|
||||
await store.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('retains durable entries beyond the memory cache cap', async () => {
|
||||
// Lookups can be evicted from memory, but historical metadata survives.
|
||||
const cap = 80;
|
||||
const store = open('evict', cap);
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
await store.rememberImage({
|
||||
file: `file-${i}.png`,
|
||||
url: `https://example.com/${i}.png`,
|
||||
fileSize: i,
|
||||
fileName: `file-${i}.png`,
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: '',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
}
|
||||
|
||||
const { images } = await store.size();
|
||||
expect(images).toBe(200);
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
expect((await store.findImage(`file-${i}.png`))?.fileSize).toBe(i);
|
||||
}
|
||||
|
||||
expect((await store.findImage('file-0.png'))?.fileSize).toBe(0);
|
||||
// Most recent entries should still be present.
|
||||
expect((await store.findImage('file-199.png'))?.fileSize).toBe(199);
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('does not persist inline base64 as a key or JSON payload (#463)', async () => {
|
||||
const store = open('inline-b64');
|
||||
const payload = `base64://${'A'.repeat(8192)}`;
|
||||
await store.rememberImage({
|
||||
file: payload,
|
||||
url: payload,
|
||||
fileSize: 99,
|
||||
fileName: '',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: payload,
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
md5Hex: 'aa'.repeat(16),
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
const found = await store.findImage(payload);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.fileSize).toBe(99);
|
||||
expect(found!.md5Hex).toBe('aa'.repeat(16));
|
||||
expect(found!.url).toBe('');
|
||||
expect(found!.imageUrl).toBe('');
|
||||
expect(found!.file.startsWith('inline:')).toBe(true);
|
||||
expect(found!.file.includes('base64://')).toBe(false);
|
||||
|
||||
await store.close();
|
||||
const rows = inspectMediaDb(dbs[dbs.length - 1]!);
|
||||
expect(rows.longestKey).toBeLessThanOrEqual(MEDIA_KEY_MAX_CHARS);
|
||||
expect(rows.longestData).toBeLessThanOrEqual(MEDIA_DATA_MAX_CHARS);
|
||||
expect(rows.longestKey).toBeLessThan(100);
|
||||
expect(rows.concatenated).not.toContain('base64://');
|
||||
expect(rows.concatenated).not.toContain('A'.repeat(64));
|
||||
});
|
||||
|
||||
it('folds oversized non-inline keys the same way', async () => {
|
||||
const store = open('long-url');
|
||||
const url = `https://cdn.example/${'a'.repeat(3000)}.png`;
|
||||
await store.rememberImage({
|
||||
file: 'short.png',
|
||||
url,
|
||||
fileSize: 12,
|
||||
fileName: 'short.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: url,
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
|
||||
expect((await store.findImage('short.png'))?.fileSize).toBe(12);
|
||||
expect((await store.findImage(url))?.fileSize).toBe(12);
|
||||
expect((await store.findImage('short.png'))?.url).toBe('');
|
||||
await store.close();
|
||||
const rows = inspectMediaDb(dbs[dbs.length - 1]!);
|
||||
expect(rows.longestKey).toBeLessThanOrEqual(MEDIA_KEY_MAX_CHARS);
|
||||
expect(rows.concatenated).not.toContain('a'.repeat(64));
|
||||
});
|
||||
|
||||
it('ignores an inline URL refresh so eviction cannot be re-poisoned', async () => {
|
||||
const store = open('no-inline-refresh');
|
||||
await store.rememberImage({
|
||||
file: 'abc.png',
|
||||
url: 'https://example.com/abc.png',
|
||||
fileSize: 7,
|
||||
fileName: 'abc.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: '',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
await store.updateImageUrl('abc.png', `base64://${'B'.repeat(100)}`);
|
||||
expect((await store.findImage('abc.png'))?.url).toBe('https://example.com/abc.png');
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('does not destructively purge existing metadata on open', async () => {
|
||||
const dbPath = tempDbPath('legacy-overflow');
|
||||
dbs.push(dbPath);
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
const raw = new DatabaseSync(dbPath);
|
||||
raw.exec(`
|
||||
CREATE TABLE media_entries (
|
||||
type TEXT NOT NULL,
|
||||
primary_key TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
last_seen INTEGER NOT NULL,
|
||||
PRIMARY KEY (type, primary_key)
|
||||
);
|
||||
CREATE TABLE media_keys (
|
||||
type TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
primary_key TEXT NOT NULL,
|
||||
PRIMARY KEY (type, key)
|
||||
);
|
||||
`);
|
||||
const poisonKey = 'k'.repeat(MEDIA_KEY_MAX_CHARS + 8);
|
||||
raw.prepare(
|
||||
`INSERT INTO media_entries (type, primary_key, data, last_seen) VALUES ('image', ?, ?, 1)`,
|
||||
).run(poisonKey, '{"file":"poison"}');
|
||||
raw.prepare(`INSERT INTO media_keys (type, key, primary_key) VALUES ('image', ?, ?)`).run(
|
||||
poisonKey,
|
||||
poisonKey,
|
||||
);
|
||||
raw.close();
|
||||
|
||||
const store = new MediaStore(dbPath);
|
||||
await store.initialization;
|
||||
expect(await store.findImage(poisonKey)).toBeNull();
|
||||
expect((await store.size()).images).toBe(1);
|
||||
await store.close();
|
||||
const rows = inspectMediaDb(dbPath);
|
||||
expect(rows.entryCount).toBe(1);
|
||||
expect(rows.keyCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
function inspectMediaDb(dbPath: string): {
|
||||
longestKey: number;
|
||||
longestData: number;
|
||||
concatenated: string;
|
||||
entryCount: number;
|
||||
keyCount: number;
|
||||
} {
|
||||
const db = new DatabaseSync(dbPath);
|
||||
const entries = db.prepare(`SELECT primary_key, data FROM media_entries`).all() as Array<{
|
||||
primary_key: string;
|
||||
data: string;
|
||||
}>;
|
||||
const keys = db.prepare(`SELECT key, primary_key FROM media_keys`).all() as Array<{
|
||||
key: string;
|
||||
primary_key: string;
|
||||
}>;
|
||||
db.close();
|
||||
const keyLens = [
|
||||
...entries.map((row) => row.primary_key.length),
|
||||
...keys.map((row) => row.key.length),
|
||||
...keys.map((row) => row.primary_key.length),
|
||||
0,
|
||||
];
|
||||
return {
|
||||
longestKey: Math.max(...keyLens),
|
||||
longestData: Math.max(0, ...entries.map((row) => row.data.length)),
|
||||
concatenated: [
|
||||
...entries.map((row) => row.primary_key + row.data),
|
||||
...keys.map((row) => row.key + row.primary_key),
|
||||
].join('\n'),
|
||||
entryCount: entries.length,
|
||||
keyCount: keys.length,
|
||||
};
|
||||
}
|
||||
|
||||
describe('convertEvent media segment sink → MediaStore wiring', () => {
|
||||
const dbs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dbPath of dbs) {
|
||||
for (const ext of ['', '-wal', '-shm']) {
|
||||
try {
|
||||
fs.unlinkSync(dbPath + ext);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbs.length = 0;
|
||||
});
|
||||
|
||||
it('emits sink invocations for image segments with the right context', async () => {
|
||||
const sinkCalls: Array<{
|
||||
type: string;
|
||||
isGroup: boolean;
|
||||
sessionId: number;
|
||||
file: string;
|
||||
}> = [];
|
||||
const ctx: ConverterContext = {
|
||||
selfId: SELF_ID,
|
||||
imageUrlResolver: null,
|
||||
mediaUrlResolver: null,
|
||||
messageIdResolver: null,
|
||||
mediaSegmentSink: (type, _element, data, isGroup, sessionId) => {
|
||||
sinkCalls.push({ type, isGroup, sessionId, file: String(data.file ?? '') });
|
||||
},
|
||||
};
|
||||
|
||||
await convertEvent(ctx, makeGroupMessage([imageElement()]));
|
||||
await convertEvent(ctx, makeFriendMessage([recordElement()]));
|
||||
|
||||
expect(sinkCalls).toEqual([
|
||||
{ type: 'image', isGroup: true, sessionId: GROUP_ID, file: 'abc.png' },
|
||||
{ type: 'record', isGroup: false, sessionId: PEER_UIN, file: 'silk_test.amr' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('lets the sink populate a MediaStore that get_image-style lookups can use', async () => {
|
||||
const dbPath = tempDbPath('sink');
|
||||
dbs.push(dbPath);
|
||||
|
||||
const store = new MediaStore(dbPath);
|
||||
await store.initialization;
|
||||
const ctx: ConverterContext = {
|
||||
selfId: SELF_ID,
|
||||
imageUrlResolver: (element) => element.imageUrl ?? '',
|
||||
mediaUrlResolver: async (element) => element.url ?? '',
|
||||
messageIdResolver: null,
|
||||
mediaSegmentSink: async (type, element, data, isGroup, sessionId) => {
|
||||
const url = typeof data.url === 'string' ? data.url : '';
|
||||
const file = typeof data.file === 'string' ? data.file : '';
|
||||
|
||||
if (type === 'image') {
|
||||
await store.rememberImage({
|
||||
file: file || element.fileId || '',
|
||||
url,
|
||||
fileSize: element.fileSize ?? 0,
|
||||
fileName: element.fileId ?? '',
|
||||
subType: element.subType ?? 0,
|
||||
summary: element.summary ?? '',
|
||||
imageUrl: element.imageUrl ?? '',
|
||||
isGroup,
|
||||
sessionId,
|
||||
});
|
||||
} else {
|
||||
await store.rememberRecord({
|
||||
file: file || element.fileName || element.fileId || '',
|
||||
fileId: element.fileId ?? '',
|
||||
url,
|
||||
fileSize: element.fileSize ?? 0,
|
||||
fileName: element.fileName ?? '',
|
||||
duration: element.duration ?? 0,
|
||||
fileHash: element.fileHash ?? '',
|
||||
mediaNode: element.mediaNode,
|
||||
isGroup,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await convertEvent(ctx, makeGroupMessage([imageElement()]));
|
||||
await convertEvent(ctx, makeFriendMessage([recordElement()]));
|
||||
|
||||
const img = await store.findImage('abc.png');
|
||||
expect(img).not.toBeNull();
|
||||
expect(img!.url).toBe('https://example.com/abc.png');
|
||||
expect(img!.fileSize).toBe(4242);
|
||||
expect(img!.isGroup).toBe(true);
|
||||
expect(img!.sessionId).toBe(GROUP_ID);
|
||||
|
||||
const rec = await store.findRecord('silk_test.amr');
|
||||
expect(rec).not.toBeNull();
|
||||
expect(rec!.fileId).toBe('uuid-base64-fake');
|
||||
expect(rec!.duration).toBe(3);
|
||||
expect(rec!.isGroup).toBe(false);
|
||||
expect(rec!.sessionId).toBe(PEER_UIN);
|
||||
// Alias lookup by fileUuid still works.
|
||||
expect((await store.findRecord('uuid-base64-fake'))?.fileName).toBe('silk_test.amr');
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('does not invoke the sink when no media segments are present', async () => {
|
||||
let calls = 0;
|
||||
const ctx: ConverterContext = {
|
||||
selfId: SELF_ID,
|
||||
imageUrlResolver: null,
|
||||
mediaUrlResolver: null,
|
||||
messageIdResolver: null,
|
||||
mediaSegmentSink: () => {
|
||||
calls++;
|
||||
},
|
||||
};
|
||||
await convertEvent(ctx, makeGroupMessage([{ type: 'text', text: 'hello' }]));
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user