Files
clawX/tests/unit/session-summaries-route.test.ts
2026-05-13 15:29:44 +08:00

161 lines
4.5 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'http';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { HostApiContext } from '@electron/api/context';
import { handleSessionRoutes } from '@electron/api/routes/sessions';
const readFileMock = vi.fn();
const parseJsonBodyMock = vi.fn();
vi.mock('node:fs/promises', () => ({
readFile: (...args: unknown[]) => readFileMock(...args),
}));
vi.mock('@electron/utils/paths', () => ({
getOpenClawConfigDir: () => '/mock/.openclaw',
}));
vi.mock('@electron/api/route-utils', async () => {
const actual = await vi.importActual<typeof import('@electron/api/route-utils')>('@electron/api/route-utils');
return {
...actual,
parseJsonBody: (...args: unknown[]) => parseJsonBodyMock(...args),
};
});
function createResponse() {
const headers = new Map<string, string>();
let body = '';
const res = {
statusCode: 0,
setHeader: (name: string, value: string) => {
headers.set(name, value);
},
end: (value: string) => {
body = value;
},
} as unknown as ServerResponse;
return {
res,
get json() {
return JSON.parse(body) as { success: boolean; summaries?: Array<Record<string, unknown>> };
},
get statusCode() {
return (res as ServerResponse).statusCode;
},
};
}
describe('POST /api/sessions/summaries', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('strips sender metadata and ignores internal untrusted injections when building titles', async () => {
parseJsonBodyMock.mockResolvedValue({
sessionKeys: ['agent:main:session-a'],
});
readFileMock.mockImplementation(async (path: string) => {
if (path.endsWith('/agents/main/sessions/sessions.json')) {
return JSON.stringify({
sessions: [
{ key: 'agent:main:session-a', file: 'session-a.jsonl' },
],
});
}
if (path.endsWith('/agents/main/sessions/session-a.jsonl')) {
return [
JSON.stringify({
type: 'message',
message: {
role: 'user',
timestamp: 1700000000,
content: 'System (untrusted): internal noise',
},
}),
JSON.stringify({
type: 'message',
message: {
role: 'user',
timestamp: 1700000002,
content: 'Sender (untrusted): Alice\n\nHello from Alice',
},
}),
].join('\n');
}
throw new Error(`Unexpected readFile path: ${path}`);
});
const response = createResponse();
const handled = await handleSessionRoutes(
{ method: 'POST' } as IncomingMessage,
response.res,
new URL('http://127.0.0.1/api/sessions/summaries'),
{} as HostApiContext,
);
expect(handled).toBe(true);
expect(response.statusCode).toBe(200);
expect(response.json).toMatchObject({
success: true,
summaries: [
{
sessionKey: 'agent:main:session-a',
firstUserText: 'Hello from Alice',
lastTimestamp: 1700000002000,
},
],
});
});
it('drops sender json metadata blocks instead of using them as the label', async () => {
parseJsonBodyMock.mockResolvedValue({
sessionKeys: ['agent:main:session-json'],
});
readFileMock.mockImplementation(async (path: string) => {
if (path.endsWith('/agents/main/sessions/sessions.json')) {
return JSON.stringify({
sessions: [
{ key: 'agent:main:session-json', file: 'session-json.jsonl' },
],
});
}
if (path.endsWith('/agents/main/sessions/session-json.jsonl')) {
return [
JSON.stringify({
type: 'message',
message: {
role: 'user',
timestamp: 1700000010,
content: 'Sender (untrusted): ```json\n{"name":"Alice","id":"u1"}\n```\n\nActual user title',
},
}),
].join('\n');
}
throw new Error(`Unexpected readFile path: ${path}`);
});
const response = createResponse();
await handleSessionRoutes(
{ method: 'POST' } as IncomingMessage,
response.res,
new URL('http://127.0.0.1/api/sessions/summaries'),
{} as HostApiContext,
);
expect(response.json).toMatchObject({
success: true,
summaries: [
{
sessionKey: 'agent:main:session-json',
firstUserText: 'Actual user title',
lastTimestamp: 1700000010000,
},
],
});
});
});