From a327b5fd404b02aafeb4471fc24783bfe04cd902 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:46:34 +0800 Subject: [PATCH] fix(packages): register PATCH /packages/:id so edit-manifest reaches the handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handlePackages has handled `PATCH /packages/:id` (edit a package's name / description / version) all along, but the route was never mounted in dispatcher-plugin — only GET/DELETE `/:id` and PATCH `/:id/enable|disable` were. So `PATCH /packages/:id` 405'd over HTTP and the Studio "edit package" form silently failed (surfaced dogfooding the builder header refresh). Register the missing route, mirroring the GET/DELETE `/:id` handlers. `/:id` is a single path segment, so it does not shadow the `/:id/enable|disable` routes. Adds a route-registration regression test (same class as the /ready seam bug, #2217). Co-Authored-By: Claude Opus 4.8 --- .../runtime/src/dispatcher-plugin.routes.test.ts | 16 ++++++++++++++++ packages/runtime/src/dispatcher-plugin.ts | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/runtime/src/dispatcher-plugin.routes.test.ts b/packages/runtime/src/dispatcher-plugin.routes.test.ts index 594531d10f..14635abb91 100644 --- a/packages/runtime/src/dispatcher-plugin.routes.test.ts +++ b/packages/runtime/src/dispatcher-plugin.routes.test.ts @@ -73,6 +73,22 @@ describe('createDispatcherPlugin — HTTP route registration', () => { expect(routes).toContain('GET /api/v1/ready'); }); + // Regression: PATCH /packages/:id (edit a package's manifest — name / + // description / version) had a handlePackages() branch all along but NO + // server.patch() registration, so it 405'd over HTTP and the Studio "edit + // package" form silently failed. Same class of bug as /ready above. The + // sibling /:id/enable|disable PATCH routes were mounted; the bare /:id was not. + it('mounts PATCH /packages/:id so the edit-manifest form reaches dispatch()', async () => { + const { server, routes } = makeFakeServer(); + const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false }); + await plugin.start?.(makeCtx(server)); + + expect(routes).toContain('PATCH /api/v1/packages/:id'); + // Sanity: the sibling routes that WERE always mounted. + expect(routes).toContain('PATCH /api/v1/packages/:id/enable'); + expect(routes).toContain('POST /api/v1/packages'); + }); + it('also mounts a known existing route (sanity that start() ran)', async () => { const { server, routes } = makeFakeServer(); const plugin = createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false }); diff --git a/packages/runtime/src/dispatcher-plugin.ts b/packages/runtime/src/dispatcher-plugin.ts index 64bfcffedf..7d8c2b765d 100644 --- a/packages/runtime/src/dispatcher-plugin.ts +++ b/packages/runtime/src/dispatcher-plugin.ts @@ -721,6 +721,20 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu } }); + // Edit a package's manifest (name / description / version). The + // handler for this has existed in handlePackages all along, but the + // route was never registered, so PATCH /packages/:id 405'd and the + // Studio "edit package" form silently failed. `/:id` is a single + // segment, so this does not shadow the `/:id/enable|disable` routes. + server.patch(`${prefix}/packages/:id`, async (req: any, res: any) => { + try { + const result = await dispatcher.handlePackages(`/${req.params.id}`, 'PATCH', req.body, req.query, { request: req }); + sendResult(result, res); + } catch (err: any) { + errorResponse(err, res); + } + }); + server.patch(`${prefix}/packages/:id/enable`, async (req: any, res: any) => { try { const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, 'PATCH', {}, {}, { request: req });