diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index 4afecfd4e7..3c5579d425 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -1179,3 +1179,28 @@ describe('HTTP error shaping', () => { expect(caught.httpStatus).toBe(500); }); }); + +describe('packages.install', () => { + const MANIFEST = { id: 'com.acme.crm', name: 'Acme CRM', version: '1.0.0', type: 'app' }; + + it('POSTs the manifest and omits `overwrite` unless requested', async () => { + const { client, fetchMock } = createMockClient({ success: true, data: { package: { manifest: MANIFEST } } }); + await client.packages.install(MANIFEST, { enableOnInstall: true }); + + expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/packages', expect.any(Object)); + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body.manifest).toEqual(MANIFEST); + expect(body.enableOnInstall).toBe(true); + // Duplicate-id guard semantics: never send `overwrite` implicitly — + // the server must keep 409ing on an existing id by default. + expect('overwrite' in body).toBe(false); + }); + + it('passes `overwrite: true` through for intentional upgrade / re-install', async () => { + const { client, fetchMock } = createMockClient({ success: true, data: { package: { manifest: MANIFEST } } }); + await client.packages.install(MANIFEST, { overwrite: true }); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body.overwrite).toBe(true); + }); +}); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index f0549fd426..db88be760a 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -592,8 +592,16 @@ export class ObjectStackClient { /** * Install a new package from its manifest. - */ - install: async (manifest: any, options?: { settings?: Record; enableOnInstall?: boolean }) => { + * + * By default the server rejects a manifest whose `id` is already + * installed with **409 Conflict** (duplicate-id guard) instead of + * silently overwriting the existing package. Intentional upgrade / + * re-install flows opt back in with `overwrite: true`. + */ + install: async ( + manifest: any, + options?: { settings?: Record; enableOnInstall?: boolean; overwrite?: boolean }, + ) => { const route = this.getRoute('packages'); const res = await this.fetch(`${this.baseUrl}${route}`, { method: 'POST', @@ -601,6 +609,7 @@ export class ObjectStackClient { manifest, settings: options?.settings, enableOnInstall: options?.enableOnInstall, + ...(options?.overwrite !== undefined ? { overwrite: options.overwrite } : {}), }), }); return this.unwrapResponse<{ package: any; message?: string }>(res);