n8n/packages/testing/playwright/services/agent-api-helper.ts
2026-09-22 16:21:39 +02:00

114 lines
4.0 KiB
TypeScript

import type { AgentChatMessagesResponse, AgentJsonConfig, AgentSseEvent } from '@n8n/api-types';
import type { ApiHelpers } from './api-helper';
import { N8N_AUTH_COOKIE } from '../config/constants';
import { TestError } from '../Types';
export class AgentApiHelper {
constructor(private readonly api: ApiHelpers) {}
async create(projectId: string, schema: AgentJsonConfig): Promise<{ id: string }> {
const response = await this.api.request.post(`/rest/projects/${projectId}/agents/v2`, {
data: { name: schema.name, schema },
});
if (!response.ok()) throw new TestError(`Failed to create agent: ${await response.text()}`);
return (await response.json()).data;
}
async delete(projectId: string, agentId: string): Promise<void> {
const response = await this.api.request.delete(
`/rest/projects/${projectId}/agents/v2/${agentId}`,
);
if (!response.ok()) throw new TestError(`Failed to delete agent: ${await response.text()}`);
}
async history(
projectId: string,
agentId: string,
threadId: string,
): Promise<AgentChatMessagesResponse> {
const response = await this.api.request.get(
`/rest/projects/${projectId}/agents/v2/${agentId}/chat/${threadId}/messages`,
);
if (!response.ok()) throw new TestError(`Failed to read chat: ${await response.text()}`);
return (await response.json()).data;
}
async executions(
projectId: string,
agentId: string,
threadId: string,
): Promise<Array<{ id: string; status: string }>> {
const response = await this.api.request.get(
`/rest/projects/${projectId}/agents/v2/${agentId}/threads/${threadId}`,
);
if (!response.ok()) throw new TestError(`Failed to read executions: ${await response.text()}`);
return (await response.json()).data.executions;
}
async stop(
projectId: string,
agentId: string,
threadId: string,
executionId: string,
): Promise<{ cancelRequested: boolean }> {
const response = await this.api.request.delete(
`/rest/projects/${projectId}/agents/v2/${agentId}/chat/${threadId}/executions/${executionId}`,
);
if (!response.ok()) throw new TestError(`Failed to stop chat: ${await response.text()}`);
return (await response.json()).data;
}
/** Playwright buffers responses. Read acceptance with fetch, then close only the reader. */
async startAndDisconnect(
baseUrl: string,
projectId: string,
agentId: string,
threadId: string,
): Promise<string> {
const { cookies } = await this.api.request.storageState();
const cookie = cookies.find((entry) => entry.name === N8N_AUTH_COOKIE);
if (!cookie) throw new TestError('Missing authentication cookie');
const controller = new AbortController();
try {
const response = await fetch(
`${baseUrl}/rest/projects/${projectId}/agents/v2/${agentId}/chat`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: `${cookie.name}=${cookie.value}` },
body: JSON.stringify({ sessionId: threadId, message: 'Reply with the word survived.' }),
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)]),
},
);
if (!response.ok || !response.body)
throw new TestError(`Chat returned HTTP ${response.status}`);
return await this.readAcceptance(response.body);
} finally {
controller.abort();
}
}
private async readAcceptance(body: ReadableStream<Uint8Array>): Promise<string> {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) throw new TestError('Chat closed before acceptance');
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const event: AgentSseEvent = JSON.parse(line.slice(6));
if (event.type === 'error') throw new TestError(event.message);
if (event.type === 'execution-started') return event.executionId;
}
}
} finally {
reader.releaseLock();
}
}
}