SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5710dd0 verified
Raw History Blame Contribute Delete
8.53 kB
/**
* Tests for the v2 session list REST client (`GET /api/v2/sessions`): query
* string serialization, `{ code, msg, data }` envelope parsing, and error
* mapping (business codes on 2xx, infra statuses on non-2xx).
*/
import { describe, expect, it } from 'vitest';
import { fetchV2SessionGroups, fetchV2SessionsPage } from './api';
function fakeFetch(status: number, body: unknown) {
const calls: { url: string; init?: RequestInit }[] = [];
const fetchImpl = (async (url: string | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return { ok: status >= 200 && status < 300, status, statusText: 'ERR', json: async () => body };
}) as unknown as typeof fetch;
return { calls, fetchImpl };
}
/** The shared REST envelope the server wraps every response in. */
function okBody(data: unknown) {
return { code: 0, msg: 'success', data, request_id: 'req-1' };
}
function errBody(code: number, msg: string) {
return { code, msg, data: null, request_id: 'req-1' };
}
const pageData = {
items: [
{
id: 's1',
workspace: { id: 'ws1', cwd: '/tmp/proj' },
meta: {
title: '重构会话',
last_prompt: null,
created_at: 1_700_000_000_000,
updated_at: 1_700_000_100_000,
archived: false,
},
activity: { status: 'running' },
git: {
branch: 'main',
pull_request: { number: 12, state: 'draft', url: 'https://example.com/pr/12' },
},
},
{
id: 's2',
workspace: { id: 'ws2', cwd: null },
meta: {
title: null,
last_prompt: 'hello',
created_at: 1_700_000_000_000,
updated_at: 1_700_000_050_000,
archived: true,
},
activity: { status: 'idle' },
},
// Malformed entries are dropped, not fatal.
{ id: 42 },
],
has_more: true,
next_page_token: 'tok-next',
};
describe('fetchV2SessionsPage', () => {
it('serializes filters as repeated query params and maps items to camelCase', async () => {
const { calls, fetchImpl } = fakeFetch(200, okBody(pageData));
const page = await fetchV2SessionsPage({
baseUrl: 'http://h:1',
token: 'tok',
workspaceIds: ['ws1', 'ws2'],
statuses: ['approval', 'question'],
updatedAfter: 1_700_000_000_000,
archived: 'all',
sort: 'meta.created_at_desc',
includeGit: true,
pageSize: 20,
pageToken: 'tok-prev',
fetchImpl,
});
expect(calls).toHaveLength(1);
const url = new URL(calls[0]!.url);
expect(`${url.origin}${url.pathname}`).toBe('http://h:1/api/v2/sessions');
expect(url.searchParams.getAll('workspace.id')).toEqual(['ws1', 'ws2']);
expect(url.searchParams.getAll('activity.status')).toEqual(['approval', 'question']);
expect(url.searchParams.get('meta.updated_after')).toBe('1700000000000');
expect(url.searchParams.get('meta.archived')).toBe('all');
expect(url.searchParams.get('sort')).toBe('meta.created_at_desc');
expect(url.searchParams.get('include')).toBe('git');
expect(url.searchParams.get('page_size')).toBe('20');
expect(url.searchParams.get('page_token')).toBe('tok-prev');
expect(calls[0]!.init?.headers).toEqual({ authorization: 'Bearer tok' });
expect(page.items).toHaveLength(2);
const first = page.items[0]!;
expect(first.id).toBe('s1');
expect(first.workspace).toEqual({ id: 'ws1', cwd: '/tmp/proj' });
expect(first.meta).toEqual({
title: '重构会话',
lastPrompt: null,
createdAt: 1_700_000_000_000,
updatedAt: 1_700_000_100_000,
archived: false,
});
expect(first.activity.status).toBe('running');
// Draft PRs are not in the v2 enum; an unknown state degrades to null.
expect(first.git?.branch).toBe('main');
expect(first.git?.pullRequest).toBeNull();
const second = page.items[1]!;
expect(second.meta.title).toBeNull();
expect(second.meta.lastPrompt).toBe('hello');
expect(second.git).toBeUndefined();
expect(page.hasMore).toBe(true);
expect(page.nextPageToken).toBe('tok-next');
});
it('omits the query string and authorization header when nothing is set', async () => {
const { calls, fetchImpl } = fakeFetch(200, okBody(pageData));
await fetchV2SessionsPage({ baseUrl: 'http://h:1', fetchImpl });
expect(calls[0]!.url).toBe('http://h:1/api/v2/sessions');
expect(calls[0]!.init?.headers).toEqual({});
});
it('parses a well-formed pull_request', async () => {
const { fetchImpl } = fakeFetch(
200,
okBody({
...pageData,
items: [
{
...pageData.items[0],
git: {
branch: 'feat/x',
pull_request: { number: 7, state: 'open', url: 'https://example.com/pr/7' },
},
},
],
}),
);
const page = await fetchV2SessionsPage({ baseUrl: 'http://h:1', includeGit: true, fetchImpl });
expect(page.items[0]!.git?.pullRequest).toEqual({
number: 7,
state: 'open',
url: 'https://example.com/pr/7',
});
});
it('throws the envelope code/msg on a business failure (2xx with code != 0)', async () => {
const { fetchImpl } = fakeFetch(200, errBody(40922, 'page_token is corrupted'));
await expect(
fetchV2SessionsPage({ baseUrl: 'http://h:1', pageToken: 'bad', fetchImpl }),
).rejects.toThrow(/40922.*page_token is corrupted/);
});
it('throws the envelope code/msg on a non-2xx response (e.g. 401 from the auth hook)', async () => {
const { fetchImpl } = fakeFetch(401, errBody(40101, 'unauthorized'));
await expect(fetchV2SessionsPage({ baseUrl: 'http://h:1', fetchImpl })).rejects.toThrow(
/40101.*unauthorized/,
);
});
it('falls back to the HTTP status when the error body is malformed', async () => {
const { fetchImpl } = fakeFetch(500, null);
await expect(fetchV2SessionsPage({ baseUrl: 'http://h:1', fetchImpl })).rejects.toThrow(
/http_500/,
);
});
it('throws on a malformed success payload', async () => {
const { fetchImpl } = fakeFetch(200, okBody({ has_more: false }));
await expect(fetchV2SessionsPage({ baseUrl: 'http://h:1', fetchImpl })).rejects.toThrow(
/unexpected response shape/,
);
});
});
describe('fetchV2SessionGroups', () => {
const groupData = {
groups: [
{
workspace: { id: 'ws1', cwd: '/tmp/proj' },
sessions: [pageData.items[0]],
total: 7,
},
{
workspace: { id: 'ws2', cwd: null },
sessions: [],
total: 0,
},
// Malformed groups are dropped, not fatal.
{ workspace: { cwd: '/x' }, sessions: [], total: 1 },
{ id: 'nope' },
],
total: 3,
has_more: true,
next_page_token: 'tok-groups',
};
it('requests the by_workspace view and parses groups with per-group totals', async () => {
const { calls, fetchImpl } = fakeFetch(200, okBody(groupData));
const page = await fetchV2SessionGroups({
baseUrl: 'http://h:1',
token: 'tok',
statuses: ['running'],
sort: 'meta.updated_at_asc',
pageSize: 10,
groupPageSize: 5,
pageToken: 'tok-prev',
fetchImpl,
});
const url = new URL(calls[0]!.url);
expect(url.searchParams.get('view')).toBe('by_workspace');
expect(url.searchParams.get('group.page_size')).toBe('5');
expect(url.searchParams.get('page_size')).toBe('10');
expect(url.searchParams.get('sort')).toBe('meta.updated_at_asc');
expect(url.searchParams.get('page_token')).toBe('tok-prev');
expect(page.groups).toHaveLength(2);
const first = page.groups[0]!;
expect(first.workspace).toEqual({ id: 'ws1', cwd: '/tmp/proj' });
expect(first.sessions.map((s) => s.id)).toEqual(['s1']);
expect(first.total).toBe(7);
expect(page.groups[1]!.workspace).toEqual({ id: 'ws2', cwd: null });
expect(page.hasMore).toBe(true);
expect(page.nextPageToken).toBe('tok-groups');
});
it('omits group.page_size when not set and throws on a malformed success payload', async () => {
const { calls, fetchImpl } = fakeFetch(200, okBody(groupData));
await fetchV2SessionGroups({ baseUrl: 'http://h:1', fetchImpl });
expect(new URL(calls[0]!.url).searchParams.get('group.page_size')).toBeNull();
const { fetchImpl: broken } = fakeFetch(200, okBody({ has_more: false }));
await expect(fetchV2SessionGroups({ baseUrl: 'http://h:1', fetchImpl: broken })).rejects.toThrow(
/unexpected response shape/,
);
});
});