mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(daemon): observe exact ACP prompt frame budgets * fix(daemon): trust catalog model diagnostics * fix(daemon): retain prompt budget evidence Scope input usage to each physical agent session attempt and retain the validated prompt-budget projection outside the run event tail.\n\nGenerated-By: looper 0.12.0 (runner=fixer, agent=codex)
208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import {
|
|
clearAgentSession,
|
|
closeDatabase,
|
|
getAgentSession,
|
|
getAgentSessionRecord,
|
|
insertConversation,
|
|
insertProject,
|
|
latestCompletedAssistantMessageId,
|
|
openDatabase,
|
|
updateAgentSessionStableHash,
|
|
upsertAgentSession,
|
|
upsertMessage,
|
|
} from '../src/db.js';
|
|
|
|
describe('agent_sessions persistence', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(path.join(os.tmpdir(), 'od-agent-sessions-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
closeDatabase();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function seed() {
|
|
const db = openDatabase(tempDir, { dataDir: tempDir });
|
|
const now = Date.now();
|
|
insertProject(db, { id: 'proj-1', name: 'P', createdAt: now, updatedAt: now });
|
|
insertConversation(db, {
|
|
id: 'conv-1',
|
|
projectId: 'proj-1',
|
|
title: 'C',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
return db;
|
|
}
|
|
|
|
it('returns null when no session is stored', () => {
|
|
const db = seed();
|
|
expect(getAgentSession(db, 'conv-1', 'claude')).toBeNull();
|
|
});
|
|
|
|
it('upserts and reads back a session id, scoped per agent', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A' });
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'codex', sessionId: 'sess-B' });
|
|
expect(getAgentSession(db, 'conv-1', 'claude')).toBe('sess-A');
|
|
expect(getAgentSession(db, 'conv-1', 'codex')).toBe('sess-B');
|
|
});
|
|
|
|
it('upsert overwrites the prior id for the same (conversation, agent)', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A' });
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-C' });
|
|
expect(getAgentSession(db, 'conv-1', 'claude')).toBe('sess-C');
|
|
});
|
|
|
|
it('persists bounded input usage for the next resumed prompt diagnostic', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, {
|
|
conversationId: 'conv-1',
|
|
agentId: 'amr',
|
|
sessionId: 'sess-A',
|
|
lastInputTokens: 123_456,
|
|
});
|
|
|
|
expect(getAgentSessionRecord(db, 'conv-1', 'amr')).toMatchObject({
|
|
sessionId: 'sess-A',
|
|
lastInputTokens: 123_456,
|
|
});
|
|
});
|
|
|
|
it('clearAgentSession removes only the targeted row', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A' });
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'codex', sessionId: 'sess-B' });
|
|
clearAgentSession(db, 'conv-1', 'claude');
|
|
expect(getAgentSession(db, 'conv-1', 'claude')).toBeNull();
|
|
expect(getAgentSession(db, 'conv-1', 'codex')).toBe('sess-B');
|
|
});
|
|
});
|
|
|
|
describe('latestCompletedAssistantMessageId (resume-identity cursor)', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(path.join(os.tmpdir(), 'od-resume-cursor-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
closeDatabase();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function seed() {
|
|
const db = openDatabase(tempDir, { dataDir: tempDir });
|
|
const now = Date.now();
|
|
insertProject(db, { id: 'proj-1', name: 'P', createdAt: now, updatedAt: now });
|
|
insertConversation(db, {
|
|
id: 'conv-1',
|
|
projectId: 'proj-1',
|
|
title: 'C',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
return db;
|
|
}
|
|
|
|
function addAssistant(db: ReturnType<typeof seed>, id: string, runStatus: string | null) {
|
|
upsertMessage(db, 'conv-1', { id, role: 'assistant', content: 'x', runStatus });
|
|
}
|
|
|
|
it('returns null when there is no prior completed assistant turn', () => {
|
|
const db = seed();
|
|
expect(latestCompletedAssistantMessageId(db, 'conv-1', 'in-flight')).toBeNull();
|
|
});
|
|
|
|
it('does NOT advance the cursor across an intervening failed/canceled assistant run', () => {
|
|
const db = seed();
|
|
// A resumable session was last produced by m-1 (a succeeded turn).
|
|
addAssistant(db, 'm-1', 'succeeded');
|
|
expect(latestCompletedAssistantMessageId(db, 'conv-1', 'in-flight')).toBe('m-1');
|
|
|
|
// Another agent runs and FAILS, then one is CANCELED, before producing any
|
|
// completed turn. These must not read as conversation advancement — the
|
|
// stored session (m-1) is still the latest completed turn, so an otherwise
|
|
// resumable session stays resumable. (Pre-fix this query ignored run_status
|
|
// and returned the failed placeholder, forcing a needless cold reseed.)
|
|
addAssistant(db, 'm-2-failed', 'failed');
|
|
addAssistant(db, 'm-3-canceled', 'canceled');
|
|
expect(latestCompletedAssistantMessageId(db, 'conv-1', 'in-flight')).toBe('m-1');
|
|
|
|
// A genuinely completed intervening turn DOES advance the cursor.
|
|
addAssistant(db, 'm-4', 'succeeded');
|
|
expect(latestCompletedAssistantMessageId(db, 'conv-1', 'in-flight')).toBe('m-4');
|
|
});
|
|
|
|
it('excludes the current run in-flight placeholder (null run_status)', () => {
|
|
const db = seed();
|
|
addAssistant(db, 'm-1', 'succeeded');
|
|
// The current turn's own placeholder is the latest by position but in-flight.
|
|
addAssistant(db, 'm-current', null);
|
|
expect(latestCompletedAssistantMessageId(db, 'conv-1', 'm-current')).toBe('m-1');
|
|
});
|
|
});
|
|
|
|
describe('agent_sessions stable_prompt_hash', () => {
|
|
let tempDir: string;
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(path.join(os.tmpdir(), 'od-agent-sessions-hash-'));
|
|
});
|
|
afterEach(() => {
|
|
closeDatabase();
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
function seed() {
|
|
const db = openDatabase(tempDir, { dataDir: tempDir });
|
|
const now = Date.now();
|
|
insertProject(db, { id: 'proj-1', name: 'P', createdAt: now, updatedAt: now });
|
|
insertConversation(db, {
|
|
id: 'conv-1', projectId: 'proj-1', title: 'C', createdAt: now, updatedAt: now,
|
|
});
|
|
return db;
|
|
}
|
|
|
|
it('round-trips the stable hash through upsert + getAgentSessionRecord', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, {
|
|
conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A', stablePromptHash: 'hash-1',
|
|
});
|
|
expect(getAgentSessionRecord(db, 'conv-1', 'claude')).toMatchObject({
|
|
sessionId: 'sess-A', stablePromptHash: 'hash-1',
|
|
});
|
|
});
|
|
|
|
it('stores null stable hash when omitted', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, { conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A' });
|
|
expect(getAgentSessionRecord(db, 'conv-1', 'claude')).toMatchObject({
|
|
sessionId: 'sess-A', stablePromptHash: null,
|
|
});
|
|
});
|
|
|
|
it('updateAgentSessionStableHash changes only the hash, keeps the session id', () => {
|
|
const db = seed();
|
|
upsertAgentSession(db, {
|
|
conversationId: 'conv-1', agentId: 'claude', sessionId: 'sess-A', stablePromptHash: 'hash-1',
|
|
});
|
|
updateAgentSessionStableHash(db, 'conv-1', 'claude', 'hash-2');
|
|
expect(getAgentSessionRecord(db, 'conv-1', 'claude')).toMatchObject({
|
|
sessionId: 'sess-A', stablePromptHash: 'hash-2',
|
|
});
|
|
});
|
|
|
|
it('getAgentSessionRecord returns null when no row exists', () => {
|
|
const db = seed();
|
|
expect(getAgentSessionRecord(db, 'conv-1', 'claude')).toBeNull();
|
|
});
|
|
});
|