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
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