Skip to content
Merged
16 changes: 11 additions & 5 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,11 +831,6 @@ export class CopilotClient {

private setupClientGlobalHandlers(): void {
const handlers: import("./generated/rpc.js").ClientGlobalApiHandlers = {};
// `hooks.invoke` is a client-global RPC method whose payload carries a
// `sessionId`; route each invocation to the matching session's dispatcher.
handlers.hooks = {
invoke: async (params) => await this.handleHooksInvoke(params),
};
if (this.requestHandler) {
handlers.llmInference = createCopilotRequestAdapter(this.requestHandler, () => {
if (!this.connection) {
Expand Down Expand Up @@ -2843,6 +2838,17 @@ export class CopilotClient {
// — the runtime calls into a single handler for the whole connection.
registerClientGlobalApiHandlers(this.connection, this.clientGlobalHandlers);

// `hooks.invoke` is an internal RPC method: the runtime calls it to
// invoke a hook callback on the client. Route each call to the matching
// session's dispatcher. Not part of the public ClientGlobalApiHandlers
// interface because HookInvokeRequest/HookType are internal types.
this.connection.onRequest(
"hooks.invoke",
async (params: { sessionId: string; hookType: string; input: unknown }) => {
return await this.handleHooksInvoke(params);
}
);

this.connection.onClose(() => {
this.state = "disconnected";
});
Expand Down
19 changes: 0 additions & 19 deletions nodejs/src/generated/rpc.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 0 additions & 100 deletions nodejs/src/generated/session-events.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 42 additions & 11 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3139,16 +3139,47 @@ describe("CopilotClient", () => {
expect(failureCalls).toEqual(["fail-tool"]);
});

it("registers hooks.invoke on the JSON-RPC connection and routes it to handleHooksInvoke", async () => {
const client = new CopilotClient();
const handleHooksInvoke = vi
.spyOn(client as any, "handleHooksInvoke")
.mockResolvedValue({ output: { additionalContext: "ok" } });

const fakeConnection = {
onNotification: vi.fn(),
onRequest: vi.fn(),
onClose: vi.fn(),
onError: vi.fn(),
};

(client as any).connection = fakeConnection;
(client as any).attachConnectionHandlers();

const hooksRegistration = fakeConnection.onRequest.mock.calls.find(
([method]: [string, unknown]) => method === "hooks.invoke"
);
expect(hooksRegistration).toBeDefined();

const handler = hooksRegistration![1] as (params: {
sessionId: string;
hookType: string;
input: unknown;
}) => Promise<{ output?: unknown }>;
const payload = {
sessionId: "session-1",
hookType: "postToolUseFailure",
input: { toolName: "shell" },
};

await expect(handler(payload)).resolves.toEqual({
output: { additionalContext: "ok" },
});
expect(handleHooksInvoke).toHaveBeenCalledWith(payload);
});

it("routes hooks.invoke JSON-RPC requests to the SessionHooks handler", async () => {
// Validates the full JSON-RPC entry point used by the CLI:
// clientGlobalHandlers.hooks.invoke({sessionId, hookType, input})
// → CopilotSession._handleHooksInvoke(hookType, input)
// → SessionHooks.onPostToolUseFailure(normalizedInput, {sessionId})
//
// This guards the wire-format contract that the bundled Copilot
// CLI relies on: the hookType string "postToolUseFailure" and the
// input shape `{toolName, toolArgs, error, timestamp, cwd}`.
// The SDK maps that to public `{..., timestamp: Date, workingDirectory}`.
// Validates the dispatch behavior for the internal `hooks.invoke`
// payload after the JSON-RPC connection hands it to the SDK.
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));
Expand All @@ -3172,7 +3203,7 @@ describe("CopilotClient", () => {
cwd: "/tmp",
};

const response = await (client as any).clientGlobalHandlers.hooks.invoke({
const response = await (client as any).handleHooksInvoke({
sessionId: session.sessionId,
hookType: "postToolUseFailure",
input: failureInput,
Expand Down Expand Up @@ -3249,7 +3280,7 @@ describe("CopilotClient", () => {
},
});

const response = await (client as any).clientGlobalHandlers.hooks.invoke({
const response = await (client as any).handleHooksInvoke({
sessionId: session.sessionId,
hookType: "agentStop",
input: {
Expand Down
Loading
Loading