import { EnvironmentId } from "@t3tools/contracts";import { describe, expect, it, vi } from "vitest"; import { createEnvironmentConnection } from "./connection";import type { WsRpcClient } from "~/rpc/wsRpcClient"; function createTestClient(options?: { readonly getSnapshot?: () => Promise<{ readonly snapshotSequence: number }>; readonly replayEvents?: () => Promise<ReadonlyArray<any>>;}) { const lifecycleListeners = new Set<(event: any) => void>(); const configListeners = new Set<(event: any) => void>(); const terminalListeners = new Set<(event: any) => void>(); let domainResubscribe: (() => void) | undefined; const getSnapshot = vi.fn( options?.getSnapshot ?? (async () => ({ snapshotSequence: 1, projects: [], threads: [], }) as any), ); const replayEvents = vi.fn(options?.replayEvents ?? (async () => [])); const client = { dispose: vi.fn(async () => undefined), reconnect: vi.fn(async () => undefined), server: { getConfig: vi.fn(async () => ({ environment: { environmentId: EnvironmentId.makeUnsafe("env-1"), }, })), subscribeConfig: (listener: (event: any) => void) => { configListeners.add(listener); return () => configListeners.delete(listener); }, subscribeLifecycle: (listener: (event: any) => void) => { lifecycleListeners.add(listener); return () => lifecycleListeners.delete(listener); }, subscribeAuthAccess: () => () => undefined, refreshProviders: vi.fn(async () => undefined), upsertKeybinding: vi.fn(async () => undefined), getSettings: vi.fn(async () => undefined), updateSettings: vi.fn(async () => undefined), }, orchestration: { getSnapshot, dispatchCommand: vi.fn(async () => undefined), getTurnDiff: vi.fn(async () => undefined), getFullThreadDiff: vi.fn(async () => undefined), replayEvents, onDomainEvent: vi.fn((_: (event: any) => void, options?: { onResubscribe?: () => void }) => { domainResubscribe = options?.onResubscribe; return () => { if (domainResubscribe === options?.onResubscribe) { domainResubscribe = undefined; } }; }), }, terminal: { open: vi.fn(async () => undefined), write: vi.fn(async () => undefined), resize: vi.fn(async () => undefined), clear: vi.fn(async () => undefined), restart: vi.fn(async () => undefined), close: vi.fn(async () => undefined), onEvent: (listener: (event: any) => void) => { terminalListeners.add(listener); return () => terminalListeners.delete(listener); }, }, projects: { searchEntries: vi.fn(async () => []), writeFile: vi.fn(async () => undefined), }, shell: { openInEditor: vi.fn(async () => undefined), }, git: { pull: vi.fn(async () => undefined), refreshStatus: vi.fn(async () => undefined), onStatus: vi.fn(() => () => undefined), runStackedAction: vi.fn(async () => ({}) as any), listBranches: vi.fn(async () => []), createWorktree: vi.fn(async () => undefined), removeWorktree: vi.fn(async () => undefined), createBranch: vi.fn(async () => undefined), checkout: vi.fn(async () => undefined), init: vi.fn(async () => undefined), resolvePullRequest: vi.fn(async () => undefined), preparePullRequestThread: vi.fn(async () => undefined), }, } as unknown as WsRpcClient; return { client, getSnapshot, replayEvents, emitWelcome: (environmentId: EnvironmentId) => { for (const listener of lifecycleListeners) { listener({ type: "welcome", payload: { environment: { environmentId, }, }, }); } }, emitConfigSnapshot: (environmentId: EnvironmentId) => { for (const listener of configListeners) { listener({ type: "snapshot", config: { environment: { environmentId, }, }, }); } }, triggerDomainResubscribe: () => { domainResubscribe?.(); }, };} describe("createEnvironmentConnection", () => { it("bootstraps a snapshot immediately for a new connection", async () => { const environmentId = EnvironmentId.makeUnsafe("env-1"); const { client, getSnapshot } = createTestClient(); const syncSnapshot = vi.fn(); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { id: "env-1", label: "Remote env", source: "manual", target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, environmentId, }, client, applyEventBatch: vi.fn(), syncSnapshot, applyTerminalEvent: vi.fn(), }); await Promise.resolve(); await Promise.resolve(); expect(getSnapshot).toHaveBeenCalledTimes(1); expect(syncSnapshot).toHaveBeenCalledWith( expect.objectContaining({ snapshotSequence: 1 }), environmentId, ); await connection.dispose(); }); it("rejects welcome/config identity drift", async () => { const environmentId = EnvironmentId.makeUnsafe("env-1"); const { client, emitWelcome } = createTestClient(); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { id: "env-1", label: "Remote env", source: "manual", target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, environmentId, }, client, applyEventBatch: vi.fn(), syncSnapshot: vi.fn(), applyTerminalEvent: vi.fn(), }); expect(() => emitWelcome(EnvironmentId.makeUnsafe("env-2"))).toThrow( "Environment connection env-1 changed identity to env-2 via server lifecycle welcome.", ); await connection.dispose(); }); it("rejects ensureBootstrapped when snapshot recovery fails", async () => { const environmentId = EnvironmentId.makeUnsafe("env-1"); const snapshotError = new Error("snapshot failed"); const { client } = createTestClient({ getSnapshot: async () => { throw snapshotError; }, }); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { id: "env-1", label: "Remote env", source: "manual", target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, environmentId, }, client, applyEventBatch: vi.fn(), syncSnapshot: vi.fn(), applyTerminalEvent: vi.fn(), }); await expect(connection.ensureBootstrapped()).rejects.toThrow("snapshot failed"); await connection.dispose(); }); it("retries replay recovery after transport disconnects during resubscribe", async () => { const environmentId = EnvironmentId.makeUnsafe("env-1"); let replayAttempts = 0; const applyEventBatch = vi.fn(); const { client, replayEvents, triggerDomainResubscribe } = createTestClient({ replayEvents: async () => { replayAttempts += 1; if (replayAttempts === 1) { throw new Error("SocketCloseError: 1006"); } return [ { sequence: 2, type: "thread.created", payload: {}, }, ]; }, }); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { id: "env-1", label: "Remote env", source: "manual", target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, environmentId, }, client, applyEventBatch, syncSnapshot: vi.fn(), applyTerminalEvent: vi.fn(), }); await Promise.resolve(); await Promise.resolve(); triggerDomainResubscribe(); await vi.waitFor(() => { expect(replayEvents).toHaveBeenCalledTimes(2); expect(applyEventBatch).toHaveBeenCalledWith( [ expect.objectContaining({ sequence: 2, }), ], environmentId, ); }); await connection.dispose(); }); it("swallows replay recovery failures triggered by resubscribe", async () => { const environmentId = EnvironmentId.makeUnsafe("env-1"); const snapshotError = new Error("snapshot failed"); let snapshotCalls = 0; const { client, triggerDomainResubscribe } = createTestClient({ getSnapshot: async () => { snapshotCalls += 1; if (snapshotCalls === 1) { return { snapshotSequence: 1, projects: [], threads: [], } as any; } throw snapshotError; }, replayEvents: async () => { throw new Error("SocketCloseError: 1006"); }, }); const connection = createEnvironmentConnection({ kind: "saved", knownEnvironment: { id: "env-1", label: "Remote env", source: "manual", target: { httpBaseUrl: "http://example.test", wsBaseUrl: "ws://example.test", }, environmentId, }, client, applyEventBatch: vi.fn(), syncSnapshot: vi.fn(), applyTerminalEvent: vi.fn(), }); await Promise.resolve(); await Promise.resolve(); const onUnhandledRejection = vi.fn(); process.on("unhandledRejection", onUnhandledRejection); try { triggerDomainResubscribe(); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); } finally { process.off("unhandledRejection", onUnhandledRejection); } expect(onUnhandledRejection).not.toHaveBeenCalled(); await connection.dispose(); });});
import { EnvironmentId } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vitest";
import { createEnvironmentConnection } from "./connection";
import type { WsRpcClient } from "~/rpc/wsRpcClient";
function createTestClient(options?: {
readonly getSnapshot?: () => Promise<{ readonly snapshotSequence: number }>;
readonly replayEvents?: () => Promise<ReadonlyArray<any>>;
}) {
const lifecycleListeners = new Set<(event: any) => void>();
const configListeners = new Set<(event: any) => void>();
const terminalListeners = new Set<(event: any) => void>();
let domainResubscribe: (() => void) | undefined;
const getSnapshot = vi.fn(
options?.getSnapshot ??
(async () =>
({
snapshotSequence: 1,
projects: [],
threads: [],
}) as any),
);
const replayEvents = vi.fn(options?.replayEvents ?? (async () => []));
const client = {
dispose: vi.fn(async () => undefined),
reconnect: vi.fn(async () => undefined),
server: {
getConfig: vi.fn(async () => ({
environment: {
environmentId: EnvironmentId.makeUnsafe("env-1"),
},
})),
subscribeConfig: (listener: (event: any) => void) => {
configListeners.add(listener);
return () => configListeners.delete(listener);
},
subscribeLifecycle: (listener: (event: any) => void) => {
lifecycleListeners.add(listener);
return () => lifecycleListeners.delete(listener);
},
subscribeAuthAccess: () => () => undefined,
refreshProviders: vi.fn(async () => undefined),
upsertKeybinding: vi.fn(async () => undefined),
getSettings: vi.fn(async () => undefined),
updateSettings: vi.fn(async () => undefined),
},
orchestration: {
getSnapshot,
dispatchCommand: vi.fn(async () => undefined),
getTurnDiff: vi.fn(async () => undefined),
getFullThreadDiff: vi.fn(async () => undefined),
replayEvents,
onDomainEvent: vi.fn((_: (event: any) => void, options?: { onResubscribe?: () => void }) => {
domainResubscribe = options?.onResubscribe;
return () => {
if (domainResubscribe === options?.onResubscribe) {
domainResubscribe = undefined;
}
};
}),
},
terminal: {
open: vi.fn(async () => undefined),
write: vi.fn(async () => undefined),
resize: vi.fn(async () => undefined),
clear: vi.fn(async () => undefined),
restart: vi.fn(async () => undefined),
close: vi.fn(async () => undefined),
onEvent: (listener: (event: any) => void) => {
terminalListeners.add(listener);
return () => terminalListeners.delete(listener);
},
},
projects: {
searchEntries: vi.fn(async () => []),
writeFile: vi.fn(async () => undefined),
},
shell: {
openInEditor: vi.fn(async () => undefined),
},
git: {
pull: vi.fn(async () => undefined),
refreshStatus: vi.fn(async () => undefined),
onStatus: vi.fn(() => () => undefined),
runStackedAction: vi.fn(async () => ({}) as any),
listBranches: vi.fn(async () => []),
createWorktree: vi.fn(async () => undefined),
removeWorktree: vi.fn(async () => undefined),
createBranch: vi.fn(async () => undefined),
checkout: vi.fn(async () => undefined),
init: vi.fn(async () => undefined),
resolvePullRequest: vi.fn(async () => undefined),
preparePullRequestThread: vi.fn(async () => undefined),
},
} as unknown as WsRpcClient;
return {
client,
getSnapshot,
replayEvents,
emitWelcome: (environmentId: EnvironmentId) => {
for (const listener of lifecycleListeners) {
listener({
type: "welcome",
payload: {
environment: {
environmentId,
},
},
});
}
},
emitConfigSnapshot: (environmentId: EnvironmentId) => {
for (const listener of configListeners) {
listener({
type: "snapshot",
config: {
environment: {
environmentId,
},
},
});
}
},
triggerDomainResubscribe: () => {
domainResubscribe?.();
},
};
}
describe("createEnvironmentConnection", () => {
it("bootstraps a snapshot immediately for a new connection", async () => {
const environmentId = EnvironmentId.makeUnsafe("env-1");
const { client, getSnapshot } = createTestClient();
const syncSnapshot = vi.fn();
const connection = createEnvironmentConnection({
kind: "saved",
knownEnvironment: {
id: "env-1",
label: "Remote env",
source: "manual",
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
environmentId,
},
client,
applyEventBatch: vi.fn(),
syncSnapshot,
applyTerminalEvent: vi.fn(),
});
await Promise.resolve();
await Promise.resolve();
expect(getSnapshot).toHaveBeenCalledTimes(1);
expect(syncSnapshot).toHaveBeenCalledWith(
expect.objectContaining({ snapshotSequence: 1 }),
environmentId,
);
await connection.dispose();
});
it("rejects welcome/config identity drift", async () => {
const environmentId = EnvironmentId.makeUnsafe("env-1");
const { client, emitWelcome } = createTestClient();
const connection = createEnvironmentConnection({
kind: "saved",
knownEnvironment: {
id: "env-1",
label: "Remote env",
source: "manual",
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
environmentId,
},
client,
applyEventBatch: vi.fn(),
syncSnapshot: vi.fn(),
applyTerminalEvent: vi.fn(),
});
expect(() => emitWelcome(EnvironmentId.makeUnsafe("env-2"))).toThrow(
"Environment connection env-1 changed identity to env-2 via server lifecycle welcome.",
);
await connection.dispose();
});
it("rejects ensureBootstrapped when snapshot recovery fails", async () => {
const environmentId = EnvironmentId.makeUnsafe("env-1");
const snapshotError = new Error("snapshot failed");
const { client } = createTestClient({
getSnapshot: async () => {
throw snapshotError;
},
});
const connection = createEnvironmentConnection({
kind: "saved",
knownEnvironment: {
id: "env-1",
label: "Remote env",
source: "manual",
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
environmentId,
},
client,
applyEventBatch: vi.fn(),
syncSnapshot: vi.fn(),
applyTerminalEvent: vi.fn(),
});
await expect(connection.ensureBootstrapped()).rejects.toThrow("snapshot failed");
await connection.dispose();
});
it("retries replay recovery after transport disconnects during resubscribe", async () => {
const environmentId = EnvironmentId.makeUnsafe("env-1");
let replayAttempts = 0;
const applyEventBatch = vi.fn();
const { client, replayEvents, triggerDomainResubscribe } = createTestClient({
replayEvents: async () => {
replayAttempts += 1;
if (replayAttempts === 1) {
throw new Error("SocketCloseError: 1006");
}
return [
{
sequence: 2,
type: "thread.created",
payload: {},
},
];
},
});
const connection = createEnvironmentConnection({
kind: "saved",
knownEnvironment: {
id: "env-1",
label: "Remote env",
source: "manual",
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
environmentId,
},
client,
applyEventBatch,
syncSnapshot: vi.fn(),
applyTerminalEvent: vi.fn(),
});
await Promise.resolve();
await Promise.resolve();
triggerDomainResubscribe();
await vi.waitFor(() => {
expect(replayEvents).toHaveBeenCalledTimes(2);
expect(applyEventBatch).toHaveBeenCalledWith(
[
expect.objectContaining({
sequence: 2,
}),
],
environmentId,
);
});
await connection.dispose();
});
it("swallows replay recovery failures triggered by resubscribe", async () => {
const environmentId = EnvironmentId.makeUnsafe("env-1");
const snapshotError = new Error("snapshot failed");
let snapshotCalls = 0;
const { client, triggerDomainResubscribe } = createTestClient({
getSnapshot: async () => {
snapshotCalls += 1;
if (snapshotCalls === 1) {
return {
snapshotSequence: 1,
projects: [],
threads: [],
} as any;
}
throw snapshotError;
},
replayEvents: async () => {
throw new Error("SocketCloseError: 1006");
},
});
const connection = createEnvironmentConnection({
kind: "saved",
knownEnvironment: {
id: "env-1",
label: "Remote env",
source: "manual",
target: {
httpBaseUrl: "http://example.test",
wsBaseUrl: "ws://example.test",
},
environmentId,
},
client,
applyEventBatch: vi.fn(),
syncSnapshot: vi.fn(),
applyTerminalEvent: vi.fn(),
});
await Promise.resolve();
await Promise.resolve();
const onUnhandledRejection = vi.fn();
process.on("unhandledRejection", onUnhandledRejection);
try {
triggerDomainResubscribe();
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
process.off("unhandledRejection", onUnhandledRejection);
}
expect(onUnhandledRejection).not.toHaveBeenCalled();
await connection.dispose();
});
});