Advance TS port Effect workbench

This commit is contained in:
elpresidank 2026-06-01 16:22:25 -05:00
parent 92dae8c374
commit 3515106670
116 changed files with 12286 additions and 9584 deletions

View file

@ -1,285 +0,0 @@
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);
});
});

View file

@ -1,239 +0,0 @@
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);
});
});

View file

@ -0,0 +1,195 @@
import { describe, expect, it, vi } from "vitest";
import {
BaseApi,
ConfigApi,
KnowledgeApi,
LibrarianApi,
} from "../socket/trustgraph-socket";
function makeApi() {
const makeRequest = vi.fn();
const base = {
user: "alice",
makeRequest,
} as unknown as BaseApi;
return { base, makeRequest };
}
describe("workbench API contracts", () => {
describe("ConfigApi", () => {
it("returns Python-style getvalues entries", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({
values: [{ type: "prompt", key: "welcome", value: "hello" }],
});
const result = await new ConfigApi(base).getValues("prompt");
expect(makeRequest).toHaveBeenCalledWith(
"config",
{ operation: "getvalues", type: "prompt" },
60000,
);
expect(result).toEqual([{ type: "prompt", key: "welcome", value: "hello" }]);
});
it("parses token-cost values stored as config JSON strings", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({
values: [
{
type: "token-cost",
key: "gpt-test",
value: JSON.stringify({ input_price: 0.1, output_price: 0.2 }),
},
],
});
const result = await new ConfigApi(base).getTokenCosts();
expect(result).toEqual([
{ model: "gpt-test", input_price: 0.1, output_price: 0.2 },
]);
});
it("writes and deletes config using Python-style key/value arrays", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
const config = new ConfigApi(base);
await config.putConfig([{ type: "tool", key: "search", value: "{}" }]);
await config.deleteConfig({ type: "tool", key: "search" });
expect(makeRequest).toHaveBeenNthCalledWith(
1,
"config",
{
operation: "put",
values: [{ type: "tool", key: "search", value: "{}" }],
},
60000,
);
expect(makeRequest).toHaveBeenNthCalledWith(
2,
"config",
{
operation: "delete",
keys: [{ type: "tool", key: "search" }],
},
30000,
);
});
});
describe("LibrarianApi", () => {
it("reads Python-style document and processing list responses", async () => {
const { base, makeRequest } = makeApi();
const document = { id: "doc-1", title: "Document" };
const processing = { id: "proc-1", "document-id": "doc-1" };
const librarian = new LibrarianApi(base);
makeRequest
.mockResolvedValueOnce({ "document-metadatas": [document] })
.mockResolvedValueOnce({ "processing-metadatas": [processing] });
await expect(librarian.getDocuments()).resolves.toEqual([document]);
await expect(librarian.getProcessing()).resolves.toEqual([processing]);
});
it("sends both kebab-case and camel-case document identifiers", async () => {
const { base, makeRequest } = makeApi();
const document = { id: "doc-1", title: "Document" };
makeRequest.mockResolvedValue({ "document-metadata": document });
const result = await new LibrarianApi(base).getDocumentMetadata("doc-1");
expect(makeRequest).toHaveBeenCalledWith(
"librarian",
{
operation: "get-document-metadata",
"document-id": "doc-1",
documentId: "doc-1",
user: "alice",
},
30000,
);
expect(result).toEqual(document);
});
it("uploads documents with Python and TypeScript metadata aliases", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
await new LibrarianApi(base).loadDocument(
"SGVsbG8=",
"text/plain",
"Hello",
"comment",
["tag"],
"doc-1",
);
const request = makeRequest.mock.calls[0]?.[1] as Record<string, unknown>;
expect(request["document-metadata"]).toMatchObject({
id: "doc-1",
kind: "text/plain",
title: "Hello",
user: "alice",
"document-type": "source",
documentType: "source",
});
expect(request.documentMetadata).toEqual(request["document-metadata"]);
});
});
describe("KnowledgeApi", () => {
it("lists and loads document embedding cores", async () => {
const { base, makeRequest } = makeApi();
const knowledge = new KnowledgeApi(base);
makeRequest
.mockResolvedValueOnce({ ids: ["de-core"] })
.mockResolvedValueOnce({});
await expect(knowledge.getDocumentEmbeddingCores()).resolves.toEqual(["de-core"]);
await knowledge.loadDeCore("de-core", "default", "library");
expect(makeRequest).toHaveBeenNthCalledWith(
1,
"knowledge",
{ operation: "list-de-cores", user: "alice" },
60000,
);
expect(makeRequest).toHaveBeenNthCalledWith(
2,
"knowledge",
{
operation: "load-de-core",
id: "de-core",
flow: "default",
user: "alice",
collection: "library",
},
30000,
);
});
it("unloads knowledge graph cores from a flow", async () => {
const { base, makeRequest } = makeApi();
makeRequest.mockResolvedValue({});
await new KnowledgeApi(base).unloadKgCore("kg-core", "default");
expect(makeRequest).toHaveBeenCalledWith(
"knowledge",
{
operation: "unload-kg-core",
id: "kg-core",
flow: "default",
user: "alice",
},
30000,
);
});
});
});