diff --git a/packages/server/src/server/plugins/runtime.posix.test.ts b/packages/server/src/server/plugins/runtime.posix.test.ts index 1792a9d7ff..ccaf00a332 100644 --- a/packages/server/src/server/plugins/runtime.posix.test.ts +++ b/packages/server/src/server/plugins/runtime.posix.test.ts @@ -1936,6 +1936,38 @@ export default function contribute(plugin: any) { ); }); + it("reports and removes a plugin it terminated for an invalid message", async () => { + const directory = await createPlugin( + "invalid-message", + `export default function contribute(plugin: unknown) { + void plugin; + setTimeout(() => process.send?.({ type: "not-a-plugin-message" }), 20); + return () => undefined; +}`, + ); + const sessions = createTrackedSessionHost(); + const runtime = createTestRuntime({ sessionHost: sessions.host }); + const exited = new Promise((resolve) => { + runtime.subscribe((pluginId, error) => { + if (pluginId === "invalid-message" && error) resolve(error); + }); + }); + await runtime.startPlugin("invalid-message", directory); + + // The runtime kills the process; it must then treat it like any other exited plugin. + await expect(exited).resolves.toBe("Plugin process exited: invalid-message"); + expect(sessions.active.size).toBe(0); + expect(runtime.catalog()).toEqual([]); + expect( + pluginMessages(runtime, "invalid-message").some((message) => + message.includes("sent an invalid message"), + ), + ).toBe(true); + await expect(runtime.invoke("invalid-message", "anything", {})).rejects.toThrow( + "Plugin is not available", + ); + }); + it("re-attaches a plugin session when the daemon closes the socket under a live child", async () => { const directory = await createPlugin( "reattaching", diff --git a/packages/server/src/server/plugins/runtime.ts b/packages/server/src/server/plugins/runtime.ts index 98cbf8fcf0..f8fe8a91b5 100644 --- a/packages/server/src/server/plugins/runtime.ts +++ b/packages/server/src/server/plugins/runtime.ts @@ -247,8 +247,9 @@ function spawnPluginChild(): PluginChild { }) as PluginChild; } +// Kill only. Node never emits "close" for a child whose IPC channel the parent disconnected, and +// "close" is how the runtime learns that a plugin exited; the channel closes with the process. function terminatePluginChild(child: PluginChild): void { - if (child.connected) child.disconnect(); if (!child.killed) child.kill(); }