Merge branch 'main' into cw/continuous-code-quality-conflict-1772832123778

# Conflicts:
#	apps/server/drizzle/meta/0037_snapshot.json
#	apps/server/drizzle/meta/_journal.json
This commit is contained in:
Lukas May
2026-03-06 22:30:21 +01:00
39 changed files with 5291 additions and 419 deletions

View File

@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { createTestDatabase } from './test-helpers.js';
import { agentMetrics } from '../../schema.js';
describe('agentMetrics table', () => {
it('select from empty agentMetrics returns []', async () => {
const db = createTestDatabase();
const rows = await db.select().from(agentMetrics);
expect(rows).toEqual([]);
});
it('insert and select a metrics row round-trips correctly', async () => {
const db = createTestDatabase();
await db.insert(agentMetrics).values({
agentId: 'agent-abc',
questionsCount: 3,
subagentsCount: 1,
compactionsCount: 0,
updatedAt: new Date('2024-01-01T00:00:00Z'),
});
const rows = await db.select().from(agentMetrics);
expect(rows).toHaveLength(1);
expect(rows[0].agentId).toBe('agent-abc');
expect(rows[0].questionsCount).toBe(3);
expect(rows[0].subagentsCount).toBe(1);
expect(rows[0].compactionsCount).toBe(0);
});
it('agentId is primary key — duplicate insert throws', async () => {
const db = createTestDatabase();
await db.insert(agentMetrics).values({
agentId: 'agent-dup',
questionsCount: 0,
subagentsCount: 0,
compactionsCount: 0,
updatedAt: new Date(),
});
await expect(
db.insert(agentMetrics).values({
agentId: 'agent-dup',
questionsCount: 1,
subagentsCount: 0,
compactionsCount: 0,
updatedAt: new Date(),
})
).rejects.toThrow();
});
});

View File

@@ -0,0 +1,129 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleLogChunkRepository } from './log-chunk.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
describe('DrizzleLogChunkRepository', () => {
let db: DrizzleDatabase;
let repo: DrizzleLogChunkRepository;
const testAgentId = 'agent-test-001';
beforeEach(() => {
db = createTestDatabase();
repo = new DrizzleLogChunkRepository(db);
});
it('AskUserQuestion chunk — questionsCount upserted correctly', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([{
agentId: testAgentId,
questionsCount: 2,
subagentsCount: 0,
compactionsCount: 0,
}]);
});
it('Agent tool chunk — subagentsCount incremented', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'tool_use', name: 'Agent' }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([{
agentId: testAgentId,
questionsCount: 0,
subagentsCount: 1,
compactionsCount: 0,
}]);
});
it('Compaction event — compactionsCount incremented', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'system', subtype: 'init', source: 'compact' }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([{
agentId: testAgentId,
questionsCount: 0,
subagentsCount: 0,
compactionsCount: 1,
}]);
});
it('Irrelevant chunk type — no metrics row created', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'text', text: 'hello' }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([]);
});
it('Malformed JSON chunk — chunk persisted, metrics row absent', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: 'not-valid-json',
});
const chunks = await repo.findByAgentId(testAgentId);
expect(chunks).toHaveLength(1);
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([]);
});
it('Multiple inserts, same agent — counts accumulate additively', async () => {
// 3 Agent tool chunks
for (let i = 0; i < 3; i++) {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'tool_use', name: 'Agent' }),
});
}
// 1 AskUserQuestion with 2 questions
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([{
agentId: testAgentId,
questionsCount: 2,
subagentsCount: 3,
compactionsCount: 0,
}]);
});
it('findMetricsByAgentIds with empty array — returns []', async () => {
const metrics = await repo.findMetricsByAgentIds([]);
expect(metrics).toEqual([]);
});
it('findMetricsByAgentIds with agentId that has no metrics row — returns []', async () => {
await repo.insertChunk({
agentId: testAgentId,
agentName: 'test-agent',
sessionNumber: 1,
content: JSON.stringify({ type: 'text', text: 'hello' }),
});
const metrics = await repo.findMetricsByAgentIds([testAgentId]);
expect(metrics).toEqual([]);
});
});

View File

@@ -4,10 +4,10 @@
* Implements LogChunkRepository interface using Drizzle ORM.
*/
import { eq, asc, max, inArray } from 'drizzle-orm';
import { eq, asc, max, inArray, sql } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js';
import { agentLogChunks } from '../../schema.js';
import { agentLogChunks, agentMetrics } from '../../schema.js';
import type { LogChunkRepository } from '../log-chunk-repository.js';
export class DrizzleLogChunkRepository implements LogChunkRepository {
@@ -19,13 +19,58 @@ export class DrizzleLogChunkRepository implements LogChunkRepository {
sessionNumber: number;
content: string;
}): Promise<void> {
await this.db.insert(agentLogChunks).values({
id: nanoid(),
agentId: data.agentId,
agentName: data.agentName,
sessionNumber: data.sessionNumber,
content: data.content,
createdAt: new Date(),
// better-sqlite3 is synchronous — transaction callback must be sync, use .run() not await
this.db.transaction((tx) => {
// 1. Always insert the chunk row first
tx.insert(agentLogChunks).values({
id: nanoid(),
agentId: data.agentId,
agentName: data.agentName,
sessionNumber: data.sessionNumber,
content: data.content,
createdAt: new Date(),
}).run();
// 2. Parse content and determine metric increments
// Wrap only the parse + upsert block — chunk insert is not rolled back on parse failure
try {
const parsed = JSON.parse(data.content);
let deltaQuestions = 0;
let deltaSubagents = 0;
let deltaCompactions = 0;
if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') {
deltaQuestions = parsed.input?.questions?.length ?? 0;
} else if (parsed.type === 'tool_use' && parsed.name === 'Agent') {
deltaSubagents = 1;
} else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') {
deltaCompactions = 1;
}
// 3. Only upsert if there is something to increment
if (deltaQuestions > 0 || deltaSubagents > 0 || deltaCompactions > 0) {
tx.insert(agentMetrics)
.values({
agentId: data.agentId,
questionsCount: deltaQuestions,
subagentsCount: deltaSubagents,
compactionsCount: deltaCompactions,
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: agentMetrics.agentId,
set: {
questionsCount: sql`${agentMetrics.questionsCount} + ${deltaQuestions}`,
subagentsCount: sql`${agentMetrics.subagentsCount} + ${deltaSubagents}`,
compactionsCount: sql`${agentMetrics.compactionsCount} + ${deltaCompactions}`,
updatedAt: new Date(),
},
})
.run();
}
} catch {
// Malformed JSON — skip metric upsert, chunk insert already committed within transaction
}
});
}
@@ -69,4 +114,22 @@ export class DrizzleLogChunkRepository implements LogChunkRepository {
return result[0]?.maxSession ?? 0;
}
async findMetricsByAgentIds(agentIds: string[]): Promise<{
agentId: string;
questionsCount: number;
subagentsCount: number;
compactionsCount: number;
}[]> {
if (agentIds.length === 0) return [];
return this.db
.select({
agentId: agentMetrics.agentId,
questionsCount: agentMetrics.questionsCount,
subagentsCount: agentMetrics.subagentsCount,
compactionsCount: agentMetrics.compactionsCount,
})
.from(agentMetrics)
.where(inArray(agentMetrics.agentId, agentIds));
}
}