mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-02 02:58:10 +02:00
init
This commit is contained in:
parent
c386f68743
commit
b6536eca38
100 changed files with 17680 additions and 377 deletions
221
ts/packages/client/src/__tests__/flows-api.test.ts
Normal file
221
ts/packages/client/src/__tests__/flows-api.test.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { FlowsApi } from "../socket/trustgraph-socket";
|
||||
import { FlowResponse } from "../models/messages";
|
||||
|
||||
describe("FlowsApi", () => {
|
||||
let mockApi: {
|
||||
makeRequest: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let flowsApi: FlowsApi;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApi = {
|
||||
makeRequest: vi.fn(),
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
flowsApi = new FlowsApi(mockApi as any);
|
||||
});
|
||||
|
||||
describe("startFlow", () => {
|
||||
it("should call makeRequest with correct types and parameters", async () => {
|
||||
const mockResponse: FlowResponse = {
|
||||
flow: "started",
|
||||
description: "Flow started successfully",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.startFlow(
|
||||
"test-flow-id",
|
||||
"test-class",
|
||||
"Test description",
|
||||
);
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "start-flow",
|
||||
"flow-id": "test-flow-id",
|
||||
"blueprint-name": "test-class",
|
||||
description: "Test description",
|
||||
},
|
||||
30000,
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should use FlowRequest and FlowResponse types", async () => {
|
||||
const mockResponse: FlowResponse = {};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
await flowsApi.startFlow("id", "class", "desc");
|
||||
|
||||
// Verify the call signature matches FlowRequest/FlowResponse types
|
||||
const callArgs = mockApi.makeRequest.mock.calls[0];
|
||||
const request = callArgs[1];
|
||||
|
||||
// These properties should match FlowRequest interface
|
||||
expect(request).toHaveProperty("operation");
|
||||
expect(request).toHaveProperty("flow-id");
|
||||
expect(request).toHaveProperty("blueprint-name");
|
||||
expect(request).toHaveProperty("description");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopFlow", () => {
|
||||
it("should call makeRequest with correct types and parameters", async () => {
|
||||
const mockResponse: FlowResponse = {
|
||||
flow: "stopped",
|
||||
description: "Flow stopped successfully",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.stopFlow("test-flow-id");
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "stop-flow",
|
||||
"flow-id": "test-flow-id",
|
||||
},
|
||||
30000,
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should use FlowRequest and FlowResponse types", async () => {
|
||||
const mockResponse: FlowResponse = {};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
await flowsApi.stopFlow("id");
|
||||
|
||||
// Verify the call signature matches FlowRequest/FlowResponse types
|
||||
const callArgs = mockApi.makeRequest.mock.calls[0];
|
||||
const request = callArgs[1];
|
||||
|
||||
// These properties should match FlowRequest interface
|
||||
expect(request).toHaveProperty("operation");
|
||||
expect(request).toHaveProperty("flow-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlows", () => {
|
||||
it("should return flow-ids array from response", async () => {
|
||||
const mockResponse: FlowResponse = {
|
||||
"flow-ids": ["flow1", "flow2", "flow3"],
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlows();
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "list-flows",
|
||||
},
|
||||
60000,
|
||||
);
|
||||
expect(result).toEqual(["flow1", "flow2", "flow3"]);
|
||||
});
|
||||
|
||||
it("should return empty array when flow-ids is undefined", async () => {
|
||||
const mockResponse: FlowResponse = {};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlows();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle response with flow-ids property correctly", async () => {
|
||||
// This test ensures we're accessing the hyphenated property name correctly
|
||||
const mockResponse = {
|
||||
"flow-ids": ["test-flow"],
|
||||
"other-property": "should-be-ignored",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlows();
|
||||
|
||||
expect(result).toEqual(["test-flow"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlowBlueprints", () => {
|
||||
it("should return blueprint-names array from response", async () => {
|
||||
const mockResponse: FlowResponse = {
|
||||
"blueprint-names": ["class1", "class2"],
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlowBlueprints();
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "list-blueprints",
|
||||
},
|
||||
60000,
|
||||
);
|
||||
expect(result).toEqual(["class1", "class2"]);
|
||||
});
|
||||
|
||||
it("should handle response with blueprint-names property correctly", async () => {
|
||||
// This test ensures we're accessing the hyphenated property name correctly
|
||||
const mockResponse = {
|
||||
"blueprint-names": ["test-class"],
|
||||
"other-property": "should-be-ignored",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlowBlueprints();
|
||||
|
||||
expect(result).toEqual(["test-class"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlow", () => {
|
||||
it("should call makeRequest with correct parameters and parse JSON", async () => {
|
||||
const flowDefinition = { type: "flow", config: "test" };
|
||||
const mockResponse: FlowResponse = {
|
||||
flow: JSON.stringify(flowDefinition), // Must be valid JSON string
|
||||
description: "Test flow",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlow("test-flow-id");
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "get-flow",
|
||||
"flow-id": "test-flow-id",
|
||||
},
|
||||
60000,
|
||||
);
|
||||
expect(result).toEqual(flowDefinition); // Result should be parsed JSON
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlowBlueprint", () => {
|
||||
it("should call makeRequest with correct parameters and parse JSON", async () => {
|
||||
const blueprintDefinition = { type: "blueprint", name: "test-blueprint" };
|
||||
const mockResponse: FlowResponse = {
|
||||
"blueprint-definition": JSON.stringify(blueprintDefinition), // Must be valid JSON string
|
||||
description: "Test blueprint",
|
||||
};
|
||||
mockApi.makeRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await flowsApi.getFlowBlueprint("test-class");
|
||||
|
||||
expect(mockApi.makeRequest).toHaveBeenCalledWith(
|
||||
"flow",
|
||||
{
|
||||
operation: "get-blueprint",
|
||||
"blueprint-name": "test-class",
|
||||
},
|
||||
60000,
|
||||
);
|
||||
expect(result).toEqual(blueprintDefinition); // Result should be parsed JSON
|
||||
});
|
||||
});
|
||||
});
|
||||
370
ts/packages/client/src/__tests__/messages.test.ts
Normal file
370
ts/packages/client/src/__tests__/messages.test.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import type {
|
||||
RequestMessage,
|
||||
ApiResponse,
|
||||
TextCompletionRequest,
|
||||
TextCompletionResponse,
|
||||
GraphRagRequest,
|
||||
GraphRagResponse,
|
||||
AgentRequest,
|
||||
AgentResponse,
|
||||
EmbeddingsRequest,
|
||||
EmbeddingsResponse,
|
||||
GraphEmbeddingsQueryRequest,
|
||||
GraphEmbeddingsQueryResponse,
|
||||
TriplesQueryRequest,
|
||||
LoadDocumentRequest,
|
||||
LoadTextRequest,
|
||||
LibraryRequest,
|
||||
LibraryResponse,
|
||||
FlowRequest,
|
||||
FlowResponse,
|
||||
DocumentMetadata,
|
||||
ProcessingMetadata,
|
||||
} from "../models/messages";
|
||||
|
||||
describe("Message Types", () => {
|
||||
describe("RequestMessage", () => {
|
||||
it("should have correct structure", () => {
|
||||
const message: RequestMessage = {
|
||||
id: "test-id",
|
||||
service: "test-service",
|
||||
request: { test: "data" },
|
||||
};
|
||||
|
||||
expect(message.id).toBe("test-id");
|
||||
expect(message.service).toBe("test-service");
|
||||
expect(message.request).toEqual({ test: "data" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("ApiResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: ApiResponse = {
|
||||
id: "test-id",
|
||||
response: { result: "success" },
|
||||
};
|
||||
|
||||
expect(response.id).toBe("test-id");
|
||||
expect(response.response).toEqual({ result: "success" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextCompletionRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: TextCompletionRequest = {
|
||||
system: "You are a helpful assistant",
|
||||
prompt: "Hello, world!",
|
||||
};
|
||||
|
||||
expect(request.system).toBe("You are a helpful assistant");
|
||||
expect(request.prompt).toBe("Hello, world!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextCompletionResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: TextCompletionResponse = {
|
||||
response: "Hello! How can I help you today?",
|
||||
};
|
||||
|
||||
expect(response.response).toBe("Hello! How can I help you today?");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GraphRagRequest", () => {
|
||||
it("should have correct structure with required query", () => {
|
||||
const request: GraphRagRequest = {
|
||||
query: "What is the capital of France?",
|
||||
};
|
||||
|
||||
expect(request.query).toBe("What is the capital of France?");
|
||||
});
|
||||
|
||||
it("should have correct structure with optional parameters", () => {
|
||||
const request: GraphRagRequest = {
|
||||
query: "What is the capital of France?",
|
||||
"entity-limit": 100,
|
||||
"triple-limit": 50,
|
||||
"max-subgraph-size": 2000,
|
||||
"max-path-length": 3,
|
||||
};
|
||||
|
||||
expect(request.query).toBe("What is the capital of France?");
|
||||
expect(request["entity-limit"]).toBe(100);
|
||||
expect(request["triple-limit"]).toBe(50);
|
||||
expect(request["max-subgraph-size"]).toBe(2000);
|
||||
expect(request["max-path-length"]).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GraphRagResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: GraphRagResponse = {
|
||||
response: "The capital of France is Paris.",
|
||||
};
|
||||
|
||||
expect(response.response).toBe("The capital of France is Paris.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: AgentRequest = {
|
||||
question: "What is the weather like today?",
|
||||
};
|
||||
|
||||
expect(request.question).toBe("What is the weather like today?");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentResponse", () => {
|
||||
it("should have correct structure with all fields", () => {
|
||||
const response: AgentResponse = {
|
||||
thought: "I need to check the weather",
|
||||
observation: "Weather API shows sunny conditions",
|
||||
answer: "It is sunny today",
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
expect(response.thought).toBe("I need to check the weather");
|
||||
expect(response.observation).toBe("Weather API shows sunny conditions");
|
||||
expect(response.answer).toBe("It is sunny today");
|
||||
expect(response.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle error response", () => {
|
||||
const response: AgentResponse = {
|
||||
error: { type: "agent-error", message: "Weather service unavailable" },
|
||||
};
|
||||
|
||||
expect(response.error?.message).toBe("Weather service unavailable");
|
||||
expect(response.error?.type).toBe("agent-error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EmbeddingsRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: EmbeddingsRequest = {
|
||||
texts: ["This is a test sentence for embedding", "Another text"],
|
||||
};
|
||||
|
||||
expect(request.texts).toEqual(["This is a test sentence for embedding", "Another text"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EmbeddingsResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
// vectors[text_index][dimension_index] - one vector per input text
|
||||
const response: EmbeddingsResponse = {
|
||||
vectors: [
|
||||
[0.1, 0.2, 0.3], // First text's vector
|
||||
[0.4, 0.5, 0.6], // Second text's vector
|
||||
],
|
||||
};
|
||||
|
||||
expect(response.vectors).toEqual([
|
||||
[0.1, 0.2, 0.3],
|
||||
[0.4, 0.5, 0.6],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GraphEmbeddingsQueryRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: GraphEmbeddingsQueryRequest = {
|
||||
vector: [0.1, 0.2, 0.3],
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
expect(request.vector).toEqual([0.1, 0.2, 0.3]);
|
||||
expect(request.limit).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GraphEmbeddingsQueryResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: GraphEmbeddingsQueryResponse = {
|
||||
entities: [
|
||||
{ entity: { t: "i", i: "http://example.org/entity1" }, score: 0.95 },
|
||||
{ entity: { t: "i", i: "http://example.org/entity2" }, score: 0.87 },
|
||||
],
|
||||
};
|
||||
|
||||
expect(response.entities).toHaveLength(2);
|
||||
expect(response.entities[0].score).toBe(0.95);
|
||||
expect(response.entities[0].entity?.t).toBe("i");
|
||||
expect((response.entities[0].entity as { t: "i"; i: string }).i).toBe("http://example.org/entity1");
|
||||
expect(response.entities[1].score).toBe(0.87);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TriplesQueryRequest", () => {
|
||||
it("should have correct structure with all fields", () => {
|
||||
const request: TriplesQueryRequest = {
|
||||
s: { t: "i", i: "http://example.org/subject" },
|
||||
p: { t: "i", i: "http://example.org/predicate" },
|
||||
o: { t: "l", v: "object value" },
|
||||
limit: 100,
|
||||
};
|
||||
|
||||
expect((request.s as { t: "i"; i: string }).i).toBe("http://example.org/subject");
|
||||
expect((request.p as { t: "i"; i: string }).i).toBe("http://example.org/predicate");
|
||||
expect((request.o as { t: "l"; v: string }).v).toBe("object value");
|
||||
expect(request.limit).toBe(100);
|
||||
});
|
||||
|
||||
it("should handle optional fields", () => {
|
||||
const request: TriplesQueryRequest = {
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
expect(request.s).toBeUndefined();
|
||||
expect(request.p).toBeUndefined();
|
||||
expect(request.o).toBeUndefined();
|
||||
expect(request.limit).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoadDocumentRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: LoadDocumentRequest = {
|
||||
id: "doc-123",
|
||||
data: "base64-encoded-document-data",
|
||||
metadata: [
|
||||
{
|
||||
s: { t: "i", i: "http://example.org/doc-123" },
|
||||
p: { t: "i", i: "http://example.org/title" },
|
||||
o: { t: "l", v: "Test Document" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(request.id).toBe("doc-123");
|
||||
expect(request.data).toBe("base64-encoded-document-data");
|
||||
expect(request.metadata).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoadTextRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: LoadTextRequest = {
|
||||
id: "text-123",
|
||||
text: "This is some text to load",
|
||||
charset: "utf-8",
|
||||
metadata: [],
|
||||
};
|
||||
|
||||
expect(request.id).toBe("text-123");
|
||||
expect(request.text).toBe("This is some text to load");
|
||||
expect(request.charset).toBe("utf-8");
|
||||
expect(request.metadata).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DocumentMetadata", () => {
|
||||
it("should have correct structure", () => {
|
||||
const metadata: DocumentMetadata = {
|
||||
id: "doc-123",
|
||||
time: 1640995200000,
|
||||
kind: "pdf",
|
||||
title: "Test Document",
|
||||
comments: "A test document",
|
||||
metadata: [],
|
||||
user: "test-user",
|
||||
tags: ["test", "document"],
|
||||
};
|
||||
|
||||
expect(metadata.id).toBe("doc-123");
|
||||
expect(metadata.time).toBe(1640995200000);
|
||||
expect(metadata.kind).toBe("pdf");
|
||||
expect(metadata.title).toBe("Test Document");
|
||||
expect(metadata.comments).toBe("A test document");
|
||||
expect(metadata.user).toBe("test-user");
|
||||
expect(metadata.tags).toEqual(["test", "document"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProcessingMetadata", () => {
|
||||
it("should have correct structure", () => {
|
||||
const metadata: ProcessingMetadata = {
|
||||
id: "proc-123",
|
||||
"document-id": "doc-123",
|
||||
time: 1640995200000,
|
||||
flow: "default-flow",
|
||||
user: "test-user",
|
||||
collection: "test-collection",
|
||||
tags: ["processing", "test"],
|
||||
};
|
||||
|
||||
expect(metadata.id).toBe("proc-123");
|
||||
expect(metadata["document-id"]).toBe("doc-123");
|
||||
expect(metadata.time).toBe(1640995200000);
|
||||
expect(metadata.flow).toBe("default-flow");
|
||||
expect(metadata.user).toBe("test-user");
|
||||
expect(metadata.collection).toBe("test-collection");
|
||||
expect(metadata.tags).toEqual(["processing", "test"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LibraryRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: LibraryRequest = {
|
||||
operation: "list_documents",
|
||||
user: "test-user",
|
||||
collection: "test-collection",
|
||||
};
|
||||
|
||||
expect(request.operation).toBe("list_documents");
|
||||
expect(request.user).toBe("test-user");
|
||||
expect(request.collection).toBe("test-collection");
|
||||
});
|
||||
});
|
||||
|
||||
describe("LibraryResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: LibraryResponse = {
|
||||
error: new Error(),
|
||||
"document-metadatas": [
|
||||
{
|
||||
id: "doc-1",
|
||||
title: "Document 1",
|
||||
time: 1640995200000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(response.error).toBeInstanceOf(Error);
|
||||
expect(response["document-metadatas"]).toHaveLength(1);
|
||||
expect(response["document-metadatas"]![0].id).toBe("doc-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FlowRequest", () => {
|
||||
it("should have correct structure", () => {
|
||||
const request: FlowRequest = {
|
||||
operation: "get_flow",
|
||||
"flow-id": "default-flow",
|
||||
};
|
||||
|
||||
expect(request.operation).toBe("get_flow");
|
||||
expect(request["flow-id"]).toBe("default-flow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FlowResponse", () => {
|
||||
it("should have correct structure", () => {
|
||||
const response: FlowResponse = {
|
||||
"flow-ids": ["flow-1", "flow-2"],
|
||||
flow: "flow-definition",
|
||||
description: "A test flow",
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
expect(response["flow-ids"]).toEqual(["flow-1", "flow-2"]);
|
||||
expect(response.flow).toBe("flow-definition");
|
||||
expect(response.description).toBe("A test flow");
|
||||
expect(response.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
285
ts/packages/client/src/__tests__/service-call-multi.test.ts
Normal file
285
ts/packages/client/src/__tests__/service-call-multi.test.ts
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ServiceCallMulti } from "../socket/service-call-multi";
|
||||
|
||||
// Mock WebSocket constants
|
||||
vi.stubGlobal("WebSocket", {
|
||||
OPEN: 1,
|
||||
CONNECTING: 0,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3,
|
||||
});
|
||||
|
||||
// Mock Socket interface
|
||||
const mockSocket = {
|
||||
inflight: {} as Record<string, unknown>,
|
||||
ws: {
|
||||
send: vi.fn(),
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
},
|
||||
reopen: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock setTimeout and clearTimeout
|
||||
const mockSetTimeout = vi.fn();
|
||||
const mockClearTimeout = vi.fn();
|
||||
|
||||
vi.stubGlobal("setTimeout", mockSetTimeout);
|
||||
vi.stubGlobal("clearTimeout", mockClearTimeout);
|
||||
|
||||
describe("ServiceCallMulti", () => {
|
||||
let mockSuccess: ReturnType<typeof vi.fn>;
|
||||
let mockError: ReturnType<typeof vi.fn>;
|
||||
let mockReceiver: ReturnType<typeof vi.fn>;
|
||||
let serviceCallMulti: ServiceCallMulti;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSuccess = vi.fn();
|
||||
mockError = vi.fn();
|
||||
mockReceiver = vi.fn();
|
||||
mockSocket.inflight = {} as Record<string, unknown>;
|
||||
mockSocket.ws = {
|
||||
send: vi.fn(),
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
};
|
||||
mockSocket.reopen.mockClear();
|
||||
|
||||
serviceCallMulti = new ServiceCallMulti(
|
||||
"test-mid",
|
||||
{ id: "test-id", service: "test-service", request: { test: "data" } },
|
||||
mockSuccess,
|
||||
mockError,
|
||||
5000, // 5 second timeout
|
||||
3, // 3 retries
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockSocket as any,
|
||||
mockReceiver,
|
||||
);
|
||||
});
|
||||
|
||||
it("should initialize with correct properties", () => {
|
||||
expect(serviceCallMulti.mid).toBe("test-mid");
|
||||
expect(serviceCallMulti.timeout).toBe(5000);
|
||||
expect(serviceCallMulti.retries).toBe(3);
|
||||
expect(serviceCallMulti.complete).toBe(false);
|
||||
expect(serviceCallMulti.socket).toBe(mockSocket);
|
||||
expect(serviceCallMulti.receiver).toBe(mockReceiver);
|
||||
});
|
||||
|
||||
it("should register itself in socket inflight when started", () => {
|
||||
serviceCallMulti.start();
|
||||
|
||||
expect(mockSocket.inflight["test-mid"]).toBe(serviceCallMulti);
|
||||
});
|
||||
|
||||
it("should send message on successful attempt", () => {
|
||||
serviceCallMulti.start();
|
||||
|
||||
expect(mockSocket.ws.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
id: "test-id",
|
||||
service: "test-service",
|
||||
request: { test: "data" },
|
||||
}),
|
||||
);
|
||||
expect(mockSetTimeout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle response when receiver returns true (completion)", () => {
|
||||
mockReceiver.mockReturnValue(true); // Signal completion
|
||||
const response = { result: "success" };
|
||||
|
||||
serviceCallMulti.start();
|
||||
serviceCallMulti.onReceived(response);
|
||||
|
||||
expect(mockReceiver).toHaveBeenCalledWith(response);
|
||||
expect(serviceCallMulti.complete).toBe(true);
|
||||
expect(mockSuccess).toHaveBeenCalledWith(response);
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle response when receiver returns false (continue)", () => {
|
||||
mockReceiver.mockReturnValue(false); // Signal to continue
|
||||
const response = { partial: "data" };
|
||||
|
||||
serviceCallMulti.start();
|
||||
serviceCallMulti.onReceived(response);
|
||||
|
||||
expect(mockReceiver).toHaveBeenCalledWith(response);
|
||||
expect(serviceCallMulti.complete).toBe(false);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
expect(mockClearTimeout).not.toHaveBeenCalled();
|
||||
expect(mockSocket.inflight["test-mid"]).toBe(serviceCallMulti);
|
||||
});
|
||||
|
||||
it("should handle timeout and retry", () => {
|
||||
serviceCallMulti.start();
|
||||
|
||||
// Initial retries should be 3, but start() calls attempt() which decrements to 2
|
||||
expect(serviceCallMulti.retries).toBe(2);
|
||||
|
||||
// Simulate timeout
|
||||
serviceCallMulti.onTimeout();
|
||||
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(serviceCallMulti.retries).toBe(1); // Should decrement from 2 to 1
|
||||
});
|
||||
|
||||
it("should exhaust retries and call error callback", () => {
|
||||
// Set retries to 0 to force immediate failure
|
||||
serviceCallMulti.retries = 0;
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith("Ran out of retries");
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle WebSocket send failure", () => {
|
||||
mockSocket.ws.send.mockImplementation(() => {
|
||||
throw new Error("Connection failed");
|
||||
});
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
expect(mockSocket.reopen).toHaveBeenCalled();
|
||||
|
||||
// With exponential backoff, the delay should be calculated as:
|
||||
// SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - retries) + random
|
||||
// Since retries is decremented to 2 after start(), it's 3 - 2 = 1
|
||||
// So base delay is 2000 * 2^1 = 4000, plus random up to 1000
|
||||
// The delay should be between 4000 and 5000ms (capped at 30000)
|
||||
const callArgs = mockSetTimeout.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(expect.any(Function));
|
||||
expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
|
||||
expect(callArgs[1]).toBeLessThanOrEqual(5000);
|
||||
});
|
||||
|
||||
it("should handle missing WebSocket connection", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(mockSocket as any).ws = null;
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
// Should trigger reopen and schedule with exponential backoff
|
||||
expect(mockSocket.reopen).toHaveBeenCalled();
|
||||
|
||||
// Same calculation as above - base delay 4000ms + random up to 1000ms
|
||||
const callArgs = mockSetTimeout.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(expect.any(Function));
|
||||
expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
|
||||
expect(callArgs[1]).toBeLessThanOrEqual(5000);
|
||||
});
|
||||
|
||||
it("should not process response if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCallMulti.complete = true;
|
||||
serviceCallMulti.onReceived({ result: "test" });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"should not happen, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not timeout if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCallMulti.complete = true;
|
||||
serviceCallMulti.onTimeout();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"timeout should not happen, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not attempt if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCallMulti.complete = true;
|
||||
serviceCallMulti.attempt();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"attempt should not be called, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle streaming responses correctly", () => {
|
||||
mockReceiver
|
||||
.mockReturnValueOnce(false) // First response - continue
|
||||
.mockReturnValueOnce(false) // Second response - continue
|
||||
.mockReturnValueOnce(true); // Third response - complete
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
// First response
|
||||
serviceCallMulti.onReceived({ chunk: 1 });
|
||||
expect(serviceCallMulti.complete).toBe(false);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
|
||||
// Second response
|
||||
serviceCallMulti.onReceived({ chunk: 2 });
|
||||
expect(serviceCallMulti.complete).toBe(false);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
|
||||
// Third response (final)
|
||||
serviceCallMulti.onReceived({ chunk: 3, final: true });
|
||||
expect(serviceCallMulti.complete).toBe(true);
|
||||
expect(mockSuccess).toHaveBeenCalledWith({ chunk: 3, final: true });
|
||||
});
|
||||
|
||||
it("should handle receiver function errors gracefully", () => {
|
||||
mockReceiver.mockImplementation(() => {
|
||||
throw new Error("Receiver error");
|
||||
});
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
expect(() => {
|
||||
serviceCallMulti.onReceived({ test: "data" });
|
||||
}).toThrow("Receiver error");
|
||||
});
|
||||
|
||||
it("should handle multiple timeout scenarios", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
// After start, retries should be 2 (decremented from 3)
|
||||
expect(serviceCallMulti.retries).toBe(2);
|
||||
|
||||
// First timeout
|
||||
serviceCallMulti.onTimeout();
|
||||
expect(serviceCallMulti.retries).toBe(1);
|
||||
|
||||
// Second timeout
|
||||
serviceCallMulti.onTimeout();
|
||||
expect(serviceCallMulti.retries).toBe(0);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should clean up properly when receiver signals completion", () => {
|
||||
mockReceiver.mockReturnValue(true);
|
||||
|
||||
serviceCallMulti.start();
|
||||
|
||||
const response = { final: true };
|
||||
serviceCallMulti.onReceived(response);
|
||||
|
||||
expect(serviceCallMulti.complete).toBe(true);
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
expect(mockSuccess).toHaveBeenCalledWith(response);
|
||||
});
|
||||
});
|
||||
239
ts/packages/client/src/__tests__/service-call.test.ts
Normal file
239
ts/packages/client/src/__tests__/service-call.test.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ServiceCall } from "../socket/service-call";
|
||||
|
||||
// Mock WebSocket constants
|
||||
vi.stubGlobal("WebSocket", {
|
||||
OPEN: 1,
|
||||
CONNECTING: 0,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3,
|
||||
});
|
||||
|
||||
// Mock Socket interface
|
||||
const mockSocket = {
|
||||
inflight: {} as Record<string, unknown>,
|
||||
ws: {
|
||||
send: vi.fn(),
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
},
|
||||
reopen: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock setTimeout and clearTimeout
|
||||
const mockSetTimeout = vi.fn();
|
||||
const mockClearTimeout = vi.fn();
|
||||
|
||||
vi.stubGlobal("setTimeout", mockSetTimeout);
|
||||
vi.stubGlobal("clearTimeout", mockClearTimeout);
|
||||
|
||||
describe("ServiceCall", () => {
|
||||
let mockSuccess: ReturnType<typeof vi.fn>;
|
||||
let mockError: ReturnType<typeof vi.fn>;
|
||||
let serviceCall: ServiceCall;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSuccess = vi.fn();
|
||||
mockError = vi.fn();
|
||||
mockSocket.inflight = {} as Record<string, unknown>;
|
||||
mockSocket.ws = {
|
||||
send: vi.fn(),
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
};
|
||||
mockSocket.reopen.mockClear();
|
||||
|
||||
serviceCall = new ServiceCall(
|
||||
"test-mid",
|
||||
{ id: "test-id", service: "test-service", request: { test: "data" } },
|
||||
mockSuccess,
|
||||
mockError,
|
||||
5000, // 5 second timeout
|
||||
3, // 3 retries
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockSocket as any,
|
||||
);
|
||||
});
|
||||
|
||||
it("should initialize with correct properties", () => {
|
||||
expect(serviceCall.mid).toBe("test-mid");
|
||||
expect(serviceCall.timeout).toBe(5000);
|
||||
expect(serviceCall.retries).toBe(3);
|
||||
expect(serviceCall.complete).toBe(false);
|
||||
expect(serviceCall.socket).toBe(mockSocket);
|
||||
});
|
||||
|
||||
it("should register itself in socket inflight when started", () => {
|
||||
serviceCall.start();
|
||||
|
||||
expect(mockSocket.inflight["test-mid"]).toBe(serviceCall);
|
||||
});
|
||||
|
||||
it("should send message on successful attempt", () => {
|
||||
serviceCall.start();
|
||||
|
||||
expect(mockSocket.ws.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
id: "test-id",
|
||||
service: "test-service",
|
||||
request: { test: "data" },
|
||||
}),
|
||||
);
|
||||
expect(mockSetTimeout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle successful response", () => {
|
||||
const responseData = { result: "success" };
|
||||
const message = { response: responseData };
|
||||
|
||||
serviceCall.start();
|
||||
serviceCall.onReceived(message);
|
||||
|
||||
expect(serviceCall.complete).toBe(true);
|
||||
expect(mockSuccess).toHaveBeenCalledWith(responseData);
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle timeout and retry", () => {
|
||||
serviceCall.start();
|
||||
|
||||
// Initial retries should be 3, but start() calls attempt() which decrements to 2
|
||||
expect(serviceCall.retries).toBe(2);
|
||||
|
||||
// Simulate timeout
|
||||
serviceCall.onTimeout();
|
||||
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(serviceCall.retries).toBe(1); // Should decrement from 2 to 1
|
||||
});
|
||||
|
||||
it("should exhaust retries and call error callback", () => {
|
||||
// Set retries to 0 to force immediate failure
|
||||
serviceCall.retries = 0;
|
||||
|
||||
serviceCall.start();
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith("Ran out of retries");
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle WebSocket send failure", () => {
|
||||
mockSocket.ws.send.mockImplementation(() => {
|
||||
throw new Error("Connection failed");
|
||||
});
|
||||
|
||||
serviceCall.start();
|
||||
|
||||
// Should NOT call reopen anymore - BaseApi handles reconnection
|
||||
expect(mockSocket.reopen).not.toHaveBeenCalled();
|
||||
|
||||
// With exponential backoff, the delay should be calculated as:
|
||||
// SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - retries) + random
|
||||
// Since retries is decremented to 2 after start(), it's 3 - 2 = 1
|
||||
// So base delay is 2000 * 2^1 = 4000, plus random up to 1000
|
||||
// The delay should be between 4000 and 5000ms (capped at 30000)
|
||||
const callArgs = mockSetTimeout.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(expect.any(Function));
|
||||
expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
|
||||
expect(callArgs[1]).toBeLessThanOrEqual(5000);
|
||||
});
|
||||
|
||||
it("should handle missing WebSocket connection", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(mockSocket as any).ws = null;
|
||||
|
||||
serviceCall.start();
|
||||
|
||||
// Should NOT trigger reopen - just wait for BaseApi to reconnect
|
||||
expect(mockSocket.reopen).not.toHaveBeenCalled();
|
||||
|
||||
// Same calculation as above - base delay 4000ms + random up to 1000ms
|
||||
const callArgs = mockSetTimeout.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(expect.any(Function));
|
||||
expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
|
||||
expect(callArgs[1]).toBeLessThanOrEqual(5000);
|
||||
});
|
||||
|
||||
it("should not process response if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCall.complete = true;
|
||||
serviceCall.onReceived({ result: "test" });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"should not happen, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not timeout if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCall.complete = true;
|
||||
serviceCall.onTimeout();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"timeout should not happen, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not attempt if already complete", () => {
|
||||
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
serviceCall.complete = true;
|
||||
serviceCall.attempt();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"test-mid",
|
||||
"attempt should not be called, request is already complete",
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle multiple retries correctly", () => {
|
||||
mockSocket.ws.send.mockImplementation(() => {
|
||||
throw new Error("Connection failed");
|
||||
});
|
||||
|
||||
serviceCall.start();
|
||||
|
||||
// Should have decremented retries and scheduled a retry
|
||||
expect(serviceCall.retries).toBe(2);
|
||||
// Should NOT call reopen - BaseApi handles reconnection
|
||||
expect(mockSocket.reopen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clean up properly on successful response", () => {
|
||||
serviceCall.start();
|
||||
|
||||
const responseData = { success: true };
|
||||
const message = { response: responseData };
|
||||
serviceCall.onReceived(message);
|
||||
|
||||
expect(serviceCall.complete).toBe(true);
|
||||
expect(mockClearTimeout).toHaveBeenCalled();
|
||||
expect(mockSocket.inflight["test-mid"]).toBeUndefined();
|
||||
expect(mockSuccess).toHaveBeenCalledWith(responseData);
|
||||
});
|
||||
|
||||
it("should handle edge case of negative retries", () => {
|
||||
serviceCall.retries = -1;
|
||||
|
||||
serviceCall.attempt();
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith("Ran out of retries");
|
||||
});
|
||||
|
||||
it("should bind timeout callbacks correctly", () => {
|
||||
serviceCall.start();
|
||||
|
||||
// Verify that setTimeout was called with a bound function
|
||||
expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 5000);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue