import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
import {
truncate, jsonStringify, jsonChunk, buildToolText,
postSessionMessage, SIDE_EFFECT_TOOLS,
} from '../src/index.js';
import { saveEnv, resetEnv, stubEnv, createMockFetch } from './helpers.js';
import { setOpencodeClient } from '../src/index.js';
beforeAll(() => saveEnv());
describe('TestTruncate', () => {
it('returns text unchanged when within limit', () => {
expect(truncate('short', 100)).toBe('short');
});
it('truncates and appends omission notice', () => {
const text = 'a'.repeat(15_000);
const result = truncate(text, 10_000);
expect(result.length).toBeLessThan(15_000);
expect(result).toContain('more chars omitted');
});
it('uses default MAX_CHUNK when maxLen omitted', () => {
const text = 'x'.repeat(20_000);
const result = truncate(text);
expect(result.length).toBeLessThan(20_000);
});
});
describe('TestJsonStringify', () => {
it('handles bigint', () => {
expect(jsonStringify({ n: BigInt(9007199254740991) })).toBe('{"n":"9007199254740991"}');
});
it('handles RegExp', () => {
expect(jsonStringify({ r: /test/g })).toBe('{"r":"/test/g"}');
});
it('handles Date', () => {
const d = new Date('2025-01-01T00:00:00.000Z');
const result = jsonStringify({ d });
expect(result).toContain('2025-01-01');
});
it('passes through normal values', () => {
expect(jsonStringify({ a: 1, b: 'hello' })).toBe('{"a":1,"b":"hello"}');
});
});
describe('TestBuildToolText', () => {
it('includes tool name', () => {
const text = buildToolText('Bash', {}, {});
expect(text).toContain('[PostToolUse] Bash');
});
it('includes input and response', () => {
const text = buildToolText('Read', { file_path: '/foo' }, { content: 'bar' });
expect(text).toContain('tool_input:');
expect(text).toContain('tool_response:');
expect(text).toContain('/foo');
});
it('handles null values', () => {
const text = buildToolText('Edit', null, null);
expect(text).toContain('[PostToolUse] Edit');
expect(text).toContain('null');
});
it('truncates large input', () => {
const big = 'x'.repeat(20_000);
const text = buildToolText('Bash', big, 'ok');
expect(text.length).toBeLessThan(25_000);
});
});
describe('TestPostSessionMessage', () => {
beforeEach(() => {
resetEnv();
setOpencodeClient(null);
});
it('returns true on successful POST', async () => {
stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
vi.stubGlobal('fetch', createMockFetch({
ok: true, status: 200,
json: () => Promise.resolve({ ok: true }),
}));
const result = await postSessionMessage('sess-1', 'tool', 'content');
expect(result).toBe(true);
});
it('returns false on POST failure', async () => {
stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
const result = await postSessionMessage('sess-1', 'tool', 'content');
expect(result).toBe(false);
});
});
describe('TestToolExecuteAfterHookLogic', () => {
beforeEach(() => {
resetEnv();
setOpencodeClient(null);
vi.restoreAllMocks();
});
it('skips non-side-effect tools (Read/Glob/Grep)', () => {
expect(SIDE_EFFECT_TOOLS.has('Read')).toBe(false);
expect(SIDE_EFFECT_TOOLS.has('Glob')).toBe(false);
expect(SIDE_EFFECT_TOOLS.has('Grep')).toBe(false);
});
it('includes side-effect tools (Bash/Write/Edit)', () => {
expect(SIDE_EFFECT_TOOLS.has('Bash')).toBe(true);
expect(SIDE_EFFECT_TOOLS.has('Write')).toBe(true);
expect(SIDE_EFFECT_TOOLS.has('Edit')).toBe(true);
});
it('POSTs for side-effect tools via postSessionMessage', async () => {
stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
vi.stubGlobal('fetch', createMockFetch({
ok: true, status: 200,
json: () => Promise.resolve({ ok: true }),
}));
const sessionId = 'sess-1';
const toolName = 'Bash';
const text = buildToolText(toolName, { command: 'ls' }, 'file.txt');
const result = await postSessionMessage(sessionId, 'tool', text);
expect(result).toBe(true);
expect(fetch).toHaveBeenCalled();
});
});