Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { urbitFetch } from "./fetch.js";
import { UrbitSSEClient } from "./sse-client.js";
// Mock urbitFetch to avoid real network calls
vi.mock("./fetch.js", () => ({
urbitFetch: vi.fn(),
}));
// Mock channel-ops to avoid real channel operations
vi.mock("./channel-ops.js", () => ({
ensureUrbitChannelOpen: vi.fn().mockResolvedValue(undefined),
pokeUrbitChannel: vi.fn().mockResolvedValue(undefined),
scryUrbitPath: vi.fn().mockResolvedValue({}),
}));
describe("UrbitSSEClient", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("subscribe", () => {
it("sends subscriptions added after connect", async () => {
const mockUrbitFetch = vi.mocked(urbitFetch);
mockUrbitFetch.mockResolvedValue({
response: { ok: true, status: 200 } as unknown as Response,
finalUrl: "
https://example.com",
release: vi.fn().mockResolvedValue(undefined),
});
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
// Simulate connected state
(client as { isConnected: boolean }).isConnected = true;
await client.subscribe({
app: "chat",
path: "/dm/~zod",
event: () => {},
});
expect(mockUrbitFetch).toHaveBeenCalledTimes(1);
const callArgs = mockUrbitFetch.mock.calls[0][0];
expect(callArgs.path).toContain("/~/channel/");
expect(callArgs.init?.method).toBe("PUT");
const body = JSON.parse(callArgs.init?.body as string);
expect(body).toHaveLength(1);
expect(body[0]).toMatchObject({
action: "subscribe",
app: "chat",
path: "/dm/~zod",
});
});
it("queues subscriptions before connect", async () => {
const mockUrbitFetch = vi.mocked(urbitFetch);
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
// Not connected yet
await client.subscribe({
app: "chat",
path: "/dm/~zod",
event: () => {},
});
// Should not call urbitFetch since not connected
expect(mockUrbitFetch).not.toHaveBeenCalled();
// But subscription should be queued
expect(client.subscriptions).toHaveLength(1);
expect(client.subscriptions[0]).toMatchObject({
app: "chat",
path: "/dm/~zod",
});
});
});
describe("updateCookie", () => {
it("normalizes cookie when updating", () => {
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
// Cookie with extra parts that should be stripped
client.updateCookie("urbauth-~zod=456; Path=/; HttpOnly");
expect(client.cookie).toBe("urbauth-~zod=456");
});
it("handles simple cookie values", () => {
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
client.updateCookie("urbauth-~zod=newvalue");
expect(client.cookie).toBe("urbauth-~zod=newvalue");
});
});
describe("reconnection", () => {
it("has autoReconnect enabled by default", () => {
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
expect(client.autoReconnect).toBe(true);
});
it("can disable autoReconnect via options", () => {
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123", {
autoReconnect: false,
});
expect(client.autoReconnect).toBe(false);
});
it("stores onReconnect callback", () => {
const onReconnect = vi.fn();
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123", {
onReconnect,
});
expect(client.onReconnect).toBe(onReconnect);
});
it("resets reconnect attempts on successful connect", async () => {
const mockUrbitFetch = vi.mocked(urbitFetch);
// Mock a response that returns a readable stream
const mockStream = new ReadableStream({
start(controller) {
controller.close();
},
});
mockUrbitFetch.mockResolvedValue({
response: {
ok: true,
status: 200,
body: mockStream,
} as unknown as Response,
finalUrl: "
https://example.com",
release: vi.fn().mockResolvedValue(undefined),
});
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123", {
autoReconnect: false, // Disable to prevent reconnect loop
});
client.reconnectAttempts = 5;
await client.connect();
expect(client.reconnectAttempts).toBe(0);
});
});
describe("event acking", () => {
it("tracks lastHeardEventId and ackThreshold", () => {
const client = new UrbitSSEClient("
https://example.com", "urbauth-~zod=123");
// Access private properties for testing
const lastHeardEventId = (client as unknown as { lastHeardEventId: number }).lastHeardEven
tId;
const ackThreshold = (client as unknown as { ackThreshold: number }).ackThreshold;
expect(lastHeardEventId).toBe(-1);
expect(ackThreshold).toBeGreaterThan(0);
});
});
describe("constructor", () => {
it("generates unique channel ID", () => {
const client1 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
const client2 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
expect(client1.channelId).not.toBe(client2.channelId);
});
it("normalizes cookie in constructor", () => {
const client = new UrbitSSEClient(
"https://example.com",
"urbauth-~zod=123; Path=/; HttpOnly",
);
expect(client.cookie).toBe("urbauth-~zod=123");
});
it("sets default reconnection parameters", () => {
const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
expect(client.maxReconnectAttempts).toBe(10);
expect(client.reconnectDelay).toBe(1000);
expect(client.maxReconnectDelay).toBe(30000);
});
it("allows overriding reconnection parameters", () => {
const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
maxReconnectAttempts: 5,
reconnectDelay: 500,
maxReconnectDelay: 10000,
});
expect(client.maxReconnectAttempts).toBe(5);
expect(client.reconnectDelay).toBe(500);
expect(client.maxReconnectDelay).toBe(10000);
});
});
});