Files
SnowLuma/packages/mcp/tests/tracing.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

275 lines
11 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { createCallTrace } from '../src/tracing';
import { makeHttpClient } from '../src/client';
import { callTool } from '../src/tools';
const parent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const directories: string[] = [];
afterEach(() => {
vi.restoreAllMocks();
for (const directory of directories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
function directory(): string {
const value = fs.mkdtempSync(path.join(os.tmpdir(), 'snowluma-mcp-trace-'));
directories.push(value);
return value;
}
function streamResponse(data: unknown, failed = false): Response {
return new Response(
`${JSON.stringify({ status: failed ? 'failed' : 'ok', retcode: failed ? 100 : 0, data })}\r\n\r\n`,
);
}
describe('tool call trace context', () => {
it('preserves a valid trace ID and sampling flag with a fresh span for every call', () => {
const first = createCallTrace({ traceparent: parent, tracestate: 'vendor=opaque' });
const second = createCallTrace({ traceparent: parent });
expect(first.traceparent).toMatch(/^00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01$/);
expect(first.traceparent).not.toBe(parent);
expect(second.traceparent).not.toBe(first.traceparent);
expect(first.tracestate).toBe('vendor=opaque');
});
it('creates independent nonzero IDs without inbound context and does not copy baggage', () => {
const first = createCallTrace({ baggage: 'private=value' });
const second = createCallTrace();
expect(first.traceparent).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-00$/);
expect(first.traceparent.slice(3, 35)).not.toBe('0'.repeat(32));
expect(first.traceparent.slice(36, 52)).not.toBe('0'.repeat(16));
expect(second.traceparent.slice(3, 35)).not.toBe(first.traceparent.slice(3, 35));
expect(first).not.toHaveProperty('baggage');
});
it.each([
null,
123,
parent.toUpperCase(),
parent.replace(/^00/, 'ff'),
parent.replace(parent.slice(3, 35), '0'.repeat(32)),
parent.replace(parent.slice(36, 52), '0'.repeat(16)),
`${parent}-extra`,
`${parent}\n`,
`${parent}\nprivate-secret`,
`${parent},${parent}`,
])('ignores malformed traceparent %j and drops its tracestate', (traceparent) => {
const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined);
const result = createCallTrace({ traceparent, tracestate: 'vendor=private-secret' });
expect(result.traceparent.slice(3, 35)).not.toBe(parent.slice(3, 35));
expect(result.tracestate).toBeUndefined();
expect(diagnostic).toHaveBeenCalledWith(
'[snowluma-mcp] trace metadata ignored: invalid traceparent',
);
expect(JSON.stringify(diagnostic.mock.calls)).not.toContain('private-secret');
});
it('parses future versions using known fields and clears unknown flags', () => {
const result = createCallTrace({ traceparent: `01-${parent.slice(3, 53)}ff-future` });
expect(result.traceparent).toMatch(/^00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01$/);
});
it('accepts W3C tracestate members and normalizes optional whitespace and empty members', () => {
const result = createCallTrace({
traceparent: parent,
tracestate: ' vendor=opaque , , 1tenant@system=value , other=leading space',
});
expect(result.tracestate).toBe('vendor=opaque,1tenant@system=value,other=leading space');
expect(
createCallTrace({ traceparent: parent, tracestate: ' , ' }).tracestate,
).toBeUndefined();
});
it.each([
123,
'vendor=a,vendor=b',
'Vendor=a',
'1vendor=a',
'vendor=',
'vendor=a=b',
'vendor=private\r\nsecret',
'vendor=private\n',
`vendor=${'a'.repeat(257)}`,
Array.from({ length: 33 }, (_, i) => `key${i}=a`).join(','),
`a=${'x'.repeat(256)},b=${'x'.repeat(256)}`,
])('drops invalid tracestate %j without breaking the trace', (tracestate) => {
const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined);
const result = createCallTrace({ traceparent: parent, tracestate });
expect(result.traceparent.slice(3, 35)).toBe(parent.slice(3, 35));
expect(result.tracestate).toBeUndefined();
expect(diagnostic.mock.calls).toEqual([
['[snowluma-mcp] trace metadata ignored: invalid tracestate'],
]);
});
it('drops tracestate without a parent and reports only a fixed diagnosis', () => {
const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined);
expect(createCallTrace({ tracestate: 'vendor=secret' }).tracestate).toBeUndefined();
expect(diagnostic.mock.calls).toEqual([
['[snowluma-mcp] trace metadata ignored: tracestate without valid traceparent'],
]);
});
});
describe('trace propagation through tool execution', () => {
it('isolates concurrent ordinary and stream headers without changing payloads or authentication', async () => {
const calls: Array<{ headers: Headers; body: { action: string; params: unknown } }> = [];
const allStarted = Promise.withResolvers<void>();
const fetcher: typeof fetch = async (_input, init) => {
const body = JSON.parse(String(init?.body));
calls.push({ headers: new Headers(init?.headers), body });
if (calls.length === 3) {
allStarted.resolve();
}
await allStarted.promise;
return body.action === 'test_download_stream'
? streamResponse({ type: 'response', data: 'complete' })
: Response.json({ status: 'ok', retcode: 0, data: { online: true } });
};
const client = makeHttpClient({
endpoint: 'https://unused.invalid',
accessToken: 'credential',
streamDir: directory(),
fetch: fetcher,
});
const first = createCallTrace({ traceparent: parent, tracestate: 'vendor=one' });
const second = createCallTrace();
const outcomes = await Promise.all([
callTool(
'query_action',
{ action: 'get_status', params: { marker: 'first' } },
{ mode: 'read', client, trace: first },
),
callTool(
'invoke_action',
{ action: 'test_download_stream', params: { marker: 'second' } },
{ mode: 'write', client, trace: second },
),
client.call('get_status', { marker: 'untraced' }),
]);
expect(outcomes).toHaveLength(3);
expect(calls[0]?.headers.get('traceparent')).toBe(first.traceparent);
expect(calls[0]?.headers.get('tracestate')).toBe('vendor=one');
expect(calls[1]?.headers.get('traceparent')).toBe(second.traceparent);
expect(calls[1]?.headers.has('tracestate')).toBe(false);
expect(calls[2]?.headers.has('traceparent')).toBe(false);
for (const call of calls) {
expect(call.headers.get('authorization')).toBe('Bearer credential');
expect(call.headers.has('baggage')).toBe(false);
expect(Object.keys(call.body)).toEqual(['action', 'params']);
expect(Object.keys(call.body.params as object)).toEqual(['marker']);
}
});
it('keeps trace metadata outside action permissions and ordinary result envelopes', async () => {
const fetcher = vi.fn<typeof fetch>(async () =>
Response.json({ status: 'failed', retcode: 123, wording: 'denied', data: null }),
);
const client = makeHttpClient({
endpoint: 'https://unused.invalid',
streamDir: directory(),
fetch: fetcher,
});
const trace = createCallTrace();
const denied = await callTool(
'invoke_action',
{ action: 'send_group_msg' },
{ mode: 'read', client, trace },
);
expect(denied.structuredContent.diagnostics.kind).toBe('permission');
expect(fetcher).not.toHaveBeenCalled();
const result = await callTool(
'query_action',
{ action: 'get_status' },
{ mode: 'read', client, trace },
);
expect(result.isError).toBeUndefined();
expect(result.structuredContent.result).toEqual({
status: 'failed',
retcode: 123,
wording: 'denied',
data: null,
});
expect(result.content[0]).toEqual({
type: 'text',
text: JSON.stringify(result.structuredContent.result, null, 2),
});
});
it.each([false, true])(
'uses one call trace for every upload chunk and completion or cleanup (failure: %s)',
async (fail) => {
const root = directory();
const input = path.join(root, 'upload.bin');
const bytes = Buffer.alloc(512 * 1024 + 1, 97);
fs.writeFileSync(input, bytes);
const trace = createCallTrace({ traceparent: parent, tracestate: 'vendor=transfer' });
const calls: Array<{ headers: Headers; params: Record<string, unknown> }> = [];
const fetcher: typeof fetch = async (_input, init) => {
const { params } = JSON.parse(String(init?.body));
calls.push({ headers: new Headers(init?.headers), params });
if (params.reset) {
return streamResponse({ type: 'response' });
}
if (fail && params.chunk_index === 1) {
return streamResponse({ type: 'error' }, true);
}
return streamResponse(
params.is_complete
? {
type: 'response',
status: 'file_complete',
file_path: '/server/upload.bin',
file_size: bytes.length,
sha256: createHash('sha256').update(bytes).digest('hex'),
}
: { type: 'stream', status: 'chunk_received' },
);
};
const client = makeHttpClient({
endpoint: 'https://unused.invalid',
streamDir: root,
uploadRoot: root,
fetch: fetcher,
});
const result = await callTool(
'invoke_action',
{
action: 'upload_file_stream',
execution: { input_file: input },
},
{ mode: 'write', client, trace },
);
expect(result.isError === true).toBe(fail);
expect(calls).toHaveLength(3);
expect(calls[2]?.params[fail ? 'reset' : 'is_complete']).toBe(true);
for (const call of calls) {
expect(call.headers.get('traceparent')).toBe(trace.traceparent);
expect(call.headers.get('tracestate')).toBe(trace.tracestate);
expect(call.params).not.toHaveProperty('traceparent');
expect(call.params).not.toHaveProperty('tracestate');
expect(call.headers.has('baggage')).toBe(false);
}
},
);
});