228 lines
6.3 KiB
TypeScript
228 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { printCallOutput } from '../src/cli/output-utils.js';
|
|
import { createCallResult } from '../src/result-utils.js';
|
|
|
|
describe('printCallOutput format selection', () => {
|
|
it.each([
|
|
[
|
|
'auto prefers json payloads when available',
|
|
'auto',
|
|
{
|
|
content: [
|
|
{ type: 'text', text: 'fallback text' },
|
|
{ type: 'json', json: { source: 'json' } },
|
|
{ type: 'markdown', text: '# heading' },
|
|
],
|
|
},
|
|
(logged: unknown) => {
|
|
expect(JSON.parse(String(logged))).toEqual({ source: 'json' });
|
|
},
|
|
],
|
|
[
|
|
'auto prints the full structuredContent object instead of only its data field',
|
|
'auto',
|
|
{
|
|
structuredContent: {
|
|
status: 'error',
|
|
summary: 'Failed to create base: name is required',
|
|
data: {},
|
|
meta: {},
|
|
},
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: '{"status":"error","summary":"Failed to create base: name is required","data":{},"meta":{}}',
|
|
},
|
|
],
|
|
},
|
|
(logged: unknown) => {
|
|
expect(JSON.parse(String(logged))).toEqual({
|
|
status: 'error',
|
|
summary: 'Failed to create base: name is required',
|
|
data: {},
|
|
meta: {},
|
|
});
|
|
},
|
|
],
|
|
[
|
|
'auto prints structuredContent nested inside a result wrapper',
|
|
'auto',
|
|
{
|
|
result: {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: '{\n "entities": [],\n "relations": []\n}',
|
|
},
|
|
],
|
|
structuredContent: {
|
|
entities: [],
|
|
relations: [],
|
|
},
|
|
},
|
|
},
|
|
(logged: unknown) => {
|
|
expect(JSON.parse(String(logged))).toEqual({
|
|
entities: [],
|
|
relations: [],
|
|
});
|
|
},
|
|
],
|
|
[
|
|
'text prefers text over markdown/json',
|
|
'text',
|
|
{
|
|
content: [
|
|
{ type: 'text', text: 'plain text wins' },
|
|
{ type: 'markdown', text: '# heading' },
|
|
{ type: 'json', json: { source: 'json' } },
|
|
],
|
|
},
|
|
(logged: unknown) => {
|
|
expect(logged).toBe('plain text wins\n# heading');
|
|
},
|
|
],
|
|
[
|
|
'markdown prefers markdown content',
|
|
'markdown',
|
|
{
|
|
content: [
|
|
{ type: 'text', text: 'plain text' },
|
|
{ type: 'markdown', text: '## markdown wins' },
|
|
],
|
|
},
|
|
(logged: unknown) => {
|
|
expect(logged).toBe('## markdown wins');
|
|
},
|
|
],
|
|
[
|
|
'json falls back to raw output when no JSON candidate exists',
|
|
'json',
|
|
'raw-only-string',
|
|
(logged: unknown) => {
|
|
expect(logged).toBe('"raw-only-string"');
|
|
},
|
|
],
|
|
[
|
|
'json emits valid JSON for object raw fallback instead of inspect output',
|
|
'json',
|
|
{ content: [{ type: 'text', text: 'no json here' }] },
|
|
(logged: unknown) => {
|
|
expect(JSON.parse(String(logged))).toEqual({ content: [{ type: 'text', text: 'no json here' }] });
|
|
},
|
|
],
|
|
[
|
|
'json emits valid JSON for MCP error envelopes instead of inspect output',
|
|
'json',
|
|
{ content: [{ type: 'text', text: 'MCP error -32602: Tool search not found' }], isError: true },
|
|
(logged: unknown) => {
|
|
expect(JSON.parse(String(logged))).toEqual({
|
|
content: [{ type: 'text', text: 'MCP error -32602: Tool search not found' }],
|
|
isError: true,
|
|
});
|
|
},
|
|
],
|
|
[
|
|
'json emits null for undefined raw fallback',
|
|
'json',
|
|
undefined,
|
|
(logged: unknown) => {
|
|
expect(logged).toBe('null');
|
|
},
|
|
],
|
|
[
|
|
'json emits a JSON string when raw fallback is circular',
|
|
'json',
|
|
(() => {
|
|
const circular: { self?: unknown } = {};
|
|
circular.self = circular;
|
|
return circular;
|
|
})(),
|
|
(logged: unknown) => {
|
|
expect(typeof logged).toBe('string');
|
|
expect(() => JSON.parse(String(logged))).not.toThrow();
|
|
},
|
|
],
|
|
[
|
|
'raw prints inspect output even when json exists',
|
|
'raw',
|
|
{ content: [{ type: 'json', json: { id: 1 } }] },
|
|
(logged: unknown) => {
|
|
expect(String(logged)).toContain("type: 'json'");
|
|
},
|
|
],
|
|
[
|
|
'auto falls back to readable raw output for plain object payloads',
|
|
'auto',
|
|
{ result: 'Available pages for facebook/react' },
|
|
(logged: unknown) => {
|
|
expect(String(logged)).toContain("result: 'Available pages for facebook/react'");
|
|
},
|
|
],
|
|
] as const)('%s', (_name, format, raw, assertLogged) => {
|
|
const wrapped = createCallResult(raw);
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
try {
|
|
printCallOutput(wrapped, raw, format);
|
|
expect(log).toHaveBeenCalledTimes(1);
|
|
const logged = log.mock.calls[0]?.[0];
|
|
assertLogged(logged);
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('printCallOutput raw output', () => {
|
|
it('does not truncate long strings when printing raw output', () => {
|
|
const longText = 'x'.repeat(15000);
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
const raw = { t: longText };
|
|
const wrapped = createCallResult(raw);
|
|
|
|
try {
|
|
printCallOutput(wrapped, raw, 'raw');
|
|
|
|
expect(log).toHaveBeenCalledTimes(1);
|
|
const logged = log.mock.calls[0]?.[0];
|
|
expect(typeof logged).toBe('string');
|
|
expect(logged).not.toContain('... 5000 more characters');
|
|
expect(logged).toContain(longText.slice(-50));
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('prints nested values beyond the default inspect depth', () => {
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
const raw = {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
level4: {
|
|
level5: {
|
|
level6: {
|
|
leaf: 'done',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const wrapped = createCallResult(raw);
|
|
|
|
try {
|
|
printCallOutput(wrapped, raw, 'raw');
|
|
|
|
expect(log).toHaveBeenCalledTimes(1);
|
|
const logged = log.mock.calls[0]?.[0];
|
|
expect(typeof logged).toBe('string');
|
|
expect(logged).toContain("leaf: 'done'");
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
});
|