Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/backend/backend-connector.adapter-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe("BackendConnector per-session adapter", () => {
metrics: null,
broadcaster: { broadcast: vi.fn(), sendTo: vi.fn() } as any,
routeUnifiedMessage: vi.fn(),
routeSystemSignal: vi.fn(),
emitEvent: vi.fn(),
getRuntime: (session: any) => {
if (!runtimeCache.has(session)) {
Expand Down Expand Up @@ -311,6 +312,7 @@ describe("BackendConnector per-session adapter", () => {
metrics: null,
broadcaster: { broadcast: vi.fn(), sendTo: vi.fn() } as any,
routeUnifiedMessage: vi.fn(),
routeSystemSignal: vi.fn(),
emitEvent: vi.fn(),
getRuntime: () => mockRuntime as any,
});
Expand Down
56 changes: 33 additions & 23 deletions src/core/backend/backend-connector.failure-injection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,19 @@ function buildConnectorDeps(
emitEvent = vi.fn(),
broadcaster = { broadcast: vi.fn(), broadcastToParticipants: vi.fn(), sendTo: vi.fn() } as any,
) {
const routeSystemSignal = vi.fn();
const manager = new BackendConnector({
adapter,
adapterResolver: null,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
metrics: null,
broadcaster,
routeUnifiedMessage: vi.fn(),
routeSystemSignal,
emitEvent,
getRuntime: () => createMockRuntime(session),
});
return { manager, emitEvent, broadcaster };
return { manager, emitEvent, broadcaster, routeSystemSignal };
}

async function waitForAssertion(assertFn: () => void, timeoutMs = 500): Promise<void> {
Expand All @@ -113,17 +115,22 @@ describe("BackendConnector failure injection", () => {
} as any;

const session = createSession("sess-fi");
const { manager } = buildConnectorDeps(adapter, session, emitEvent, broadcaster);
const { manager, routeSystemSignal } = buildConnectorDeps(
adapter,
session,
emitEvent,
broadcaster,
);

await manager.connectBackend(session);

adapter.failStream("sess-fi", new Error("Injected stream failure"));

await waitForAssertion(() => {
expect(emitEvent).toHaveBeenCalledWith(
"backend:disconnected",
expect.objectContaining({ sessionId: "sess-fi" }),
);
expect(routeSystemSignal).toHaveBeenCalledWith(session, {
kind: "BACKEND_DISCONNECTED",
reason: "stream ended",
});
});

expect(emitEvent).toHaveBeenCalledWith(
Expand All @@ -133,7 +140,6 @@ describe("BackendConnector failure injection", () => {
sessionId: "sess-fi",
}),
);
expect(broadcaster.broadcast).toHaveBeenCalledWith(session, { type: "cli_disconnected" });
});

it("drains pending passthroughs with slash_command_error when stream fails (lines 594-605)", async () => {
Expand All @@ -145,7 +151,12 @@ describe("BackendConnector failure injection", () => {
sendTo: vi.fn(),
} as any;
const session = createSession("sess-drain-fail");
const { manager } = buildConnectorDeps(adapter, session, emitEvent, broadcaster);
const { manager, routeSystemSignal } = buildConnectorDeps(
adapter,
session,
emitEvent,
broadcaster,
);

// Pre-populate pending passthrough entries
session.pendingPassthroughs.push(makePassthrough("/compact", "req-compact"));
Expand All @@ -154,16 +165,15 @@ describe("BackendConnector failure injection", () => {
adapter.failStream("sess-drain-fail", new Error("Backend crashed"));

await waitForAssertion(() => {
expect(broadcaster.broadcast).toHaveBeenCalledWith(
expect(routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ type: "slash_command_error", command: "/compact" }),
expect.objectContaining({
kind: "SLASH_PASSTHROUGH_ERROR",
command: "/compact",
error: "Backend crashed",
}),
);
});

expect(emitEvent).toHaveBeenCalledWith(
"slash_command:failed",
expect.objectContaining({ command: "/compact", error: "Backend crashed" }),
);
});

it("drains pending passthroughs with slash_command_error when stream ends unexpectedly (lines 619-630)", async () => {
Expand All @@ -175,27 +185,27 @@ describe("BackendConnector failure injection", () => {
sendTo: vi.fn(),
} as any;
const session = createSession("sess-drain-end");
const { manager } = buildConnectorDeps(adapter, session, emitEvent, broadcaster);
const { manager, routeSystemSignal } = buildConnectorDeps(
adapter,
session,
emitEvent,
broadcaster,
);

session.pendingPassthroughs.push(makePassthrough("/status", "req-status"));

await manager.connectBackend(session);
adapter.endStream("sess-drain-end");

await waitForAssertion(() => {
expect(broadcaster.broadcast).toHaveBeenCalledWith(
expect(routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({
type: "slash_command_error",
kind: "SLASH_PASSTHROUGH_ERROR",
command: "/status",
error: "Backend stream ended unexpectedly",
}),
);
});

expect(emitEvent).toHaveBeenCalledWith(
"slash_command:failed",
expect.objectContaining({ command: "/status", error: "Backend stream ended unexpectedly" }),
);
});
});
77 changes: 34 additions & 43 deletions src/core/backend/backend-connector.lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ function createDeps(overrides?: Partial<BackendConnectorDeps>): BackendConnector
broadcastToParticipants: vi.fn(),
} as any,
routeUnifiedMessage: vi.fn(),
routeSystemSignal: vi.fn(),
emitEvent: vi.fn(),
getRuntime: (session) => {
if (!runtimeCache.has(session)) {
Expand Down Expand Up @@ -278,8 +279,7 @@ describe("BackendConnector", () => {
await mgr.connectBackend(session);

expect(session.backendSession).not.toBeNull();
expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(session, { type: "cli_connected" });
expect(deps.emitEvent).toHaveBeenCalledWith("backend:connected", { sessionId: "sess-1" });
expect(deps.routeSystemSignal).toHaveBeenCalledWith(session, { kind: "BACKEND_CONNECTED" });
});

it("closes existing backend session on reconnect", async () => {
Expand Down Expand Up @@ -355,9 +355,9 @@ describe("BackendConnector", () => {
} as CLIMessage);

expect(result).toBe(true);
expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ type: "slash_command_result", content: "echo result" }),
expect.objectContaining({ kind: "SLASH_PASSTHROUGH_RESULT", content: "echo result" }),
);
expect(session.pendingPassthroughs).toHaveLength(0);
});
Expand Down Expand Up @@ -492,8 +492,11 @@ describe("BackendConnector", () => {

await mgr.disconnectBackend(session);

// Should not broadcast or emit events for disconnection
expect(deps.emitEvent).not.toHaveBeenCalledWith("backend:disconnected", expect.anything());
// Should not route signals for disconnection
expect(deps.routeSystemSignal).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ kind: "BACKEND_DISCONNECTED" }),
);
});

it("records metrics when disconnecting", async () => {
Expand Down Expand Up @@ -605,12 +608,12 @@ describe("BackendConnector", () => {

await tick();

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({
type: "slash_command_result",
kind: "SLASH_PASSTHROUGH_RESULT",
command: "/context",
request_id: "req-ctx",
requestId: "req-ctx",
content: "Context: 23% used",
source: "cli",
}),
Expand Down Expand Up @@ -641,12 +644,12 @@ describe("BackendConnector", () => {

await tick();

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({
type: "slash_command_result",
kind: "SLASH_PASSTHROUGH_RESULT",
command: "/context",
request_id: "req-ctx",
requestId: "req-ctx",
content: "Context summary line",
source: "cli",
}),
Expand Down Expand Up @@ -689,12 +692,12 @@ describe("BackendConnector", () => {

await tick();

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({
type: "slash_command_result",
kind: "SLASH_PASSTHROUGH_RESULT",
command: "/context",
request_id: "req-ctx",
requestId: "req-ctx",
content: "Context Usage\nTokens: 43.5k / 200k (22%)",
source: "cli",
}),
Expand Down Expand Up @@ -734,12 +737,12 @@ describe("BackendConnector", () => {

await tick();

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({
type: "slash_command_error",
kind: "SLASH_PASSTHROUGH_ERROR",
command: "/context",
request_id: "req-ctx",
requestId: "req-ctx",
}),
);
expect(session.pendingPassthroughs).toHaveLength(0);
Expand Down Expand Up @@ -783,7 +786,7 @@ describe("BackendConnector", () => {
`);
});

it("broadcasts cli_disconnected when stream ends unexpectedly", async () => {
it("routes BACKEND_DISCONNECTED signal when stream ends unexpectedly", async () => {
const testSession = new TestBackendSession("sess-1");
const adapter = new TestAdapter();
adapter.nextSession = testSession;
Expand All @@ -799,16 +802,10 @@ describe("BackendConnector", () => {

await tick(50);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(session, {
type: "cli_disconnected",
expect(deps.routeSystemSignal).toHaveBeenCalledWith(session, {
kind: "BACKEND_DISCONNECTED",
reason: "stream ended",
});
expect(deps.emitEvent).toHaveBeenCalledWith(
"backend:disconnected",
expect.objectContaining({
sessionId: "sess-1",
reason: "stream ended",
}),
);
expect(session.backendSession).toBeNull();
});

Expand Down Expand Up @@ -859,15 +856,9 @@ describe("BackendConnector", () => {
error: expect.any(Error),
}),
);
expect(deps.emitEvent).toHaveBeenCalledWith(
"backend:disconnected",
expect.objectContaining({
sessionId: "sess-1",
reason: "stream ended",
}),
);
expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(session, {
type: "cli_disconnected",
expect(deps.routeSystemSignal).toHaveBeenCalledWith(session, {
kind: "BACKEND_DISCONNECTED",
reason: "stream ended",
});
expect(session.backendSession).toBeNull();
});
Expand Down Expand Up @@ -939,7 +930,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
},
} as unknown as CLIMessage);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ content: "plain string and object" }),
);
Expand All @@ -965,7 +956,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
message: { content: { text: "object text" } },
} as unknown as CLIMessage);

expect(deps2.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps2.routeSystemSignal).toHaveBeenCalledWith(
session2,
expect.objectContaining({ content: "object text" }),
);
Expand All @@ -979,7 +970,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
message: { content: null },
} as unknown as CLIMessage);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ content: "" }),
);
Expand All @@ -1001,7 +992,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
message: { content: { notText: "value" } },
} as unknown as CLIMessage);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ content: "" }),
);
Expand All @@ -1023,7 +1014,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
message: { content: { text: 42 } },
} as unknown as CLIMessage);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ content: "" }),
);
Expand All @@ -1045,7 +1036,7 @@ describe("BackendConnector — cliUserEchoToText via passthrough", () => {
message: { content: "<local-command-stdout>Context Usage</local-command-stdout>" },
} as unknown as CLIMessage);

expect(deps.broadcaster.broadcast).toHaveBeenCalledWith(
expect(deps.routeSystemSignal).toHaveBeenCalledWith(
session,
expect.objectContaining({ content: "Context Usage" }),
);
Expand Down
1 change: 1 addition & 0 deletions src/core/backend/backend-connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function createDeps(overrides?: Partial<BackendConnectorDeps>): BackendConnector
sendTo: vi.fn(),
} as any,
routeUnifiedMessage: vi.fn(),
routeSystemSignal: vi.fn(),
emitEvent: vi.fn(),
getRuntime: () => mockRuntime as any,
...overrides,
Expand Down
Loading