|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// [#29617, ADR-0141 D5 / ADR-0146 D2] A `.custom` pair exported by |
| 4 | +// `GET /packages/:id/export` reinstalls, through |
| 5 | +// `POST /api/v1/marketplace/install-local`, as the pair it was: under the |
| 6 | +// namespace it was installed with, depending on its base, its override |
| 7 | +// admitted. |
| 8 | +// |
| 9 | +// ## What was measured before the fix |
| 10 | +// |
| 11 | +// The export answered the pair's items and `id`, `name`, `version`, and |
| 12 | +// nothing of the two install-manifest keys its registry row holds. So the |
| 13 | +// reinstall knew no namespace (the install door derives one from the id: |
| 14 | +// `custom`) and no base edge, and the base edge is what orders the pair after |
| 15 | +// its base (ADR-0141 D5) and what admits the pair's `overrides` declarations |
| 16 | +// at the managed install (ADR-0146 D2). |
| 17 | +// |
| 18 | +// ## What this file pins, over two showcase boots |
| 19 | +// |
| 20 | +// 1. source: the ensure door creates the showcase's pair, a dashboard |
| 21 | +// override is published into it, and the pair's export carries the |
| 22 | +// `namespace` and `dependencies` its registry row declares (the row is |
| 23 | +// the control); |
| 24 | +// 2. target: a fresh showcase installs that export through install-local, |
| 25 | +// 200, and the reinstalled pair's registry row declares the same |
| 26 | +// namespace and the same edge to the showcase, its override carried |
| 27 | +// with its `overrides` block. |
| 28 | +// |
| 29 | +// Boots fixture stacks of its own (the install-local plugin mounted on the |
| 30 | +// target), so it stays out of `SHARED_SHOWCASE`. |
| 31 | + |
| 32 | +import { mkdtempSync, rmSync } from 'node:fs'; |
| 33 | +import { tmpdir } from 'node:os'; |
| 34 | +import { join } from 'node:path'; |
| 35 | + |
| 36 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
| 37 | +import { type VerifyStack } from '@objectstack/verify'; |
| 38 | +import { MarketplaceInstallLocalPlugin } from '@objectstack/cloud-connection'; |
| 39 | +import { bootShowcase } from './showcase-boot.js'; |
| 40 | +import { declaredOverride, ensurePair, inPair, pairOf } from './customization-pair.js'; |
| 41 | + |
| 42 | +const SHOWCASE = 'com.example.showcase'; |
| 43 | +const PAIR = pairOf(SHOWCASE); |
| 44 | +const ROUTE = '/marketplace/install-local'; |
| 45 | +const DASHBOARD = 'showcase_revenue_pulse'; |
| 46 | +const EDITED = 'Revenue pulse (customized)'; |
| 47 | + |
| 48 | +type Answer = { status: number; body: any }; |
| 49 | + |
| 50 | +async function call(stack: VerifyStack, token: string, method: string, path: string, body?: unknown): Promise<Answer> { |
| 51 | + const res = await stack.apiAs(token, method, path, body); |
| 52 | + return { status: res.status, body: await res.json().catch(() => null) }; |
| 53 | +} |
| 54 | + |
| 55 | +/** The manifest a `GET /packages/:id` answer carries for the package's registry row. */ |
| 56 | +const rowManifest = (answer: Answer): Record<string, any> | undefined => |
| 57 | + answer.body?.data?.manifest ?? answer.body?.data?.package?.manifest; |
| 58 | + |
| 59 | +describe('dogfood: a `.custom` pair\'s export reinstalls under its own namespace, depending on its base', () => { |
| 60 | + let storageDir: string; |
| 61 | + let sourceRow: Answer; |
| 62 | + let published: Answer; |
| 63 | + let exported: Answer; |
| 64 | + let installed: Answer; |
| 65 | + let targetRow: Answer; |
| 66 | + |
| 67 | + beforeAll(async () => { |
| 68 | + const source = await bootShowcase(); |
| 69 | + try { |
| 70 | + const token = await source.signIn(); |
| 71 | + await ensurePair(source, token, SHOWCASE); |
| 72 | + sourceRow = await call(source, token, 'GET', `/packages/${PAIR}`); |
| 73 | + const saved = await call(source, token, 'PUT', inPair(`/meta/dashboard/${DASHBOARD}?mode=draft`, SHOWCASE), |
| 74 | + declaredOverride({ name: DASHBOARD, label: EDITED, columns: 12, widgets: [] }, SHOWCASE, '0.1.0')); |
| 75 | + expect(saved.status, JSON.stringify(saved.body)).toBe(200); |
| 76 | + published = await call(source, token, 'POST', `/packages/${PAIR}/publish-drafts`, {}); |
| 77 | + exported = await call(source, token, 'GET', `/packages/${PAIR}/export`); |
| 78 | + } finally { |
| 79 | + await source.stop(); |
| 80 | + } |
| 81 | + |
| 82 | + storageDir = mkdtempSync(join(tmpdir(), 'dogfood-export-reinstall-pair-')); |
| 83 | + const target = await bootShowcase({ |
| 84 | + extraPlugins: [new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir })], |
| 85 | + }); |
| 86 | + try { |
| 87 | + const token = await target.signIn(); |
| 88 | + installed = await call(target, token, 'POST', ROUTE, { manifest: exported.body?.data }); |
| 89 | + targetRow = await call(target, token, 'GET', `/packages/${PAIR}`); |
| 90 | + } finally { |
| 91 | + await target.stop(); |
| 92 | + } |
| 93 | + }, 600_000); |
| 94 | + |
| 95 | + afterAll(() => { |
| 96 | + if (storageDir) rmSync(storageDir, { recursive: true, force: true }); |
| 97 | + }); |
| 98 | + |
| 99 | + it('source: the pair\'s export carries the namespace and the base edge its registry row declares', () => { |
| 100 | + expect(published.status, JSON.stringify(published.body)).toBe(200); |
| 101 | + expect(sourceRow.status, JSON.stringify(sourceRow.body)).toBe(200); |
| 102 | + // CONTROL: the row the export reads declares both. |
| 103 | + const row = rowManifest(sourceRow); |
| 104 | + expect(row?.namespace, JSON.stringify(sourceRow.body)).toBe('showcase_custom'); |
| 105 | + expect(Object.keys(row?.dependencies ?? {})).toEqual([SHOWCASE]); |
| 106 | + |
| 107 | + expect(exported.status, JSON.stringify(exported.body)).toBe(200); |
| 108 | + const manifest = exported.body?.data; |
| 109 | + expect(manifest?.namespace).toBe(row?.namespace); |
| 110 | + expect(manifest?.dependencies).toEqual(row?.dependencies); |
| 111 | + expect((manifest?.dashboards ?? []).map((d: any) => [d.name, d.label, d.overrides?.package])) |
| 112 | + .toEqual([[DASHBOARD, EDITED, SHOWCASE]]); |
| 113 | + }); |
| 114 | + |
| 115 | + it('target: install-local reinstalls the export, 200, with its override admitted', () => { |
| 116 | + expect(installed.status, JSON.stringify(installed.body)).toBe(200); |
| 117 | + expect(installed.body?.data?.manifestId).toBe(PAIR); |
| 118 | + }); |
| 119 | + |
| 120 | + it('target: the reinstalled pair declares the namespace it was exported with and its edge to the showcase', () => { |
| 121 | + expect(targetRow.status, JSON.stringify(targetRow.body)).toBe(200); |
| 122 | + const row = rowManifest(targetRow); |
| 123 | + expect(row?.namespace, JSON.stringify(targetRow.body)).toBe('showcase_custom'); |
| 124 | + expect(row?.dependencies).toEqual(rowManifest(sourceRow)?.dependencies); |
| 125 | + }); |
| 126 | +}); |
0 commit comments