Files
dashlibstorage/scripts/test-modelconversation.js
2026-10-12 18:01:25 +02:00

117 lines
5.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing store.modelconversation / store.modelmessage ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-modelconv-'));
const provider = new BetterSqlite3Provider({
filename: join(tempDir, 'test.db'),
storageDir: join(tempDir, 'storage'),
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const userA = await store.google_auth.add('mc-a@example.com');
const company = await store.company.create({
name: 'MC Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId: userA,
});
await store.google_auth.setSelectedCompany(userA, company.id);
const conv = await store.modelconversation.create(userA, {
provider: 'openrouter', model: 'test/model',
});
assert.equal(conv.state, 'idle');
assert.equal(conv.provider, 'openrouter');
await assert.rejects(store.modelconversation.create(userA, { model: 'x' }), /Provider required/);
await assert.rejects(store.modelconversation.create(userA, { provider: 'x' }), /Model required/);
const list = await store.modelconversation.list(userA);
assert.equal(list.length, 1);
assert.equal(list[0].first_user_message, null);
assert.deepEqual(await store.modelmessage.list(userA, conv.id), []);
const m1 = await store.modelmessage.add(userA, conv.id, { message: 'hello' });
assert.equal(m1.role, 'user');
assert.equal(m1.conversation_id, conv.id);
await assert.rejects(store.modelmessage.add(userA, conv.id, { message: ' ' }), /Message required/);
await assert.rejects(store.modelmessage.add(userA, 9999, { message: 'x' }), /Conversation not found/);
// Non-idle conversation refuses user messages but allows assistant messages
const busyConv = await store.modelconversation.setState(userA, conv.id, 'busy');
assert.equal(busyConv.state, 'busy');
const fetchedBusy = await store.modelconversation.get(userA, conv.id);
assert.equal(fetchedBusy.state, 'busy');
await assert.rejects(
store.modelmessage.add(userA, conv.id, { message: 'again' }),
/Conversation is not idle/,
);
// Assistant message can be added while busy
const mAssistant = await store.modelmessage.add(userA, conv.id, { message: 'echo: hello', role: 'assistant' });
assert.equal(mAssistant.role, 'assistant');
assert.equal(mAssistant.message, 'echo: hello');
// Back to idle
await store.modelconversation.setState(userA, conv.id, 'idle');
await store.modelmessage.add(userA, conv.id, { message: 'second' });
const msgs = await store.modelmessage.list(userA, conv.id);
assert.deepEqual(msgs.map((m) => m.message), ['hello', 'echo: hello', 'second']);
assert.deepEqual(msgs.map((m) => m.role), ['user', 'assistant', 'user']);
assert.equal((await store.modelconversation.list(userA)).find((c) => c.id === conv.id).first_user_message, 'hello');
// Other user cannot see or write
const userB = await store.google_auth.add('mc-b@example.com');
await store.companyuser.add(company.id, userB);
await store.google_auth.setSelectedCompany(userB, company.id);
assert.equal((await store.modelconversation.list(userB)).length, 0);
await assert.rejects(store.modelmessage.list(userB, conv.id), /Conversation not found/);
await assert.rejects(store.modelmessage.add(userB, conv.id, { message: 'x' }), /Conversation not found/);
// started_by: top-level is null, subagents link to their originator, list exposes it
assert.equal(conv.started_by, null);
const sub1 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: conv.id });
assert.equal(sub1.started_by, conv.id);
const sub2 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
const sibling = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm' });
await assert.rejects(
store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: 0 }),
/Invalid started_by/,
);
const listed = await store.modelconversation.list(userA);
assert.equal(listed.find((c) => c.id === sub2.id).started_by, sub1.id);
assert.equal((await store.modelconversation.get(userA, sub1.id)).started_by, conv.id);
await store.modelmessage.add(userA, sub2.id, { message: 'deep' });
// Deleting a child removes only its own chain; deleting the root removes the whole chain
await store.modelconversation.delete(userA, sub2.id);
await assert.rejects(store.modelconversation.get(userA, sub2.id), /Conversation not found/);
await store.modelconversation.get(userA, sub1.id);
const sub3 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
await store.modelconversation.delete(userA, conv.id);
for (const gone of [conv.id, sub1.id, sub3.id]) {
await assert.rejects(store.modelconversation.get(userA, gone), /Conversation not found/);
}
await store.modelconversation.get(userA, sibling.id);
await assert.rejects(store.modelconversation.delete(userA, conv.id), /Conversation not found/);
console.log('PASS');
} finally {
await provider.close?.();
rmSync(tempDir, { recursive: true, force: true });
}
}
run().catch((err) => {
console.error('FAIL', err);
process.exit(1);
});