import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
import {
buildAdditionalContext, extractPromptText, MAX_CONTEXT_CHARS,
MIN_PROMPT_LEN, getApiBaseUrl, baseCtx, postJson, COMPOSE_TIMEOUT,
} from '../src/index.js';
import { saveEnv, resetEnv, stubEnv, createMockFetch } from './helpers.js';
import { setOpencodeClient } from '../src/index.js';
beforeAll(() => saveEnv());
describe('TestBuildAdditionalContext', () => {
it('includes identity and evidence', () => {
const result = buildAdditionalContext({ identityContext: 'I am Alice', retrievedEvidence: 'Past: X' });
expect(result).toContain('I am Alice');
expect(result).toContain('Past: X');
expect(result!.startsWith('[oG-Memory]')).toBe(true);
});
it('returns null when both empty', () => {
expect(buildAdditionalContext({})).toBeNull();
expect(buildAdditionalContext({ identityContext: '', retrievedEvidence: '' })).toBeNull();
});
it('works with only identity', () => {
const result = buildAdditionalContext({ identityContext: 'profile data' });
expect(result).not.toBeNull();
expect(result).toContain('profile data');
});
it('truncates to MAX_CONTEXT_CHARS', () => {
const result = buildAdditionalContext({ identityContext: 'x'.repeat(20_000) });
expect(result!.length).toBeLessThanOrEqual(MAX_CONTEXT_CHARS + '[oG-Memory]\n'.length);
});
});
describe('TestExtractPromptText', () => {
it('extracts text parts', () => {
const output = {
message: { role: 'user' },
parts: [
{ type: 'text', text: 'hello' },
{ type: 'tool', callID: 'x', tool: 'Bash', state: { status: 'completed' } },
{ type: 'text', text: 'world' },
],
};
expect(extractPromptText(output as any)).toBe('hello\nworld');
});
it('returns empty string when no text parts', () => {
const output = {
message: { role: 'user' },
parts: [{ type: 'tool', callID: 'x', tool: 'Bash', state: {} }],
};
expect(extractPromptText(output as any)).toBe('');
});
});
describe('TestChatMessageHookLogic', () => {
beforeEach(() => {
resetEnv();
setOpencodeClient(null);
vi.restoreAllMocks();
});
it('skips short prompts (< MIN_PROMPT_LEN)', () => {
const prompt = extractPromptText({
message: { role: 'user' },
parts: [{ type: 'text', text: 'hi' }],
} as any);
expect(prompt.length).toBeLessThan(MIN_PROMPT_LEN);
});
it('skips slash commands', () => {
const prompt = extractPromptText({
message: { role: 'user' },
parts: [{ type: 'text', text: '/og-compose test' }],
} as any);
expect(prompt.startsWith('/')).toBe(true);
});
it('POSTs compose and builds additionalContext on success', async () => {
stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
const composeResponse = { identityContext: 'Alice is a dev', retrievedEvidence: '' };
vi.stubGlobal('fetch', createMockFetch({
ok: true, status: 200,
json: () => Promise.resolve(composeResponse),
}));
const sessionId = 's1';
const prompt = 'what did we discuss';
const url = `${getApiBaseUrl()}/api/v1/compose`;
const body = { ...baseCtx(sessionId), prompt };
const data = await postJson(url, body, COMPOSE_TIMEOUT) as { identityContext?: string; retrievedEvidence?: string } | null;
expect(data).not.toBeNull();
const additional = buildAdditionalContext(data!);
expect(additional).not.toBeNull();
expect(additional).toContain('[oG-Memory]');
expect(additional).toContain('Alice is a dev');
});
it('returns null additionalContext when compose returns empty', async () => {
stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
vi.stubGlobal('fetch', createMockFetch({
ok: true, status: 200,
json: () => Promise.resolve({ identityContext: '', retrievedEvidence: '' }),
}));
const sessionId = 's1';
const prompt = 'tell me about X';
const url = `${getApiBaseUrl()}/api/v1/compose`;
const body = { ...baseCtx(sessionId), prompt };
const data = await postJson(url, body, COMPOSE_TIMEOUT) as { identityContext?: string; retrievedEvidence?: string } | null;
const additional = buildAdditionalContext(data!);
expect(additional).toBeNull();
});
});