Add hybrid research copilot workspace

This commit is contained in:
2026-03-14 19:32:00 -04:00
parent 7a42d73a48
commit 2ee9a549a3
27 changed files with 2864 additions and 323 deletions

View File

@@ -0,0 +1,165 @@
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it
} from 'bun:test';
import { mock } from 'bun:test';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Database } from 'bun:sqlite';
const TEST_USER_ID = 'copilot-user';
let tempDir: string | null = null;
let sqliteClient: Database | null = null;
let copilotRepo: typeof import('./research-copilot') | null = null;
async function loadRepoModule() {
const moduleUrl = new URL(`./research-copilot.ts?test=${Date.now()}`, import.meta.url).href;
return await import(moduleUrl) as typeof import('./research-copilot');
}
function resetDbSingletons() {
const globalState = globalThis as typeof globalThis & {
__fiscalSqliteClient?: Database;
__fiscalDrizzleDb?: unknown;
};
globalState.__fiscalSqliteClient?.close();
globalState.__fiscalSqliteClient = undefined;
globalState.__fiscalDrizzleDb = undefined;
}
function applyMigration(client: Database, fileName: string) {
const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8');
client.exec(sql);
}
function ensureUser(client: Database) {
const now = Date.now();
client.exec(`
INSERT OR REPLACE INTO user (id, name, email, emailVerified, image, createdAt, updatedAt, role, banned, banReason, banExpires)
VALUES ('${TEST_USER_ID}', 'Copilot User', 'copilot@example.com', 1, NULL, ${now}, ${now}, NULL, 0, NULL, NULL);
`);
}
describe('research copilot repo', () => {
beforeAll(async () => {
mock.restore();
tempDir = mkdtempSync(join(tmpdir(), 'fiscal-copilot-repo-'));
process.env.DATABASE_URL = `file:${join(tempDir, 'repo.sqlite')}`;
(process.env as Record<string, string | undefined>).NODE_ENV = 'test';
resetDbSingletons();
sqliteClient = new Database(join(tempDir, 'repo.sqlite'), { create: true });
sqliteClient.exec('PRAGMA foreign_keys = ON;');
applyMigration(sqliteClient, '0000_cold_silver_centurion.sql');
applyMigration(sqliteClient, '0008_research_workspace.sql');
applyMigration(sqliteClient, '0013_research_copilot.sql');
ensureUser(sqliteClient);
const globalState = globalThis as typeof globalThis & {
__fiscalSqliteClient?: Database;
__fiscalDrizzleDb?: unknown;
};
globalState.__fiscalSqliteClient = sqliteClient;
globalState.__fiscalDrizzleDb = undefined;
copilotRepo = await loadRepoModule();
});
afterAll(() => {
mock.restore();
sqliteClient?.close();
resetDbSingletons();
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
}
});
beforeEach(() => {
sqliteClient?.exec('DELETE FROM research_copilot_message;');
sqliteClient?.exec('DELETE FROM research_copilot_session;');
});
it('creates and reloads ticker-scoped sessions', async () => {
if (!copilotRepo) {
throw new Error('repo not initialized');
}
const session = await copilotRepo.getOrCreateResearchCopilotSession({
userId: TEST_USER_ID,
ticker: 'msft',
selectedSources: ['documents', 'research'],
pinnedArtifactIds: [2, 2, 5]
});
const loaded = await copilotRepo.getResearchCopilotSessionByTicker(TEST_USER_ID, 'MSFT');
expect(session.ticker).toBe('MSFT');
expect(session.selected_sources).toEqual(['documents', 'research']);
expect(session.pinned_artifact_ids).toEqual([2, 5]);
expect(loaded?.id).toBe(session.id);
});
it('appends messages and updates session state', async () => {
if (!copilotRepo) {
throw new Error('repo not initialized');
}
const session = await copilotRepo.getOrCreateResearchCopilotSession({
userId: TEST_USER_ID,
ticker: 'NVDA'
});
await copilotRepo.appendResearchCopilotMessage({
userId: TEST_USER_ID,
sessionId: session.id,
role: 'user',
contentMarkdown: 'What changed in the latest filing?',
selectedSources: ['filings'],
pinnedArtifactIds: [7],
memoSection: 'thesis'
});
await copilotRepo.appendResearchCopilotMessage({
userId: TEST_USER_ID,
sessionId: session.id,
role: 'assistant',
contentMarkdown: 'Demand remained strong [1]',
citations: [{
index: 1,
label: 'NVDA 10-K [1]',
chunkId: 1,
href: '/filings?ticker=NVDA',
source: 'filings',
sourceKind: 'filing_brief',
sourceRef: '0001',
title: '10-K brief',
ticker: 'NVDA',
accessionNumber: '0001',
filingDate: '2026-01-01',
excerpt: 'Demand remained strong.',
artifactId: 3
}]
});
const updated = await copilotRepo.upsertResearchCopilotSessionState({
userId: TEST_USER_ID,
ticker: 'NVDA',
title: 'NVDA demand update',
selectedSources: ['filings'],
pinnedArtifactIds: [7]
});
expect(updated.title).toBe('NVDA demand update');
expect(updated.messages).toHaveLength(2);
expect(updated.messages[0]?.selected_sources).toEqual(['filings']);
expect(updated.messages[0]?.memo_section).toBe('thesis');
expect(updated.messages[1]?.citations[0]?.artifactId).toBe(3);
});
});